Skia如何在窗口上绘图

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;
}

这段代码主要完成了两个任务:

  1. initWindow 是一个自定义方法,用于使用 Windows API 创建一个窗口。

  2. 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 无关,所以简单介绍一下,代码主要做了以下三个工作:

  1. 注册窗口类 (RegisterClassEx)这里定义了窗口消息处理函数wndProc

  2. 创建窗口(CreateWindow)此处用到了全局变量 w 和 h 来控制窗口初始化时的宽度和高度。

  3. 显示窗口(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);
}
  1. WM_SIZE 消息 ShowWindow 代码执行后,窗口的消息处理函数会先收到 WM_SIZE 消息, 不仅如此,当用户通过拖拽窗口边框改变窗口大小时、窗口最大化时、最小化时都会收到 WM_SIZE 消息。 在处理 WM_SIZE 消息时,重置在全局变量中缓存的窗口宽度和高度(全局变量 w 和 h )。

  2. WM_PAINT消息 当系统需要重绘窗口时,会向窗口发送 WM_PAINT 消息, 比如窗口大小改变或应用程序内调用InvalidateRect系统API(强制重绘窗口)时,系统都会向窗口发送WM_PAINT 消息。 在处理 WM_PAINT 消息时会执行一个自定义的 paint 方法。

  3. 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;
}

这段代码有以下几点需要注意:

  1. 判断当前窗口的宽度和高度是否为 0 ,                                                                                  如果宽度或高度为 0 ,则不执行后面的渲染工作。 当窗口最小化时,窗口的宽度和高度为 0 ,在一些特殊情况下,系统会向最小化状态的窗口发送重绘指令。 这行代码的作用就是处理类似的特殊情况,避免不必要的渲染工作消耗资源。

  2. 创建并初始化像素数组                                                                                               surfaceMemory是一个二维像素数组,这个二维像素数组的行数是窗口的高度,列数是窗口的宽度。 将把这个像素数组里的像素铺满整个窗口表面。 像素数组用 SK_ColorBLACK 初始化,也就是说,这个像素数组内所有的像素颜色都是黑色。

  3. 创建画布对象                                                                                      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消耗)。

运行一下程序,就是开始时提供的动图效果啦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liulun

如果文章真帮到了你,谢谢您打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值