32

I am trying to write a small interactive game-like application, where I need to have a Draw method that's gonna draw on screen, but can't figure out how to structure the method for WPF.

If this was Winforms, I could use:

public void Draw (Graphics g)
{

}

But for a WPF Window, what should I have on it in the xaml (currently only have a Grid), and what should this Draw method receive as an argument?

First I want to do it like this to get it working, then I can think about how to make it more WPF, etc. But now I am more interested in getting this to work.

Joan Venge
  • 315,713
  • 212
  • 479
  • 689
  • 2
    For quite fast drawing in WPF see this complete example https://stackoverflow.com/questions/16107877/fast-2d-graphics-in-wpf – oo_dev May 20 '19 at 07:34

5 Answers5

25

Typically, you "draw" in WPF in a completely different manner.

In Windows Forms/GDI, the graphics API is an immediate mode graphics API. Each time the window is refreshed/invalidated, you explicitly draw the contents using Graphics.

In WPF, however, things work differently. You rarely ever directly draw - instead, it's a retained mode graphics API. You tell WPF where you want the objects, and it takes care of the drawing for you.

The best way to think of it is, in Windows Forms, you'd say "Draw a line from X1 to Y1. Then draw a line from X2 to Y2. Then ...". And you repeat this every time you need to "redraw" since the screen is invalidated.

In WPF, instead, you say "I want a line from X1 to Y1. I want a line from X2 to Y2." WPF then decides when and how to draw it for you.

This is done by placing the shapes on a Canvas, and then letting WPF do all of the hard work.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • Thanks Reed, I went with this route but now I ran into an issue where it doesn't let me to update the canvas from a different thread (I use System.Timers.Timer) Error message is 'The calling thread cannot access this object because a different thread owns it.' Any ideas? – Joan Venge Apr 07 '11 at 20:38
  • 3
    @Joan: You can't - just like WinForms - all UI stuff has to happen on the UI thread, so you'll need to use Dispatcher.Invoke to marshal it back to the UI thread. – Reed Copsey Apr 07 '11 at 20:55
  • Thanks Reed I did this and it's working. But one thing I was gonna ask you that's related is, I am creating lots of `Line` objects but it complains I can't do these outside the STA. Aren't `Line` objects independent from any UI till I add them? I just want to create say 1000 of them and then pass them to the canvas using the Dispatcher, instead of calling the Dispatcher 1000 times. – Joan Venge Apr 07 '11 at 21:08
  • 1
    @Joan: Line objects are a full ui element, so they have to be done on the dispatcher. My suggestion - Do the processing to create the collection of information required to create the lines, and dispatch it and create the lines in one shot. – Reed Copsey Apr 07 '11 at 23:58
  • Thanks Reed. When you said "do it in one shot", do you mean draw the lines by looping? So dispatch once but this time just to draw the lines (a draw call per line)? – Joan Venge Apr 08 '11 at 00:02
  • 1
    @Joan: Exactly - one dispatch call, passing (or closing over) the collection of line info, that loops through and builds the entire collection of lines in "one shot". This way, you're reducing marshaling overhead. – Reed Copsey Apr 08 '11 at 00:08
  • Also Reed, do you think I should be using `LineGeometry` instead of `Line`? I only want them as shapes, not clickable controls. I see there are shapes like `Line`, `Ellipse`, but they also have `LineGeometry` and `EllipseGeometry` types. Do you know the difference between them? – Joan Venge Apr 08 '11 at 00:09
  • 1
    @Joan: `LineGeometry` is used to build up a `Path` instance. If the lines all share properties (ie: all one color, etc), you can put many `LineGeometry` instances into one `Path`, and draw the `Path`. If they're all different colors/widths/etc, then just use Line. – Reed Copsey Apr 08 '11 at 00:12
  • 1
    @Joan: No - you add all of the LineGeomoetry instances to a Path (which is a UI element like Line). Since they're freezable, you can probably build them up on a background thread and Freeze() them, I believe, and then reuse them in the Path (built on the UI thread) - but I haven't tried it. – Reed Copsey Apr 08 '11 at 00:22
  • Thanks Reed, I will use `LineGeometry` then, they are all the same color, thickness, etc. But is `LineGeometry` a UI element like `Line`? Also this potentially would speed up my app, right? Since I will have 1 `LineGeometry` instead of `Line`, right? – Joan Venge Apr 08 '11 at 00:22
  • Thanks Reed, sorry I couldn't edit my comment so had to repost. I got it now, will try. Thanks. – Joan Venge Apr 08 '11 at 00:23
13

When there are just too many objects to be drawn very quickly (huge Visual Tree) another option would be to use a WriteableBitmap. Just use the Pixels property to set the pixels and/or use the Render method to draw UIElements.

Emond
  • 50,210
  • 11
  • 84
  • 115
8

I preffer to use OnRender method like in this example:

protected override void OnRender(DrawingContext drawingContext)
{
    base.OnRender(drawingContext);
     drawingContext.DrawRectangle(null, new Pen(Brushes.Black, 2), new Rect(0, 0, ActualWidth, Height));
}
Marek Kwiendacz
  • 9,524
  • 14
  • 48
  • 72
  • 4
    This will work, but is rarely a good idea in WPF. It's typically a much better practice to use Canvas. – Reed Copsey Apr 07 '11 at 18:56
  • 2
    Isn't this also a matter of performance, with Canvases being more heavy? – H.B. Apr 07 '11 at 19:07
  • What's the difference between using this vs drawing on a canvas? DrawingContext draws on top of everything? Like a topmost overlayed layer? – Joan Venge Apr 07 '11 at 20:20
  • @JoanVenge That is incorrect I'm afraid. The `DrawingContext` for a control will draw to that control's background – Gusdor Dec 08 '14 at 10:28
6

To Implement a Draw loop type behavior in WPF you can use the CompositionTarget.Rendering event. This is raised once per frame when the WPF drawing system is painting frames.

As others have pointed out this is not very WPF friendly but it will work and can be used to get more immediate drawing behavior out of a WPF app.

In most cases you would use a single root canvas and update say the Canvas position of an element on the CompositionTarget.Rendering event.

For example to make a ellipse fly all over the screen do this:

In your XAML (For a Window that is 640 by 480 in size):

<Canvas x:Name="theCanvas">
    <Ellipse x:Name="theEllipse" Height="10" Width="10" Fill="Black" />     
</Canvas>

In your Code behind for the Window that the above XAML is in (Make sure to add a reference to System.Windows.Media in order to see the CompsitionTarget object :

    public static Random rand = new Random();
    public View()
    {
        InitializeComponent();
        CompositionTarget.Rendering += CompositionTarget_Rendering;
    }

    void CompositionTarget_Rendering(object sender, System.EventArgs e)
    {
        double newLeft = rand.Next(0, 640);
        double newTop = rand.Next(0, 480);

        theEllipse.SetValue(Canvas.LeftProperty,newLeft);
        theEllipse.SetValue(Canvas.TopProperty, newTop);
    }
Brad Cunningham
  • 6,402
  • 1
  • 32
  • 39
3

Yow should add a Canvas (or change the Grid for a Canvas) and then draw over it. Here is Microsoft tut on drawing over a canvas

Also, I don't know how related is this other question to yours, but you might want to check it out.

Community
  • 1
  • 1
Felix Martinez
  • 3,928
  • 3
  • 31
  • 32