本文以一个Win32的helloworld程序开篇,
-
程序入口WinMain
- 注册窗口类别
- 建立窗口,在屏幕上显示
- 进入事件循环,不断从事件队列中取出消息来处理
而后尝试解释前述各部分分别隐藏在Qt何处:
main() |
程序入口 |
Qt提供一个WinMain来调用main |
QWidget::show() |
注册窗口类别 |
第一次使用时会注册类别 |
显示窗体 |
和hide、setHidden都是setVisble的马甲 |
|
QApplication::exec() |
进入事件循环 |
核心是 QEventDispatcherWin32 |
声明,我对Win32编程不了解,本文只是Qt学习过程中的学习笔记,本文提到内容以及用词描述可能会不太准确。
一个简单的Win32程序
这是Win32编程的一个hello world程序:
包含头文件,定义入口函数WinMain
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("Hello");
HWND hwnd;
MSG msg;
创建窗口类别(注意里面的WndProc是我们后面定义的回调函数),并注册窗口类别
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground=(HBRUSH) GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName= szAppName;
if(!RegisterClass(&wndclass))
{
MessageBox( NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
return 0;
}
创建窗体,并显示与更新窗体
hwnd = CreateWindow( szAppName, // window class name
TEXT("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL); // creation parameters
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
启动事件循环
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
以上我们所讨论的都是必要的东西:注册窗口类别,建立窗口,在屏幕上显示窗口,进入事件循环,不断从事件队列中取出消息来处理。
实际的动作发生在消息处理函数中。该函数确定了在视窗的显示区域中显示些什么以及怎样响应用户的输入等。
消息处理函数(回调函数)
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch(message)
{
case WM_CREATE:
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &rect);
DrawText(hdc, TEXT("Hello, Windows!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
用cl或gcc编译该程序:
gcc hello.c -o hello -lgdi32 -Wl,-subsystem,windows
cl hello.c user32.lib gdi32.lib
考虑Qt?
既然是 hello world,也就基本是最简单的Win32程序了。但看着还是挺复杂,难怪大家都不怎么喜欢它。
Qt(或其他的图形库/框架)多简单啊,简单几行代码一个程序就出来了。可是... 简单的表象下面呢?我们如何把一个Windows下的 Qt 程序和上面的代码对应上?
#include <QtGui/QApplication>
#include <QtGui/QLabel>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QLabel w("Hello world!");
w.show();
return app.exec();
}
入口函数 WinMain
在Qt中我们只写main函数,不写WinMain,挺有意思哈,不过在Qt Windows下链接子系统与入口函数(终结版) 中我们已经详细讨论过这个问题了(简单地说:就是qtmain.lib或libqtmain.a提供了一个WinMain,它会调用我们的main)
创建并注册窗体类别与创建并显示窗体部分
这个东西挺隐蔽的哈,当你对一个QWidget调用setVisble()或者它的众多马甲(比如show() )之一时,会执行这部分代码。源码位于qwidget_win.cpp 和 qapplication_win.cpp 文件中
事件循环
这个循环体现在Qt中就是 QApplication::exec()。核心代码在qeventdispatcher_win.cpp 中。
注册窗体类别
当第一次调用setVisible()时,会进行窗口类别的创建,这最终会调用 qapplication_win.cpp 中的下面一个函数:
const QString qt_reg_winclass(QWidget *w)
{
...
WNDCLASS wc;
wc.style = style;
wc.lpfnWndProc = (WNDPROC)QtWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = qWinAppInst();
...
wc.lpszMenuName = 0;
wc.lpszClassName = (wchar_t*)cname.utf16();
ATOM atom = RegisterClass(&wc);
...
恩,和前面的比对一下,应该是一样的吧(注意这儿注册的回调函数函数名:QtWndProc)
附:调用关系:
QWidget::create()
==>QWidgetPrivate::sys_create()
==>qt_reg_winclass()
消息处理函数
接前面,不妨直接看看QtWndProc这个回调函数(在同一个文件内), 尽管我们都知道它里面是一个大大的switch语句,我还是贴一点它的代码出来:
//
// QtWndProc() receives all messages from the main event loop
//
extern "C" LRESULT QT_WIN_CALLBACK QtWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
MSG msg;
msg.hwnd = hwnd; // create MSG structure
msg.message = message; // time and pt fields ignored
msg.wParam = wParam;
msg.lParam = lParam;
msg.pt.x = GET_X_LPARAM(lParam);
msg.pt.y = GET_Y_LPARAM(lParam);
// If it's a non-client-area message the coords are screen coords, otherwise they are
// client coords.
if (message < WM_NCMOUSEMOVE || message > WM_NCMBUTTONDBLCLK)
ClientToScreen(msg.hwnd, &msg.pt);
...
// send through app filter
if (qApp->filterEvent(&msg, &res))
return res;
...
res = 0;
if (widget->winEvent(&msg, &res)) // send through widget filter
RETURN(res);
...
if (qt_is_translatable_mouse_event(message)) {
...
}else{
switch (message) {
...
case WM_MOUSEWHEEL:
case WM_MOUSEHWHEEL:
result = widget->translateWheelEvent(msg);
break;
...
}
...
注意:里面出现两处msg消息的过滤。可以对照 Manual 看。
QCoreApplication::filterEvent()
QWidget::winEvent()
接下来以whell事件为例,看一下Windows事件如果变成Qt中的事件,并进入Qt自身的消息循环的:
bool QETWidget::translateWheelEvent(const MSG &msg)
{
...
// if there is a widget under the mouse and it is not shadowed
// by modality, we send the event to it first
int ret = 0;
QWidget* w = QApplication::widgetAt(globalPos);
if (!w || !qt_try_modal(w, (MSG*)&msg, ret)) {
//synaptics touchpad shows its own widget at this position
//so widgetAt() will fail with that HWND, try child of this widget
w = this->childAt(this->mapFromGlobal(globalPos));
if (!w)
w = this;
}
// send the event to the widget or its ancestors
{
QWidget* popup = QApplication::activePopupWidget();
if (popup && w->window() != popup)
popup->close();
QWheelEvent e(w->mapFromGlobal(globalPos), globalPos, delta,
Qt::MouseButtons(state & Qt::MouseButtonMask),
Qt::KeyboardModifier(state & Qt::KeyboardModifierMask), orient);
if (QApplication::sendSpontaneousEvent(w, &e))
return true;
}
// send the event to the widget that has the focus or its ancestors, if different
if (w != QApplication::focusWidget() && (w = QApplication::focusWidget())) {
QWidget* popup = QApplication::activePopupWidget();
if (popup && w->window() != popup)
popup->close();
QWheelEvent e(w->mapFromGlobal(globalPos), globalPos, delta,
Qt::MouseButtons(state & Qt::MouseButtonMask),
Qt::KeyboardModifier(state & Qt::KeyboardModifierMask), orient);
if (QApplication::sendSpontaneousEvent(w, &e))
return true;
}
return false;
通过这个,我们看到:
消息函数接受到的消息被封装成相应的QEvent,然后发送到Qt自身的事件循环中。
我们可以看到接收事件的对象是如何一步一步被确定的。对于wheel事件:
首先是光标下的widget(注意popup widget的处理)
如果该widget不接受,则发送到有焦点的widget。
通过这儿,我们应该容易理解QWheel中这句话的真实含义了:
事件循环
在 QDialog 模态对话框与事件循环 与 QEventLoop 的使用两例 等blog中,已经对此做过介绍:QApplication::exec()最终将(在一个while循环内)不断调用 qeventdispatcher_win.cpp 文件中的processEvents函数:
bool QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags flags)
{
...
haveMessage = PeekMessage(&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_CLOSE)) {
// queue user input events for later processing
haveMessage = false;
d->queuedUserInputEvents.append(msg);
}
if (haveMessage && (flags & QEventLoop::ExcludeSocketNotifiers)
&& (msg.message == WM_QT_SOCKETNOTIFIER && msg.hwnd == d->internalHwnd)) {
// queue socket events for later processing
haveMessage = false;
d->queuedSocketEvents.append(msg);
}
...
if (!filterEvent(&msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
...
}
这个文件很复杂,此处只摘取了个人感兴趣的片段(其实是因为其他的大段我看不太懂):
可以看到win32中熟悉的 PeekMessage、TranslateMessage、DispatchMessage
注意用户输入事件和socket通知事件的处理(放入queued队列)
注意此处也有一个 filterEvent,和QApplication提供的过滤器比较一下。发现谁更厉害没?
写到到这个地方,似乎从win32到Qt的对比分析已经做完了。恩,我也觉得差不多,只是...
对与 QEventDispatcherWin32 这个东西,我们还有很多话没说。它是事件循环的关键,而且它不止在主线程使用(我们肯定都知道QThread::exec())。
QEventDispatcherWin32补遗
在QTimer源码分析(以Windows下实现为例) 我们提到了它和定时器Timer的密切关系,刚刚又提到它是事件循环的关键,还有一点似乎还需要提一下:
QApplication 初始化时创建该对象
void QApplicationPrivate::createEventDispatcher()
{
Q_Q(QApplication);
if (q->type() != QApplication::Tty)
eventDispatcher = new QGuiEventDispatcherWin32(q);
else
eventDispatcher = new QEventDispatcherWin32(q);
}
在构造函数中,它创建并注册了一个内部用的窗口类别,而后创建一个窗口。
static HWND qt_create_internal_window(const QEventDispatcherWin32 *eventDispatcher)
{
// make sure that multiple Qt's can coexist in the same process
QString className = QLatin1String("QEventDispatcherWin32_Internal_Widget") + QString::number(quintptr(qt_internal_proc));
WNDCLASS wc;
wc.style = 0;
wc.lpfnWndProc = qt_internal_proc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = qWinAppInst();
wc.hIcon = 0;
wc.hCursor = 0;
wc.hbrBackground = 0;
wc.lpszMenuName = NULL;
wc.lpszClassName = reinterpret_cast<const wchar_t *> (className.utf16());
RegisterClass(&wc);
HWND wnd = CreateWindow(wc.lpszClassName, // classname
wc.lpszClassName, // window name
0, // style
0, 0, 0, 0, // geometry
0, // parent
0, // menu handle
qWinAppInst(), // application
0); // windows creation data.
...
其回调函数 qt_internal_proc 也在该文件内(略过)
在创建内部窗口的同时,它还安装了一个钩子(Hook)
d->getMessageHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC) qt_GetMessageHook, NULL, GetCurrentThreadId());
if (!d->getMessageHook) {
qFatal("Qt: INTERNALL ERROR: failed to install GetMessage hook");
}
在钩子的回调函数中,一些消息被PostMessage到上面提到的内部窗口中
LRESULT QT_WIN_CALLBACK qt_GetMessageHook(int code, WPARAM wp, LPARAM lp)
{
...
MSG *msg = (MSG *) lp;
if (localSerialNumber != d->lastSerialNumber
// if this message IS the one that triggers sendPostedEvents(), no need to post it again
&& (msg->hwnd != d->internalHwnd
|| msg->message != WM_QT_SENDPOSTEDEVENTS)) {
PostMessage(d->internalHwnd, WM_QT_SENDPOSTEDEVENTS, 0, 0);
总算大体上理了一遍,尽管很多东西还是不懂。
作者:dbzhang800