资讯详情

资讯详情

MFC视图渲染SVG:自研解析与GDI+绘制实战

简介这份资源面向具备一定C与Windows开发基础的MFC学习者聚焦于在MFC视图中解析并显示SVG矢量图形这一具体问题。SVG本质是基于XML的矢量图像格式需先借助XML解析库提取circle、rect、path等元素与属性再通过GDI的Graphics对象在重写的OnDraw函数中完成绘制同时处理颜色、填充、描边乃至渐变与透明度等样式。资源包共75个文件约237KB以h头文件与cpp源文件为主体分别承载SvgCircle、SvgRect、SvgPolyline、SvgText等图形类的声明与实现并配有bmp、ico等界面资源及sln、vcxproj工程文件另附一份svg示例便于调试。目前已有1890人学习下载。读者可从中获得一套可编译运行的完整工程理解从XML解析、图形对象建模到GDI绘制的完整链路并参考其类划分与资源释放思路快速搭建自己的SVG显示模块。1. 解析 SVG 并显示在 MFC 视图为什么值得自己写一个渲染层手上有个桌面工具客户丢过来一批 SVG 图标和机械设计图纸要求直接嵌进现有的 MFC 界面里显示还要能缩放、能换背景色。第一反应可能是找个现成控件塞进去但真动手就会发现MFC 原生那套 GDI/GDI 对 SVG 的支持基本等于没有而 SVG 本身是 XML 描述的矢量格式路径、渐变、变换、裁剪一个都不少。这个标题要解决的就是这件事把 SVG 文件解析成内存里的图形对象再在 MFC 的视图窗口里画出来。适合谁看写过 MFC 对话框、知道 CDC 和 OnDraw 是怎么回事但没系统处理过矢量图形的同学。也适合那些被「svg 图片」显示需求卡住、试过用浏览器控件绕路但嫌重的老手。整条链路的核心难点不在画线而在解析 XML、处理 viewBox 坐标映射、以及把 SVG 的路径指令翻译成 GDI 的 GraphicsPath。把这三块打通剩下的就是视图刷新和交互。下面按「先立住原理、再动手复现、最后踩坑」的顺序讲透。2. SVG 解析与 MFC 视图渲染的技术选型为什么不用浏览器控件2.1 三种可选路线的真实成本对比在 MFC 里显示 SVG业内常见做法无非三条路。第一条是嵌入 WebBrowser 控件让 IE 内核去渲染 SVG。这条路看起来最省事但 IE 内核对 SVG 的支持停留在很老的版本渐变、滤镜、部分路径指令都有兼容问题而且控件本身重、启动慢、和 MFC 的消息循环容易打架。热词里出现的mfc iwebbrowser2 createex就是这类方案的典型入口但实际项目里用它显示矢量图缩放时的锯齿和重绘闪烁会让人很头疼。第二条路是引入第三方库比如用 librsvg 或者直接上 Qt 的 SVG 模块。librsvg 依赖 glib 和 cairo在 Windows 上编译链复杂部署时要带一堆 dll。Qt 方案倒是成熟但为了显示个 SVG 把整个 Qt 拖进来对一个已经成型的 MFC 工程来说太重而且热词里「桌面软件开发 用 mfc 还是 qt」这个纠结本身就说明混用两套框架的维护成本不低。第三条路是自己写解析加渲染。听起来吓人但 SVG 的核心子集其实不大path、rect、circle、line、polygon、g、defs、linearGradient再加上transform和viewBox。用微软自带的 MSXML 解析 XML用 GDI 画图零外部依赖编译出来就是一个 exe。我一般会选这条路因为可控、可调试、体积小出问题能直接断点进去看。方案依赖SVG 支持度部署成本适合场景WebBrowser 控件IE 内核部分老版本差低临时展示简单图形librsvg / Qt大量第三方库完整高已有 Qt 工程MSXML GDI 自研系统自带核心子集极低MFC 工程内嵌2.2 解析层用 MSXML 把 SVG 读成 DOMMSXML 是 Windows 自带的 COM 组件MFC 里用#import导入类型库就能调。解析 SVG 的第一步是把文件读成 DOM 树然后递归遍历节点。这里要注意命名空间SVG 根节点通常带xmlnshttp://www.w3.org/2000/svg用selectNodes时要么带命名空间前缀要么用selectSingleNode配合通配。我一般直接遍历 childNodes按节点名判断省去命名空间的麻烦。// 用 MSXML 加载 SVG 并遍历顶层节点 #import msxml6.dll named_guids void ParseSvg(LPCTSTR filePath) { CoInitialize(NULL); CComPtrIXMLDOMDocument2 pDoc; pDoc.CoCreateInstance(CLSID_DOMDocument60); VARIANT_BOOL ok VARIANT_FALSE; pDoc-load(CComVariant(filePath), ok); if (ok ! VARIANT_TRUE) return; CComPtrIXMLDOMElement pRoot; pDoc-get_documentElement(pRoot); // 读取 viewBox决定坐标映射 CComVariant vb; pRoot-getAttribute(CComBSTR(LviewBox), vb); if (vb.vt VT_BSTR) { // 解析 minX minY width height ParseViewBox(vb.bstrVal); } // 递归遍历子节点 CComPtrIXMLDOMNode pChild; pRoot-get_firstChild(pChild); while (pChild) { WalkNode(pChild); CComPtrIXMLDOMNode pNext; pChild-get_nextSibling(pNext); pChild pNext; } CoUninitialize(); }这段代码的关键点是CLSID_DOMDocument60对应 MSXML 6.0比老的 3.0 稳定。load的第二个参数是同步加载标志传VARIANT_TRUE表示等文件读完再返回。getAttribute拿到的viewBox是字符串需要自己按空格拆成四个浮点数。WalkNode是递归函数遇到g就继续往下钻遇到图形节点就生成对应的 GDI 对象存进一个列表。参数上viewBox的四个值分别是用户坐标系的最小 x、最小 y、宽度、高度它决定了后面怎么把 SVG 坐标映射到窗口像素。2.3 渲染层把 SVG 路径指令翻译成 GraphicsPathSVG 的path元素里d属性是一串指令比如M 10 10 L 100 10 L 100 100 Z。大写字母是绝对坐标小写是相对坐标。GDI 的GraphicsPath提供AddLine、AddBezier、AddArc等方法需要把 SVG 指令逐条翻译过去。常见的指令有 M/m移动、L/l直线、H/h水平线、V/v垂直线、C/c三次贝塞尔、Q/q二次贝塞尔、A/a圆弧、Z/z闭合。其中圆弧 A 最麻烦参数多需要转换成AddArc或者用贝塞尔逼近。// 解析 path 的 d 属性并构建 GraphicsPath void BuildPathFromD(LPCWSTR d, GraphicsPath path) { std::wstringstream ss(d); wchar_t cmd; float x, y, x1, y1, x2, y2; float curX 0, curY 0; while (ss cmd) { switch (cmd) { case LM: // 绝对移动 ss x y; curX x; curY y; path.StartFigure(); break; case LL: // 绝对直线 ss x y; path.AddLine(curX, curY, x, y); curX x; curY y; break; case LC: // 三次贝塞尔 ss x1 y1 x2 y2 x y; path.AddBezier(curX, curY, x1, y1, x2, y2, x, y); curX x; curY y; break; case LZ: // 闭合 path.CloseFigure(); break; } } }这段代码只处理了最常用的几个指令实际工程里还要补上相对坐标的小写版本、H/V、Q、A。逻辑说明StartFigure开始一条新子路径AddLine从当前点画到目标点AddBezier需要两个控制点和一个终点。参数上所有坐标都是 SVG 用户坐标系里的值画之前要经过viewBox到窗口的缩放矩阵变换。注意ss cmd会跳过空白但 SVG 的 d 属性里数字之间可能用逗号分隔需要在读取前把逗号替换成空格否则流会读失败。3. 在 MFC 视图里跑通最小可显示版本从 OnDraw 到坐标映射3.1 创建视图类并接管 OnDrawMFC 里显示图形最自然的位置是CView的派生类。用 Visual Studio 的类向导新建一个继承自CView的类比如CSvgView然后在OnDraw里做渲染。如果是在对话框程序里也可以派生CStatic或者直接在一个子窗口的OnPaint里画。核心是拿到CDC*再取到底层HDC交给 GDI 的Graphics对象。// SvgView.h class CSvgView : public CView { protected: std::vectorstd::shared_ptrSvgShape m_shapes; // 解析后的图形列表 float m_scale; // 缩放比例 float m_offsetX; // 平移偏移 float m_offsetY; DECLARE_MESSAGE_MAP() public: virtual void OnDraw(CDC* pDC); afx_msg void OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); afx_msg BOOL OnEraseBkgnd(CDC* pDC); };// SvgView.cpp void CSvgView::OnDraw(CDC* pDC) { Graphics graphics(pDC-GetSafeHdc()); graphics.SetSmoothingMode(SmoothingModeAntiAlias); // 抗锯齿矢量图必开 // 用双缓冲避免闪烁 CRect rc; GetClientRect(rc); Bitmap buffer(rc.Width(), rc.Height(), PixelFormat32bppARGB); Graphics* pBuf Graphics::FromImage(buffer); pBuf-SetSmoothingMode(SmoothingModeAntiAlias); pBuf-Clear(Color(255, 255, 255, 255)); // 应用缩放和平移 pBuf-TranslateTransform(m_offsetX, m_offsetY); pBuf-ScaleTransform(m_scale, m_scale); for (auto shape : m_shapes) { shape-Draw(pBuf); // 每个图形自己负责画 } graphics.DrawImage(buffer, 0, 0); delete pBuf; }逻辑说明Graphics graphics(pDC-GetSafeHdc())把 MFC 的 DC 包装成 GDI 的绘图上下文。SmoothingModeAntiAlias对矢量图是必须的否则斜线和曲线会有明显锯齿。双缓冲用Bitmap做离屏画布先画到内存位图最后一次性DrawImage到屏幕这样缩放和重绘时不会闪。参数上m_scale初始设为 1.0m_offsetX/Y初始为 0后续由鼠标滚轮和拖拽修改。OnEraseBkgnd要返回 TRUE 并啥都不做防止背景擦除导致的闪烁。3.2 viewBox 到窗口坐标的映射矩阵SVG 的viewBox定义了用户坐标系而窗口有自己的一套像素坐标。如果直接按 SVG 坐标画图形可能跑到窗口外面或者小得看不见。正确的做法是算一个变换矩阵把 viewBox 矩形映射到窗口客户区。常见策略是「等比缩放并居中」也就是保持宽高比让图形完整显示。// 根据 viewBox 和客户区大小计算缩放和平移 void CSvgView::UpdateTransform() { CRect rc; GetClientRect(rc); if (m_viewBoxW 0 || m_viewBoxH 0) return; float scaleX rc.Width() / m_viewBoxW; float scaleY rc.Height() / m_viewBoxH; m_scale min(scaleX, scaleY) * 0.95f; // 留 5% 边距 // 居中先缩放再平移 float drawW m_viewBoxW * m_scale; float drawH m_viewBoxH * m_scale; m_offsetX (rc.Width() - drawW) / 2.0f - m_viewBoxX * m_scale; m_offsetY (rc.Height() - drawH) / 2.0f - m_viewBoxY * m_scale; }这段代码里m_viewBoxX/Y/W/H是从 SVG 根节点解析出来的四个值。min(scaleX, scaleY)保证等比0.95f是留边距避免图形贴边。平移量的计算要减去viewBox原点乘以缩放因为viewBox的 minX/minY 不一定是 0。参数上如果希望图形铺满窗口而不保持比例可以把min换成各自独立的scaleX和scaleY但那样图形会变形机械图纸场景下不能这么干。3.3 鼠标滚轮缩放与拖拽平移静态显示只是第一步实际用的时候用户肯定要缩放看细节。MFC 的OnMouseWheel能拿到滚轮增量按增量调整m_scale然后重绘。拖拽平移用OnLButtonDown记录起点OnMouseMove里算偏移量。这里有个坑缩放要以鼠标位置为中心而不是以窗口中心否则放大后想看的地方会跑掉。BOOL CSvgView::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) { float oldScale m_scale; float factor (zDelta 0) ? 1.1f : 0.9f; m_scale * factor; m_scale max(0.1f, min(m_scale, 50.0f)); // 限制范围 // 以鼠标点为中心缩放 float ratio m_scale / oldScale; m_offsetX pt.x - ratio * (pt.x - m_offsetX); m_offsetY pt.y - ratio * (pt.y - m_offsetY); Invalidate(); return TRUE; }逻辑说明zDelta正数表示滚轮向前放大负数缩小。ratio是新旧缩放比用鼠标点坐标反推新的平移量保证鼠标下的那个 SVG 点不动。参数上缩放范围限制在 0.1 到 50 倍之间防止极端值导致矩阵溢出或者图形糊成一片。Invalidate触发重绘配合前面的双缓冲缩放过程很顺滑。4. 避坑与排查SVG 显示不出来时先查这五处4.1 现象窗口一片空白啥都没有原因通常是解析阶段就失败了。MSXML 加载文件时如果路径含中文或者文件被占用load会返回失败但不会弹窗。解决在load之后检查返回值失败时用get_parseError拿到具体错误码和行号。另外确认CoInitialize调用了COM 没初始化的话CoCreateInstance直接失败。4.2 现象图形显示但位置偏移或者大小不对八成是viewBox没解析对或者解析了但没参与变换。有些 SVG 文件不写viewBox只写width和height这时候要把width/height当成 viewBox 用。还有的文件viewBox里是逗号分隔而不是空格拆分时要同时处理两种分隔符。解决打印出解析到的四个值和文件头对照。4.3 现象路径画出来是断的或者形状扭曲SVG 的d属性里相对坐标指令小写字母没处理。比如m 10 10 l 20 0如果按绝对坐标解析第二个点会跑到 (20,0) 而不是 (30,10)。解决在解析循环里维护curX/curY遇到小写指令时把参数加上当前点。另外Z闭合后当前点要回到子路径起点否则后续指令会从错误位置开始。4.4 现象缩放时图形闪烁严重没开双缓冲或者OnEraseBkgnd返回了默认值导致背景先被擦成白色再画图。解决重写OnEraseBkgnd直接返回 TRUE所有绘制都在内存位图里完成最后一次性贴到屏幕。如果还闪检查Graphics对象的SmoothingMode是不是在每帧都设置设置一次就够。4.5 现象渐变填充显示成纯色或者黑色GDI 的LinearGradientBrush需要指定两个点和两个颜色而 SVG 的linearGradient用的是x1/y1/x2/y2百分比或者gradientUnits。如果直接拿 SVG 的百分比当像素用渐变范围会错。解决把百分比乘以图形的包围盒宽高转换成绝对坐标再构造画刷。gradientUnitsuserSpaceOnUse时则直接用用户坐标。5. 进阶技巧用状态栏显示缩放比例和坐标以及一个验证渲染正确性的土办法5.1 把缩放比例和鼠标 SVG 坐标显示到状态栏MFC 的状态栏用CStatusBar在MainFrm.cpp里定义好窗格后视图类通过GetParentFrame拿到主框架指针再调SetPaneText更新。热词里「mfc状态栏怎么显示」问的就是这个。关键是把屏幕坐标反算回 SVG 坐标这样用户移动鼠标时能看到自己在图纸的哪个位置。void CSvgView::OnMouseMove(UINT nFlags, CPoint point) { if (m_pFrame m_pStatusBar) { // 屏幕坐标 - SVG 用户坐标 float svgX (point.x - m_offsetX) / m_scale; float svgY (point.y - m_offsetY) / m_scale; CString str; str.Format(L缩放: %.0f%% X: %.1f Y: %.1f, m_scale * 100, svgX, svgY); m_pStatusBar-SetPaneText(1, str); } CView::OnMouseMove(nFlags, point); }逻辑说明反算公式就是变换的逆运算先减平移再除缩放。SetPaneText的第二个参数是窗格索引要在indicators数组里预留位置。参数上%.0f%%把缩放显示成百分比整数坐标保留一位小数。这个功能看着小但调试渲染问题时特别有用能直观看到坐标映射对不对。5.2 用一张已知尺寸的测试 SVG 验证渲染精度自己写的渲染器最怕的是「看起来差不多但实际偏了几个像素」。我一般会准备一张测试 SVG一个 100x100 的矩形viewBox 设成0 0 100 100里面画一个从 (0,0) 到 (100,100) 的对角线再画一个圆心在 (50,50) 半径 50 的圆。渲染出来后用截图工具量对角线的端点是不是正好落在客户区的角上圆心是不是在正中间。如果偏了就是变换矩阵算错了。测试项预期结果偏差原因对角线端点落在 viewBox 映射后的角点平移量算错圆心位置客户区正中心缩放中心不对线宽缩放后按比例变化线宽没乘 scale圆度正圆无椭圆化scaleX 和 scaleY 不等这个土办法比看代码靠谱因为人眼对几何偏差很敏感。线宽那个尤其容易漏GDI 的Pen宽度是在变换前设置的如果希望线宽随缩放变化要把m_scale乘进去如果希望线宽恒定就在变换后画。机械图纸场景下通常希望线宽随缩放变化这样放大后线条不会显得太细。5.3 一个我踩过的坑忘记处理use和defs早期版本我只处理了直接出现的图形节点结果遇到用defs定义、use引用的 SVG 就抓瞎。use的xlink:href指向一个 id需要先在文档里建一个 id 到节点的映射表遇到use时把引用的节点复制一份并应用x、y偏移。这个逻辑不复杂但容易忘忘了就是图形缺一块。后来我养成习惯解析前先扫一遍所有带id的节点存进 map再遍历时遇到use就查表。写这类渲染器我的习惯是先把最小可显示版本跑通再逐个补特性每补一个就用测试 SVG 验证一次。别想着一次写全SVG 规范太厚按需实现才是正路。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →