QEventLoop屏蔽了底层消息循环实现细节,向上提供了与平台无关的消息/事件循环。
本文拟对Windows系统下QEventLoop的实现原理予以分析。
注1:限于研究水平,分析难免不当,欢迎批评指正。
注2:文章内容会不定期更新。
一、研究素材:Win32应用程序框架
在Win32应用程序中,wWinMain是整个程序的入口点,整个代码段主要包括窗口类注册、创建窗口、消息循环等。
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,_In_opt_ HINSTANCE hPrevInstance,_In_ LPWSTR lpCmdLine,_In_ int nCmdShow)
{UNREFERENCED_PARAMETER(hPrevInstance);UNREFERENCED_PARAMETER(lpCmdLine);// TODO: Place code here.// Initialize global stringsLoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);LoadStringW(hInstance, IDC_WINDOWSPROJECT1, szWindowClass, MAX_LOADSTRING);MyRegisterClass(hInstance);// Perform application initialization:if (!InitInstance (hInstance, nCmdShow)){return FALSE;}HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WINDOWSPROJECT1));MSG msg;// Main message loop:while (GetMessage(&msg, nullptr, 0, 0)){if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)){TranslateMessage(&msg);DispatchMessage(&msg);}}return (int) msg.wParam;
}
MyRegisterClass用于注册创库类,可以指定窗口样式、窗口过程函数等。
//
// FUNCTION: MyRegisterClass()
//
// PURPOSE: Registers the window class.
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{WNDCLASSEXW wcex;wcex.cbSize = sizeof(WNDCLASSEX);wcex.style = CS_HREDRAW | CS_VREDRAW;wcex.lpfnWndProc = WndProc;wcex.cbClsExtra = 0;wcex.cbWndExtra = 0;wcex.hInstance = hInstance;wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WINDOWSPROJECT1));wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WINDOWSPROJECT1);wcex.lpszClassName = szWindowClass;wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));return RegisterClassExW(&wcex);
}
完成窗口类注册之后,可以依据窗口类创建窗口实例,
//
// FUNCTION: InitInstance(HINSTANCE, int)
//
// PURPOSE: Saves instance handle and creates main window
//
// COMMENTS:
//
// In this function, we save the instance handle in a global variable and
// create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{hInst = hInstance; // Store instance handle in our global variableHWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);if (!hWnd){return FALSE;}ShowWindow(hWnd, nCmdShow);UpdateWindow(hWnd);return TRUE;
}
当完成窗口类注册之后,便可以依据窗口类名称来创建窗口。在Windows系统下,消息队列用于管理线程内窗口相关的消息,同一线程内的窗口对象共享同一各消息队列。
The Message Loop
For each thread that creates a window, the operating system creates a queue for window messages. This queue holds messages for all the windows that are created on that thread.
MSG msg;// Main message loop:while (GetMessage(&msg, nullptr, 0, 0)){if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)){TranslateMessage(&msg);DispatchMessage(&msg);}}
当消息被投递到窗口时,便会调用对应的窗口过程函数,
//
// FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// PURPOSE: Processes messages for the main window.
//
// WM_COMMAND - process the application menu
// WM_PAINT - Paint the main window
// WM_DESTROY - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{switch (message){case WM_COMMAND:{int wmId = LOWORD(wParam);// Parse the menu selections:switch (wmId){case IDM_ABOUT:DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);break;case IDM_EXIT:DestroyWindow(hWnd);break;default:return DefWindowProc(hWnd, message, wParam, lParam);}}break;case WM_PAINT:{PAINTSTRUCT ps;HDC hdc = BeginPaint(hWnd, &ps);// TODO: Add any drawing code that uses hdc here...EndPaint(hWnd, &ps);}break;case WM_DESTROY:PostQuitMessage(0);break;default:return DefWindowProc(hWnd, message, wParam, lParam);}return 0;
}
二、QEventLoop实现原理
QEventLoop实际上是通过QAbstractEventDispatcher子类来屏蔽了底层窗口系统的消息循环。
bool QEventLoop::processEvents(ProcessEventsFlags flags)
{Q_D(QEventLoop);if (!d->threadData->hasEventDispatcher())return false;return d->threadData->eventDispatcher.loadRelaxed()->processEvents(flags);
}
从中可以看出,QAbstractEventDispatcher是线程级别的存在,每个线程有唯一的QAbstractEventDispatcher实现,线程内的所有QEventLoop共享同一个QAbstractEventDispatcher实现。
2.1 QEventDispatcherWin32的创建
实际上,在Windows系统下,当创建QCoreApplication时,便会创建主线程相关的QEventDispatcherWin32,而QEventDispatcherWin32正是QAbstractEventDispatcher的Windows系统实现。
void QCoreApplicationPrivate::init()
{// ...
#ifndef QT_NO_QOBJECT// use the event dispatcher created by the app programmer (if any)Q_ASSERT(!eventDispatcher);eventDispatcher = threadData->eventDispatcher.loadRelaxed();// otherwise we create oneif (!eventDispatcher)createEventDispatcher();Q_ASSERT(eventDispatcher);if (!eventDispatcher->parent()) {eventDispatcher->moveToThread(threadData->thread.loadAcquire());eventDispatcher->setParent(q);}threadData->eventDispatcher = eventDispatcher;eventDispatcherReady();
#endif// ...
}
void QCoreApplicationPrivate::createEventDispatcher()
{Q_Q(QCoreApplication);QThreadData *data = QThreadData::current();Q_ASSERT(!data->hasEventDispatcher());eventDispatcher = data->createEventDispatcher();eventDispatcher->setParent(q);
}
QAbstractEventDispatcher *QThreadData::createEventDispatcher()
{QAbstractEventDispatcher *ed = QThreadPrivate::createEventDispatcher(this);eventDispatcher.storeRelease(ed);ed->startingUp();return ed;
}
QAbstractEventDispatcher *QThreadPrivate::createEventDispatcher(QThreadData *data)
{Q_UNUSED(data);
#ifndef Q_OS_WINRTreturn new QEventDispatcherWin32;
#elsereturn new QEventDispatcherWinRT;
#endif
}
2.2 QEventDispatcherWin32的实现
由前面的分析可知,在Windows系统下,QEventLoop实际上是使用QEventDispatcherWin32来完成消息循环。那QEventDispatcherWin32又是如何实现对Windows消息循环的封装呢?
在QEventDispatcherWin32中,注册了一个"QEventDispatcherWin32_Internal_Widget"窗口类,
QWindowsMessageWindowClassContext::QWindowsMessageWindowClassContext(): atom(0), className(0)
{// make sure that multiple Qt's can coexist in the same processconst QString qClassName = QStringLiteral("QEventDispatcherWin32_Internal_Widget")+ QString::number(quintptr(qt_internal_proc));className = new wchar_t[qClassName.size() + 1];qClassName.toWCharArray(className);className[qClassName.size()] = 0;WNDCLASS wc;wc.style = 0;wc.lpfnWndProc = qt_internal_proc;wc.cbClsExtra = 0;wc.cbWndExtra = 0;wc.hInstance = GetModuleHandle(0);wc.hIcon = 0;wc.hCursor = 0;wc.hbrBackground = 0;wc.lpszMenuName = NULL;wc.lpszClassName = className;atom = RegisterClass(&wc);if (!atom) {qErrnoWarning("%ls RegisterClass() failed", qUtf16Printable(qClassName));delete [] className;className = 0;}
}
同时,依据该窗口类,创建了一个内部窗口,
void QEventDispatcherWin32::createInternalHwnd()
{Q_D(QEventDispatcherWin32);if (d->internalHwnd)return;d->internalHwnd = qt_create_internal_window(this);// setup GetMessage hook needed to drive our posted eventsd->getMessageHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC) qt_GetMessageHook, NULL, GetCurrentThreadId());if (Q_UNLIKELY(!d->getMessageHook)) {int errorCode = GetLastError();qFatal("Qt: INTERNAL ERROR: failed to install GetMessage hook: %d, %ls",errorCode, qUtf16Printable(qt_error_string(errorCode)));}// start all normal timersfor (int i = 0; i < d->timerVec.count(); ++i)d->registerTimer(d->timerVec.at(i));
}
static HWND qt_create_internal_window(const QEventDispatcherWin32 *eventDispatcher)
{QWindowsMessageWindowClassContext *ctx = qWindowsMessageWindowClassContext();if (!ctx->atom)return 0;HWND wnd = CreateWindow(ctx->className, // classnamectx->className, // window name0, // style0, 0, 0, 0, // geometryHWND_MESSAGE, // parent0, // menu handleGetModuleHandle(0), // application0); // windows creation data.if (!wnd) {qErrnoWarning("CreateWindow() for QEventDispatcherWin32 internal window failed");return 0;}#ifdef GWLP_USERDATASetWindowLongPtr(wnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(eventDispatcher));
#elseSetWindowLong(wnd, GWL_USERDATA, reinterpret_cast<LONG>(eventDispatcher));
#endifreturn wnd;
}
在QEventDispatcherWin32::processEvents中,会不断的调用PeekMessage检查消息队列,然后调用TranslateMessage与DispatchMessage进行消息转发。
bool QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags flags)
{Q_D(QEventDispatcherWin32);if (!d->internalHwnd) {createInternalHwnd();wakeUp(); // trigger a call to sendPostedEvents()}d->interrupt.storeRelaxed(false);emit awake();// To prevent livelocks, send posted events once per iteration.// QCoreApplication::sendPostedEvents() takes care about recursions.sendPostedEvents();bool canWait;bool retVal = false;do {DWORD waitRet = 0;DWORD nCount = 0;HANDLE *pHandles = nullptr;if (d->winEventNotifierActivatedEvent) {nCount = 1;pHandles = &d->winEventNotifierActivatedEvent;}QVarLengthArray<MSG> processedTimers;while (!d->interrupt.loadRelaxed()) {MSG msg;bool haveMessage;if (!(flags & QEventLoop::ExcludeUserInputEvents) && !d->queuedUserInputEvents.isEmpty()) {// process queued user input eventshaveMessage = true;msg = d->queuedUserInputEvents.takeFirst();} else if(!(flags & QEventLoop::ExcludeSocketNotifiers) && !d->queuedSocketEvents.isEmpty()) {// process queued socket eventshaveMessage = true;msg = d->queuedSocketEvents.takeFirst();} else {haveMessage = PeekMessage(&msg, 0, 0, 0, PM_REMOVE);if (haveMessage) {if (flags.testFlag(QEventLoop::ExcludeUserInputEvents)&& isUserInputMessage(msg.message)) {// queue user input events for later processingd->queuedUserInputEvents.append(msg);continue;}if ((flags & QEventLoop::ExcludeSocketNotifiers)&& (msg.message == WM_QT_SOCKETNOTIFIER && msg.hwnd == d->internalHwnd)) {// queue socket events for later processingd->queuedSocketEvents.append(msg);continue;}}}if (!haveMessage) {// no message - check for signalled objectswaitRet = MsgWaitForMultipleObjectsEx(nCount, pHandles, 0, QS_ALLINPUT, MWMO_ALERTABLE);if ((haveMessage = (waitRet == WAIT_OBJECT_0 + nCount))) {// a new message has arrived, process itcontinue;}}if (haveMessage) {if (d->internalHwnd == msg.hwnd && msg.message == WM_QT_SENDPOSTEDEVENTS) {// Set result to 'true', if the message was sent by wakeUp().if (msg.wParam == WMWP_QT_FROMWAKEUP)retVal = true;continue;}if (msg.message == WM_TIMER) {// avoid live-lock by keeping track of the timers we've already sentbool found = false;for (int i = 0; !found && i < processedTimers.count(); ++i) {const MSG processed = processedTimers.constData()[i];found = (processed.wParam == msg.wParam && processed.hwnd == msg.hwnd && processed.lParam == msg.lParam);}if (found)continue;processedTimers.append(msg);} else if (msg.message == WM_QUIT) {if (QCoreApplication::instance())QCoreApplication::instance()->quit();return false;}if (!filterNativeEvent(QByteArrayLiteral("windows_generic_MSG"), &msg, 0)) {TranslateMessage(&msg);DispatchMessage(&msg);}} else if (waitRet - WAIT_OBJECT_0 < nCount) {activateEventNotifiers();} else {// nothing todo so breakbreak;}retVal = true;}// still nothing - wait for message or signalled objectscanWait = (!retVal&& !d->interrupt.loadRelaxed()&& (flags & QEventLoop::WaitForMoreEvents));if (canWait) {emit aboutToBlock();waitRet = MsgWaitForMultipleObjectsEx(nCount, pHandles, INFINITE, QS_ALLINPUT, MWMO_ALERTABLE | MWMO_INPUTAVAILABLE);emit awake();if (waitRet - WAIT_OBJECT_0 < nCount) {activateEventNotifiers();retVal = true;}}} while (canWait);return retVal;
}
三、总结
依据上述对QEventLoop的实现分析,大体上可有以下结论:
1. QEventLoop内部实际上是通过QAbstractEventDispatcher子类来完成了消息(事件)分发,这实际上就是GoF's Bridge Pattern。
2. QAbstractEventDispatcher定义了事件分发的接口,而针对具体窗口系统的实现则有QEventDispatcherWin32、QEventDispatcherWinRT、QEventDispatcherUNIX、QEventDispatcherGlib等负责。
3. 每个线程都有唯一的同一事件分发器,线程内的窗体共用同一事件分发器。
4. 在Windows系统下,QEventLoop::processEvents函数大体等价于PeekMessage/TranslateMessage/DispatchMessage。
参考文献
Erich Gamma. Design Patterns:elements of reusable object-oriented software. Addison Wesley, 1994.
Joseph Ingeno. Handbook of Software Architecture.
参考资料
QEventLoop
Get Started with Win32 and C++
QAbstractEventDispatcher
QThread
QCoreApplication