Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Drawing a grid of images with WPF

Tags:

c#

wpf

grid

2d

drawing

I'm trying to draw a grid of images/icons with WPF. The grid dimensions will vary but will typically range from 10x10 to 200x200. The user should be able to click on cells, and some cells will need to update (change image) 10-20 times per second. The grid should be able to grow and shrink in all four directions, and it should be able to switch to a different "slice" of the 3D structure it represents. My goal is to find a suitably efficient method for drawing the grid given those requirements.

My current implementation uses a WPF Grid. I generate row and column definitions at runtime and populate the grid with Line (for the gridlines) and Border (for the cells, since they're currently just on/off) objects at the appropriate row/column. (The Line objects span all the way across.)

Current grid implementation

While expanding the grid (holding down Num6) I found that it draws too slowly to redraw on every operation, so I modified it to simply add a new ColumnDefinition, Line and set of Border objects for each column of growth. That solved my growth issue, and a similar tactic could be used to make shrinking fast as well. For updating individual cells mid-simulation, I could simply store references to the cell objects and change the displayed image. Even changing to a new Z-level could be improved by only updating cell contents instead of rebuilding the entire grid.

However, before I could make all of those optimizations, I ran into another problem. Whenever I mouse over the grid (even at slow/normal speeds) the application's CPU usage spikes. I removed all event handlers from the grid's child elements, but that had no effect. Finally, the only way to keep CPU usage in check was to set IsHitTestVisible = false for the Grid. (Setting this for every child element of the Grid did nothing!)

I believe that using individual controls to build my grid is too intensive and inappropriate for this application, and that using WPF's 2D drawing mechanisms might be more efficient. I'm a beginner to WPF, though, so I'm seeking advice on how to best achieve this. From what little I've read, I might use a DrawingGroup to compose each cell's image together onto a single image for display. I could then use a click event handler for the entire image and compute the coordinates of the clicked cell by the mouse location. That seems messy, though, and I just don't know if there's a better way.

Thoughts?

Update 1:

I took a friend's advice and switched to using a Canvas with a Rectangle for each cell. When I first draw the grid, I store references to all the Rectangle in a two-dimensional array, and then when I update the grid contents, I simply access those references.

private void UpdateGrid()
{
    for (int x = simGrid.Bounds.Lower.X; x <= simGrid.Bounds.Upper.X; x++)
    {
        for (int y = simGrid.Bounds.Lower.Y; y <= simGrid.Bounds.Upper.Y; y++)
        {
            CellRectangles[x, y].Fill = simGrid[x, y, ZLevel] ? Brushes.Yellow : Brushes.White;
        }
    }
}

Drawing the grid initially seems faster, and subsequent updates are definitely faster, but there are still a few problems.

  • No matter how small the area I mouse over is, CPU usage still spikes whenever I mouse over the grid when it has more than a few hundred cells.

  • Updates are still too slow, so when I hold down the up arrow key to change the Z-level (a common use case) the program freezes for seconds at a time and then appears to jump 50 Z-levels at once.

  • Once the grid holds ~5000 cells, updates take on the order of one second. This is prohibitively slow, and 5000 cells fits within typical use cases.

I haven't yet tried the UniformGrid approach because I think it may exhibit the same problems I've already encountered. I might give it a try once I've exhausted a few more options, though.

like image 911
Henry Merriam Avatar asked Apr 14 '11 18:04

Henry Merriam


2 Answers

Your Question

Let's rephrase your question. These are your problem constraints:

  1. You want to draw a grid of dynamic sizes
  2. Each cell changes on/off rapidly
  3. Grid sizes change rapidly
  4. There are a large number of cells (i.e. the grid dimensions are not trivial)
  5. You want all these changes to occur with a fast frame rate (e.g. 30fps)
  6. The positioning and layout of the grid and cells are deterministic, simple and not very interactive

Judging from these constraints, you can immediately see that you're using the wrong approach.

Reqruiement: Fast Refresh of Deterministic Positions with Little Interactivity

Fast refresh frame rate + many changes per frame + large number of cells + one WPF object per cell = dissaster.

Unless you have very fast graphics hardware and a very fast CPU, your frame rate is always going to suffer with increases in grid dimensions.

What your problem dictates is more like a video game or a CAD drawing program with dynamic zooming. It is lesss like a normal desktop application.

Immediate Mode vs. Retained Mode Drawing

In other words, you want "immediate mode" drawing, not "retained mode" drawing (WPF is retained mode). That is because your constraints do not require much of the functionalities provided by treating each cell as a separate WPF object.

For example, you won't need layout support because each cell's position is deterministic. You won't need hit-testing support because, again, positions are deterministic. You won't need container support, because each cell is a simple rectangle (or an image). You won't need complex formatting support (e.g. transparency, rounded borders etc.) because nothing overlap. In other words, there is no benefit to use a Grid (or UniformGrid) and one WPF object per cell.

Concept of Immediate Mode Drawing to Buffer Bitmap

In order to achieve the frame rate you required, essentially you'll be drawing to a large bitmap (which covers the whole screen) -- or a "screen buffer". For your cells, simply draw to this bitmap/buffer (perhaps using GDI). Hit testing is easy as the cell positions are all deterministic.

This method will be fast because there is only one object (the screen buffer bitmap). You can either refresh the entire bitmap for each frame, or update only those screen positions that change, or an intelligent combination of these.

Notice that although you are drawing a "grid" here, you don't use a "Grid" element. Choose your algorithm and your data structures based on what your problem constraints are, not what it looks like to be the obvious solution -- in other words, a "Grid" may not be the right solution for drawing a "grid".

Immediate Mode Drawing in WPF

WPF is based on DirectX, so essentially it is already using a screen buffer bitmap (called the back-buffer) behind the scene.

The way you to use immediate mode drawing in WFP is to create the cells as GeometryDrawing's (not Shape's, which is retained mode). GemoetryDrawing is usually extremely fast because GemoetryDrawing objects map directly to DirectX primitives; they are not laid out and tracked individually as Framework Elements, so they are very light-weight -- you can have a large number of them without adversely affecting performance.

Select ths GeometryDrawing's into a DrawingImage (this is essentially your back-buffer) and you get a fast-changing image for your screen. Behind the scene, WPF does exactly what you expect it to do -- i.e. draw each rectangle that changes onto the image buffer.

Again, do not use Shape's -- these are Framework Elements and will incur significant overheads as they participate in layout. For instance, DO NOT USE Rectangle, but use RectangleGeometry instead.

Optimizations

Several more optimizations you may consider:

  1. Reuse GeometryDrawing objects -- just change position and size
  2. If the grid has a maximum size, pre-create the objects
  3. Only modify those GeometryDrawing objects that changed -- so WPF won't unnecessarily refresh them
  4. Fill the bitmap in "stages" -- that is, for different zoom levels, always update to a grid that is much larger than the previous one, and use scaling to scale it back. For example, move from a 10x10 grid directly to a 20x20 grid, but scale it back by 55% to show 11x11 squares. This way, when zooming from 11x11 all the way to 20x20, your GeometryDrawing objects are never changed; only the scaling on the bitmap is changed, making it extremely fast to update.

EDIT: Do Frame by Frame Rendering

Override OnRender as suggested in the answer awarded the bounty for this question. Then you essentially draw the entire scene on a canvas.

Use DirectX for Absolute Control

Alternatively, consider using raw DirectX if you want absolute control over each frame.

like image 72
Stephen Chung Avatar answered Sep 23 '22 18:09

Stephen Chung


You can write you own custom control (based on Canvas, Panel, etc...) and override OnRender, like this:

   public class BigGrid : Canvas
    {
        private const int size = 3; // do something less hardcoded

        public BigGrid()
        {
        }

        protected override void OnRender(DrawingContext dc)
        {
            Pen pen = new Pen(Brushes.Black, 0.1);

            // vertical lines
            double pos = 0;
            int count = 0;
            do
            {
                dc.DrawLine(pen, new Point(pos, 0), new Point(pos, DesiredSize.Height));
                pos += size;
                count++;
            }
            while (pos < DesiredSize.Width);

            string title = count.ToString();

            // horizontal lines
            pos = 0;
            count = 0;
            do
            {
                dc.DrawLine(pen, new Point(0, pos), new Point(DesiredSize.Width, pos));
                pos += size;
                count++;
            }
            while (pos < DesiredSize.Height);

            // display the grid size (debug mode only!)
            title += "x" + count;
            dc.DrawText(new FormattedText(title, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, new Typeface("Arial"), 20, Brushes.White), new Point(0, 0));
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            return availableSize;
        }
    }

I can successfully draw and resize a 400x400 grid with this on y laptop (not a competition machine...).

There are more fancy and better ways of doing this (using StreamGeometry on the DrawingContext), but this is at least a good test workbench.

Of course, you'll have to override the HitTestXXX methods.

like image 20
Simon Mourier Avatar answered Sep 19 '22 18:09

Simon Mourier