99、实战:GUI框架(Win32/Qt基础+事件循环)
GUI编程,说白了就是让程序跟用户「面对面」交流。你写个控制台程序,用户只能敲命令;但有了GUI,用户点个按钮、拖个滑块,程序就能响应。这件事背后的核心机制,就是事件循环。
我最早接触GUI是在大学,用Win32 API写了个计算器。那时候不懂事件循环,代码写得跟屎山一样——用户点一下按钮,界面就卡死。后来才明白,嗯,事件循环没处理好。
什么是事件循环?
事件循环,你可以把它想象成一个「邮局」。用户的操作(鼠标点击、键盘输入、窗口移动)就是寄来的信件。程序不断检查有没有新信件,有就拆开处理,处理完继续等下一封。
伪代码长这样:
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
这段代码就是Win32事件循环的骨架。GetMessage从消息队列里取消息,如果取到WM_QUIT就返回false,循环结束。TranslateMessage负责把键盘消息翻译成字符消息,DispatchMessage把消息分发给对应的窗口过程函数。
核心要点:事件循环不是「轮询」,而是「阻塞等待」。没有消息时,线程挂起,不占CPU。这是Windows消息驱动的精髓。
Win32 GUI:从零搭建一个窗口
我个人习惯用Win32做底层教学,因为它让你看到GUI的「裸体」——没有框架帮你封装,每一步都是操作系统API的直接调用。
创建一个窗口,需要这几步:
- 注册窗口类(RegisterClassEx)
- 创建窗口(CreateWindowEx)
- 显示窗口(ShowWindow)
- 进入消息循环(GetMessage循环)
- 在窗口过程函数里处理消息
看个完整的例子:
#include <windows.h>
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
TextOut(hdc, 10, 10, L"Hello, Win32 GUI!", 17);
EndPaint(hwnd, &ps);
return 0;
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
const wchar_t CLASS_NAME[] = L"SampleWindowClass";
WNDCLASS wc = {};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc);
HWND hwnd = CreateWindowEx(
0, CLASS_NAME, L"我的第一个窗口",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
800, 600,
nullptr, nullptr, hInstance, nullptr
);
if (!hwnd) return 0;
ShowWindow(hwnd, nCmdShow);
MSG msg = {};
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
这段代码我当年调试了整整一下午。为什么?因为忘记调用RegisterClass了,窗口死活创建不出来。你想想看,这种低级错误,现在看觉得可笑,但当时就是找不到原因。
注意:Win32的窗口过程函数是回调函数,由操作系统在消息分发时调用。不要在窗口过程里做耗时操作,否则界面会卡死。我曾经在一个WM_PAINT处理里做了文件读写,结果窗口拖拽时一卡一卡的,用户体验极差。
Qt的事件循环:更优雅的封装
Qt把事件循环封装得更高级。你不需要手动写GetMessage循环,QApplication::exec()帮你搞定一切。
Qt的事件循环底层其实还是调用了操作系统的消息机制(Windows上就是GetMessage),但上层做了信号与槽的抽象。你想想看,这比Win32的switch-case优雅了多少?
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPushButton button("点击我");
QObject::connect(&button, &QPushButton::clicked, [&]() {
button.setText("被点击了!");
});
button.show();
return app.exec();
}
app.exec()就是Qt的事件循环入口。它会一直运行,直到你调用QApplication::quit()或者窗口被关闭。
小技巧:在Qt中,如果你需要执行耗时操作,千万别放在主线程的事件循环里。用QThread或者QtConcurrent跑异步任务,否则界面会「假死」。我有个项目就是因为在按钮点击事件里直接做了网络请求,结果用户点一下按钮,界面卡了5秒,用户以为程序崩溃了。
事件循环的底层对比
| 特性 | Win32 | Qt |
|---|---|---|
| 消息获取 | GetMessage | QEventLoop::processEvents |
| 消息分发 | DispatchMessage | QCoreApplication::notify |
| 自定义消息 | WM_USER + n | QEvent子类 + 事件过滤器 |
| 异步处理 | PostMessage | QTimer / QMetaObject::invokeMethod |
| 线程安全 | SendMessage/PostMessage | 信号槽跨线程(队列连接) |
这张表是我在实际项目中总结出来的。Win32的SendMessage是同步的,会阻塞直到消息被处理;PostMessage是异步的,把消息丢进队列就返回。Qt的信号槽默认是同步调用(直接连接),跨线程时自动切换为队列连接。
事件循环的常见陷阱
我遇到过不少坑,挑几个典型的说说:
- 嵌套事件循环:在消息处理里再开一个事件循环(比如QDialog::exec()),会导致外层循环暂停。这在某些场景下是需要的,但滥用会让程序逻辑混乱。
- 消息积压:如果某个消息处理太慢,后续消息会排队等待。Win32的消息队列默认能存10000条消息,但UI响应会越来越迟钝。
- 跨线程发消息:Win32里跨线程发消息要小心,PostMessage是线程安全的,但SendMessage如果目标线程卡死,当前线程也会卡死。
避坑指南:我曾经在Win32项目里用SendMessage从工作线程向主线程发消息,结果工作线程里有个死循环,主线程被阻塞,整个界面卡死。后来改成PostMessage + 自定义消息,问题解决。记住:跨线程通信,能用异步就别用同步。
知识体系结构图
下面这张图展示了GUI框架的核心逻辑:
这张图把整个流程串起来了。用户点鼠标、敲键盘,操作系统把这些事件塞进消息队列。事件循环不断从队列里取消息,分发给对应的处理函数。Win32里是窗口过程,Qt里是信号槽。说白了,万变不离其宗。
总结
GUI编程的核心就是事件循环。Win32让你看到底层细节,Qt让你写得更快。我个人建议初学者先用Win32写一个简单窗口,感受一下消息是怎么来的、怎么走的。然后再用Qt做实际项目,效率会高很多。
嗯,最后提醒一句:事件循环里别做耗时操作。这个坑我踩过,你也别踩。
公众号:蓝海资料掘金营,微信deep3321