How to draw on a Window in WPF (best practice)?

44,496

Solution 1

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.

Solution 2

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.

Solution 3

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

Solution 4

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

Solution 5

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.

Share:
44,496
Joan Venge
Author by

Joan Venge

Professional hitman.

Updated on April 08, 2020

Comments

  • Joan Venge
    Joan Venge about 4 years

    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.

  • Reed Copsey
    Reed Copsey about 13 years
    This will work, but is rarely a good idea in WPF. It's typically a much better practice to use Canvas.
  • H.B.
    H.B. about 13 years
    Isn't this also a matter of performance, with Canvases being more heavy?
  • Joan Venge
    Joan Venge about 13 years
    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
    Joan Venge about 13 years
    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?
  • Reed Copsey
    Reed Copsey about 13 years
    @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.
  • Joan Venge
    Joan Venge about 13 years
    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.
  • Reed Copsey
    Reed Copsey about 13 years
    @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.
  • Joan Venge
    Joan Venge about 13 years
    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)?
  • Reed Copsey
    Reed Copsey about 13 years
    @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.
  • Joan Venge
    Joan Venge about 13 years
    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?
  • Reed Copsey
    Reed Copsey about 13 years
    @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
    Reed Copsey about 13 years
    @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.
  • Joan Venge
    Joan Venge about 13 years
    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
    Joan Venge about 13 years
    Thanks Reed, sorry I couldn't edit my comment so had to repost. I got it now, will try. Thanks.
  • Gusdor
    Gusdor over 9 years
    @JoanVenge That is incorrect I'm afraid. The DrawingContext for a control will draw to that control's background