从一块画布开始:用 Win32 API 写一个「随机红色正方形」绘图程序

从一块画布开始:用 Win32 API 写一个「随机红色正方形」绘图程序

集美大学《Windows 窗口程序设计》课程作业 · 08 组
庞希颖 202421336032 · 赵玉娇 202421336033 · 李羽凡 202421336034 · 李秀清 202421336039


一、写在前面

这次的作业题目听起来特别"入门":设计一个 Windows 窗口程序,在窗口内任意位置按下鼠标左键,就画一个边长 10~100 随机、红色填充的正方形。

但真动手写的时候,我们四个人反倒觉得它是一道很妙的题——它足够小,小到能把 Win32 窗口程序的骨架完整地跑一遍;又足够坑,让我们在"让代码真的跑起来"这件事上,结结实实地上了一课。

这篇文章既记录实现思路,也如实记下我们踩过的每一个坑。如果你也在用 VS 写 Win32,希望它能帮你少走点弯路。


二、先看效果

程序启动后,窗口背景是柔和的浅灰白,中央给出操作提示:

程序启动后的初始界面

在窗口内任意位置反复按下鼠标左键,每个落点都会长出一个大小随机的红色正方形:

多次点击后的绘制效果

正方形用红色填充,并加了一圈深红描边,看起来更精致一点点。


三、整体思路:消息驱动的三件套

Win32 程序的核心思想是消息驱动:我们几乎不"主动"画任何东西,而是被动地响应系统投递过来的消息。整个绘图程序,其实就靠两条消息撑起来:

消息 职责
WM_LBUTTONDOWN 用户按下了左键 → 记录意图:算出正方形的位置和大小
WM_PAINT 系统说"该刷新窗口了" → 呈现画面:把全部正方形画出来

两条消息之间靠一个全局容器 std::vector<SquareRect> 解耦:按下时往里 push_back 一个正方形,重绘时把它全部画一遍。结构非常清晰。

程序的主干是一个标准的消息循环:

MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);   // 把消息分发给窗口过程 WndProc
}

四、四个关键实现

1. 随机大小:用均匀分布,把区间卡在 [10, 100]

题目要求边长落在 10~100 之间。相比 rand() % 91 + 10,我们用 C++11 的随机数设施,分布更均匀、可读性也更好:

std::mt19937 g_rng{ std::random_device{}() };            // 梅森旋转引擎
std::uniform_int_distribution<int> g_sizeDist(10, 100);  // 均匀分布于 [10,100]

int size = g_sizeDist(g_rng);   // 每次按下随机得到边长

2. 位置:以鼠标点为中心生成正方形

如果直接拿鼠标坐标当左上角,正方形总是往右下方长,观感很别扭。我们改为以落点为中心反推左上角:

g_squares.push_back(SquareRect{ mx - size / 2, my - size / 2, size });

这样无论边长 10 还是 100,正方形都稳稳地"包住"鼠标点。

3. 重绘:不依赖 WM_PAINT 的顺序,全量重画

新手很容易只画"新增的那一个",结果窗口一被遮挡、一缩放,图形就没了。我们的做法是:数据永远保存,重绘永远全量。WM_PAINT 里老老实实遍历 g_squares 全部画一遍,简单但最可靠。

4. 防闪烁:内存 DC 双缓冲

这是本次最有收获的一个点。最初我们直接在窗口 DC 上作图,快速点击时画面明显闪烁。解决办法是经典的双缓冲——先画到内存里的兼容 DC,再一次性 BitBlt 贴到窗口:

HDC     memDC  = CreateCompatibleDC(hdc);
HBITMAP memBmp = CreateCompatibleBitmap(hdc, cx, cy);
HGDIOBJ oldBmp = SelectObject(memDC, memBmp);

DrawScene(memDC, cx, cy);                        // 在内存里画完
BitBlt(hdc, 0, 0, cx, cy, memDC, 0, 0, SRCCOPY); // 一次拷贝上屏

SelectObject(memDC, oldBmp);
DeleteObject(memBmp);
DeleteDC(memDC);

再配合让 WM_ERASEBKGND 直接 return 1(阻止系统重复擦背景),闪烁就彻底消失了。


五、一点点"高级感"

题目只要求"红色填充",但既然要做,就想让它好看一点。我们在三处做了克制的小设计:

配色不刺眼。 填充红没有用纯红 RGB(255,0,0),而是选了偏稳重的 RGB(220,38,38);窗口背景用浅灰白 RGB(250,250,250) 而非纯白;正方形再加一圈深红 RGB(165,24,24) 描边,图形立刻"挺"了起来。

有反馈、有余地。 启动后画面中央有一行浅灰提示,告诉用户该干什么;同时我们顺手加了个清空功能——点右键或按 C 键即可清空画布重来,方便老师演示,也方便我们自己测试。


六、真机调试实录:那些让程序"一行都编译不过"的坑

说出来你可能不信——我们的代码主体只写了一个下午,但让它第一次成功运行,花了远更长的时间。下面是完整踩坑记录,每一个都真实发生过。

坑 1:中文注释导致"整个文件都是语法错误"

现象:一编译,错误列表里刷出几十条语法错误,什么 struct 定义、变量声明全都报错,其中还有一句话特别扎眼——"该文件包含不能在当前代码页(936)中表示的字符"。看着像是整个文件被写坏了。

真相:代码里有中文注释和中文提示文字。文件是被存成 UTF-8 的,而 Visual Studio 默认按系统的代码页 936(GBK)去读源文件。编码对不上,中文字节就被解析乱了,连带"吃掉"了换行和引号,于是从某一行开始全面崩盘。

解法(推荐):给项目加一个编译开关,让编译器固定用 UTF-8 读文件。右键项目 → 属性 → 配置属性 → C/C++ → 命令行 → 在"其他选项"里填 /utf-8。一劳永逸,之后再也不用管文件怎么存。

另一个办法是"文件 → 另存为 → 使用编码保存 → UTF-8 带签名",但它依赖你每次存文件都记得选对,不如 /utf-8 省心。

坑 2:一个变量名,撞上了 Windows 的系统宏

现象:第 45 行报 E0040:应输入标识符 和 C2059:语法错误:"常数"。可那一行明明长这样,毫无破绽:

const COLORREF COLOR_WINDOW = RGB(250, 250, 250); // 窗口背景

真相:COLOR_WINDOW 是 Windows 头文件里早就定义好的系统宏(它的值是 5)!编译器在预处理阶段把它替换掉,这一行就变成了 const COLORREF 5 = ...;——一个常量当然不能叫 5,于是报错。

解法:把名字改成 COLOR_BG 就好了。

教训:在 Windows 编程里,别用那些全大写的"看起来很像系统宏"的名字当变量名。COLOR_WINDOW、COLOR_BACKGROUND 这些都是系统占用的,踩一次记一辈子。

坑 3:项目类型不对,窗口变黑框

现象:编译能过,但链接时报"无法解析的外部符号 WinMain";或者运行后弹出来的是一个黑色控制台窗口,不是我们的图形窗口。

真相:我们的程序入口是 wWinMain(窗口程序专用),而默认的"空项目"是控制台子系统,它期待的入口是普通的 main。门牌号对不上,链接器自然找不到人。

解法:新建项目时选"Windows 桌面向导 → 桌面应用程序 (.exe)",向导会自动配好窗口子系统;也可以在已有项目里改:右键项目 → 属性 → 链接器 → 系统 → 子系统 → 选"窗口 (/SUBSYSTEM:WINDOWS)"。

坑 4:模板自带的代码和我们"撞车"

现象:把我们的代码粘进新建的"Windows 桌面应用程序"项目后,报 wWinMain 重定义,或者找不到 pch.h。

真相:这个模板会自动生成一段它自己的示例代码,里面本来就有个 wWinMain,还默认启用了预编译头。我们的代码和它挤在同一个文件里,入口函数就重复了。

解法:新建项目时直接勾"空项目",让模板什么都别生成,这是最干净的做法。已经踩了的话,把那个 .cpp 的内容全删掉再贴我们的代码,或者在项目属性里把"预编译头"设为"不使用预编译头"。

坑 5:别忘了还 GDI 对象

CreateSolidBrush、CreatePen、CreateFont 创建的 GDI 对象,用完必须 SelectObject 恢复旧对象、再 DeleteObject 释放,否则跑久了会漏。这个坑我们一开始没注意,后来养成习惯,代码其实也更整齐了。


七、小组分工

姓名 学号 承担工作
庞希颖 202421336032 总体方案设计、窗口框架与消息循环搭建、工程配置调试
赵玉娇 202421336033 GDI 绘图与双缓冲实现、配色与界面优化
李羽凡 202421336034 随机数模块与鼠标交互调试、边界情况与编译问题定位
李秀清 202421336039 实验记录、源码整理、报告撰写与截图

八、小结

这次作业最大的感受是:Windows 里的一个"窗口",不过是操作系统递给我们的一块画布。 而真正花时间的,往往不是"怎么画",而是"怎么让它能画"。

一长串 RegisterClass、CreateWindow、GetMessage、WndProc 乍看唬人,拆开看不过是一个有秩序的循环——程序注册自己、等待消息、响应消息、再回到等待。可编码、宏名、子系统、模板这些"环境层面"的小坑,又足以让一个逻辑完全正确的程序一行都跑不起来。

所以我们学到的另一件事是:看到报错,别急着乱改代码,先读懂编译器到底在哪里、因为什么"听不懂"我们。 这种"读报错"的能力,可能比多背几个 API 更有用。

后记:代码没有用到任何第三方库,一个 .cpp 文件、一个空项目就能跑起来。记得给项目加上 /utf-8。
如果你也在学 Win32,不妨就从"画一个正方形"开始——这块画布,比想象中有趣,也比想象中"坑"。


本文由 08 组四位同学共同完成,代码与报告已随作业一并提交。

posted @ 2026-09-25 02:02  西红柿桶装面  阅读(4)  评论(0)    收藏  举报