【Qt源码笔记】Qt事件与Windows消息循环的联系

2021-07-27 09:42:10 浏览数 (1)

上次研究了一下Qt是如何对Win32初始化程序进行包装的。这次研究下Qt的事件循环和Windows消息循环之间的联系。

上次说到QApplication注册了一个qt_internal_proc方法来处理消息循环,但是在这个方法中并没有看到一些关于Qt事件的蛛丝马迹。例如鼠标事件键盘事件等。

其实在qt_internal_proc方法中有个调用值得注意:sendPostedEvents()。如果在我们自己Demo的鼠标事件中打个断点,不难发现,就是从这个调用来到我们的mousePressEvent()的。但是如果我们查看堆栈信息,按图索骥,会发现:

代码语言:javascript复制
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()方法中的内容是这样的:

代码语言:javascript复制
QWindowSystemInterfacePrivate::WindowSystemEvent * QWindowSystemInterfacePrivate::getWindowSystemEvent()
{
    return windowSystemEventQueue.takeFirstOrReturnNull();
}

可以说这个事件队列就是我们要关注的焦点。那事件是如何被添加到这个队列里的,这里暂时按下不表,先记住他的名字windowSystemEventQueue

###从QWidget谈起 回过头来想,鼠标键盘事件其实都是依托于窗口的,但其实QApplication本身并不属于窗体,我们如果想在程序中加入一些可视的窗口,就要自己做个QWidget或者是QMainWindow等等。所以可以得出一个大概的结论,这些事件的接收处理必然和QWidget有着千丝万缕的联系。另外关于Win32消息的处理,我们必然要关注的一个,那就是回调函数。

拿着这两个线索,花了一点时间,简单梳理一下,不难发现这里边的调用。以下调用非必要的会省略掉参数

  1. 初始化QWidget会初始化QWidgetPrivate,在QWidgetPrivateinit()中会调用QWidget::create();
  2. 接着在QWidget::create()中调用QWidgetPrivate::create_sys(),在这个方法中,会创建一个QWindow,在创建之后如果QWidget是显示的,会调用QWindow::setVisible(true);
  3. QWindow::setVisible(true)中调用QWindow::create(),这个方法中没有别的只是转调QWindowPrivate::create()
代码语言:javascript复制
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

代码语言:javascript复制
/*!
    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的事件相关的回调方法,这里暂时按下不表,先观察这个重载方法的内容:

代码语言:javascript复制
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,这里边的内容,其实比较简短:

代码语言:javascript复制
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()方法。

代码语言:javascript复制
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()

代码语言:javascript复制
void QWindowSystemInterfacePrivate::postWindowSystemEvent(WindowSystemEvent *ev)
{
    windowSystemEventQueue.append(ev);
    QAbstractEventDispatcher *dispatcher = QGuiApplicationPrivate::qt_qpa_core_dispatcher();
    if (dispatcher)
        dispatcher->wakeUp();
}

看到了非常熟悉的一个队列windowSystemEventQueue,就是在这里将事件加入队列,至此整个Qt事件和Windows消息循环彻底联系起来……

其实这只是一个添加事件、获取事件的简单流程,仅仅为了研究Qt事件和Windows消息循环的联系。 在这中间省略的很多其他细节,包括注册窗口,反注册,具体的事件处理规则,还有一些防止事件错误发送的保护机制,都是很好的研究内容……

0 人点赞