Rendering into a custom DrawingContext

5k views Asked by At

I'd like to hijack the usual WPF rendering to split the controls into primitives, do the layout management, apply the bindings etc. for me.

As far as I understand, the whole rendering in WPF boils down to rendering of primitives (text, image, line, curve) at the locations calculated by the layout manager with values defined by the dependency property system. If I could supply my own primitive-rendering logic, I would be able to render e.g. to a custom document type, transfer the primitives for real rendering over the network etc.

My plan is following:

  1. Implement a custom DrawingContext. The DrawingContext is an abstract class, which defines a bunch of methods like DrawEllipse, DrawText, DrawImage etc. — I'll need to supply my own implementation for this functionality.
  2. Create a WPF UserControl and force it to render into a given DrawingContext.

However I've encountered the following problems:

  1. DrawingContext contains abstract internal methods void PushGuidelineY1(double coordinate) and void PushGuidelineY2(double leadingCoordinate, double offsetToDrivenCoordinate), which I cannot override easily. (Perhaps there is some trick to overcome this?)
  2. There seems to be no method to render the whole visual on a DrawingContext? Why?

I can do something like

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);
}

— but I wonder if there is a direct way to render an UIElement. (Of course, this problem is a minor one, but seeing no infrastructure for it makes me wonder if this is the proper way.)

So, is the DrawingContext not intended for inheriting from? Is the whole idea of supplying a custom DrawingContext a step in the right direction, or I need to rethink the strategy? Is drawing onto a custom context supported in WPF, or I need to look for a different interception point?

4

There are 4 answers

5
Ian Griffiths On BEST ANSWER

You may need to approach this problem from the opposite direction. Instead of aiming to provide your own DrawingContext, you can instead ask WPF to provide you with a Drawing. So it's more of a 'pull' approach' than the 'push' approach you're aiming for, but it should make it possible to get to the same place: if you have a Drawing that is a complete representation of the appearance of part of the visual tree, that's a data structure you can walk and discover everything that you would have discovered from calls to a custom DrawingContext.

I believe this is the same basic approach that the XPS document export Sebastian mentions uses internally. But using it directly yourself is a more direct approach than using it through the XPS APIs

At the heart is something fairly simple: VisualTreeHelper.GetDrawing. This returns a DrawingGroup. (Drawing is an abstract base class.) That documentation page shows you how to walk through the tree that you get back. Unfortunately, this doesn't do the whole job: it just provides the visuals for whichever node you happen to call it in, and if that node has children, they won't be included.

So you will, unfortunately, still have to write something that recurses the visual tree, much like you were already planning. And you will also need to handle any opacity masks, non-mask-based opacity, clip regions, effects, and transformations that are attached to the visual to get the correct results; you'd have had to do all that too to make your proposed approach work correctly, so nothing really changes here. (One potential advantage of using the XPS API as Sebastian suggests is that it does all this for you. However, it's then your problem to extract the information from the XPS document in the form you want, and that may end up losing information that you might want to preserve.)

1
Sameer Vartak On

I tried to do similar thing to create a FlowDocumentViewer for winRT. But as WinRT far less matured compared to WPF, also it delegates too much to the native layer (via render thread) I could not get anywhere. But this is what I've learned and I hope I'm explaining it well.

WPF uses hardware accelerated graphics rendering. So in simplistic terms, the WPF LayoutEngine constructs logical visual tree which is then translated to rendering instructions which are then sent to the Graphics hardware to execute or render.

DrawingContext is a non trivial class, it interacts with the underlying graphics system for rendering, manages scaling, caching and so forth. WPF runtime comes with default implementation which does the rendering of all visuals. IMO, the reason it's made into an abstract class so Microsoft can provide different implementations say for Silverlight etc. But it's meant to be overridden by us.

If you must replace the WPF rendering then your best bet is to create a UserControl, override Arrange and Measure calls and render each element to DrawingVisual using DrawingVisual.RenderOpen() and arrange them etc. from your code. Managing the DataBinding notifications will be another thing you will have to do yourself.

Seems like very interesting project. Good luck!

2
Sebastian On

I think your approach will not work, because (as others have mentioned) you cannot provide your own DrawingContext implementation.

I suggest the following instead: In order to "flatten" the WPF rendering, have WPF export your visuals to an XPS document. During that process, all rendering is basically enumerated as simple rendering primitives and all that you are left with is Canvass, basic shapes, glyphs, and other drawing primitives.

Then iterate over the visuals in the pages of the document. As far as I know the resulting visual will consist of primitives, only, so there is no need to call OnRender. Instead this enables you to externally introspect the visual instances (using instanceof-cascades and reading/interpreting the properties). That's still quite a lot of work, because you need to interpret the properties just like WPF does, but as far as I could see, this should work at least for the many major use-cases.

2
randcd On

Instead of trying to write your own DrawingContext maybe you could create a class that derives from FrameworkElement or UIElement or even Visual that performs your activities in its OnRender method. You still have to use the given implementations of Draw[Something] but you will be in control of the arguments and order of operations. You could still parse primitives and instructions from a secondary source and your one UIElement/FrameworkElement could compose the instructions at runtime.