1、注册窗口类
- 我们需要基于窗口类来定义窗口的行为,窗口类是一种数据结构,需要在运行中向系统进行注册。为了注册新的窗口类,我们首先需要定义一个
WNDCLASS
结构。
// Register the window class.
const wchar_t CLASS_NAME[] = L"Sample Window Class";
WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
定义完成之后,我们使用RegisterClass
来注册窗口类
RegisterClass(&wc);
- 定义窗口类之后,我们需要调用
CreateWindowEx
函数来创建窗口的新实例:
HWND hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"Learn to Program Windows", // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
NULL // Additional application data
);
if (hwnd == NULL)
{
return 0;
}
CreateWindowEx
返回新窗口的句柄,如果函数失败,则返回零。为了显示窗口,将窗口句柄传递给ShowWindow
函数:
ShowWindow(hwnd, nCmdShow);
2、窗口消息
对于GUI应用程序来说,程序必须响应来自用户和操作系统的事件。来自用户的事件包括人可以与程序交互的所有方式:鼠标单击、击键、触摸屏手势等。来自操作系统的事件包括可能影响程序行为方式的程序“外部”的任何内容。例如,用户可能会插入新的硬件设备,或者 Windows 可能会进入低功耗状态(睡眠或休眠)。为了响应这些事件,Windows 使用了消息传递模型。操作系统通过向其传递消息来与您的应用程序窗口进行通信,消息只是指定特定事件的数字代码。例如,如果用户按下鼠标左键,则窗口会收到一条消息代码WM_LBUTTONDOWN
。
- 定义一个消息循环,来处理这些信息。
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
3、编写窗口
窗口过程(Window Procedure) 定义了窗口的大部分行为,我们在注册窗口类的时候定义的
WindowProc
就是执行窗口过程函数的指针。在消息循环中捕获到 msg
后,程序会调用窗口过程来进行消息处理,接下来我们需要完成之前声明的 WindowProc
的定义。我们可以将消息参数直接传递给DefWindowProc
函数,这个函数可以对消息执行默认操作。
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg) {
case WM_DESTROY: // 捕获到窗口关闭事件则退出
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
将上述代码整合写入main.cpp
文件,此时我们可以成功运行代码,得到一个空白窗口。
4、实现简易画笔
通过在窗口中设置像素来实现简易画笔,首先我们需要在WindowProc
接收鼠标左键按下的事件,然后对这个事件进行处理。
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam,
LPARAM lParam) {
switch (uMsg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_LBUTTONDOWN: // 捕获鼠标按下事件
int pixelX = GET_X_LPARAM(lParam);
int pixelY = GET_Y_LPARAM(lParam);
HDC hdc = GetDC(hwnd); // 获取当前 device context(DC)
SetPixel(hdc, pixelX, pixelY, RGB(210, 80, 50)); // RGB宏用于定义颜色
ReleaseDC(hwnd, hdc);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
运行代码之后,按下鼠标左键,我们就可以在窗口对应的位置绘制像素点。
为了实现鼠标左键移动并移动的时候可以绘制线条,我们可以在函数内部设置一个静态局部变量is_draw
来检测鼠标左键的状态,在左键按下时将is_draw
设置为true
,左键放开时设置为false
, 同时 WM_MOUNSEMOVE
会捕获鼠标移动事件,在 is_draw
为 true
时在鼠标位置 SetPixel
。
将上述代码修改:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
static bool isDraw = false;
switch (uMsg) {
case WM_DESTROY:
PostQuitMessage(0);
case WM_LBUTTONUP:
isDraw = false;
return 0;
case WM_LBUTTONDOWN:
isDraw = true;
case WM_MOUSEMOVE:
if (isDraw) {
int pixelX = GET_X_LPARAM(lParam);
int pixelY = GET_Y_LPARAM(lParam);
HDC hdc = GetDC(hwnd); // 获取当前窗口 HDC
SetPixel(hdc, pixelX, pixelY, RGB(210, 80, 50));
ReleaseDC(hwnd, hdc);
}
return 0;
case WM_PAINT:
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
FillRect(hdc, &ps.rcPaint, (HBRUSH)(COLOR_WINDOW + 1));
EndPaint(hwnd, &ps);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
实验的关键在于检测鼠标状态,通过设置状态变量is_draw
实现状态的检测, 在左键左键按下时将is_draw
设置为true
,左键放开时设置为false
, 在 is_draw
为 true
时在鼠标位置 SetPixel
,这样我们就可以在窗口对应的位置连续绘制像素点,从而实现简易画笔。