Qt 에서 이벤트는 3가지 카테고리로 나눌 수 있다.
- Spontaneous 이벤트는 윈도우 시스템에 의해 생성된다. 이 이벤트는 시스템 큐에 들어간 후 이벤트 루프에서 차례대로 처리된다.
- Posted 이벤트들은 Qt 또는 응용 프로그램에 의해 생성된다. Qt에 의해 큐에 들어간 후 이벤트 루프에서 처리된다.
- Sent 이벤트들은 Qt 또는 응용 프로그램에 의해 생성된다. 이 이벤트들은 큐를 거치지 않고 직접 대상이 되는 오브젝트에 전달되어 즉시 처리된다.
우리가 흔히 Qt 용 프로젝트를 생성하면 main() 안에서 QApplication::exec()가 호출되는 것을 볼 수 있는 데 이때 비로소 응용프로그램은 이벤트 루프에 들어가게 된다.
int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); // 이벤트 루프 시작 }
한마디로 이 함수가 호출 되기 전까지 응용프로그램은 유저의 입력을 받을 수 없다.
(예외적으로 QMessageBox 와 같은 Modal 위젯은 자체적으로 이벤트 루프를 실행하기 때문에 exec() 호출 전이라도 이벤트를 받을 수 있다.)
개념적으로 이벤트 루프는 다음과 같이 처리된다고 생각할 수 있다.
while ( !exit ) { while ( !posted_event_queue_is_empty) { ////////// (1) process_next_posted_event(); } while ( !spontaneous_event_queue_is_empty) { ///////////// (2) process_next_spontaneous_event(); } while ( !posted_event_qeueu_is_empty) { /////////////// (3) process_next_posted_event(); } }
먼저 (1) 에서 큐가 빌 때 까지 포스트 된 이벤트들이 처리된다.
처리가 완료되면 다음 단계(2)로 넘어가 역시 큐가 빌 때 까지 Spontaneous 이벤트들이 처리된다.
이때 이 이벤트들이 처리(Dispatch)되면서 새로운 이벤트들로 변환이 되어 다시 Post 큐에 들어가게 된다.
마지막으로(3) Spontaneous 이벤트들에 의해 다시 채워진 이벤트 큐를 처리하며 한번의 이벤트 루프를 완료하게 된다.
Sent 로 보내진 이벤트들은 즉시 처리되기 때문에 이벤트 루프에서는 존재하지 않게 된다.
페인트 이벤트 처리를 예로 들며 자세히 살펴 보도록 하자.
위젯이 만들어 지고 처음 Visible이 되거나 Hidden 상태에서 Visible이 될 때 윈도우 시스템은 프로그램에게 위젯을 redraw하기 위해 (Spontaneous) 페인트 이벤트를 생성하게 된다.
이벤트 루프에서 이 이벤트를 꺼내어 디스패치를 하면 위젯이 redraw 된다.
그런데 윈도우 시스템에 의해서만 페인트 이벤트가 발생하는 것은 아니다.
우리가 흔히 사용하는 update()를 호출하면 위젯은 그 자신에게 페인트 이벤트를 Post하여 큐에 집어 넣고 이후 이벤트 루프에서 디스패치 되게 된다.
(디스패치 되면 그 자신에게 QPaintEvent 객체를 생성하여 QWidget::paintEvent(QPaintEvent*)의 인자로 넘겨 호출할 것이다.)
물론 이벤트루프에서의 처리를 기다리지 않고 즉시 위젯을 redraw하고자 한다면 paintEvent()를 직접 호출하는 방법도 있다.
다만 이 맴버함수는 protected로 선언되었기 때문에 자신이 아닌 외부에서 호출은 불가능하다.
어쨌든 직접호출하게 되면 이벤트 필터에 의해 해당 이벤트가 걸러지는 부분도 피할 수 있다.
이런 이유로 Qt에서는 Post하지 않고 즉시 이벤트 처리를 하기 위한 매커니즘(Send event)을 만들어 놓았는데 QWidget::repaint()가 대표적이다.
그렇다면 Post 된 이벤트들이 즉시 처리되는 Send 이벤트에 비해 가지는 장점은 무엇일까?
이벤트를 Post 했을 때의 장점은 이벤트들을 최적화 할 수 있다는 것이다.
우리가 자주 사용하는 update()가 좋은 예가 되는 데 10번의 update()를 호출했다고 하면 이것들을 최적화 하여 갱신되는 영역을 union연산으로 합쳐서 하나로 만든 후 하나의 이벤트로 압축할 수 있다는 것이다.
paint, move, resize, layout hint, 그리고 language changed 와 같은 이벤트들이 이 범주에 들어간다.
이벤트 생성(Synthetic Events)
응용 프로그램은 직접 이벤트(객체)를 생성할 수도 있다.
QEvent 또는 이를 상속한 클래스의 인스턴스를 생성하고 QApplication::poseEvent(QObject *receiver, QEvent *e), QApplication::sendEvent(QObject *receiver, QEvent *e)를 호출해 처리 할 수 있는 것이다.
이 두함수들은 QObject와 QEvent 각각의 포인터를 인자로 받는다.
즉시 또는 지연 처리할 수 있느냐의 차이점 외에 호출 시의 차이점도 있는 데
postEvent()를 호출 할 때 QEvent 를 new로 생성하여 넘기면 이후의 이벤트 루프에서 삭제를 책임지게 된다.
sendEvent()는 즉시 호출되어 제어가 되돌아 오므로 스택에 이벤트 객체를 생성하면 된다.
//Post 방식 QApplication::postEvent(mainWindow, new QKeyEvent(QEvent::KeyPress, Qt::Key_X, 'X', 0));
//Send 방식 QKeyEvent e(QEvent::KeyPress, Qt::Key_X, 'X', 0); QApplication::sendEvent(mainWindow, &e);
개발자가 postEvent(), sendEvent()를 호출하는 경우는 그렇게 많지 않다.
왜냐하면 대부분의 경우 Qt 또는 윈도우 시스템에 의해 필요 시 자동으로 생성되기 때문이다.
사용자 정의 이벤트 타입(Custom Event Types)
Qt는 미리 정의되어 있는 이벤트 외에 개발자들이 직접 이벤트를 만들 수 있도록 하고 있다.
이 방법은 특히 쓰레드를 만들어 GUI 쓰레드와 통신하고자 할 때 유용하다.
물론 멀티쓰레드가 아니더라도 단일 쓰레드에서도 유용하게 사용될 수 있다.
일반 함수 호출도 있고 signal/slot 처리도 있는 데 굳이 사용자 처리 이벤트를 선택하는 건 동기(send)/비동기(post) 처리 모두에서 사용될 수 있기 때문이다.
일반 함수 호출 및 signal/slot에 의한 처리는 늘 동기적(synchronous)이다. 호출 즉시 처리되어 호출 지점으로 되돌아 오는 것이다.
또 다른 장점은 필요한 경우 이벤트 필터에 의해 필터링 되어 처리를 생략될 수 있다는 점이다.
자세한 건 다음에 보기로 하고 사용자 정의 이벤트 처리의 간단한 코드 샘플을 보자.
const QEvent::Type MyEvent = (QEvent::Type)1234; ... QApplication::postEvent(obj, new QCustomEvent(MyEvent));
위의 예에서 이벤트는 QCustomEvent 이거나 이를 상속받은 클래스 이어야 한다.
이 이벤트를 처리하려면 obj 객체에 해당하는 클래스는 customEvent(QCustomEvent*)를 오버라이딩 하여야 한다.
void MyLineEdit::customEvent(QCustomEvent *e) { if (e->type() == MyEvent) { myEvent(); } else { QLineEdit::customEvent(e); } }
QCustomEvent 클래스에는 범용적으로 사용될 수 있는 void* 형의 맴버가 준비되어 있다.
필요하면 추가정보를 void*에 설정해 customEvent()에서 캐스팅 해서 사용할 수 있다.
하지만 QCustomEvent를 상속받아 필요한 맴버 변수나 함수를 추가하여 처리하는 편이 타입안정성면에서 더 좋을 수 있다.
QCustomEvent는 Qt3에 해당하는 내용이며 Qt4에서는 Qt3 support 라이브러리를 이용해사용할 수 있는 듯 하다.Qt5 부터는 이를 사용할 수 없다.
이벤트 핸들링과 필터링
Qt에서 이벤트들은 5가지 방법으로 처리할 수 있다.
- 특정 이벤트 핸들러를 오버라이딩
QObject와 QWidget에는 각기 다른 이벤트들을 처리하기 위한 많은 가상함수들이 준비되어 있다.
예) QPaintEvent 를 처리하는 paintEvent()
- QObject::event()를 오버라이딩.
이 함수는 오브젝트가 이벤트를 처리하는 진입점이다. QObject와 QWidget는 간단히 해당 이벤트들을 이벤트 핸들러에 전달하도록 기본 구현되어 있다.
- QObject 에 이벤트 필터 등록하기
이벤트 필터를 등록하면 오브젝트가 처리하기 전에 이벤트 필터가 먼저 이벤트를 받을 수 있다.
- qApp에 이벤트 필터 등록하기
특별히 qApp에 이벤트 필터를 등록하면 응용프로그램 내에서 오브젝트들에게 가는 모든 이벤트를 모니터링 할 수 있다.
- QApplication::notify() 오버라이딩 하기
Qt 이벤트 루프와 sendEvent()는 이벤트를 디스패치하기 위해 이 함수를 호출한다.
이 함수를 오버라이딩 하면 가장 앞서 이벤트를 받아 처리할 수 있는 우선순위를 가지게 된다.
어떤 이벤트들은 이벤트 처리 대상 객체가 처리를 하지 않을 경우(거부) 전파(Propergated)되는 특성을 가지는 데 이 때 Qt는 이를 처리할 객체를 찾을 때 까지 새로운 대상 오브젝트에서 QApplication::notify()를 호출하게 된다.
예를 들어 키 이벤트를 처리하고자 할 때 포커스를 가진 대상 오브젝트가 처리하지 않으면 최상위 오브젝트에 도달할 때 까지 계속 부모에게 전파하게 된다.
Accept or Ignore ?
전파될 수 있는 이벤트들은 accept()와 ignore() 맴버함수를 가진다.
만약 이벤트 핸들러안에서 accept()가 호출 된다면 이 이벤트는 더이상 전파되지 않을 것이다.
반대로 ignore()를 호출하면 처리할 수 있는 receiver를 찾을 때 까지 계속 시도할 것이다.
대부분의 Qt 개발자들 처럼 accept()와 ignore()를 사용하는 경우는 드물다.
왜냐하면 기본적으로 QEvent 는 "accept"를 기본 값으로 가지고 있고 QWidget 의 이벤트 핸들러들은 기본적으로 ignore()를 호출하기 때문이다.
아래 예를 보자.
void MyObject::keyPressEvent(QKeyEvent *e) // e는 기본적으로 "accept" { if (e->key() == Qt::Key_Escape) doEscape(); else QWidget::keyPressEvent(e); // ignore }
위의 이벤트 핸들러에서 QWidget::keyPressEvent(e); 처럼 QWidget(또는 이를 상속받은 Qt의 위젯들)의 기본 이벤트 핸들러를 호출 하지 않는다면 이벤트 핸들러가 이벤트를 처리 했다고 판단하여 더이상 전파하지 않는다.
아래 코드를 보면 더 이해가 쉬울 것이다.
void QWidget::keyPressEvent(QKeyEvent *e) { e->ignore(); }
이벤트 핸들러가 아닌 QWidget::event()를 오버라이딩 하여 구현하고 있다면 bool 값을 리턴하여 처리 완료할지 아니면 계속 전파를 할지 결정할 수 있다.
true는 "accept"를 의미하고 false는 "ignore"를 의미한다.
QWidget::event() 안에서 QEvent의 accept(), ignore()를 호출하는 것은 무의미 하다.
"accpet" 플래그는 특정 이벤트 핸들러와 event()사이에 처리여부를 판단하기 위한 용도이기 때문이다.
또 event()에서 리턴되는 bool값은 QApplication::notify() 내에서 처리여부를 판단하기 위한 용도로 쓰여진다.
아래 예를 보면서 event()의 기본 구현이 어떤식으로 되어 있는 살펴보자.
bool QWidget::event(QEvent *e) { switch (e->type()) { case QEvent::KeyPress: keyPressEvent((QKeyEvent*)e); if (!e->isAccepted()) return false; case QEvent::KeyRelease: keyReleaseEvent((QKeyEvent*)e); if (!e->isAccepted(e)) return false; ... } return true; }
예외적으로 close 이벤트의 경우 구현이 조금 다른데 QCloseEvent::ignore() 를 호출하면 응용프로그램 닫기를 취소하고 accept()를 하면 Qt 가 closing 처리를 계속 이어갈 수 있도록 한다.
이로인한 혼란을 피하기 위해서는 명시적으로 accept(), ignore()를 직접 호출하는 코드를 작성하면 된다.
void MainWindow::closeEvent(QCloseEvent *e) { if (userReallyWantsToQuit) e->accept(); else e->ignore(); }