WinDrawLib





5.00/5 (10投票s)
Direct2D 还是 GDI+?也许两者兼而有之,借助合适的库。
引言
您是否曾在 Win32 应用程序中需要高质量图形,并支持透明(alpha 通道)或抗锯齿?您是否曾解决过使用旧的相对较慢的 GDI+,还是使用更新的 Direct2D,但代价是您的应用程序不支持旧系统这一两难问题?如果这两个问题的答案都是“是”,那么您可能会发现本文(及其提供的代码)很有用。
不久前,我在为 mCtrl 项目 开发图表控件时,就遇到了上述两难问题。我最终的答案是创建了一个轻量级包装器代码,它将两者封装在一个统一且易于使用的 API 之下。随着时间的推移,由于我对它的需求越来越多,代码也随之增长。最终,它已经达到了可以独立使用的程度,因此我决定将这些代码分离成一个独立的、可重用的包。
昨天,这个“孩子”终于 上传到了 GitHub。由于我缺乏灵感,它被赋予了一个非常没有创意的名字:WinDrawLib。所以,让我们向它问好,并在此向我希望会觉得它有用的受众介绍它。
它的主要目标是允许在新机器上使用 Direct2D,但仍然允许应用程序在没有 Direct2D 的旧系统上尽可能良好地运行,最著名的是 Windows XP 和 Vista(不带 ServicePack),这通过回退到使用 GDI+ 来实现。

WinDrawLib 只需几行代码就能实现的示例。
设计
如前所述,该库的目的是为 GDI+ 和 Direct2D(以及一些相关库)提供统一的接口,同时尽可能轻量级,并拥有易于理解和采用的接口。
为此,代码必须处理 GDI+ 和 Direct2D 之间的许多差异。最显著的区别是它们各自使用完全不同的类型。幸运的是,这两个库的接口在概念上有很多相似之处,因此,当隐藏在一些不透明句柄类型之后时,应用程序不需要知道句柄 `WD_HBRUSH` 背后是 GDI+ 的 `GpBrush` 还是 Direct2D 的 `ID2D1Brush`。
另一个障碍在于 GDI+ 提供了 C++ 接口,该接口或多或少不适合与 `LoadLibrary()` 和 `GetProcAddress()` 配合使用。这通过使用底层的 C“扁平”接口得到了解决。
最后但同样重要的是,为了保持 API 的简洁性,我决定为了简单起见忽略某些功能。例如,API 中没有明确的像素格式概念。
简而言之,WinDrawLib 主要被设计为“带有 Alpha 通道和抗锯齿的更好 GDI”,而不是一个通用且功能强大的图形库。
系统要求
该库应在任何具有以下功能的 Windows 版本上运行:GDIPLUS.DLL可用或更新。这应该涵盖一些 Windows 2000,以及任何更新的版本。
据我所知,原始的纯 Windows 2000 没有提供GDIPLUS.DLL但它可能随某些 Service Pack 或系统更新而存在。此外,Microsoft 还提供了一些旧的可再发行版本GDIPLUS.DLL如果仍然需要支持所有 Windows 2000 机器,这会有所帮助:-)
默认情况下,如果系统上可用,该库使用 Direct2D;如果不可用,则回退到 GDI+。
当前状态
API 远未完成。GDI+ 和 Direct2D 都提供了比 WinDrawLib 公开的功能和选项多得多。如上所述,有些是故意省略的。未来可能会添加更多功能,但到目前为止我只是不需要它们。
无论如何,我相信这个库目前已经很有用了。此外,如果您认为它是一个有用的工具,但缺少某些功能,您可以考虑自己实现并与我和其他人分享,从而使其对每个人都更好。
另请注意,我并不认为 API 稳定可靠:命名或参数数量可能在此处或彼处发生一些变化,但可能不会有大的变化,也不会从概念上改变它,而且我希望随着更多人关注它,API 应该会趋于稳定。
如上所述,该库托管在 GitHub 上,它以相当宽松的开源许可 (MIT) 发布,因此您可以根据其条款自由修改代码。欢迎任何尊重代码精神的有用添加。因此,请随时提出 GitHub 的拉取请求,如果您不熟悉 git 或 GitHub,也可以以原始补丁的形式提供。
欢迎为文档的改进做出贡献(目前文档仅以公共头文件中的稀疏注释形式存在)wdl.h) 或提供展示 API 使用的新示例:一些示例也已在仓库中。
无发布周期
请不要期望我提供任何标准发布周期或定期二进制发布包。我认为代码相当简单和小巧,它具有辅助性质,我计划尽可能保持仓库的主分支稳定,但您需要时从源代码构建库是您的责任。
为此,您必须使用 CMake 或手动创建一些 Makefile 或 Project File。鉴于该库或多或少没有硬依赖(它在运行时通过 `LoadLibrary()` 加载所有内容,因为它事先不知道需要哪个库或系统上有什么),这应该不是问题。
用法
解释 WinDrawLib 中每个绘制基本图元的功能可能没有意义。因此,让我只列出当前提供的功能。它们的名称大多是自解释的,因此可以让你了解该库能做什么。
在文章后面,我们将看看如何使用一些初看起来可能不太明显的 API 部分。
毕竟,大多数函数的使用方式都非常直接,无需解释其参数的含义,公共头文件中也有一些文档,wdl.h,它公开了所有 WinDrawLib 的公共类型和函数。
因此,要使用 WinDrawLib,只需包含此头文件并链接静态库,WINDRAWLIB.LIB。(假设您使用 Visual Studio 构建它,因为其他编译器可能使用不同的命名约定。)
API 概述
辅助类型
WD_COLOR
WD_CIRCLE
WD_LINE
WD_POINT
WD_RECT
不透明句柄类型
WD_HCANVAS
:任何可绘制的对象。WD_HBRUSH
:用于绘图的虚拟画刷。WD_HFONT
:用于文本输出的字体。WD_HIMAGE
:任何类似图像的对象。WD_HPATH
:路径对象表示复杂且可重用的形状。
初始化
wdPreInitialize()
wdInitialize()
wdTerminate()
画布管理
wdCreateCanvasWithPaintStruct()
wdCreateCanvasWithHDC()
wdDestroyCanvas()
wdBeginPaint()
wdEndPaint()
wdResizeCanvas()
wdStartGdi()
wdEndGdi()
wdClear()
wdSetClip()
wdRotateWorld()
wdTranslateWorld()
wdResetWorld()
画刷管理
wdCreateSolidBrush()
wdDestroyBrush()
wdSetSolidBrushColor()
字体管理
wdCreateFont()
wdCreateFontWithGdiHandle()
wdDestroyFont()
wdFontMetrics()
图像管理
wdCreateImageFromHBITMAP()
wdLoadImageFromFile()
wdLoadImageFromIStream()
wdLoadImageFromResource()
wdDestroyImage()
wdGetImageSize()
路径管理
wdCreatePath()
wdCreatePolygonPath()
wdDestroyPath()
wdOpenPathSink()
wdClosePathSink()
wdBeginFigure()
wdEndFigure()
wdAddLine()
wdAddArc()
绘制操作
wdDrawArc()
wdDrawLine()
wdDrawPath()
wdDrawPie()
wdDrawRect()
填充操作
wdFillCircle()
wdFillPath()
wdFillPie()
wdFillRect()
位块传输操作
wdBitBltImage()
wdBitBltHICON()
字符串输出
wdDrawString()
wdMeasureString()
wdStringWidth()
关于初始化
作为调用任何其他 WinDrawLib 函数之前的可选步骤,您可以调用函数 `wdPreInitialize()`。此函数有两个目的:
- 它允许提供 `CRITICAL_SECTION` 的指针。如果它不是 `NULL`,WinDrawLib 会使用它来同步对某些内部全局变量的访问,从而允许从多个线程并发使用。
- 此外,它允许禁用 Direct2D 或 GDI+ 后端,这对于调试目的可能很有用。例如,在 Direct2D 可用的新系统上强制库使用 GDI+ 后端,您可以使用 `WD_DISABLE_D2D` 标志。
请注意,`wdPreInitialize()` 只能调用一次,并且如果提供了临界区,则必须已初始化并保持初始化状态,只要 WinDrawLib 正在使用中。
主要初始化由 `wdInitialize()` 执行。此函数与 `wdTerminate()` 配对,后者释放 `wdInitialize()` 占用的所有资源。
wdInitialize()
负责加载D2D1.DLL或GDIPLUS.DLL。如果您使用参数显式要求,它也可能加载其他库dwFlags对于需要它的某些功能
标志 | 更新的 Windows (Direct2D 可用) | 较旧的 Windows (GDI+) |
---|---|---|
WD_INIT_COREAPI | D2D1.DLL | GDIPLUS.DLL |
WD_INIT_IMAGEAPI | WINDOWSCODECS.DLL | |
WD_INIT_STRINGAPI | DWRITE.DLL |
注释
- 库的核心部分始终会被初始化,因为其他部分依赖于它。
- 请参阅wdl.h中关于哪些函数集需要哪些初始化标志才能正常工作的注释。
这种方法允许应用程序指示 WinDrawLib 只加载真正需要的库。
`wdInitialize()` 可以调用任意多次:它管理每个模块的内部初始化计数器。相应的 `wdTerminate()` 会递减计数器,如果计数器达到零,则模块真正被卸载。
在窗口上绘图
当 WinDrawLib 初始化完成后,我们需要一个可以绘图的画布对象。
在 `WM_PAINT` 处理程序上下文中向窗口 (`HWND`) 绘图时,您可以使用函数 `wdCreateCanvasWithPaintStruct()` 创建画布。
在 `WM_PAINT` 之外绘图时,可以使用 `wdCreateCanvasWithHDC()` 函数为任何设备上下文 (`HDC`) 创建画布。
绘图代码本身必须包含在 `wdBeginPaint()` 和 `wdEndPaint()` 调用之间。只需创建画刷或其他对象,调用绘制各种基本图形的函数等等。
因此,`WM_PAINT` 处理程序可能看起来像这样:
LRESULT CALLBACK
WinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg) {
// ...
case WM_PAINT:
{
PAINTSTRUCT ps;
WD_HCANVAS hCanvas;
WD_HBRUSH hBrush;
WD_RECT rc = { 10.0f, 10.0f, 50.0f, 50.0f };
BeginPaint(hwndMain, &ps);
hCanvas = wdCreateCanvasWithPaintStruct(hwndMain, &ps, 0);
// Paint for example a rectangle with a stroke of 5-pixel width:
wdClear(hCanvas, WD_RGB(255,255,255));
hBrush = wdCreateSolidBrush(hCanvas, WD_RGB(0,0,0));
wdDrawRect(hCanvas, hBrush, &rc, 5.0f);
wdDestroyBrush(hBrush);
wdDestroyCanvas(hCanvas);
EndPaint(hwndMain, &ps);
return 0;
}
// ..
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
请注意,如果满足以下所有条件,上一个示例中的一些对象(如画布或画刷)可以缓存以在后续 `WM_PAINT` 消息中重用:
- 画布是使用 `wdCreateCanvasWithPaintStruct()` 创建的。
- `EndPaint()` 的返回值为 `TRUE`。
- 应用程序在收到 `WM_DISPLAYCHANGE` 消息时重新创建对象。
然而,为了保持本文的篇幅适中,请允许我向您指出 展示该技术的示例。
关于路径
路径,用不透明句柄 `WD_HPATH` 表示,表示由图形组成的复杂形状。每个图形都是一系列连续的线段,通常是直线或弧线。每个图形可以是开放的或闭合的。
路径可用于画布的复杂剪切,或用于在画布上绘制或填充定义的复杂形状。
然而,为了隐藏 Direct2D 和 GDI+ 之间的差异,路径的创建有点繁琐,因此值得简要解释一下。创建过程如下:
- 创建路径句柄
WD_HPATH hPath = wdCreatePath(hCanvas);
- 打开一个接收器(sink)
WD_PATHSINK pathSink; wdOpenPathSink(&pathSink, hPath);
- 添加任意数量的图形,每个图形由一组线段和/或弧段组成
WD_POINT ptStart = { x, y }; wdBeginFigure(&pathSink, &ptStart); wdAddLine(...); wdAddArc(...); // ... add more segments wdEndFigure(&pathSink, TRUE); // TRUE to close the figure, FALSE to keep it open // ... add more figures the same way wdClosePathSink(&pathSink);
还有一个方便的包装器 `wdCreatePolygonPath()`,如果您仅限于由单个闭合图形(仅由线段组成,即多边形)组成的路径,它会为您完成上述所有操作。
一旦路径对象如上所述初始化,您就可以轻松调用 `wdSetClip()`、`wdDrawPath()` 或 `wdFillPath()` 来使用它。
最后,当不再需要时,调用 `wdDestroyPath()` 来释放路径对象。
结论
好吧,我知道这篇文章本身并没有那么有趣。但我希望文章中介绍的这个库,尽管处于不成熟的状态,也能有一些吸引力,至少对于解决我所遇到的相同问题的开发者来说。
文档历史
(此处仅跟踪对文档的非外观性更改。)
- 2017-09-26:许可证从 LGPL 更改为更宽松的 MIT。
- 2016-04-11:文档版本 1.0。