Semaphore
Semaphore

Reputation: 160

Fastest way to draw large, coloured grids in WPF without using third party libraries?

I need to represent a large amount of real time data in a coloured grid (about ~100 by ~150, and may grow in the future). What's the best practice in achieving this in WPF, without resorting to third party libraries or controls? The data is updated several times per second, so the display needs to be reasonably responsive.

I've previously used WinForm for this, by overriding OnPaint in a panel. But it is apparently bad practice to do this in (what I think is) the WPF equivalent, OnRender in a Canvas. So I'm wondering what the best practice is, for my particular use case.

Upvotes: 0

Views: 451

Answers (3)

Andrew Stephens
Andrew Stephens

Reputation: 10203

Performance issues aside, here is what I think is a fairly elegant MVVM-based solution for the binding, although you might find that its simplicity helps keep the performance sweet.

I'm assuming you have a class representing each data point, e.g.:

public class DataPoint : INotifyPropertyChanged
{
    private int _value;

    public int X {get;set;}

    public int Y {get;set;}

    public int Value
    {
        get { return _value; }
        set
        {
            if (_value!= value)
            {
               _value= value;
               OnPropertyChanged("Value");
            }
        }
    }

    // INotifyPropertyChanged implementation ommitted for brevity
}

The X and Y properties are the point's position in your grid, so would be assigned [0,0], [0, 1] ... [99, 149].

Next, expose a collection of these objects via your VM:

public List<DataPoint> DataPoints {get; private set;}

(You may need to use an `ObservableCollection instead, depending on how/where you instantiate and populate the collection).

In the XAML, bind an ItemsControl to this collection, and use a Canvas as its ItemsPanelTemplate:

<ItemsControl ItemsSource="{Binding DataPoints}"
              ItemContainerStyle="{StaticResource DataPointStyle}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas Width="100" Height="150" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

(I've kept this example simple by setting the canvas size to 100x150, so each "data point" will be 1x1 pixel).

And here is the item container style:

<Style x:Key="DataPointStyle" TargetType="ContentPresenter">
    <Setter Property="Canvas.Left" Value="{Binding X}" />
    <Setter Property="Canvas.Top" Value="{Binding Y}" />
    <Setter Property="ContentTemplate">
        <Setter.Value>
            <DataTemplate DataType="{x:Type DataPoint}">
                <Border Width="1" Height="1" 
                        Background="{Binding Value, Converter={...}" />
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

This binds the list item's Canvas.Left and Canvas.Top properties to the model's X and Y properties. The item template is just a 1x1 border whose background is bound to the model's Value property (you'll need a value converter to convert the integer value to a 'Color' based on your requirements).

Using 1x1 borders means you'll run into issues unless you SnapToDevicePixels. You can of course make the border larger than 1x1 pixel - you'll need a value converter on the 'Canvas.Left' and 'Canvas.Top' bindings that multiplies the bound value by the size of the "square" that you need.

(This code is mostly dumped from memory so no guarantees it will work out of the box, but should get you on your way!)

I'd be interested to know how it handles performance. Our application is a "soft real-time" system that updates hundreds of controls every 100ms, but still a long way off the numbers you are dealing with!

Upvotes: 1

Congenital Optimist
Congenital Optimist

Reputation: 486

For drawing on Canvas I have created custom controls derived from FrameworkElement. If there are smaller things inside the control (like cells in your case) one could have multiple Visual objects inside that FrameworkElement.

Here's an (untested) example of class that implements something like this. It does not override OnRender but just populates the Visuals inside. It is up to you how to define and when to update the inner Visuals (meaning the implementation of Populate function).

public class CanvasDrawingObject : FrameworkElement
{
    VisualCollection m_children;
    DrawingVisual m_drawingVisual;

    public CanvasDrawingObject( )
        : base( )
    {
        m_children = new VisualCollection(this);
        m_drawingVisual = new DrawingVisual();
        m_children.Add(m_drawingVisual);
    }

    public void PopulateMyContent( )
    {
        // place it somewhere
        Canvas.SetLeft(this, 7);
        Canvas.SetTop(this, 42);

        // draw
        DrawingContext context = this.DrawingVisual.RenderOpen();
        if (context != null)
        {
            // do your drawing
            //context.DrawLine(... );
            context.Close();
        }
    }

    protected override int VisualChildrenCount
    {
        get { return m_children.Count; }
    }

    protected override Visual GetVisualChild(int index)
    {
        if (index < 0 || index >= m_children.Count)
            throw new ArgumentOutOfRangeException();

        return m_children[index];
    }
}

That said, the application where I used similar construct has no true real-time requirements, but it is used for drawing lots of vector graphics and text.

Upvotes: 1

Adam Brown
Adam Brown

Reputation: 1729

I think you might want to use a Canvas to output the necessary display. Given your very high refresh rate, it might also be necessary to do some frame synchronisation using the CompositionTarget.Rendering event.

Upvotes: 0

Related Questions