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编程过程中不可或缺。它其实有第五个参数,只是一般使用默认值,在满足某些特殊需求的时候可能需要手动设置。

  1. Qt::AutoConnection:
    默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
  2. Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成奔溃。
  3. Qt::QueuedConnection:槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循环之后,槽函数才会被调用。多线程环境下一般用这个。
  4. Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
  5. Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。