Rendering into a custom DrawingContext
Asked Answered
M

4

17

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?

Moton answered 12/8, 2013 at 17:32 Comment(7)
From the Remarks on DrawingContext: You never directly instantiate a DrawingContext; you can, however, acquire a drawing context from certain methods, such as DrawingGroup.Open and DrawingVisual.RenderOpen. To me this means that there is no way to actually supply a custom DrawingContext somewhere.Elkins
@Clemens: yes, I saw this remark, but I understood it as "you should not usually create it yourself, we do it for you internally; and for drawing into DrawingVisual just let the DrawingVisual properly initialize it". Anyway, interesting is if there are valid interception points for drawing.Moton
The only way to draw into a DrawingVisual is to draw into the DrawingContext provided by DrawingVisual.RenderOpen. There is simply no way to associate your custom DrawingContext with a Visual. The idea is pointless.Elkins
Moreover, the fact that a class is abstract does not necessarly mean that you can derive from it.Elkins
@Clemens: yes, this is exactly what I write in my question. However having it public and abstract left me with a hope that I might be able to derive from it. Besides, its list of unimplemented methods looks like a valid interception point, so I really expected it to be such.Moton
I think the implementation you give as example is the correct path. It is better interception point than what you ask for as you have the ability to check for type of child you get and react to it. if you had an event to receive just the primitives like line, arc etc.. your next step would probably be to check which parent is of this primitive... so.. yes you are in the right path in my opinion.Photoactinic
DrawingContext and all its derived classes have no public constructor, so no, you can't derive from it. WPF is not like EMF or WMF, or a printer driver, or say, Postscript. There is no notion of "primitive". The underlying implementation (the media interface layer "MIL") is totally unmanaged and largely undocumented.Hateful
C
4

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.)

Crumpet answered 21/8, 2013 at 14:18 Comment(5)
In my tests this did not work. I think you need to use the approach I suggested because GetDrawing will yield null for most Controls because the visuals that are drawn during OnRender are not enumerated by that helper method - try enumerating a Border and you will get null.Tiffany
If I call GetDrawing on a Border that has a non-null BorderBrush and a non-zero BorderThickness, I get back a non-null DrawingGroup that contains a single GeometryDrawing with the same size as the Border, a RectangleGeometry as its Geometry, and a Pen that matches the Thickness and BorderBrush settings. Did you by any chance do your tests on either a) a border that didn't actually paint anything or b) a border that had not yet realised its visuals?Crumpet
You might be right here - the Border had never been visualized before. That's probably the issue, though I did not check, yet. Sorry for the false accusation!Tiffany
Indeed I am using right now an approach like this, but with just inspecting the UIElements instead of Drawings. Disadvantage of my approach is that text rendering is really complicated and requires implementing a custom TextSource for semi-automatic splitting the text in TextBlocks into lines. With your approach I get this for free.Moton
What's the benefit of the approach you're suggesting? If you want WPF to do all the work of layout, including text layout, the visual layer (i.e., Drawing) will be the right place to go. Why go in at a level where you're giving yourself more work to do?Crumpet
T
4

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.

Tiffany answered 21/8, 2013 at 13:43 Comment(2)
The approach with XPS document seems to be a good way to go. The quick check-up of XPS document contents shown that the original fonts are not preserved but rather embedded into the document -- this however should be not a problem, as one can draw use the embedded font itself. A possible problem with this approach is that with WPF I can control the complexity, but for XPS I might need to parse all the XPS features even for a simple WPF source.Moton
Thanks a lot for your answer. Although I didn't choose to go the way you propose, it's clearly a very good idea and deserves much more upvotes.Moton
C
4

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.)

Crumpet answered 21/8, 2013 at 14:18 Comment(5)
In my tests this did not work. I think you need to use the approach I suggested because GetDrawing will yield null for most Controls because the visuals that are drawn during OnRender are not enumerated by that helper method - try enumerating a Border and you will get null.Tiffany
If I call GetDrawing on a Border that has a non-null BorderBrush and a non-zero BorderThickness, I get back a non-null DrawingGroup that contains a single GeometryDrawing with the same size as the Border, a RectangleGeometry as its Geometry, and a Pen that matches the Thickness and BorderBrush settings. Did you by any chance do your tests on either a) a border that didn't actually paint anything or b) a border that had not yet realised its visuals?Crumpet
You might be right here - the Border had never been visualized before. That's probably the issue, though I did not check, yet. Sorry for the false accusation!Tiffany
Indeed I am using right now an approach like this, but with just inspecting the UIElements instead of Drawings. Disadvantage of my approach is that text rendering is really complicated and requires implementing a custom TextSource for semi-automatic splitting the text in TextBlocks into lines. With your approach I get this for free.Moton
What's the benefit of the approach you're suggesting? If you want WPF to do all the work of layout, including text layout, the visual layer (i.e., Drawing) will be the right place to go. Why go in at a level where you're giving yourself more work to do?Crumpet
F
2

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!

Fronia answered 21/8, 2013 at 12:10 Comment(1)
In fact, there are a lot of internal DrawingContexts, used for simple things like hit testing. My ultimate goal is to render WPF into some document type, so a custom UserControl wouldn't help. Thank you anyway for your advice!Moton
G
2

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.

Gredel answered 22/8, 2013 at 16:33 Comment(2)
This would work, but only for custom Visuals. My problem is to catch what e.g. TextBlock's OnRender is doing.Moton
This is a interesting problem. Do consider sharing your experiences if you find answer. :-)Fronia

© 2022 - 2024 — McMap. All rights reserved.