Windows图形编程的核心骨架
在深入探索Windows图形编程之前,让我们先了解一个最基本的Windows窗口程序骨架。这个骨架程序包含了创建窗口、处理消息和基本绘图的全部要素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| #include <Windows.h>
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain( _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) { const wchar_t CLASS_NAME[] = L"SampleWindowClass";
WNDCLASSEX wcex = {}; wcex.cbSize = sizeof(WNDCLASSEX); wcex.lpfnWndProc = WindowProc; wcex.hInstance = hInstance; wcex.lpszClassName = CLASS_NAME; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
RegisterClassEx(&wcex);
HWND hwnd = CreateWindowEx( 0, CLASS_NAME, L"Windows图形编程示例", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
NULL, NULL, hInstance, NULL );
if (hwnd == NULL) { return 0; }
ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd);
MSG msg = {}; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
return (int)msg.wParam; }
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); return 0;
case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps);
EndPaint(hwnd, &ps); } return 0; }
return DefWindowProc(hwnd, uMsg, wParam, lParam); }
|
这个基础程序包含了Windows图形编程的核心要素:
- WinMain入口点 - Windows GUI应用程序的起点
2. 窗口类注册 - 定义窗口的基本特性
3. 窗口创建 - 根据注册的类创建窗口实例
- 消息循环 - Windows程序的心脏,负责接收和分发系统发送给窗口的各种消息
5. 窗口过程函数 - 可以在此处理发送到窗口的所有消息
接下来,我们将深入探讨Windows窗口创建和图形绘制的各个方面。
窗口类
窗口类是窗口的模板,定义了窗口的基本特性,使用同一个窗口类创建的窗口拥有相同的特性,如背景色、光标样式等。
1 2 3 4 5 6 7 8 9 10 11 12
| WNDCLASSEX wcex = {}; wcex.cbSize = sizeof(WNDCLASSEX); wcex.lpfnWndProc = WindowProc; wcex.hInstance = hInstance; wcex.lpszClassName = L"MyWindowClass"; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION); wcex.hIconSm = LoadIcon(NULL, IDI_WINLOGO);
RegisterClassEx(&wcex);
|
窗口类名不区分大小写。
系统预定义窗口类
Windows提供了一些预定义的窗口类,可以直接使用,如Button、Edit、ListBox、Static、ComboBox、ScrollBar等。
详见:about-window-classes#system-classes
系统窗口类是由操作系统自动注册的用户进程的窗口类。操作系统会在用户进程首次调用Windows图形设备接口(GDI)函数时,为其注册相应的系统窗口类。
虽然将系统窗口类用来创建主窗口是被允许的,比如用Button窗口类来创建一个主窗口,但这样只会创建一个大大的按钮,没有实际意义,因此通常不将系统窗口类用来创建主窗口,仅用于创建子窗口(子控件)。
需要注意的是,无论是系统窗口类还是用户自定义的窗口类,都是是与进程相关联的,而不是线程,一旦窗口类被注册,进程内的所有线程都可以使用。
自定义窗口类
自定义窗口类允许开发者完全控制窗口的行为和外观:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| WNDCLASSEX wcex = {}; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = WindowProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION)); wcex.hIconSm = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_SMALL)); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wcex.lpszMenuName = NULL; wcex.lpszClassName = L"CustomWindowClass";
RegisterClassEx(&wcex);
|
窗口类样式
窗口类样式(WNDCLASSEX.style)是在注册窗口类时设置的,它定义了窗口类的一些通用行为,这些行为将应用于所有使用这个类创建的窗口。
窗口类样式主要影响窗口的行为(如重绘、双击等),而窗口的外观、边框、标题栏这些样式则有CreateWindow函数中的窗口样式所指定。
常见的类样式包括:
- CS_HREDRAW | CS_VREDRAW: 当窗口的宽度或高度改变时,重绘整个窗口。
- CS_DBLCLKS: 允许窗口接收双击消息(WM_LBUTTONDBLCLK等)。
- CS_OWNDC: 为每个窗口实例分配一个独有的设备上下文(DC)。
- CS_CLASSDC: 为窗口类分配一个共享的设备上下文(DC)。
- CS_NOCLOSE: 禁用(而非隐藏)窗口系统菜单中的关闭按钮。
- CS_SAVEBITS: 将窗口图像保存为位图,以便在窗口被遮挡后快速重绘(常用于小窗口,如菜单)。
- CS_GLOBALCLASS: 表示这个窗口类是应用程序全局的,可以被同一进程中的其他模块使用(通常在DLL中使用)。
窗口默认背景色
WNDCLASSEX.hbrBackground用于指定窗口类的默认背景画刷。当窗口需要重绘背景时(收到 WM_ERASEBKGND消息),系统会自动使用这个画刷填充背景。我们也可以在WM_ERASEBKGND消息处理函数中使用自定义背景色进行填充。
由于窗口是先收到WM_ERASEBKGND消息,后收到WM_PAINT消息的,因此对于复杂界面,在背景擦除和内容绘制之间就可能出现人眼可见间隔(闪烁)。为了避免这种问题,我们通常采用如下方法:将WNDCLASSEX.hbrBackground 设置为NULL,并在WM_ERASEBKGND处理过程中直接返回TRUE(这里的返回值表示背景是否已经擦除,实际会影响PAINTSTRUCT.fErase的值),然后在WM_PAINT处理过程中采用双缓冲的方式绘制背景和内容。
WNDCLASSEX.hbrBackground是HBRUSH类型,创建HBRUSH类型的方式有几种。
使用系统颜色常量创建
1 2 3 4 5 6 7 8
| wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
wcex.hbrBackground = (HBRUSH)(COLOR_BACKGROUND + 1);
|
使用内置画刷
1 2 3 4 5 6 7 8 9 10 11
| wcex.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wcex.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wcex.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);
wcex.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);
|
创建自定义画刷
1 2 3 4 5 6 7 8 9 10 11 12
| HBRUSH hBlueBrush = CreateSolidBrush(RGB(0, 0, 255)); wcex.hbrBackground = hBlueBrush;
HBITMAP hBmp = LoadBitmap(hInstance, MAKEINTRESOURCE(IDB_BACKGROUND)); HBRUSH hPatternBrush = CreatePatternBrush(hBmp); wcex.hbrBackground = hPatternBrush;
HBRUSH hHatchBrush = CreateHatchBrush(HS_CROSS, RGB(255, 0, 0)); wcex.hbrBackground = hHatchBrush;
|
不同窗口样式的区别与应用
虽然窗口类中也包含样式,但窗口类样式主要对窗口的行为(如重绘、双击等)进行少量的设置,更多的窗口外观和功能都由窗口样式所指定。
我们通过CreateWindowEx函数中的dwStyle和dwExStyle参数指定窗口样式。
dwStyle
dwStyle定义了窗口的基本外观和功能特性,是创建窗口时必须指定的参数。
窗口类型样式:
- WS_OVERLAPPED 有标题栏和边框,不能调整窗口大小,没有最大化、最小化、关闭按钮
- WS_POPUP 弹窗,顶级窗口,不能指定父窗口,没有标题栏和边框
- WS_CHILD 子窗口,必须指定父窗口
- WS_OVERLAPPEDWINDOW 组合样式,创建的窗口包含:标题栏、边框、窗口菜单(系统菜单)、最小化按钮、最大化按钮、可调整大小的边框
可见性(都是指定的初始可见性,后面可以通过程序改变):
- WS_VISIBLE 窗口初始可见
- WS_DISABLED 窗口初始禁用
- WS_MINIMIZE 窗口初始最小化
- WS_MAXIMIZE 窗口初始最大化
边框与标题栏样式
- WS_BORDER 细边框,不能调整窗口大小
- WS_DLGFRAME 对话框风格边框
- WS_CAPTION 标题栏(包含WS_BORDER)
- WS_THICKFRAME 可调整大小的边框
- WS_SYSMENU 系统菜单
- WS_MINIMIZEBOX 最小化按钮
- WS_MAXIMIZEBOX 最大化按钮
滚动条样式
- WS_HSCROLL 窗口始终有水平滚动条
- WS_VSCROLL 窗口始终有垂直滚动条
dwExStyle
dwExStyle定义了窗口的高级特性和特殊效果,通常用于实现更复杂的界面需求。
分层与透明度样式
- WS_EX_LAYERED 分层窗口(支持透明度)
- WS_EX_TRANSPARENT 透明窗口(鼠标点击穿透)
位置与行为样式
- WS_EX_TOPMOST 窗口始终置顶
- WS_EX_TOOLWINDOW 工具窗口(不在任务栏显示)
- WS_EX_NOACTIVATE 窗口激活时不带到前台
- WS_EX_APPWINDOW 强制窗口在任务栏显示
边框效果样式
WS_EX_WINDOWEDGE 凸起边框
- WS_EX_CLIENTEDGE 凹陷边框
- WS_EX_STATICEDGE 静态边框(无3D效果)
其他高级样式
- WS_EX_CONTEXTHELP 标题栏添加帮助按钮
- WS_EX_ACCEPTFILES 接受文件拖放
- WS_EX_COMPOSITED 双缓冲绘制所有子窗口,在启动了DWM的系统上没有效果,因为DWM已经支持桌面级的合成
- WS_EX_RIGHT 窗口右对齐,比如在阿拉伯语等环境下比较有用
- WS_EX_RTLREADING 从右到左显示文本,比如在阿拉伯语等环境下比较有用
运行时修改窗口样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| LONG_PTR style = GetWindowLongPtr(hWnd, GWL_STYLE); LONG_PTR exStyle = GetWindowLongPtr(hWnd, GWL_EXSTYLE);
BOOL isTopmost = exStyle & WS_EX_TOPMOST;
style |= WS_VISIBLE; exStyle |= WS_EX_TOPMOST;
style &= ~WS_MAXIMIZEBOX;
SetWindowLongPtr(hWnd, GWL_STYLE, style); SetWindowLongPtr(hWnd, GWL_EXSTYLE, exStyle);
SetWindowPos(hWnd, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
窗口消息处理过程
WNDCLASSEX.lpfnWndProc 函数指针指向应用程序定义的窗口过程函数,这个函数负责处理发送到该窗口类的所有消息。
我们通常会对一些常用的消息进行处理,对于未处理的消息则调用DefWindowProc函数使用默认的方式进行处理。
- WM_DESTROY 窗口销毁时发送,如果要退出程序,应在此处调用 PostQuitMessage
- WM_CLOSE 窗口关闭前请发送,可在此取消窗口关闭操作
- WM_CREATE 窗口已被创建,但还未可见时发送,可以在此处初始化资源
- WM_PAINT 窗口需要绘制时发送,可以在此处进行窗口绘制
窗口过程函数的返回值虽然为LRESULT类型,但却没有固定的值,需要根据消息的类型的不同,返回不同的值。
GDI绘图基础
GDI(Graphics Device Interface)是Windows的图形设备接口,其核心原理是通过一个抽象的“设备上下文”(DC)来屏蔽不同硬件设备的差异,实现设备无关的图形绘制,应用程序不需要直接操作物理设备。
可以将DC想象成一张画布,在开始绘画前,我们需要选择(SelectObject函数)画笔Pen、画刷Brush、字体Font、位图Bitmap等工具,然后进行绘制,绘制完成后需要删除创建的工具,并将老的工具选入到DC中。
设备上下文
设备上下文(Device Context,DC)是GDI的核心概念,代表一个绘图表面。
1 2 3 4 5 6 7 8 9
| HDC hdc = GetDC(hWnd); HDC hdc = BeginPaint(hWnd, &ps); HDC hdc = CreateDC(L"DISPLAY", NULL, NULL, NULL);
ReleaseDC(hWnd, hdc); EndPaint(hWnd, &ps); DeleteDC(hdc);
|
基本绘图函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| MoveToEx(hdc, 10, 10, NULL); LineTo(hdc, 100, 100);
Rectangle(hdc, 50, 50, 150, 150); Ellipse(hdc, 50, 50, 150, 150); RoundRect(hdc, 50, 50, 150, 150, 20, 20);
RECT rc = {0,0,100,100}; HBRUSH hbrWhite = CreateSolidBrush(RGB(255,255,255)); FillRect(hdc, &rc, hbrWhite); DeleteObject(hbrWhite);
SetBkMode(hdcMem, TRANSPARENT); SetTextColor(hdcMem, RGB(0, 0, 255));
TextOut(hdc, 10, 10, L"Hello, GDI!", 11);
DrawText(hdcMem, L"Hello, GDI!", -1, &rcClient, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
GDI对象:画笔、画刷和字体
1 2 3 4 5 6 7 8 9 10 11 12 13
| HPEN hPen = CreatePen(PS_SOLID, 1, RGB(255, 0, 0)); HPEN hOldPen = (HPEN)SelectObject(hdc, hPen);
HBRUSH hBrush = CreateSolidBrush(RGB(0, 0, 255)); HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, hBrush);
SelectObject(hdc, hOldPen); SelectObject(hdc, hOldBrush); DeleteObject(hPen); DeleteObject(hBrush);
|
双缓冲(GDI版)
双缓冲技术通过在内存中绘制再一次性显示来消除闪烁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); HDC hdcMem = CreateCompatibleDC(hdc); HBITMAP hbmMem = CreateCompatibleBitmap(hdc, width, height); SelectObject(hdcMem, hbmMem); BitBlt(hdc, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY); DeleteObject(hbmMem); DeleteDC(hdcMem); EndPaint(hWnd, &ps); break; }
|
GDI+绘图基础
GDI+是GDI的增强版,提供了更现代、更强大的绘图功能。GDI是面向过程的,有点OpenGL状态机的味道,而GDI+是面向对象的。
初始化与清理
在使用GDI+之前需要进行初始化,使用完之后进行清理,这些操作通常在程序启动和退出时进行。
1 2 3 4 5 6 7 8 9 10
| #include <gdiplus.h> using namespace Gdiplus;
ULONG_PTR gdiplusToken; GdiplusStartupInput gdiplusStartupInput; GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
GdiplusShutdown(gdiplusToken);
|
基本绘图操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| Graphics graphics(hdc);
graphics.SetSmoothingMode(SmoothingModeAntiAlias); graphics.SetTextRenderingHint(TextRenderingHintAntiAlias);
Pen redPen(Color(255, 255, 0, 0), 2.0f); SolidBrush blueBrush(Color(255, 0, 0, 255));
graphics.DrawLine(&redPen, 10, 10, 100, 100); graphics.DrawRectangle(&redPen, 50, 50, 100, 80); graphics.FillRectangle(&blueBrush, 50, 50, 100, 80); graphics.DrawEllipse(&redPen, 50, 50, 100, 80);
Font font(L"Arial", 16); SolidBrush blackBrush(Color(255, 0, 0, 0)); graphics.DrawString(L"Hello, GDI+!", -1, &font, PointF(10, 10), &blackBrush);
LinearGradientBrush linGrBrush( Point(0, 0), Point(100, 0), Color(255, 255, 0, 0), Color(255, 0, 0, 255) );
graphics.FillRectangle(&linGrBrush, 0, 0, 100, 100);
Image image(L"picture.jpg"); graphics.DrawImage(&image, 10, 10, image.GetWidth(), image.GetHeight());
graphics.RotateTransform(45.0f); graphics.DrawImage(&image, 50, 50); graphics.ResetTransform();
|
双缓冲(GDI+)
在GDI+中仍然需要使用双缓冲技术来减少闪烁,提升绘制效率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); RECT rc; GetClientRect(hWnd, &rc); int width = rc.right - rc.left; int height = rc.bottom - rc.top; Bitmap buffer(width, height); Graphics* memGraphics = Graphics::FromImage(&buffer); Graphics screenGraphics(hdc); screenGraphics.DrawImage(&buffer, 0, 0); delete memGraphics; EndPaint(hWnd, &ps); }
|