QT——事件处理机制
它反映了包括Qt在内的GUI应用程序的消息处理模型:
(1) 用户操作界面,被操作系统内核空间中的设备驱动程序感知
(2) 设备驱动程序向操作系统的用户空间的GUI应用程序发出系统消息
(3) GUI应用程序将系统消息转换为信号,进而触发槽函数
在GUI应用程序中,一个核心且关键的操作就是将系统消息转换为信号,涉及到Qt的事件处理机制:
(1) Qt平台将系统底层发来的消息转换为Qt事件,并将其Qt事件产生后立即被分发到QWidget对象
(2) QWidget对象中的event(QEvent* )函数对事件进行处理,即根据不同的事件,调用不同的事件处理函数
(3) 在事件处理函数中发送Qt中预定义的对应事件的Qt信号,进而调用到信号关联的槽函数
以触摸屏为例
当用户点击触摸屏,首先感知到屏幕上被触摸的XY坐标是操作系统内核空间的触摸屏设备驱动程序,然后设备驱动程序会将用户操作封装成消息传递给GUI程序运行时创建的消息队列,GUI程序在运行过程中需要实时处理队列中的消息,当队列没有消息时,程序将处于停滞状态。
windows平台上GUI程序示例
在Windows上实现GUI有很多方法,每一种方法都有着自己的一套开发理念和工具,常见的有:
a. Windows API:直接调用Windows底层绘图函数。涉及到底层的基本用c语言写的,这些API也是如此。
b. MFC:使用Windows API封装成控件类
c. Windows Form:基于.net的GUI开发,完全组件化但是需要.Net运行库支持
…
其中基于Windows API开发的GUI程序,即是函数调用 + Windows消息处理的方法,这是所有GUI程序的原理。
在Qt中,事件被封装成一个个对象,所有的事件均继承自抽象类QEvent. 接下来依次谈谈Qt中有谁来产生、分发、接受和处理事件:
1、谁来产生事件: 最容易想到的是我们的输入设备,比如键盘、鼠标产生的
keyPressEvent,keyReleaseEvent,mousePressEvent,mouseReleaseEvent事件(他们被封装成QMouseEvent和QKeyEvent),这些事件来自于底层的操作系统,它们以异步的形式通知Qt事件处理系统。当然Qt自己也会产生很多事件,比如QObject::startTimer()会触发QTimerEvent. 用户的程序可还以自己定制事件
2、谁来接受和处理事件:答案是QObject。在Qt的内省机制剖析一文已经介绍QObject 类是整个Qt对象模型的心脏,事件处理机制是QObject三大职责(内存管理、内省(intropection)与事件处理制)之一。任何一个想要接受并处理事件的对象均须继承自QObject,可以选择重载QObject::event()函数或事件的处理权转给父类。
3、谁来负责分发事件:
- 对于non-GUI的Qt程序,是由QCoreApplication负责将QEvent分发给QObject的子类即Receiver.
- 对于Qt GUI程序,由QApplication 的QtWndProc来负责 分发
/*!\reimp
*/
bool QCoreApplication::event(QEvent *e)
{
if (e->type() == QEvent::Quit) {
quit();
return true;
}
return QObject::event(e);
}
C:\Program Files (x86)\Windows Kits\10\Include\10.0.17763.0\um\WinUser.h
/*
* Message structure
*/
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
if (!(flags & QEventLoop::ExcludeUserInputEvents) && !d->queuedUserInputEvents.isEmpty()) {
// process queued user input events
haveMessage = true;
msg = d->queuedUserInputEvents.takeFirst();
} else {
haveMessage = winPeekMessage(&msg, 0, 0, 0, PM_REMOVE);
if (haveMessage && (flags & QEventLoop::ExcludeUserInputEvents)
&& ((msg.message >= WM_KEYFIRST
&& msg.message <= WM_KEYLAST)
|| (msg.message >= WM_MOUSEFIRST
&& msg.message <= WM_MOUSELAST)
|| msg.message == WM_MOUSEWHEEL)) {
// queue user input events for later processing
haveMessage = false;
d->queuedUserInputEvents.append(msg);
}
}
Q_CORE_EXPORT bool winPeekMessage(MSG* msg, HWND hWnd, UINT wMsgFilterMin,
UINT wMsgFilterMax, UINT wRemoveMsg)
{
QT_WA({ return PeekMessage(msg, hWnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg); } ,
{ return PeekMessageA(msg, hWnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg); });
}
C:\Program Files (x86)\Windows Kits\10\Include\10.0.17763.0\um\WinUser.h
WINUSERAPI
BOOL
WINAPI
PeekMessageA(
_Out_ LPMSG lpMsg,
_In_opt_ HWND hWnd,
_In_ UINT wMsgFilterMin,
_In_ UINT wMsgFilterMax,
_In_ UINT wRemoveMsg);
WINUSERAPI
BOOL
WINAPI
PeekMessageW(
_Out_ LPMSG lpMsg,
_In_opt_ HWND hWnd,
_In_ UINT wMsgFilterMin,
_In_ UINT wMsgFilterMax,
_In_ UINT wRemoveMsg);
QApplication app(argc, argv);
return app.exec(); // 进入Qpplication事件循环
//简单的交给QCoreApplication来处理事件循环
return QCoreApplication::exec();
//检查event loop是否已经创建
if (!threadData->eventLoops.isEmpty()) {
qWarning("QCoreApplication::exec: The event loop is already running");
return -1;
}
QEventLoop eventLoop;
//委任QEventLoop 处理事件队列循环
int returnCode = eventLoop.exec();
// remove posted quit events when entering a new event loop
QCoreApplication *app = QCoreApplication::instance();
if (app && app->thread() == thread())
QCoreApplication::removePostedEvents(app, QEvent::Quit);
//这里的实现代码不少,最为重要的是以下几行
#if defined(QT_NO_EXCEPTIONS)
while (!d->exit)
processEvents(flags | WaitForMoreEvents | EventLoopExec);
#else
try {
while (!d->exit) //只要没有遇见exit,循环派发事件
processEvents(flags | WaitForMoreEvents | EventLoopExec);
} catch (...) {
qWarning("Qt has caught an exception thrown from an event handler. Throwing\n"
"exceptions from an event handler is not supported in Qt. You must\n"
"reimplement QApplication::notify() and catch all exceptions there.\n");
// copied from below
locker.relock();
QEventLoop *eventLoop = d->threadData->eventLoops.pop();
Q_ASSERT_X(eventLoop == this, "QEventLoop::exec()", "internal error");
Q_UNUSED(eventLoop); // --release warning
d->inExec = false;
--d->threadData->loopLevel;
throw;
}
#endif
d->threadData->eventLoops.push(this);
//将事件派发给与平台相关的QAbstractEventDispatcher子类
return d->threadData->eventDispatcher->processEvents(flags);
Q_D(QEventDispatcherWin32);
MSG msg;
//从处理用户输入队列中取出一条事件,处理队列里面的用户输入事件
msg = d->queuedUserInputEvents.takeFirst();
// 从处理socket队列中取出一条事件,处理队列里面的socket事件
msg = d->queuedSocketEvents.takeFirst();
//将事件打包成message调用Windows API派发出去
TranslateMessage(&msg);
//分发一个消息给窗口程序。消息被分发到回调函数,将消息传递给windows系统,windows处理完毕,会调用回调函数
DispatchMessage(&msg);
windows窗口回调函数 定义在QTDIR\src\gui\kernel\qapplication_win.cpp
extern "C" LRESULT QT_WIN_CALLBACK QtWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
...
//将消息重新封装成QEvent的子类QMouseEvent ==> Section 8
result = widget->translateMouseEvent(msg);
...
}
//
// QtWndProc() receives all messages from the main event loop
//
//C:\Program Files (x86)\Windows Kits\10\Include\10.0.17763.0\um\WinUser.h
typedef struct tagWNDCLASSA {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCSTR lpszMenuName;
LPCSTR lpszClassName;
} WNDCLASSA, *PWNDCLASSA, NEAR *NPWNDCLASSA, FAR *LPWNDCLASSA;
WNDCLASSA wc;
wc.style = style;
wc.lpfnWndProc = (WNDPROC)QtWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = (HINSTANCE)qWinAppInst();
if (icon) {
wc.hIcon = LoadIconA(appInst, (char*)"IDI_ICON1");
if (!wc.hIcon)
wc.hIcon = LoadIconA(0, (char*)IDI_APPLICATION);
} else {
wc.hIcon = 0;
}
wc.hCursor = 0;
wc.hbrBackground= 0;
wc.lpszMenuName = 0;
QByteArray tempArray = cname.toLatin1();
wc.lpszClassName= tempArray;
atom = RegisterClassA(&wc);
});
消息循环中的TranslateMessage函数和DispatchMessage函数
TranslateMessage函数
函数功能描述:将虚拟键消息转换为字符消息。字符消息被送到调用线程的消息队列中,在下一次线程调用函数GetMessage或PeekMessage时被读出。
.函数原型:
BOOL TranslateMessage( CONST MSG *lpMsg );
.参数:
lpMsg
指向一个含有用GetMessage或PeekMessage函数从调用线程的消息队列中取得消息信息的MSG结构的指针。
.返回值:
如果消息被转换(即,字符消息被送到线程的消息队列中),返回非零值。
如果消息是 WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, 或 WM_SYSKEYUP,返回非零值,不考虑转换。
如果消息没有转换(即,字符消息没被送到线程的消息队列中),返回值是零。
.备注:
TranslateMessage函数不修改由参数lpMsg指向的消息。
消息WM_KEYDOWN和WM_KEYUP组合产生一个WM_CHAR或WM_DEADCHAR消息。消息WM_SYSKEYDOWN和WM_SYSKEYUP组合产生一个WM_SYSCHAR或 WM_SYSDEADCHAR 消息。
TtanslateMessage仅为那些由键盘驱动器映射为ASCII字符的键产生WM_CHAR消息。
如果应用程序为其它用途而处理虚拟键消息,不应调用TranslateMessage函数。例如,如果TranslateAccelerator函数返回一个非零值,则应用程序将不调用TranslateMessage函数。
Windows CE:Windows CE不支持扫描码或扩展键标志,因此,它不支持由TranslateMessage函数产生的WM_CHAR消息中的lKeyData参数(lParam)16-24的取值。
TranslateMessage函数只能用于转换由GetMessage或PeekMessage函数接收到的消息。
DispatchMessage函数
函数功能:该函数调度一个消息给窗口程序。通常调度从GetMessage取得的消息。消息被调度到的窗口程序即是MainProc()函数。
函数原型:LONG DispatchMessage(CONST MSG*lpmsg);
参数:
lpmsg:指向含有消息的MSG结构的指针。
返回值:返回值是窗口程序返回的值。尽管返回值的含义依赖于被调度的消息,但返回值通常被忽略。
备注:MSG结构必须包含有效的消息值。如果参数lpmsg指向一个WM_TIMER消息,并且WM_TIMER消息的参数IParam不为NULL,则调用IParam指向的函数,而不是调用窗口程序。
总结:TranslateMessage函数将键盘消息转化,DispatchMessage函数将消息传给窗体函数去处理.
联系:
在Windows的内部,GetMessage和PeekMessage执行着相同的代码,Peekmessage和Getmessage都是向系统的消息队列中取得消息,并将其放置在指定的结构。
区别:
PeekMessage:有消息时返回TRUE,没有消息返回FALSE
GetMessage:有消息时且消息不为WM_QUIT时返回TRUE,如果有消息且为WM_QUIT则返回FALSE,没有消息时不返回。
GetMessage:取得消息后,删除除WM_PAINT消息以外的消息。
PeekMessage:取得消息后,根据wRemoveMsg参数判断是否删除消息。PM_REMOVE则删除,PM_NOREMOVE不删除。
The PeekMessage function normally does not remove WM_PAINT messages from the queue. WM_PAINT messages remain in the queue until they are processed. However, if a WM_PAINT message has a null update region, PeekMessage does remove it from the queue.
不能用PeekMessage从消息队列中删除WM_PAINT消息,从队列中删除WM_PAINT消息可以令窗口显示区域的失效区域变得有效(刷新窗口),如果队列中包含WM_PAINT消息程序就会一直while循环了。
TranslateMessage(转换消息):
用来把虚拟键消息转换为字符消息。由于Windows对所有键盘编码都是采用虚拟键的定义,这样当按键按下时,并不得字符消息,需要键盘映射转换为字符的消息。
TranslateMessage函数用于将虚拟键消息转换为字符消息。字符消息被投递到调用线程的消息队列中,当下一次调用GetMessage函数时被取出。当我们敲击键盘上的某个字符键时,系统将产生WM_KEYDOWN和WM_KEYUP消息。这两个消息的附加参数(wParam和lParam)包含的是虚拟键代码和扫描码等信息,而我们在程序中往往需要得到某个字符的ASCII码,TranslateMessage这个函数就可以将WM_KEYDOWN和WM_ KEYUP消息的组合转换为一条WM_CHAR消息(该消息的wParam附加参数包含了字符的ASCII码),并将转换后的新消息投递到调用线程的消息队列中。注意,TranslateMessage函数并不会修改原有的消息,它只是产生新的消息并投递到消息队列中。
也就是说TranslateMessage会发现消息里是否有字符键的消息,如果有字符键的消息,就会产生WM_CHAR消息,如果没有就会产生什么消息。
DispatchMessage(分派消息):
把 TranslateMessage转换的消息发送到窗口的消息处理函数,此函数在窗口注册时已经指定。
事件的产生、分发、接受和处理,并以视窗系统鼠标点击QWidget为例,对代码进行了剖析,向大家分析了Qt框架如何通过Event
Loop处理进入处理消息队列循环,如何一步一步委派给平台相关的函数获取、打包用户输入事件交给视窗系统处理,函数调用栈如下:
main(int, char **)
QApplication::exec()
QCoreApplication::exec()
QEventLoop::exec(ProcessEventsFlags )
QEventLoop::processEvents(ProcessEventsFlags )
QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags)
Qt app在视窗系统回调后,事件又是怎么一步步通过QApplication分发给最终事件的接受和处理者QWidget::event, (QWidget继承Object,重载其虚函数event)
QT_WIN_CALLBACK QtWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
//Windows的回调函数将鼠标事件分发回给了Qt Widget
bool QETWidget::translateMouseEvent(const MSG &msg)
// widget是事件的接受者; e是封装好的QMouseEvent
bool QApplicationPrivate::sendMouseEvent(...)
//至此与平台相关代码处理完毕
//MouseEvent默认的发送方式是spontaneous, 所以将执行
//sendSpontaneousEvent。 sendSpontaneousEvent() 与 sendEvent的代码实现几乎相同
//除了将QEvent的属性spontaneous标记不同。 这里是解释什么spontaneous事件:如果事件由应用程序之外产生的,比如一个系统事件。
//显然MousePress事件是由视窗系统产生的一个的事件(详见上文Section 1~ Section 7),因此它是 spontaneous事件
inline bool QCoreApplication::sendSpontaneousEvent(QObject *receiver, QEvent *event)
// 以下代码主要意图为Qt强制事件只能够发送给当前线程里的对象,也就是说receiver->d_func()->threadData应该等于QThreadData::current()。
//注意,跨线程的事件需要借助Event Loop来派发
bool QCoreApplication::notifyInternal(QObject *receiver, QEvent *event)
// QCoreApplication::notify和它的重载函数QApplication::notify在Qt的派发过程中起到核心的作用,Qt的官方文档时这样说的:
//任何线程的任何对象的所有事件在发送时都会调用notify函数。
bool QApplication::notify(QObject *receiver, QEvent *e)
bool QApplicationPrivate::notify_helper(QObject *receiver, QEvent * e)
// 向事件过滤器发送该事件,这里介绍一下Event Filters. 事件过滤器是一个接受即将发送给目标对象所有事件的对象。
//如代码所示它开始处理事件在目标对象行动之前。过滤器的QObject::eventFilter()实现被调用,能接受或者丢弃过滤
//允许或者拒绝事件的更进一步的处理。如果所有的事件过滤器允许更进一步的事件处理,事件将被发送到目标对象本身。
//如果他们中的一个停止处理,目标和任何后来的事件过滤器不能看到任何事件。
// 递交事件给receiver
bool QWidget::event(QEvent *event)
//QApplication通过notify及其私有类notify_helper,将事件最终派发给了QObject的子类- QWidget.
connect用于连接qt的信号和槽,在qt编程过程中不可或缺。它其实有第五个参数,只是一般使用默认值,在满足某些特殊需求的时候可能需要手动设置。
- Qt::AutoConnection:
默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。 - Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成奔溃。
- Qt::QueuedConnection:槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循环之后,槽函数才会被调用。多线程环境下一般用这个。
- Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
- Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。