Reputation: 1093
I'm trying to create a large grid which cells have drawn a clickable (click event) ellipsis.
I'm using a Canvas as a parent and adding Ellipses to it Children List but when I try to render 100x100 ellipsis, it lags.
I've tried rendering them with the DrawingVisual and DrawingContext but then, they are not clickable and I won't be able to store ellipsis properties (color, stroke...).
Current code to render the Ellipsis
for (int i = 0; i < this.grid.Cols; i++) {
for (int j = 0; j < this.grid.Rows; j++) {
SolidColorBrush fillBrush = Brushes.Red;
SolidColorBrush strokeBrush = Brushes.Blue;
Ellipse ellipse = new Ellipse() {
Width = this.grid.Radius,
Height = this.grid.Radius,
Fill = fillBrush,
Stroke = strokeBrush,
StrokeThickness = 2
};
Canvas.SetTop(ellipse, (this.grid.Radius * j) + (j * this.grid.Margin));
Canvas.SetLeft(ellipse, (this.grid.Radius * i) + (i * this.grid.Margin));
children.Add(ellipse);
parent.Children.Add(ellipse);
}
}
Any ideas?
Upvotes: 0
Views: 1283
Reputation: 25623
In terms of efficiency, the most 'lightweight' solution would probably be to render a single ellipse to a RenderTargetBitmap
, then have a custom control paint a rectangle with a tiled ImageBrush
to repeat the same ellipse over and over. The nice part about using a RenderTargetBitmap
is that you can let the developer plug in any visual element to tile; it needn't be an ellipse.
You'll need a method to translate between screen coordinates and (column, row)
pairs. You'll know the size of the ellipse's bitmap, so that part should be pretty easy.
To handle subtle changes when the mouse is hovering over or pressing down on an ellipse, you should push some clip geometry into your DrawingContext
to draw everywhere except the region containing the ellipse under the cursor. Then pop the mask and draw a rectangle with an alternate ImageBrush
containing the ellipse in a hover/pressed state.
To handle clicking, you'll need to write some custom hit testing logic. Basically, you'll want something like this:
private bool TryHitTest(Point p, out int column, out int row)
{
column = -1;
row = -1;
if (p.X < 0 || p.X > ActualWidth || p.Y < 0 || p.Y > ActualHeight)
return false;
var image = /* your ellipse bitmap */;
if (image == null)
return false;
var tileWidth = image.Width;
var tileHeight = image.Height;
var x = (int)(p.X % tileWidth);
var y = (int)(p.Y % tileHeight);
// If you want pixel-perfect hit testing, check the alpha channel.
// Otherwise, skip this check.
if (image.GetPixel(x, y).A == 0)
return false;
column = (int)Math.Floor(p.X / tileWidth);
row = (int)Math.Floor(p.Y / tileHeight);
return true;
}
If you want pixel-perfect hit testing, you should copy your RenderTargetBitmap
into a WriteableBitmap
and then use the WriteableBitmapEx library to fetch the color at the hit-tested coordinate and check whether the alpha is non-zero.
To provide click event notification, you'll need to handle the usual mouse events:
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
e.Handled = CaptureMouse();
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (IsMouseCaptured &&
TryHitTest(e.GetPosition(this), out var column, out var row))
{
ReleaseMouseCapture();
e.Handled = true;
//
// Raise 'TileClick' event with 'column' and 'row'.
//
}
}
private (int x, int y) _lastHover;
protected override void OnMouseMove(MouseEventArgs e)
{
TryHitTest(e.GetPosition(this), out var x, out var y);
if (_lastHover.x != x || _lastHover.y != y)
InvalidateVisual();
_lastHover = (x, y);
}
Since there was some confusion about how I described rendering to rectangles, let me clarify: I was not talking about painting a Rectangle
element. The idea was to create a custom control that performs its own rendering, and draws rectangle geometry painted with the tiled image brush. Your OnRender
method would look something like this:
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
var regularBrush = /* regular cell tiled brush */;
var hoverBrush = /* hovered cell brush */;
var fullBounds = new Rect(/* full bounds */);
var hoverBounds = new Rect(/* bounds of hovered cell */);
var hasHoveredCell = /* is there a hovered cell? */;
if (hasHoveredCell)
{
// Draw everywhere *except* the hovered cell.
dc.PushClip(
Geometry.Combine(
new RectangleGeometry(fullBounds),
new RectangleGeometry(hoverBounds),
GeometryCombineMode.Exclude,
Transform.Identity));
}
dc.DrawRectangle(regularBrush , null, fullBounds);
if (hasHoveredCell)
{
// Pop the clip and draw the hovered cell.
dc.Pop();
dc.DrawRectangle(hoverBrush, null, hoverBounds);
}
}
Upvotes: 2
Reputation: 1775
You're hitting a limitation of what WPF can render with it's framework. Ellipses, or all drawing objects, are objects with quite a few properties and events on them. Rendering 10,000 will cause you to lag.
Your best bet is to use DrawingContext and create your own methods that are simpler, and create your own methods to track when something is clicked.
Upvotes: 1