xylosper's notebook

검색 :
RSS 구독 : 글 / 댓글 / 트랙백 / 글+트랙백

Qt에 대하여 - 5. 체험 signal/slot

2008/03/26 22:23, 글쓴이 xylosper
이번에는 직접 시그널과 슬롯을 구현해보고, 가능하다면 Qt의 Meta-Object 시스템에 대해서도 소개해보려고 합니다.

버튼과 라벨을 하나씩 배치하고, 버튼이 더블 클릭되면 라벨의 텍스트가 바뀌는 clicks라는 예제입니다.

QPushButton은 한번 클릭한 경우와 두번 클릭한 경우 구분없이 clicked()시그널이 나옵니다.

여기선 더블클릭하면 doubleClicked()라는 시그널을 발생시키는 DoubleButton이란 버튼을 구현하고, 이 시그널을 받아서 처리하도록 슬롯을 만들것입니다.

이번회는 클래스를 두개를 만들기 때문에 소스양이 좀 많습니다.(사실 이건 많은것도 아니죠-_-;)

doublebutton.h
[code]
#include <QPushButton>

class DoubleButton : public QPushButton {
    Q_OBJECT
public:
    DoubleButton(QWidget *parent = 0);
signals:
    void doubleClicked();
protected:
    virtual void mouseDoubleClickEvent(QMouseEvent *event);
};
[/code]
doublebutton.cpp
[code]
#include "doublebutton.h"

DoubleButton::DoubleButton(QWidget *parent)
: QPushButton(trUtf8("눌러보세요"), parent) {

}

void DoubleButton::mouseDoubleClickEvent(QMouseEvent *event) {
    QPushButton::mouseDoubleClickEvent(event);
    emit doubleClicked();
}
[/code]
mainwidget.h
[code]
#include <QWidget>

class DoubleButton;            class QLabel;

class MainWidget : public QWidget {
    Q_OBJECT
public:
    MainWidget(QWidget *parent = 0);
private slots:
    void setLabelDoubleClicked();
private:
    QLabel *label;
    DoubleButton *button;
};
[/code]
mainwidget.cpp
[code]
#include <QLabel>
#include "mainwidget.h"
#include "doublebutton.h"

MainWidget::MainWidget(QWidget *parent)
: QWidget(parent) {
    button = new DoubleButton(this);
    label = new QLabel(trUtf8("클릭 안됨"), this);

    connect(button, SIGNAL(doubleClicked()), this, SLOT(setLabelDoubleClicked()));

    label->move(100, 0);
    resize(200, 50);
}

void MainWidget::setLabelDoubleClicked() {
    label->setText(trUtf8("더블 클릭"));
}
[/code]
main.cpp
[code]
#include <QApplication>
#include "mainwidget.h"

int main(int argc, char **argv) {
    QApplication app(argc, argv);
    MainWidget mw;
    mw.show();
    return app.exec();
}
[/code]
clicks

실행화면


일단 Q_OBJECT라는 키워드는 나중에 설명할테니 패스하시구요, 우선 doublebutton.h파일의 7번째 줄을 보시면 처음보는 키워드가 보일 것입니다.

바로 signals 라는 키워드인데요, 뒤에 :를 붙여서 마치 public이나 private같은 접근한정자처럼 쓰였습니다.

이 키워드가 함수를 시그널로 만들어주는 키워드입니다.

사용법도 public등과 같이 signals:라고 그 다음부터 다른 한정자가 나오기 전까지는 전부 시그널로 처리됩니다.

일반적으로 시그널은 직접 호출될 일이 없기 때문에 원형만 선언하고 내용은 정의하지 않아도 됩니다.(물론 호출해서 일반함수로 쓰는 것도 가능하며, 이경우는 당연히 함수 정의도 있어야겠지요)

이제 그 다음줄에 있는 doubleClicked()라는 함수는 시그널로 이용할 수 있습니다.

남은 문제는 언제 이것을 발생시키느냐입니다.

이름을 그럴싸하게 지었다고 지가 알아서 튀어나올리는 만무하겠죠?

그럼 버튼이 더블클릭되는 시점이 언제인지를 알아야겠습니다.

이를 위해서 어떤 위젯이 더블클릭되면 호출되는 이벤트 처리 함수인 mouseDoubleClickEvent 함수를 오버라이딩하기로 합니다.

doublebutton.cpp파일의 8-11번째 줄을 보면 다음과 같이 적혀있습니다.
[code]
void DoubleButton::mouseDoubleClickEvent(QMouseEvent *event) {
    QPushButton::mouseDoubleClickEvent(event);
    emit doubleClicked();
}
[/code]
여기서 QPushButton::mouseDoubleClickEvent(event);를 호출하는 이유는, 혹시 모를 더블클릭관련된 다른 이벤트들이 처리되도록하기 위해서입니다.

제가아는 한에서 QPushButton이나 그 상위의 QWidget은 딱히 더블클릭에 대해서 아무것도 하지 않는 걸로 알고 있으므로 이경우는 저 라인이 있으나 없으나 똑같을 것입니다.

중요한건 그다음줄인데요, 여기서 또 emit 이라는 처음보는 키워드가 나옵니다.

사전을 찾아보시면 emit은 뱉다 라는 뜻이라고 나옵니다.

예, 말그대로 emit doubleClicked(); 라고 적으면 doubleClicked() 시그널을 뱉어냅니다.

참고로 이부분은 doubleClicked()를 호출하는 구문이 아닙니다.

큐티용 전처리기인 moc를 거쳐서 이부분은 (직접 그자리에서 치환되진 않지만 결과적으로 보면) 여기에 연결된 슬롯들을 호출하는 구문으로 치환될 것입니다.

이렇게 하여 시그널을 만들고 그것을 뱉는발생시키는 방법을 알아보았습니다.

참고로 signals는 다른 public같은 한정자와 같이 쓰면안됩니다.

signals 자체에 protected 한정자가 포함되어있기 때문입니다.(즉, 모든 시그널은 protected 멤버함수입니다.)

다음으로 슬롯을 만드는 방법을 알아보겠습니다.

(여기서도 일단 Q_OBJECT는 패스합니다)mainwidget.h를 보면 역시나 처음보는 키워드가 있습니다.

9번째 줄의 slots라는 키워드인데요, 뻔합니다. 이게 슬롯선언을 위한 키워드죠.

슬롯은 시그널과 달리 직접 호출되는 함수이기 때문에 원형만 선언하고 정의하지 않으면 링크단계에서 에러가 나며, 또 꼭 시그널에 의해 호출되지 않고 평범한 함수호출로도 자주 이용되기 때문에 접근 한정자가 없습니다.

그러므로 이경우는 private slots: 와 같이 private라는 접근 한정자를 달아주었습니다.

외부에서 호출가능하게 만들려면 public slots: 와 같이 해야겠지요.

signals와 마찬가지로 slots다음부터 다른 한정자가 다올때까지 선언된 함수들은 모두 슬롯으로 이용가능해집니다.

그러므로 다음줄에 적힌 setLabelDoubleClicked() 함수는 다른 시그널에 연결가능한 슬롯이 되었습니다.

그리고 mainwidget.cpp의 setLabelDoubleClicked()의 구현을 보면 심플합니다.

그냥 label의 setText()함수를 이용해서 더블클릭이란 글자로 바뀌도록 합니다.

이제 더블클릭되면 발생할 시그널과, 이를 연결할 슬롯이 준비되었으므로 둘을 연결하기만 하면됩니다.

mainwidget.cpp에서 MainWidget의 생성자를 봅시다.

connect(button, SIGNAL(doubleClicked()), this, SLOT(setLabelDoubleClicked()));

라고 적혀있죠? button의 시그널인 doubleClicked()와 this(MainWidget)의 setLabelDoubleClicked()를 연결합니다.

생성자에서 뭔가 하고싶은 말이 있으신 분이 계실지도 모르겠는데, 잠시 참아주세요.

이제 컴파일해보고 실행하고 버튼을 더블 클릭하면 라벨의 텍스트가 더블 클릭으로 바뀝니다.

별거 아닌 예제지만 시그널과 슬롯을 이용하기 위한 것들을 하나빼고 다 설명했습니다.

그럼 빠진 하나는? 기억력좋으신 분은 제가 Q_OBJECT를 패스라고 설명한걸 기억하실 텐데요, 바로 이 키워드가 핵심키워드입니다.

사실 이글에서 이 키워드와, 그리고 생성자에서 느낀 위화감을 설명해줄 Qt의 Meta-Object 시스템까지 소개할려고 했는데, 내용이 길어져서 다음회로 넘겨야 겠습니다.

이리하여, 다음회는 Meta-Object 시스템입니다.
2008/03/26 22:23 2008/03/26 22:23

맨 위로

Qt에 대하여 - 4. Hello, world! advanced

2008/03/26 20:00, 글쓴이 xylosper
좀더 진보된(?) Hello, world입니다.
[code]
#include <QApplication>
#include <QPushButton>

int main(int argc, char **argv) {
    QApplication app(argc, argv);
    QPushButton button(QObject::trUtf8("안녕하세요! 종료하고 싶으시면 눌러보세요."));
    QObject::connect(&button, SIGNAL(clicked()), qApp, SLOT(quit()));
    button.show();
    return app.exec();
}
[/code]
Hello, world! advanced

실행결과

signal/slot이 드디어 나왔습니다.
이번회는 signal/slot에 대한 설명과 tr/trUtf8 함수에 대한 설명을 위한 시간을 가지겠습니다.
기본적인 구조는 동일하므로 지난번같이 각 라인을 설명하진 않겠습니다.

일단 QPushButton이라는 새로운 클래스가 나왔는데요, 이 클래스도 역시 위젯이며, 이름대로 push하기 위한 button입니다.

보통 자주 보는 확인버튼이나 취소버튼등이 이 클래스로 구현될 수 있습니다.

QPushButton도 역시 QLabel과 마찬가지로 생성자로 문자열을 받아서, 그 문자열로 버튼의 text를 초기화하며, 마찬가지로 text(), setText()로 문자열을 확인하거나 지정할 수 있습니다.

그런데, 6번째 줄의 button을 생성할때, 생소한 정적함수 QObject::trUtf8()이란 함수가 호출되고 있습니다.

이 함수는 프로그램을 번역할떄 도움이 됩니다.

만약 프로그램을 다른 언어로 번역해야 할때, 소스내의 모든 문자열을 변역할 필요는 없을 것입니다.

그렇다고 소스를 일일이 돌아다니면서 번역하자니 상당한 노력이 들테고, 빠진부분도 많겠지요.

만약 번역이 필요한 부분의 문자열은 전부 QObject::tr()혹은 QObject::trUtf8()로 감싸두면, Qt를 설치할때 함께 설치된 툴중 하나인 lupdate라는 프로그램을 이용하면, 프로젝트 파일에 등록된 모든 소스를 파싱하여 번역이 필요한 부분만 골라서 번역을 위한 xml파일을 생성해주고, 번역작업은 이 파일만 가지고 하면 됩니다.

이또한 Qt Linguist 라는 프로그램을 통해서 손쉽게 열고 편집할 수 있기 때문에, 번역하는 사람이 프로그래밍을 전혀 몰라도 되고, 그러므로 프로그래밍 언어는 몰라도 말로 하는 언어에 능통한 사람이 번역이 가능하고 좀더 고품질의 번역이 가능하겠죠?

그럼 tr()과 trUtf8()의 차이는 무엇인가하면, 함수 이름에서 알 수 있는데 인코딩의 차이입니다.

만약 영어로 프로그램을 만든다면 모든 문자가 ascii범위내에 있을테니 tr로 충분합니다만, 한국어로 메뉴등을 작성한다면, Qt의 문자열 클래스인 QString은 유니코드로 처리하기 때문에 인코딩이 중요해집니다.

그래서 한글을 써서 Qt로 프로그래밍을 하실때는 꼭 소스파일을 전부 utf8로 저장하시고, 한글이 필요한 부분에서는 trUtf8이나 QString::fromUtf8()과 같은 함수로 utf8로부터 문자열을 생성해주는 함수를 이용하셔야 합니다.

사실 처음에 이걸 모르고 자꾸 에러가 나고 제대로 표시가 안되는 바람에 전부 영어로 짠다음에 위에서 말한 Linguist로 한글로 메뉴를 번역하는 짓을 하곤 했습니다-_-;

이제 그 다음줄로 가봅시다.

사실 이부분이 이번 편의 하일라이트입니다.
[code]
    QObject::connect(&button, SIGNAL(clicked()), qApp, SLOT(quit()));
[/code]
여기서 보이는 QObject::connect, SIGNAL, SLOT 이 세 키워드 모두 매우매우 중요합니다.

우선 시그널(signal)에 대해서 설명해보겠습니다.

큐티의 시그널이란, 뭐 이름 그대로 신호같은 겁니다.

예를 들어 위의 예제에서 clicked()가 시그널이며, 이름으로 부터 유추할수 있듯이 button이 clicked하면(눌러지면) clicked()라는 시그널이 발생됩니다.

이 시그널은 슬롯으로 지정된 함수들과 '연결'될 수 있습니다.

참고로 슬롯과 마찬가지로 시그널도 함수들입니다(시그널과 슬롯을 선언하는 방법은 나중에 상속받아서 클래스를 만드는 예제에서 설명하겠습니다.)

그리고 '연결'을 행하는 함수가 바로 QObject::connect()입니다.

즉 위의 한줄은, button의 clicked() 시그널과 qApp의 quit() 슬롯을 연결해라! 라고 하는 것입니다.

여기서 qApp는 QApplication의 객체에 대한 포인터를 바로 불러올수 있는 매크로입니다.

사실 여기선 같은 함수내에서위에 QApplication의 객체 app가 있으므로, qApp대신에 &app라고 직접 포인터를 넘겨줘도 되지만, 일부러 qApp라는 매크로를 소개하기 위해서 적어보았습니다.

확실친 않지만 아마도 QApplication::instance() 함수의 매크로로 정의되어있을 것같습니다.

아무튼 저렇게해서 시그널과 슬롯이 연결되면, 시그널이 발생될때마다 연결된 슬롯이 호출됩니다.

프로그램의 흐름을 적어보자면

button 클릭 -> clicked() 시그널 발생 -> quit() 슬롯 호출 -> 프로그램 종료

이런 순서가 되겠지요.

뭔가 설명이 애매한데?

하나의 시그널은 여러개의 슬롯과 연결될 수 있으며, 반대로 여러개의 시그널이 하나의 슬롯으로 연결될수도 있습니다.

또 연결하는 시그널과 슬롯들은 인자가 같거나, 슬롯쪽이 적어야 합니다.

왜냐하면 시그널에서 필요한 정보가 슬롯으로 전달될때 슬롯함수를 호출하면서 그 인자로 넘어가기 때문입니다.

예를 들어 QPushButton의 함수중에 setCheckable(bool)을 호출하여 true로 설정해주면, 이 버튼은 토글버튼(한번 누르면 들어가고, 한번 누르면 다시 나오는 방식의 버튼)이 됩니다.

이때, 사용자가 이 버튼을 눌렀을때, 예를 들어 버튼이 들어가면 on이라 적어주고, 버튼이 나오면 off라 적어주는 obj의 슬롯 onOff(bool on)이란 함수와 연결하고 싶다면,

connect(&button, SIGNAL(toggled(bool)), obj, SLOT(onOff(bool)));

과 같이 적어주어야 합니다.

toggled(bool) 시그널은 위와같이 토글 버튼일때, 토글될때마다 그 상태가 자료형 bool의 인자에 저장되어서 발생합니다.

예를 들어보면,

프로그램 실행(버튼 나온 상태:off) -> 버튼 클릭(들어감) -> toggled(true) 발생 -> onOff(true) 호출 -> 글자를 on으로 바꿈
-> 버튼 클릭(나옴) -> toggled(false) 발생 -> onOff(false) 호출 -> 글자를 off로 바꿈

이와같이 작동하겠지요.

단, 슬롯쪽이 인자 갯수가 적은 경우는 괜찮습니다.

SIGNAL(foo(bool, int))와 SLOT(foo(bool))은 연결될 수 있습니다.

단 앞에서부터 인자가 일치해야하므로, SLOT(foo(int))와는 연결될 수 없습니다.

참고로 SIGNAL()과 SLOT()에 함수 이름을 적어줄 때는 규칙이 있습니다.

함수이름(인자자료형만,...) 이런 식입니다.

예를 들어 어떤 시그널이나 슬롯의 원형이 다음과 같다면

double foo(int first, bool second)

SIGNAL(foo(int, bool))이나 SLOT(foo(int, bool))과 같이 적어줍니다.

반환형이 double이나 인자의 이름인 first나 second등은 적지 않습니다.

또한, 시그널과 시그널을 연결할 수도 있습니다.

시그널 A와 시그널 B를 연결하면 A시그널이 발생하면 자동으로 B시그널도 발생됩니다.

이경우도 시그널과 슬롯을 연결할때와 마찬가지로 인자에 대한 규칙이 적용됩니다.

이상 큐티를 이용하는데 매우 중요한 시그널/슬롯에 대한 설명이었습니다.

이거 모르고 큐티로 프로그램을 짠다는건 사실상 불가능할 정도이니, 잘 알아두시고, 혹시 잘 이해가 안되거나 설명이 난해한부분이 있으면 말씀해주시면 개선하도록 하겠습니다.
2008/03/26 20:00 2008/03/26 20:00

맨 위로