我们的第二个Win32 API程序:基于消息驱动实现鼠标交互式绘图

🎯 一、实验目标

熟悉Win32窗口程序基础架构,弄懂Windows消息驱动机制;掌握捕获鼠标消息、使用GDI完成绘图;实现鼠标点击,在点击位置绘制随机大小红色实心正方形;理解窗口重绘原理,解决窗口最小化、缩放后图形消失问题,学会保存图形数据实现画面持久显示。

💡 二、核心实现思路

Windows系统不会永久保存屏幕上绘制出来的图像。当窗口被遮挡、最小化再还原、缩放窗口时,窗口区域会被系统擦除,程序必须重新绘制内容。

  1. 数据与显示分离:鼠标点击的时候不直接绘图,只采集鼠标坐标,生成随机正方形尺寸,把正方形信息(x,y,边长)存入结构体数组保存。
  2. 触发重绘:鼠标左键按下保存完数据,调用InvalidateRect()通知系统窗口需要重绘。
  3. WM_PAINT完成绘制:系统收到重绘请求后,产生WM_PAINT消息,在该消息处理函数内读取数组里全部正方形数据,循环完成绘图。
  4. 资源管理:使用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个正方形。

作业2运行效果

⚠️ 六、踩过的坑

  1. 直接在鼠标消息里面绘图,最小化后图形全部消失

原因:直接写在WM_LBUTTONDOWN中的绘图只是临时画在屏幕上。一旦窗口被系统重绘,画面就被清空。
解决:鼠标事件只保存数据,所有绘图代码全部放到WM_PAINT,依靠InvalidateRect触发重绘。

  1. 点击鼠标之后,窗口完全看不到图形

原因:忘记写InvalidateRect(hWnd, NULL, TRUE);,没有通知系统窗口需要重绘,程序不会进入WM_PAINT分支执行绘图。

  1. GDI资源泄露,程序长时间运行异常

原因:创建画刷之后,没有SelectObject还原旧画刷,没有调用DeleteObject释放自定义GDI画刷。GDI资源属于系统有限资源,不释放会慢慢耗尽。
规范流程:创建GDI对象→选入设备上下文DC→绘图→恢复原有GDI对象→销毁手动创建对象。

  1. 数组越界风险

没有限制点击次数,无限点击会访问数组超出的位置。设置最大数量常量,存入数据前做数量判断。

✨ 七、实验收获

  1. 理解Windows消息驱动模型:程序不是从上到下顺序执行,大部分时间处于等待消息状态;鼠标点击、窗口变化都会产生消息,交给窗口回调函数处理,和控制台程序思维差别巨大。
  2. 理解窗口重绘核心:屏幕画面是临时的,数据才是持久的。想要图形不消失,就要把绘图需要的全部数据保存,每次重绘根据数据重新画一遍。
  3. GDI编程资源管理意识:GDI对象属于操作系统资源,手动创建就必须手动释放,很多资源泄漏不会直接报错,需要依靠原理排查。
  4. 学会事件驱动下的界面开发思想:界面显示与业务数据分离,数据发生变化,通知系统刷新界面,而不是直接操作屏幕。
  5. 小组协作调试的体会:不同成员分别负责窗口框架、鼠标采集、GDI绘图、测试整合,复现bug,顺着消息流程定位问题,提升排错能力。
posted on 2026-09-25 11:03  醉梦4781  阅读(5)  评论(0)    收藏  举报