如何将含图表的WPF UserControl保存为矢量图形文件?
我明白你现在的困境——想把WPF UserControl里的图表导出成矢量格式(比如WMF),但目前导出的内容是空的,又不想重写所有图形类。我来帮你分析问题并给出可行的方案:
你当前代码的核心问题
你把UserControl通过VisualHost加到Canvas里,但导出时看不到内容,大概率是两个原因:
- UserControl没有完成布局渲染:WPF控件需要经过
Measure、Arrange、UpdateLayout三个步骤才能正确计算尺寸并渲染内容,直接加到Canvas里可能还没完成这个流程,导致导出时是空的。 - 你的
WPFPainter只处理Canvas的子Shape:UserControl是复杂的Visual对象,不是普通的Shape,你的绘制工具可能没有遍历处理VisualHost里的控件内容。
方案一:用WPF原生XPS导出(最推荐,无需重写代码)
XPS是WPF原生支持的矢量格式,完全保留图表的矢量特性,操作简单,不需要修改现有图形类。直接用XpsDocument导出UserControl即可:
using System.IO; using System.Windows.Xps.Packaging; public void ExportChartAsXps(UserControl chartControl, string outputPath) { // 先确保控件完成布局渲染 chartControl.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); chartControl.Arrange(new Rect(chartControl.DesiredSize)); chartControl.UpdateLayout(); // 导出为XPS using (var xpsDoc = new XpsDocument(outputPath, FileAccess.Write)) { var writer = XpsDocument.CreateXpsDocumentWriter(xpsDoc); writer.Write(chartControl); } }
使用时直接传入你的chart1和输出路径就行,导出的XPS文件可以用系统自带的XPS Viewer打开,也能轻松转换成其他矢量格式(比如PDF)。
方案二:导出为WMF矢量文件(需要处理VisualTree)
如果你一定要WMF格式,那就需要遍历UserControl的Visual树,把每个WPF元素转换成对应的GDI+绘制命令。下面是一个简化版的实现思路:
1. 先确保控件完成布局
和XPS方案一样,导出前必须让控件完成布局:
chart1.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); chart1.Arrange(new Rect(chart1.DesiredSize)); chart1.UpdateLayout();
2. 编写VisualTree遍历与GDI+绘制方法
这个方法会递归遍历UserControl里的所有子元素,把常见的Shape、TextBlock转换成GDI+的绘制操作:
using System.Drawing; using System.Drawing.Drawing2D; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; private void DrawVisualToGraphics(Graphics g, Visual visual) { int childCount = VisualTreeHelper.GetChildrenCount(visual); for (int i = 0; i < childCount; i++) { var childVisual = VisualTreeHelper.GetChild(visual, i); // 处理Shape类元素 if (childVisual is Shape shape) { // 转换WPF画笔为GDI+画笔/画刷 Pen pen = null; Brush brush = null; if (shape.Stroke != null && shape.Stroke is SolidColorBrush strokeBrush) { var color = Color.FromArgb( strokeBrush.Color.A, strokeBrush.Color.R, strokeBrush.Color.G, strokeBrush.Color.B); pen = new Pen(color, (float)shape.StrokeThickness); } if (shape.Fill != null && shape.Fill is SolidColorBrush fillBrush) { var color = Color.FromArgb( fillBrush.Color.A, fillBrush.Color.R, fillBrush.Color.G, fillBrush.Color.B); brush = new SolidBrush(color); } // 根据Shape类型绘制 if (shape is Line line) { g.DrawLine(pen, (float)line.X1, (float)line.Y1, (float)line.X2, (float)line.Y2); } else if (shape is Rectangle rect) { var rectF = new RectangleF((float)rect.Rect.X, (float)rect.Rect.Y, (float)rect.Rect.Width, (float)rect.Rect.Height); if (brush != null) g.FillRectangle(brush, rectF); if (pen != null) g.DrawRectangle(pen, rectF.X, rectF.Y, rectF.Width, rectF.Height); } else if (shape is Ellipse ellipse) { var rectF = new RectangleF((float)ellipse.Rect.X, (float)ellipse.Rect.Y, (float)ellipse.Rect.Width, (float)ellipse.Rect.Height); if (brush != null) g.FillEllipse(brush, rectF); if (pen != null) g.DrawEllipse(pen, rectF.X, rectF.Y, rectF.Width, rectF.Height); } // 释放资源 pen?.Dispose(); brush?.Dispose(); } // 处理TextBlock文本 else if (childVisual is TextBlock textBlock) { var font = new Font(textBlock.FontFamily.Source, (float)textBlock.FontSize); var textBrush = new SolidBrush(Color.FromArgb( textBlock.Foreground.Color.A, textBlock.Foreground.Color.R, textBlock.Foreground.Color.G, textBlock.Foreground.Color.B)); g.DrawString(textBlock.Text, font, textBrush, (float)textBlock.Margin.Left, (float)textBlock.Margin.Top); font.Dispose(); textBrush.Dispose(); } // 递归处理子元素 DrawVisualToGraphics(g, childVisual); } }
3. 生成WMF文件
调用上面的方法,把UserControl的内容绘制到Metafile里:
public void ExportChartAsWmf(UserControl chartControl, string outputPath) { // 先完成布局 chartControl.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); chartControl.Arrange(new Rect(chartControl.DesiredSize)); chartControl.UpdateLayout(); using (var tempBmp = new Bitmap(1, 1)) using (var g = Graphics.FromImage(tempBmp)) { var hdc = g.GetHdc(); var rect = new RectangleF(0, 0, (float)chartControl.ActualWidth, (float)chartControl.ActualHeight); using (var mf = new Metafile(outputPath, hdc, rect, MetafileFrameUnit.Pixel)) using (var mfGraphics = Graphics.FromImage(mf)) { // 匹配WPF的96DPI float scale = mfGraphics.DpiX / 96f; mfGraphics.ScaleTransform(scale, scale); // 绘制控件内容 DrawVisualToGraphics(mfGraphics, chartControl); } g.ReleaseHdc(hdc); } }
为什么之前的Canvas方法没用?
你把UserControl加到Canvas里但没效果,是因为VisualHost只是包装了Visual,但Canvas不会自动帮UserControl完成布局。你需要手动调用Measure和Arrange给VisualHost设置尺寸,才能让UserControl正确显示:
private void InterceptChart() { var visualHost = new VisualHost { Visual=chart1}; // 给VisualHost设置尺寸 chart1.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); visualHost.Width = chart1.DesiredSize.Width; visualHost.Height = chart1.DesiredSize.Height; visualHost.Arrange(new Rect(visualHost.Width, visualHost.Height)); TChartCanvas.Children.Add(visualHost); }
不过这种方法还是需要你的WPFPainter能处理VisualHost里的Visual内容,不如直接用上面的XPS或WMF方案更可靠。
内容的提问来源于stack exchange,提问作者UabasIerdna

