65.9K
CodeProject 正在变化。 阅读更多。
Home

WinDrawLib

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2016年4月11日

CPOL

9分钟阅读

viewsIcon

30295

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()`。此函数有两个目的:

  1. 它允许提供 `CRITICAL_SECTION` 的指针。如果它不是 `NULL`,WinDrawLib 会使用它来同步对某些内部全局变量的访问,从而允许从多个线程并发使用。
  2. 此外,它允许禁用 Direct2D 或 GDI+ 后端,这对于调试目的可能很有用。例如,在 Direct2D 可用的新系统上强制库使用 GDI+ 后端,您可以使用 `WD_DISABLE_D2D` 标志。

请注意,`wdPreInitialize()` 只能调用一次,并且如果提供了临界区,则必须已初始化并保持初始化状态,只要 WinDrawLib 正在使用中。

主要初始化由 `wdInitialize()` 执行。此函数与 `wdTerminate()` 配对,后者释放 `wdInitialize()` 占用的所有资源。

wdInitialize() 负责加载D2D1.DLLGDIPLUS.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+ 之间的差异,路径的创建有点繁琐,因此值得简要解释一下。创建过程如下:

  1. 创建路径句柄
        WD_HPATH hPath = wdCreatePath(hCanvas);
        
  2. 打开一个接收器(sink)
        WD_PATHSINK pathSink;
        wdOpenPathSink(&pathSink, hPath);
        
  3. 添加任意数量的图形,每个图形由一组线段和/或弧段组成
        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。
© . All rights reserved.