上次研究了一下Qt是如何對Win32初始化程序進行包裝的。這次研究下 Qt 的事件循環和 Windows 消息循環之間的聯系。
上次說到 QApplication 注冊了一個 qt_internal_proc 方法來處理消息循環,但是在這個方法中并沒有看到一些關于 Qt 事件的蛛絲馬跡。例如鼠標事件、鍵盤事件等。
其實在 qt_internal_proc 方法中有個調用值得注意: sendPostedEvents() 。如果在我們自己 Demo 的鼠標事件中打個斷點,不難發現,就是從這個調用來到我們的 mousePressEvent() 的。但是如果我們查看堆棧信息,按圖索驥,會發現:
bool QWindowSystemInterface::sendWindowSystemEvents(QEventLoop::ProcessEventsFlags flags)
{
int nevents = 0;
while (QWindowSystemInterfacePrivate::windowSystemEventsQueued()) {
QWindowSystemInterfacePrivate::WindowSystemEvent *event =
(flags & QEventLoop::ExcludeUserInputEvents) ?
QWindowSystemInterfacePrivate::getNonUserInputWindowSystemEvent() :
QWindowSystemInterfacePrivate::getWindowSystemEvent();
if (!event)
break;
if (QWindowSystemInterfacePrivate::eventHandler) {
if (QWindowSystemInterfacePrivate::eventHandler->sendEvent(event))
nevents++;
} else {
nevents++;
QGuiApplicationPrivate::processWindowSystemEvent(event);
}
// Record the accepted state for the processed event
// (excluding flush events). This state can then be
// returned by flushWindowSystemEvents().
if (event->type != QWindowSystemInterfacePrivate::FlushEvents)
QWindowSystemInterfacePrivate::eventAccepted.store(event->eventAccepted);
delete event;
}
return (nevents > 0);
}
在上邊可以看到,這個最原始的事件就是從 getXXXXXEvent() 方法中得到的,而這個方法是從一個事件隊列中取事件。
getWindowSystemEvent() 方法中的內容是這樣的:
QWindowSystemInterfacePrivate::WindowSystemEvent * QWindowSystemInterfacePrivate::getWindowSystemEvent()
{
return windowSystemEventQueue.takeFirstOrReturnNull();
}
可以說這個事件隊列就是我們要關注的焦點。那事件是如何被添加到這個隊列里的,這里暫時按下不表,先記住他的名字 windowSystemEventQueue 。
從QWidget談起
回過頭來想,鼠標鍵盤事件其實都是依托于窗口的,但其實 QApplication 本身并不屬于窗體,我們如果想在程序中加入一些可視的窗口,就要自己做個 QWidget 或者是 QMainWindow 等等。所以可以得出一個大概的結論,這些事件的接收處理必然和 QWidget 有著千絲萬縷的聯系。另外關于 Win32 消息的處理,我們必然要關注的一個,那就是回調函數。
拿著這兩個線索,花了一點時間,簡單梳理一下,不難發現這里邊的調用。以下調用非必要的會省略掉參數
1. 初始化 QWidget 會初始化 QWidgetPrivate ,在 QWidgetPrivate 的 init() 中會調用 QWidget::create();
2. 接著在 QWidget::create() 中調用 QWidgetPrivate::create_sys() ,在這個方法中,會創建一個 QWindow,在創建之后如果 QWidget 是顯示的,會調用 QWindow::setVisible(true) ;
3. 在 QWindow::setVisible(true) 中調用 QWindow::create() ,這個方法中沒有別的只是轉調 QWindowPrivate::create() 。
void QWindowPrivate::create(bool recursive)
{
Q_Q(QWindow);
if (platformWindow)
return;
if (q->parent())
q->parent()->create();
platformWindow = QGuiApplicationPrivate::platformIntegration()->createPlatformWindow(q);
Q_ASSERT(platformWindow);
if (!platformWindow) {
qWarning() << "Failed to create platform window for" << q << "with flags" << q->flags();
return;
}
QObjectList childObjects = q->children();
for (int i = 0; i < childObjects.size(); i ++) {
QObject *object = childObjects.at(i);
if (!object->isWindowType())
continue;
QWindow *childWindow = static_cast<QWindow *>(object);
if (recursive)
childWindow->d_func()->create(recursive);
// The child may have had deferred creation due to this window not being created
// at the time setVisible was called, so we re-apply the visible state, which
// may result in creating the child, and emitting the appropriate signals.
if (childWindow->isVisible())
childWindow->setVisible(true);
if (QPlatformWindow *childPlatformWindow = childWindow->d_func()->platformWindow)
childPlatformWindow->setParent(this->platformWindow);
}
QPlatformSurfaceEvent e(QPlatformSurfaceEvent::SurfaceCreated);
QGuiApplication::sendEvent(q, &e);
}
4. 在這個方法中,可以看到 createPlatformWindow() ,顧名思義,會創建一個平臺相關的Window。這里的實際調用是 QWindowsIntegration::createPlatformWindow() 。
而在這個方法中,我們會看到這個語句 QWindowsWindowData::create(window, requested, window->title()) ;這里的 create() 是一個靜態方法。
5. 在 create() 中會搞出一個 WindowCreationData ,這個結構體在qwindowswindow.cpp中,可以看到在定義上邊的注釋,沒錯,create() 中會調用 WindowCreationData::create() 來創建一個system handle。
/*!
\class WindowCreationData
\brief Window creation code.
This struct gathers all information required to create a window.
Window creation is split in 3 steps:
\list
\li fromWindow() Gather all required information
\li create() Create the system handle.
\li initialize() Post creation initialization steps.
\endlist
The reason for this split is to also enable changing the QWindowFlags
by calling:
\list
\li fromWindow() Gather information and determine new system styles
\li applyWindowFlags() to apply the new window system styles.
\li initialize() Post creation initialization steps.
\endlist
Contains the window creation code formerly in qwidget_win.cpp.
\sa QWindowCreationContext
\internal
\ingroup qt-lighthouse-win
*/
6. 在 WindowCreationData::create() 中會發現一個非常熟悉的一段代碼const QString windowClassName = QWindowsContext::instance()->registerWindowClass(w);
7. 這段代碼是得到一個 QWindowsContext 實例,調用它的 registerWindowClass() 方法。而在 QWindowsContext::registerWindowClass() 中,我們會看到這段代碼
return registerWindowClass(cname, qWindowsWndProc, style, GetSysColorBrush(COLOR_WINDOW), icon); ,在這里我們就會看到 qWindowsWndProc ,其實這個就是最終跟每個QWidget的事件相關的回調方法,這里暫時按下不表,先觀察這個重載方法的內容:
QString QWindowsContext::registerWindowClass(QString cname,
WNDPROC proc,
unsigned style,
HBRUSH brush,
bool icon)
{
// since multiple Qt versions can be used in one process
// each one has to have window class names with a unique name
// The first instance gets the unmodified name; if the class
// has already been registered by another instance of Qt then
// add an instance-specific ID, the address of the window proc.
static int classExists = -1;
const HINSTANCE appInstance = static_cast<HINSTANCE>(GetModuleHandle(0));
if (classExists == -1) {
WNDCLASS wcinfo;
classExists = GetClassInfo(appInstance, reinterpret_cast<LPCWSTR>(cname.utf16()), &wcinfo);
classExists = classExists && wcinfo.lpfnWndProc != proc;
}
if (classExists)
cname += QString::number(reinterpret_cast<quintptr>(proc));
if (d->m_registeredWindowClassNames.contains(cname)) // already registered in our list
return cname;
#ifndef Q_OS_WINCE
WNDCLASSEX wc;
wc.cbSize = sizeof(WNDCLASSEX);
#else
WNDCLASS wc;
#endif
wc.style = style;
wc.lpfnWndProc = proc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = appInstance;
wc.hCursor = 0;
#ifndef Q_OS_WINCE
wc.hbrBackground = brush;
if (icon) {
wc.hIcon = static_cast<HICON>(LoadImage(appInstance, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
if (wc.hIcon) {
int sw = GetSystemMetrics(SM_CXSMICON);
int sh = GetSystemMetrics(SM_CYSMICON);
wc.hIconSm = static_cast<HICON>(LoadImage(appInstance, L"IDI_ICON1", IMAGE_ICON, sw, sh, 0));
} else {
wc.hIcon = static_cast<HICON>(LoadImage(0, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED));
wc.hIconSm = 0;
}
} else {
wc.hIcon = 0;
wc.hIconSm = 0;
}
#else
if (icon) {
wc.hIcon = (HICON)LoadImage(appInstance, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE);
} else {
wc.hIcon = 0;
}
#endif
wc.lpszMenuName = 0;
wc.lpszClassName = reinterpret_cast<LPCWSTR>(cname.utf16());
#ifndef Q_OS_WINCE
ATOM atom = RegisterClassEx(&wc);
#else
ATOM atom = RegisterClass(&wc);
#endif
if (!atom)
qErrnoWarning("QApplication::regClass: Registering window class '%s' failed.",
qPrintable(cname));
d->m_registeredWindowClassNames.insert(cname);
qCDebug(lcQpaWindows).nospace() << __FUNCTION__ << ' ' << cname
<< " style=0x" << hex << style << dec
<< " brush=" << brush << " icon=" << icon << " atom=" << atom;
return cname;
}
到這里,就看到了注冊窗口的基本套路 RegisterClass() ,就算是徹底把跟Qt事件相關的消息循環回調找到了。
現在再來看一下剛才說的 qWindowsWndProc ,這里邊的內容,其實比較簡短:
extern "C" LRESULT QT_WIN_CALLBACK qWindowsWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
LRESULT result;
const QtWindows::WindowsEventType et = windowsEventType(message, wParam, lParam);
const bool handled = QWindowsContext::instance()->windowsProc(hwnd, message, et, wParam, lParam, &result);
if (QWindowsContext::verbose > 1 && lcQpaEvents().isDebugEnabled()) {
if (const char *eventName = QWindowsGuiEventDispatcher::windowsMessageName(message)) {
qCDebug(lcQpaEvents) << "EVENT: hwd=" << hwnd << eventName << hex << "msg=0x" << message
<< "et=0x" << et << dec << "wp=" << int(wParam) << "at"
<< GET_X_LPARAM(lParam) << GET_Y_LPARAM(lParam) << "handled=" << handled;
}
}
if (!handled)
result = DefWindowProc(hwnd, message, wParam, lParam);
return result;
}
在這里主要做了一些微小的工作,對消息分類把消息處理成 QtWindow::WindowEventType 類型,便于后續處理,具體邏輯在 windowsEventType() 方法中,主要是做 Win32 消息和 Qt 事件的映射。然后就是調用 QWindowsContext::windowsProc() 處理消息。特定情況下輸出 debug 信息。在處理消息的時候會得到處理結果,對于沒有處理的調用 DefWindowProc() 做默認處理。
如果想看 Win32 消息和 Qt 事件對應的關系映射,在上邊說到的 windowEventType() 方法中是最快的,基本涵蓋了大部分,但是要注意有一些名字對不上,因為到這里其實分類還不是 QEvent ,而是一個中間類型
現在來重點關注一下 windowProc() 方法。
bool QWindowsContext::windowsProc(HWND hwnd, UINT message,
QtWindows::WindowsEventType et,
WPARAM wParam, LPARAM lParam, LRESULT *result)
{
*result = 0;
MSG msg;
msg.hwnd = hwnd; // re-create MSG structure
msg.message = message; // time and pt fields ignored
msg.wParam = wParam;
msg.lParam = lParam;
msg.pt.x = msg.pt.y = 0;
if (et != QtWindows::CursorEvent && (et & (QtWindows::MouseEventFlag | QtWindows::NonClientEventFlag))) {
msg.pt.x = GET_X_LPARAM(lParam);
msg.pt.y = GET_Y_LPARAM(lParam);
// For non-client-area messages, these are screen coordinates (as expected
// in the MSG structure), otherwise they are client coordinates.
if (!(et & QtWindows::NonClientEventFlag)) {
ClientToScreen(msg.hwnd, &msg.pt);
}
} else {
#ifndef Q_OS_WINCE
GetCursorPos(&msg.pt);
#endif
}
// Run the native event filters.
long filterResult = 0;
QAbstractEventDispatcher* dispatcher = QAbstractEventDispatcher::instance();
if (dispatcher && dispatcher->filterNativeEvent(d->m_eventType, &msg, &filterResult)) {
*result = LRESULT(filterResult);
return true;
}
QWindowsWindow *platformWindow = findPlatformWindow(hwnd);
if (platformWindow) {
filterResult = 0;
if (QWindowSystemInterface::handleNativeEvent(platformWindow->window(), d->m_eventType, &msg, &filterResult)) {
*result = LRESULT(filterResult);
return true;
}
}
if (et & QtWindows::InputMethodEventFlag) {
QWindowsInputContext *windowsInputContext = ::windowsInputContext();
// Disable IME assuming this is a special implementation hooking into keyboard input.
// "Real" IME implementations should use a native event filter intercepting IME events.
if (!windowsInputContext) {
QWindowsInputContext::setWindowsImeEnabled(platformWindow, false);
return false;
}
switch (et) {
case QtWindows::InputMethodStartCompositionEvent:
return windowsInputContext->startComposition(hwnd);
case QtWindows::InputMethodCompositionEvent:
return windowsInputContext->composition(hwnd, lParam);
case QtWindows::InputMethodEndCompositionEvent:
return windowsInputContext->endComposition(hwnd);
case QtWindows::InputMethodRequest:
return windowsInputContext->handleIME_Request(wParam, lParam, result);
default:
break;
}
} // InputMethodEventFlag
//...
if (platformWindow) {
// Suppress events sent during DestroyWindow() for native children.
if (platformWindow->testFlag(QWindowsWindow::WithinDestroy))
return false;
if (QWindowsContext::verbose > 1)
qCDebug(lcQpaEvents) << "Event window: " << platformWindow->window();
} else {
qWarning("%s: No Qt Window found for event 0x%x (%s), hwnd=0x%p.",
__FUNCTION__, message,
QWindowsGuiEventDispatcher::windowsMessageName(message), hwnd);
return false;
}
switch (et) {
case QtWindows::KeyboardLayoutChangeEvent:
if (QWindowsInputContext *wic = windowsInputContext())
wic->handleInputLanguageChanged(wParam, lParam); // fallthrough intended.
case QtWindows::KeyDownEvent:
case QtWindows::KeyEvent:
case QtWindows::InputMethodKeyEvent:
case QtWindows::InputMethodKeyDownEvent:
case QtWindows::AppCommandEvent:
#if !defined(Q_OS_WINCE) && !defined(QT_NO_SESSIONMANAGER)
return platformSessionManager()->isInteractionBlocked() ? true : d->m_keyMapper.translateKeyEvent(platformWindow->window(), hwnd, msg, result);
#else
return d->m_keyMapper.translateKeyEvent(platformWindow->window(), hwnd, msg, result);
#endif
//...
case QtWindows::MouseWheelEvent:
case QtWindows::MouseEvent:
case QtWindows::LeaveEvent:
#if !defined(Q_OS_WINCE) && !defined(QT_NO_SESSIONMANAGER)
return platformSessionManager()->isInteractionBlocked() ? true : d->m_mouseHandler.translateMouseEvent(platformWindow->window(), hwnd, et, msg, result);
#else
return d->m_mouseHandler.translateMouseEvent(platformWindow->window(), hwnd, et, msg, result);
#endif
case QtWindows::TouchEvent:
#if !defined(Q_OS_WINCE) && !defined(QT_NO_SESSIONMANAGER)
return platformSessionManager()->isInteractionBlocked() ? true : d->m_mouseHandler.translateTouchEvent(platformWindow->window(), hwnd, et, msg, result);
#else
return d->m_mouseHandler.translateTouchEvent(platformWindow->window(), hwnd, et, msg, result);
#endif
case QtWindows::FocusInEvent: // see QWindowsWindow::requestActivateWindow().
case QtWindows::FocusOutEvent:
handleFocusEvent(et, platformWindow);
return true;
case QtWindows::ShowEventOnParentRestoring: // QTBUG-40696, prevent Windows from re-showing hidden transient children (dialogs).
if (!platformWindow->window()->isVisible()) {
*result = 0;
return true;
}
break;
//...
}
//...
return false;
}
本著太長不看的原則,我把一些相似的都省略掉了。這里就能看到在這里會根據消息類型來進行分類處理。處理的方式也是統一的,調用 handleXXXXEvent() 或是 tranlateXXXXEvent() 。需要二次加工的就要走到 tranlateXXXXEvent() 二次加工。最終其實都是走到 handleXXXXEvent() 。而 handleXXXXEvent() 方法中會將事件包裝成一個新的類型,再統一調用 QWindowSystemInterfacePrivate::handleWindowSystemEvent(e) PS:這是個靜態方法,這個靜態方法中需要關注 postWindowSystemEvent() 。
現在來看 postWindowSystemEvent() :
void QWindowSystemInterfacePrivate::postWindowSystemEvent(WindowSystemEvent *ev)
{
windowSystemEventQueue.append(ev);
QAbstractEventDispatcher *dispatcher = QGuiApplicationPrivate::qt_qpa_core_dispatcher();
if (dispatcher)
dispatcher->wakeUp();
}
看到了非常熟悉的一個隊列 windowSystemEventQueue ,就是在這里將事件加入隊列,至此整個 Qt 事件和 Windows 消息循環徹底聯系起來……
其實這只是一個添加事件、獲取事件的簡單流程,僅僅為了研究 Qt 事件和 Windows 消息循環的聯系。
在這中間省略的很多其他細節,包括注冊窗口,反注冊,具體的事件處理規則,還有一些防止事件錯誤發送的保護機制,都是很好的研究內容……