Skia如何在 Windows 窗口中绘图,涉及到了一些基本的 Windows API 开发知识,掌握这些知识对大家使用 C++ 创建 Windows 应用非常有帮助。
示例代码在 Windows 窗口的右下角绘制了一个矩形,调整窗口大小,矩形始终位于窗口右下角,如下图所示:
入口函数
示例入口方法的代码如下所示:
// #include <windows.h>
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow)
{
initWindow();
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
这段代码主要完成了两个任务:
-
initWindow 是一个自定义方法,用于使用 Windows API 创建一个窗口。
-
while 循环是 Windows 应用程序的消息循环,应用程序退出前(收到 Quit 消息前),此循环不会退出。
接下来我们就介绍一下创建窗口的代码。
创建窗口
这段代码我们首先定义了全局变量 w 和 h ,它们用来存储窗口的宽度和高度。
initWindow方法负责创建并显示窗口,代码如下所示:
int w{ 800 }, h{ 600 };
void initWindow() {
std::wstring clsName{ L"DrawInWindow" };
auto hinstance = GetModuleHandle(NULL);
WNDCLASSEX wcx{};
wcx.cbSize = sizeof(WNDCLASSEX);
wcx.style = CS_HREDRAW | CS_VREDRAW;
wcx.lpfnWndProc = wndProc;
wcx.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcx.lpszClassName = clsName.c_str();
if (!RegisterClassEx(&wcx)) {
return;
}
auto hwnd = CreateWindow(clsName.c_str(), clsName.c_str(), WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, w, h,
nullptr, nullptr, hinstance, nullptr);
ShowWindow(hwnd, SW_SHOW);
}
这段代码完全与 Skia 无关,所以简单介绍一下,代码主要做了以下三个工作:
-
注册窗口类 (RegisterClassEx)这里定义了窗口消息处理函数wndProc
-
创建窗口(CreateWindow)此处用到了全局变量 w 和 h 来控制窗口初始化时的宽度和高度。
-
显示窗口(ShowWindow)此处使用的 hwnd 是窗口句柄,姑且把它理解为窗口指针(实际上不是)
窗口创建成功后,窗口的消息处理函数会陆续收到与窗口有关的消息,比如窗口大小调整消息(WM_SIZE), 窗口重绘消息(WM_PAINT)等。
接下来看一下 wndProc 函数是如何处理这些窗口消息的。
窗口消息处理函数
一个窗口可能会接收到很多类型的窗口消息,譬如 WM_SIZE 、 WM_PAINT 和 WM_CLOSE 消息。如下代码所示:
LRESULT CALLBACK wndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_SIZE: {
w = LOWORD(lParam);
h = HIWORD(lParam);
break;
}
case WM_PAINT: {
paint(hWnd);
break;
}
case WM_CLOSE: {
PostQuitMessage(0);
break;
}
default: {
break;
}
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
-
WM_SIZE 消息 ShowWindow 代码执行后,窗口的消息处理函数会先收到 WM_SIZE 消息, 不仅如此,当用户通过拖拽窗口边框改变窗口大小时、窗口最大化时、最小化时都会收到 WM_SIZE 消息。 在处理 WM_SIZE 消息时,重置在全局变量中缓存的窗口宽度和高度(全局变量 w 和 h )。
-
WM_PAINT消息 当系统需要重绘窗口时,会向窗口发送 WM_PAINT 消息, 比如窗口大小改变或应用程序内调用InvalidateRect系统API(强制重绘窗口)时,系统都会向窗口发送WM_PAINT 消息。 在处理 WM_PAINT 消息时会执行一个自定义的 paint 方法。
-
WM_CLOSE消息 当用户点击窗口标题栏的关闭按钮时,操作系统会向窗口发送 WM_CLOSE 消息, 收到这个消息之后,马上就调用了系统 API: PostQuitMessage ,此 API 会向应用程序主消息循环发送退出消息。 此时 wWinMain 入口方法的 while 循环会退出,整个应用程序也就退出了。 如果不调用 PostQuitMessage API,虽然窗口会关闭,但应用程序不会退出。
在窗口上绘制矩形
最核心的代码就是处理 WM_PAINT 消息时执行的 paint 方法了,此方法的代码如下所示:
void paint(const HWND hWnd)
{
if (w <= 0 || h <= 0)
{
return;
}
SkColor *surfaceMemory = new SkColor[w * h]{SK_ColorBLACK};
SkImageInfo info = SkImageInfo::MakeN32Premul(w, h);
std::unique_ptr<SkCanvas> canvas = SkCanvas::MakeRasterDirect(info, surfaceMemory, w * sizeof(SkColor));
SkPaint paint;
paint.setColor(SK_ColorRED);
SkRect rect = SkRect::MakeXYWH(w - 150, h - 150, 140, 140);
canvas->drawRect(rect, paint);
PAINTSTRUCT ps;
auto dc = BeginPaint(hWnd, &ps);
BITMAPINFO bmpInfo = {sizeof(BITMAPINFOHEADER), w, 0 - h, 1, 32, BI_RGB, h * 4 * w, 0, 0, 0, 0};
StretchDIBits(dc, 0, 0, w, h, 0, 0, w, h, surfaceMemory, &bmpInfo, DIB_RGB_COLORS, SRCCOPY);
ReleaseDC(hWnd, dc);
EndPaint(hWnd, &ps);
delete[] surfaceMemory;
}
这段代码有以下几点需要注意:
-
判断当前窗口的宽度和高度是否为 0 , 如果宽度或高度为 0 ,则不执行后面的渲染工作。 当窗口最小化时,窗口的宽度和高度为 0 ,在一些特殊情况下,系统会向最小化状态的窗口发送重绘指令。 这行代码的作用就是处理类似的特殊情况,避免不必要的渲染工作消耗资源。
-
创建并初始化像素数组 surfaceMemory是一个二维像素数组,这个二维像素数组的行数是窗口的高度,列数是窗口的宽度。 将把这个像素数组里的像素铺满整个窗口表面。 像素数组用 SK_ColorBLACK 初始化,也就是说,这个像素数组内所有的像素颜色都是黑色。
-
创建画布对象 SkCanvas::MakeRasterDirect 方法根据像素数据创建了 canvas 对象。 这个方法的第一个参数为图像信息(SkImageInfo), 第二个参数为 指向目标像素缓冲区的指针(也就是刚刚创建的像素数组的地址)。 第三个参数为是每行字节数量,这里用 w * sizeof(SkColor) 表示每行数据的字节数量。
sizeof(SkColor) 用于获取一个像素的字节数量。
SkColor 是 uint32_t 的别名,占据 4 个字节的空间(uint32_t位宽为 32 位,字节unsigned char的位宽为 8 位)。
sizeof(SkColor) 得到的 1 个像素的字节数量是4(4 = 32/8)。
可以简单的认为一个 SkColor 占据的4个字节分别为透明度、红、绿、蓝四个颜色分量的值(实际内存中的数据并不一定是这样分布的)。
4,在窗口右下角绘制矩形
在窗口的右下角绘制了一个矩形(正方形),正方形边长为 140 像素,正方形距离窗口右下角边距为 10 像素。
改变窗口大小会更新全局变量 w 和 h ,会触发窗口的重绘消息,会重新执行paint方法,重新创建像素数组,重新在窗口右下角绘制矩形。
当矩形绘制完成后,存储在 surfaceMemory 的像素就代表着一个包含黑色背景、红色矩形的图像了,可以被绘制到窗口上了。
5,复制像素数据到图形输出设备存储区
StretchDIBits 是 Windows 操作系统 API ,它负责把 surfaceMemory 管理的像素数据复制到 图形输出设备存储区 。
当像素数据被复制到图形输出设备存储区后,窗口就变成了一个包含黑色背景、红色矩形的窗口了。
值得注意的是,在绘制完成之后就删除了 surfaceMemory 管理的内存。
也就是说,每次窗口重绘都会重新分配像素内存,每次重绘完成后,都会释放像素内存(改变窗口大小时会自动触发窗口重绘)。
这样做虽然可以让应用程序占用更少的内存,但如果你的窗口尺寸固定,且有大量的动态元素,需要不断的执行重绘的话,那你的程序可能会卡。
如果真是这样的话,你最好把 surfaceMemory做成全局变量,在应用启动时初始化一次即可,避免不必要的CPU消耗(初始化和销毁内存都是CPU消耗)。
运行一下程序,就是开始时提供的动图效果啦。