介绍 EMFexplorer,一个 GDI+ 实验






4.90/5 (26投票s)
2004年9月30日
7分钟阅读

131850

3205
使用 GDI+ 实现高质量的 EMF 渲染
引言
假设您有一个“旧”的 EMF/WMF 文档数据库。如果您能将这些文档传递给 GDI+ 函数并获得更好的输出,那不是很棒吗?这是合法的,因为 GDI+ 中有一个“+”,而且这个库确实充满了新的有趣功能。不幸的是,事情并非如此。我怀疑 GDI+ 1.0 只是通过将其传递给 GDI 来渲染 EMF,尽管您可以通过提供适当的颜色矩阵来更改元文件中的颜色。总之,我没有找到一种简单的方法让 GDI+ 1.0 在元文件中平滑线条或文本。
为了获得增强的 EMF 输出,我编写了一个新的渲染引擎。它解释了 122 种 EMF 记录类型,尽可能使用 GDI+ 等效项,实现我们能做的,并在最后求助于 GDI。例如,(Moveto
, Lineto
) 对与 Drawline
指令匹配。PolyPolygon
需要一个新算法,该算法调用 GDI+ 的 DrawPolygon
。对于涉及复杂 ROP 代码的图像复制功能,我们调用 GDI。
这是学习 GDI 和 GDI+ 的好方法。本文的目的是向您介绍这项工作。
许可
在您开始使用免费代码之前,请注意 EMFexplorer 是在 EPL(Equity Public License)下发布的,这是 GLPL(GNU Lesser Public License)的扩展。根据该许可,该库对自由软件免费,但商业用户应向作者捐款。这适用于您可能选择在应用程序中使用的项目的任何部分。
项目模块
EMFexplorer 项目包含六个静态库
- SCEMFLib.lib 包含我们在本文中介绍的 EMF 渲染器
- 许多支持库可以在其他项目中重复使用(我希望)
- SCErrorLib.lib 用于错误管理(在此版本中不那么发达)
- SCGenLib.lib 包含通用支持函数
- SCWinLib.lib 包含 Windows 范围的支持函数
- SCSharedLib.lib 包含下面两个演示者共享的资源
该项目包含两个演示者,未随本文提供,但可在项目网站上找到
- EMFexplorer.exe,一个使用这些库的完整 SDI 应用程序
- SCEMFAx.ocx,一个使用这些库的完整 ActiveX 组件
SCEMFLib 库组织
以下是对本文随附演示程序中使用的一些类的简要介绍。
类 | 描述 |
CSCEMFImage | 实际的 EMF 查看器,可以加载和保存图像 |
SCEMFDoc | 子文档类,允许库独立于文档/视图架构 |
CSCEMFdcRenderer | 实际在 DC 上渲染图像 |
CSCEMFmetaDCRenderer | 实际将图像渲染到元文件中 |
CSCEMFgdiParser | 解析记录并将请求发送到实际渲染器 |
SCEMFRasterizer | 从解析器派生的方便类,并持有渲染器 |
使用 SCEMFLib 库
您可以通过两种方式使用此库
- 高级(例如,具有自己窗口的控件)
- 低级(例如,在表面的一部分上绘制的类)
a) 高级用法
无需深入研究其代码即可使用该库需要三个步骤
- S1:主应用程序负责 GDI+ 的初始化和关闭
- S2:应用程序创建一或多个
CSCEMFImage
实例,即图像查看器类 - S3:创建查看器对象后,它可以加载、显示和保存图像;它甚至可以拥有自己的上下文菜单
本文编写的演示程序 (emfxdemo.exe) 说明了这三个步骤。它非常简单。这是一个基于对话框的应用程序,执行以下操作
- 通过通用对话框加载文档(EMF、位图、RTF 等)
- 使用一个查看器对象显示加载文档的第一页
[S1] SCWinLib 静态库的 SCGDIPlus.h 文件中声明的 CSCGDIPlus
类执行 GDI+ 的初始化和关闭。您可以选择避免使用此类,并使用自己的技术。但请继续阅读本节,因为它包含与后续章节共享的信息。
CEmfxdemoApp.h 是经典的 AppWizard 生成的文件。
#include "SCGenInclude.h" #include SC_INC_WINLIB(SCGDIPlus.h) ... private: CSCGDIPlus m_GDIPlus;
这里不常见的是用于包含 SCGDIPlus.h 的 SC_INC_WINLIB
宏。它定义在 SCGenInclude.h 中(有关更多信息,请阅读此文件),用于绕过编译器对文件位置的猜测。SCGenInclude.h 是包含管理的一个中心点。
注意:由于存在此类宏,在编译项目之前,您必须设置其根目录(包含 emfx_dsw 的目录)。如果 dsw 位于 C:\ emfx_demo\Src,请将此目录添加到 Visual C++ 的全局“包含目录”中。
CEmfxdemoApp.cpp 是经典的,除了两个函数调用。在 InitInstance()
中,我们通过调用来初始化 GDI+
if (!m_GDIPlus.SCInitInstance())
{
...
}
在 ExitInstance()
中,我们通过调用来关闭 GDI+
m_GDIPlus.SCExitInstance();
[S2] 创建图像查看器
CEmfxdemoDlg.h 也是经典的 AppWizard 生成的文件。它包含使用库所需的两个头文件 SCEMFDoc.h 和 SCEMFImage.h。关于 SC_INC_EMFLIB
宏,请阅读 S1 步骤中的注释。
#include "SCGenInclude.h" #include SC_INC_EMFLIB(SCEMFImage.h) #include SC_INC_EMFLIB(SCEMFDoc.h) ... private: SCEMFDoc m_EMFDoc; CSCEMFImage m_CtlImage;
查看器对象 m_CtlImage
需要一个文档来获取页面(即使您的应用程序不使用文档/视图架构)。这就是为什么我们在 CEmfxdemoDlg
中有一个 SCEMFDoc
的实例。在 InitDialog()
中,我们通过调用将查看器连接到其文档
m_CtlImage.SCSetEMFDoc(&m_EMFDoc);
查看器对象的属性要么保留默认值,要么在 InitDialog()
中更改(硬编码)。例如
m_CtlImage.SCSetPaperColorStyle(SC_COLOR_RGBVALUE, RGB(...));
[S3] 操作图像查看器
每当用户尝试加载新文件时,我们都会检查其类型
UINT uiFileType = m_EMFDoc.SCOpenDocument(lpszPathName);
如果一切正常,我们显示新文档的第一页
CSCEMFdcRenderer::SCCleanFontCopies(); m_CtlImage.SCGotoFirstPage();
我提到第一页是因为文档对象 m_EMFDoc
会将大的 RTF 或 TXT 文件分割成页面。但在演示中,只显示第一页。请注意对静态方法 SCCleanFontCopies()
的调用,它在 SCEMFdcRenderer.h 中声明。这是为了释放为前一个文档缓存的资源,因为渲染器可能创建了字体(有关详细信息,请参阅“关注点”部分)。此外,应用程序必须在终止前调用此函数,否则可能会发生内存泄漏。这就是为什么我们在 CemfxdemoDlg::OnDestroy()
中调用它。
就是这样。做更复杂的事情将是复制 EMFexplorer.exe 已经完成的工作。
VB 开发人员(以及其他开发者)可以通过使用 ActiveX 组件 SCEMFAx.ocx 来获得相同的结果。
b) 低级用法
现在假设您只想使用库中的渲染引擎。这意味着您的应用程序已经存在,并且能够通过调用 PlayEnhMetaFile()
来显示元文件。您所要做的就是用其他东西替换这些调用,以获得更清晰的输出。在这种情况下
- 主应用程序仍然负责 GDI+ 的初始化和关闭
- 您的应用程序中的某个函数负责加载/创建 EMF
- 一旦 EMF 句柄可用,您就可以如下使用渲染引擎
#include "SCGenInclude.h" #include SC_INC_EMFLIB(SCEMFRasterizer.h) ... {// rasterizer must be detached from hDC before you reuse DC SCEMFRasterizer rasterizer; CSCEMFdcRenderer& rRenderer = rasterizer.SCGetRenderer(); // optional rRenderer.SCSetDrawingAttributes(m_DrawingAttributes); // optional // This is equivalent to an attachment rasterizer.SCBreakMetafile(hDC, hEMF, NULL, (LPRECT)&rcPlay); // DC detachment point }
请注意,传递给 SCBreakMetafile()
的参数与传递给 PlayEnhMetaFile()
的参数相同。可选的 SCGetRenderer()
和 SCSetDrawingAttributes()
调用在此处显示,因为您可能希望控制输出,否则您将使用 PlayEnhMetaFile()
。属性的一个示例是背景颜色(请参阅 a 部分)。
有关更多信息,请查看 SCEMFImage.cpp 中的 SCRasterizeEMF()
函数,因为本文只是介绍库,而不是描述每个类或函数。
关注点
在其他有趣的主题中,该项目解决了以下问题
- PolyPolygon:GDI+ 1.0 中没有等效的指令。因此,我们必须实现它。请查看 SCDCRendPoly_i.cpp 中的
T_SCDrawPolyPoly()
和 SCGdiplusUtils.cpp 中的SCConvertPolyPoly()
。该算法来自 xPDF 项目(Patrick Moreau, Martin P.J. Zinser, Glyph & Cog LLC)。 - 精确的文本定位:
DrawString()
和AddString()
不能像ExtTextOut()
那样精确自由地定位字符。我们已经解决了这个问题。请查看 SCDCRendTexts_i.cpp 中的SCDrawText()
和SCAddTextInPath()
。 - 字体替换:GDI+ 1.0 无法识别应用程序安装的临时/私有 GDI TT 字体。它会错误地回复:“NotTrueTypeFont!”请查看 SCEMFdcRenderer.cpp 中的
SCOnChangeFont()
了解我们如何解决这个问题。 - 字体属性模拟:我们还必须模拟“下划线”和“删除线”等字体属性,因为如果不做任何事情,文本将完全消失。
- 函数映射实现:枚举元文件记录有时需要一个大的 switch 语句(在我们的例子中,它包含 122 个 case)。我们使用一种函数映射技术,使我们能够避免这种情况,而是使用普通的 C++ 类和子类。请查看 SCGenLib 中的
SCBrkTarget
类及其在 SCEMFLib 中的后代:SCBrkEMF
、CSCEMFgdiParser
、CSCEMF2Text
。 - ROP2 模拟:请查看 SCGdiplusUtils.cpp 中的
SCBitmapFromHGLOBAL()
。 - ROP3、ROP4 管理:请查看 SCDCRendImages_i.cpp 中的
SCDrawImage()
、SCDrawImageMsk()
。 - Alpha 混合:GDI 可以做到;请查看 SCWinLib 中的
SCAlphaBand
类。 - 引用计数容器对象:请查看 SCGenLib 中的
SCRefdObjHolder
类。 - 一次性下载、解压缩和显示 EMF:请查看 SCEMFAx.ocx 的代码。
限制
Windows 98SE 用户:您可以尝试使用它;但是,由于 EMFexplorer.exe 和 SCEMFAx.ocx 使用 Get/SetWorldTransform
,它们将无法正常工作;如果您将包含这些转换的 EMF 传递给 SCEMFLib,它也将无法正常工作(温和、平缓的 EMF 和 WMF 应该可以正常工作)。
参考文献
完整的 EMFexplorer 源代码可在以下网站下载
http://frazmitic.free.fr/emfexplorer/index.htm
该网站还提供大量 EMF 套件供下载。
历史
- 最后发布的 EMFexplorer:版本 1.0.beta 2004-09-15。