我们的第二个Win32 API程序:基于消息驱动实现鼠标交互式绘图
🎯 一、实验目标
熟悉Win32窗口程序基础架构,弄懂Windows消息驱动机制;掌握捕获鼠标消息、使用GDI完成绘图;实现鼠标点击,在点击位置绘制随机大小红色实心正方形;理解窗口重绘原理,解决窗口最小化、缩放后图形消失问题,学会保存图形数据实现画面持久显示。
💡 二、核心实现思路
Windows系统不会永久保存屏幕上绘制出来的图像。当窗口被遮挡、最小化再还原、缩放窗口时,窗口区域会被系统擦除,程序必须重新绘制内容。
- 数据与显示分离:鼠标点击的时候不直接绘图,只采集鼠标坐标,生成随机正方形尺寸,把正方形信息(x,y,边长)存入结构体数组保存。
- 触发重绘:鼠标左键按下保存完数据,调用
InvalidateRect()通知系统窗口需要重绘。 - WM_PAINT完成绘制:系统收到重绘请求后,产生
WM_PAINT消息,在该消息处理函数内读取数组里全部正方形数据,循环完成绘图。 - 资源管理:使用GDI画刷完成实心图形绘制,使用完毕释放GDI对象,防止资源泄漏。
整体流程:注册窗口类 → 创建窗口 → 消息循环等待事件 → 鼠标点击保存图形数据 → 触发重绘 → WM_PAINT读取数据批量绘图。
🔍 三、完整源代码
#include <Windows.h>
#include <stdlib.h>
#include <time.h>
#define MAX_RECT_COUNT 100
struct Square
{
int x;
int y;
int size;
};
Square g_squares[MAX_RECT_COUNT];
int g_count = 0;
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
srand((unsigned int)time(NULL));
const wchar_t CLASS_NAME[] = L"DrawSquareWindow";
WNDCLASSEX wc = { 0 };
wc.cbSize = sizeof(WNDCLASSEX);
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
RegisterClassEx(&wc);
HWND hWnd = CreateWindowEx(
0,
CLASS_NAME,
L"点击鼠标左键绘制红色正方形",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 600, 400,
NULL, NULL, hInstance, NULL
);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
MSG msg = { 0 };
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_LBUTTONDOWN:
{
if (g_count >= MAX_RECT_COUNT)
break;
int x = LOWORD(lParam);
int y = HIWORD(lParam);
int size = rand() % 91 + 10;
g_squares[g_count].x = x;
g_squares[g_count].y = y;
g_squares[g_count].size = size;
g_count++;
InvalidateRect(hWnd, NULL, TRUE);
break;
}
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
HBRUSH hRedBrush = CreateSolidBrush(RGB(255, 0, 0));
HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, hRedBrush);
for (int i = 0; i < g_count; i++)
{
Rectangle(hdc,
g_squares[i].x,
g_squares[i].y,
g_squares[i].x + g_squares[i].size,
g_squares[i].y + g_squares[i].size
);
}
SelectObject(hdc, hOldBrush);
DeleteObject(hRedBrush);
EndPaint(hWnd, &ps);
break;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}
return 0;
}
🔍 四、关键代码解析
1. 数据结构体,保存正方形信息
#define MAX_RECT_COUNT 100
//存储每个正方形的位置、大小
struct Square
{
int x;
int y;
int size;
};
Square g_squares[MAX_RECT_COUNT];
int g_count = 0; //已经绘制的正方形数量
用全局结构体数组存储所有方块信息,设置最大数量防止数组越界。屏幕只是视图,数组才是真正的数据来源。
2. 鼠标左键消息 WM_LBUTTONDOWN
case WM_LBUTTONDOWN:
{
if (g_count >= MAX_RECT_COUNT)
break;
//从lParam中解析鼠标点击坐标
int x = LOWORD(lParam);
int y = HIWORD(lParam);
//生成10~100随机边长
int size = rand() % 91 + 10;
//存入数组
g_squares[g_count].x = x;
g_squares[g_count].y = y;
g_squares[g_count].size = size;
g_count++;
//使窗口无效,触发WM_PAINT重绘消息
InvalidateRect(hWnd, NULL, TRUE);
break;
}
重点:鼠标消息里不要直接绘图,只做数据保存;
InvalidateRect()是触发重绘的开关,没有它就不会执行绘图逻辑。
3. 重绘消息 WM_PAINT GDI绘图
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
//创建红色实心画刷
HBRUSH hRedBrush = CreateSolidBrush(RGB(255, 0, 0));
HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, hRedBrush);
//遍历数组,绘制所有保存的正方形
for (int i = 0; i < g_count; i++)
{
Rectangle(hdc,
g_squares[i].x,
g_squares[i].y,
g_squares[i].x + g_squares[i].size,
g_squares[i].y + g_squares[i].size
);
}
//恢复旧画刷,释放GDI资源,避免泄漏
SelectObject(hdc, hOldBrush);
DeleteObject(hRedBrush);
EndPaint(hWnd, &ps);
break;
}
BeginPaint/EndPaint必须成对使用,专门用于WM_PAINT消息;GDI对象用完必须还原、销毁,否则会发生资源泄漏。
4. 窗口回调与消息循环
WinMain完成窗口注册、创建、显示,消息循环GetMessage不断捕获系统消息,交给回调函数WndProc分发处理;未自定义处理的消息交给DefWindowProc系统默认处理。窗口关闭时收到WM_DESTROY调用PostQuitMessage退出消息循环。
🖼️ 五、实际运行效果
运行程序弹出600×400窗口,标题为“点击鼠标左键绘制红色正方形”。
鼠标左键任意位置点击,会在点击点左上角生成一个边长10‑100像素随机大小的红色实心正方形。
多次点击可以生成多个方块;窗口最小化恢复、拖动缩放窗口,所有绘制出来的正方形不会消失;最多支持绘制100个正方形。

⚠️ 六、踩过的坑
- 直接在鼠标消息里面绘图,最小化后图形全部消失
原因:直接写在WM_LBUTTONDOWN中的绘图只是临时画在屏幕上。一旦窗口被系统重绘,画面就被清空。
解决:鼠标事件只保存数据,所有绘图代码全部放到WM_PAINT,依靠InvalidateRect触发重绘。
- 点击鼠标之后,窗口完全看不到图形
原因:忘记写
InvalidateRect(hWnd, NULL, TRUE);,没有通知系统窗口需要重绘,程序不会进入WM_PAINT分支执行绘图。
- GDI资源泄露,程序长时间运行异常
原因:创建画刷之后,没有
SelectObject还原旧画刷,没有调用DeleteObject释放自定义GDI画刷。GDI资源属于系统有限资源,不释放会慢慢耗尽。
规范流程:创建GDI对象→选入设备上下文DC→绘图→恢复原有GDI对象→销毁手动创建对象。
- 数组越界风险
没有限制点击次数,无限点击会访问数组超出的位置。设置最大数量常量,存入数据前做数量判断。
✨ 七、实验收获
- 理解Windows消息驱动模型:程序不是从上到下顺序执行,大部分时间处于等待消息状态;鼠标点击、窗口变化都会产生消息,交给窗口回调函数处理,和控制台程序思维差别巨大。
- 理解窗口重绘核心:屏幕画面是临时的,数据才是持久的。想要图形不消失,就要把绘图需要的全部数据保存,每次重绘根据数据重新画一遍。
- GDI编程资源管理意识:GDI对象属于操作系统资源,手动创建就必须手动释放,很多资源泄漏不会直接报错,需要依靠原理排查。
- 学会事件驱动下的界面开发思想:界面显示与业务数据分离,数据发生变化,通知系统刷新界面,而不是直接操作屏幕。
- 小组协作调试的体会:不同成员分别负责窗口框架、鼠标采集、GDI绘图、测试整合,复现bug,顺着消息流程定位问题,提升排错能力。
浙公网安备 33010602011771号