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) {
// 1. 注册窗口类
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);

// 2. 创建窗口
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;
}

// 3. 显示窗口
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);

// 4. 消息循环
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);

// 在这里添加绘图代码
// 例如: TextOut(hdc, 50, 50, L"Hello, Windows!", 15);

EndPaint(hwnd, &ps);
}
return 0;
}

return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

这个基础程序包含了Windows图形编程的核心要素:

  1. ​WinMain入口点​ - Windows GUI应用程序的起点
    ​2. 窗口类注册​ - 定义窗口的基本特性
    ​3. 窗口创建​ - 根据注册的类创建窗口实例
  2. ​消息循环​ - 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); // 只能是HBRUSH画刷类型
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); // 必须加 +1,这是 Windows API 的历史设计,表示使用系统颜色索引

// 使用按钮表面颜色(灰色)
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)); // HS_CROSS表示横竖垂直交叉
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); // 获取窗口DC
HDC hdc = BeginPaint(hWnd, &ps); // 在WM_PAINT中获取DC
HDC hdc = CreateDC(L"DISPLAY", NULL, NULL, NULL); // 创建显示设备DC

// 使用后必须释放DC,否则会资源泄露
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); // 选入DC并保存旧画笔

// 创建画刷(填充区域)
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
// GDI双缓冲实现
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);

// 创建内存DC和位图
HDC hdcMem = CreateCompatibleDC(hdc);
HBITMAP hbmMem = CreateCompatibleBitmap(hdc, width, height);
SelectObject(hdcMem, hbmMem);

// 在内存DC上绘制
// ... 所有绘制操作都在hdcMem上进行 ...

// 将内存位图拷贝到屏幕
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;

// 初始化GDI+
ULONG_PTR gdiplusToken;
GdiplusStartupInput gdiplusStartupInput;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

// 程序退出时清理GDI+
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对象(相当于DC)
Graphics graphics(hdc);

// 开启抗锯齿
graphics.SetSmoothingMode(SmoothingModeAntiAlias);
graphics.SetTextRenderingHint(TextRenderingHintAntiAlias);

// 创建画笔和画刷
Pen redPen(Color(255, 255, 0, 0), 2.0f); // 红色画笔,宽度2
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); // 旋转45度
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
// GDI+双缓冲实现
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);
}