渲染到自定义DrawingContext中

17
我想劫持通常的WPF渲染,将控件分割为基元,对布局管理进行处理,应用绑定等。
据我所知,WPF中的整个渲染归结为在由依赖属性系统定义的值计算出的位置上呈现基元(文本、图像、线条、曲线)。如果我能提供自己的基元呈现逻辑,我就能够呈现到自定义文档类型,通过网络传输基元进行真正的呈现等。
我的计划如下:
1. 实现一个自定义DrawingContext。DrawingContext是一个抽象类,定义了一堆方法,如DrawEllipse、DrawText、DrawImage等——我需要为这些功能提供自己的实现。
2. 创建一个WPF UserControl,并强制它呈现到给定的DrawingContext中。
然而,我遇到了以下问题:
  1. DrawingContext 包含了抽象的内部方法 void PushGuidelineY1(double coordinate)void PushGuidelineY2(double leadingCoordinate, double offsetToDrivenCoordinate),我不能轻易地覆盖它们。(也许有一些技巧可以克服这个问题?)
  2. 似乎没有一种方法可以在 DrawingContext 上渲染整个可视化元素?为什么呢?

我可以做一些像这样的事情:

void RenderRecursively(UIElement e, DrawingContext ctx)
{
    e.OnRender(ctx);
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(e); i++)
        RenderRecursively((UIElement)VisualTreeHelper.GetChild(e, i), ctx);
}

— 但我想知道是否有一种直接的方法来呈现一个 UIElement。(当然,这个问题是一个小问题,但是看不到为它建立基础设施让我想知道这是否是正确的方式。)
那么,DrawingContext 不是用于继承的吗?提供自定义 DrawingContext 的整个想法是朝着正确的方向迈出的一步,还是我需要重新考虑策略?在 WPF 中支持在自定义上下文中绘制,还是我需要寻找不同的拦截点?

6
在关于DrawingContext的注释中指出:“您永远不会直接实例化DrawingContext; 但是,您可以从某些方法(例如DrawingGroup.Open和 DrawingVisual.RenderOpen)中获取绘图上下文。” 对我来说,这意味着实际上没有办法在任何地方提供自定义的DrawingContext。 - Clemens
2
唯一绘制到DrawingVisual的方法是通过DrawingVisual.RenderOpen提供的DrawingContext进行绘制。没有办法将自定义的DrawingContext与Visual关联起来,这个想法是毫无意义的。 - Clemens
@Clemens:是的,这正是我在我的问题中所写的。然而,将其公开和抽象化使我有一丝希望能够从中推导出来。此外,它的未实现方法列表“看起来像”一个有效的拦截点,所以我真的“期望”它是这样的。 - Vlad
1
我认为你提供的实现示例是正确的路径。这是比你所要求的更好的拦截点,因为你有能力检查你得到的子类型并对其做出反应。如果你有一个事件只接收像线、弧等基本元素,那么你下一步可能就是检查哪个父级是这个基本元素... 所以,在我看来,你走在了正确的道路上。 - G.Y
2
DrawingContext及其所有派生类都没有公共构造函数,因此不能从中派生。WPF不像EMF或WMF,也不像打印机驱动程序或者Postscript。没有所谓的“原始类型”。底层实现(媒体接口层“MIL”)是完全未托管的,并且大部分没有文档说明。 - Simon Mourier
显示剩余2条评论
4个回答

4
我认为你的方法行不通,因为(正如其他人所提到的)你无法提供自己的DrawingContext实现。
我建议采用以下方法代替:为了“展平”WPF渲染,请让WPF将您的可视内容导出到XPS文档中。在此过程中,所有呈现都基本上被枚举为简单的呈现原语,您只剩下Canvas、基本形状、字形和其他绘图原语。
然后迭代文档页面中的可视内容。据我所知,生成的可视内容将仅包含原语,因此无需调用OnRender。相反,这使您能够外部内省可视实例(使用instanceof-cascades并读取/解释属性)。这仍然需要相当多的工作,因为您需要像WPF一样解释属性,但据我所见,这应该至少适用于许多主要用例。

使用XPS文档的方法似乎是一个不错的选择。快速检查XPS文档内容显示原始字体未被保留,而是嵌入到文档中--然而这应该不是问题,因为可以直接使用嵌入的字体本身。这种方法可能存在的问题是,对于WPF,我可以控制复杂性,但对于XPS,即使是简单的WPF源,我也可能需要解析所有的XPS特性。 - Vlad
非常感谢您的回答。虽然我没有选择您提出的方法,但这显然是一个非常好的想法,值得更多的赞同。 - Vlad

4
你可能需要采用相反的方法来解决这个问题。而不是提供自己的 DrawingContext,你可以让WPF提供给你一个 Drawing。所以这更像是一种“拉”方法,而不是你所追求的“推”方法。但是使用这种方法应该能够达到同样的目的:如果你有一个 Drawing,它是可视树部分外观的完整表示,那么这就是一个数据结构,你可以从中遍历并发现你从自定义 DrawingContext 调用时会发现的所有内容。
我认为这是Sebastian提到的XPS文档导出使用的基本方法。但是直接使用它比通过XPS API使用更直接。
核心是一些相当简单的东西:VisualTreeHelper.GetDrawing。这会返回一个 DrawingGroup。(Drawing 是抽象基类。)这个文档页面向您展示了如何遍历您收到的树。不幸的是,这还做不完整的工作:它只为您调用的任何节点提供视觉效果,如果该节点有子项,则不会包括它们。
因此,不幸的是,您仍然需要编写一些递归可视树的代码,就像您已经计划的那样。并且您还需要处理附加到视觉效果的任何不透明度掩码、非掩码透明度、剪辑区域、效果和变换,以获得正确的结果;您也必须要做所有这些才能使您提出的方法在正确工作时保持正确。使用Sebastian建议的XPS API的一个潜在优势是它可以为您完成所有这些操作。然而,从XPS文档中提取您想要的形式的信息,则成为了您问题的一部分,这可能会导致您希望保留的信息丢失。

在我的测试中,这并没有起作用。我认为你需要使用我建议的方法,因为对于大多数控件来说,GetDrawing将产生null,因为在OnRender期间绘制的可视化内容不会被该辅助方法枚举 - 尝试枚举一个Border,你会得到null - Sebastian
如果我在具有非空 BorderBrush 和非零 BorderThicknessBorder 上调用 GetDrawing,那么我会得到一个非空的 DrawingGroup,其中包含一个大小与 Border 相同的单个 GeometryDrawing,它的 Geometry 是一个 RectangleGeometry,而且 PenThicknessBorderBrush 设置相匹配。你有没有碰巧在以下情况下进行测试:a) 实际上并没有绘制任何内容的边框或 b) 尚未实现其可视化效果的边框? - Ian Griffiths
你可能是对的 - 边框以前从未被可视化过。这可能是问题所在,尽管我还没有检查。对于错误的指控感到抱歉! - Sebastian
实际上,我现在正在使用这样的方法,但只是检查UIElement而不是Drawing。我的方法的缺点是文本渲染非常复杂,需要实现自定义的TextSource来半自动地将TextBlock中的文本拆分成行。使用您的方法,我可以免费获得这个功能。 - Vlad
2
你所建议的方法有什么好处?如果你想让 WPF 做所有的布局工作,包括文本布局,那么视觉层(即 Drawing)将是正确的选择。为什么要在一个需要更多工作的级别上进行操作呢? - Ian Griffiths

2
我尝试创建一个用于WinRT的FlowDocumentViewer,但由于WinRT相对于WPF不够成熟,同时它通过渲染线程将太多工作委托给本地层,所以我无法取得任何进展。但这就是我学到的东西,我希望我解释得清楚。
WPF使用硬件加速图形渲染。简单来说,WPF LayoutEngine构建逻辑可视树,然后将其转换为渲染指令,再发送到图形硬件执行或呈现。
DrawingContext是一个非常重要的类,它与底层图形系统进行交互以进行渲染,管理缩放、缓存等等。WPF运行时带有默认实现,可以渲染所有可视化元素。在我看来,它被制作成抽象类的原因是Microsoft可以为Silverlight等提供不同的实现。但它旨在被我们覆盖。
如果您必须替换WPF渲染,则最好的选择是创建一个UserControl,覆盖Arrange和Measure调用,并使用DrawingVisual.RenderOpen()将每个元素呈现为DrawingVisual,并从您的代码中排列它们等等。管理DataBinding通知也是您需要自己完成的另一件事情。
看起来是一个非常有趣的项目。祝你好运!

实际上,有很多内部的“DrawingContext”用于像命中测试这样简单的事情。我的最终目标是将WPF渲染成某种文档类型,因此自定义UserControl是无济于事的。不管怎样,还是谢谢你的建议! - Vlad

1

如果你不想编写自己的DrawingContext,也许你可以创建一个从FrameworkElementUIElement或甚至Visual派生的类,在其OnRender方法中执行你的操作。你仍然需要使用给定的Draw[Something]实现,但你将控制参数和操作顺序。你仍然可以从次要来源解析基元和指令,而你的一个UIElement/FrameworkElement可以在运行时组合这些指令。


这样做可以行得通,但仅适用于自定义 Visuals。我的问题是捕获例如 TextBlock 的 OnRender 在做什么。 - Vlad
这是一个有趣的问题。如果你找到答案,请考虑分享你的经验。 :-) - Sameer Vartak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接