Skip to content

Instantly share code, notes, and snippets.

@earthengine
Created October 7, 2016 13:16
Show Gist options
  • Save earthengine/e8c465cf3db09069d2aa2535e68b49d8 to your computer and use it in GitHub Desktop.
Save earthengine/e8c465cf3db09069d2aa2535e68b49d8 to your computer and use it in GitHub Desktop.
A WPF custom control that allows drawing the content in a thread other than the main
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace WPFZoomTest
{
/// <summary>
/// This is a canvas that allows running arbitrary drawing async functions.
///
/// It also support setting a domain with optional background brush.
///
/// It allows dragging the content with mouse, and use the wheel to zoom in/out.
///
/// When double clicking the mouse, it tries to fit the whole domain in the visual area.
/// </summary>
public class AsyncRenderingCanvas : FrameworkElement
{
static AsyncRenderingCanvas()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(AsyncRenderingCanvas), new FrameworkPropertyMetadata(typeof(AsyncRenderingCanvas)));
}
//Domain property
public static readonly DependencyProperty DomainProperty =
DependencyProperty.Register("Domain", typeof(Rect), typeof(AsyncRenderingCanvas));
public static readonly DependencyProperty BackgroundProperty =
DependencyProperty.Register("Background", typeof(Brush), typeof(AsyncRenderingCanvas));
/// <summary>
/// We own a HostVisual as the render result.
///
/// When changing size, fit all contents automatically.
///
/// When loaded without a domain set, use the actual size as the default domain
///
/// We create a background thread to run the drawing function
/// </summary>
public AsyncRenderingCanvas()
{
visual = new HostVisual();
SizeChanged += AsyncRenderingCanvas_SizeChanged;
Loaded += AsyncRenderingCanvas_Loaded;
var tcs = new TaskCompletionSource<VisualTarget>();
var thread = new Thread(() =>
{
renderDispatcher = Dispatcher.CurrentDispatcher;
renderDispatcher.Invoke(() =>
{
tcs.TrySetResult(new VisualTarget(visual));
});
Dispatcher.Run();
});
//The thread must be a STA
thread.SetApartmentState(ApartmentState.STA);
thread.Name = "ZoomCanvas";
//Automatically terminate when the main thread terminates
thread.IsBackground = true;
thread.Start();
//tcs.Task will only finish after the created thread's dispatcher starts running.
vt = tcs.Task.Result;
}
/// <summary>
/// The domain of content. We need this information to know where to show things
/// </summary>
public Rect Domain
{
get { return GetValueInDispacther<Rect>(DomainProperty, t => { }); }
set { SetValueInDispacther(DomainProperty, value, t => { }); }
}
public Brush Background
{
get
{
return GetValueInDispacther<Brush>(BackgroundProperty, r => r?.Freeze());
}
set
{
SetValueInDispacther(BackgroundProperty, value, r => r?.Freeze());
}
}
/// <summary>
/// Scale the content to show everything inside the domain.
/// </summary>
public void ZoomFit()
{
if (Domain.Width <= 0 || Domain.Height <= 0)
{
RenderTransform = Transform.Identity;
return;
}
var s1 = ActualWidth / Domain.Width;
var s2 = ActualHeight / Domain.Height;
var m = new Matrix();
var sc = s1 > s2 ? s2 : s1;
m.Scale(sc, sc);
var porig = m.Transform(new Point(Domain.Width / 2 + Domain.Left, Domain.Height / 2 + Domain.Top));
var pdest = new Point(ActualWidth / 2, ActualHeight / 2);
m.Translate(pdest.X - porig.X, pdest.Y - porig.Y);
RenderTransform = new MatrixTransform(m);
}
/// <summary>
/// Given a drawing task, run the task and update the visual to show the result.
///
/// The drawing task will be given a DrawingContext to call the drawing functions.
///
/// When a drawing is in progress, another attempt to draw will be ignored.
/// </summary>
/// <param name="drawing">The drawing task</param>
/// <returns>Finish when drown</returns>
public async Task DrawAsync(Func<DrawingContext, Task> drawing)
{
if (isDrawing) return;
isDrawing = true;
try
{
await renderDispatcher.InvokeAsync(async () =>
{
var dv = new DrawingVisual();
using (var ctx = dv.RenderOpen())
{
ctx.DrawRectangle(Background, new Pen() { Brush = Brushes.Transparent, Thickness = 0 }, Domain);
await drawing(ctx);
}
vt.RootVisual = dv;
});
}
finally
{
isDrawing = false;
}
}
protected override int VisualChildrenCount { get; } = 1;
protected override Visual GetVisualChild(int index)
{
if (index != 0) throw new ArgumentOutOfRangeException("index");
return visual;
}
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
var pt = hitTestParameters.HitPoint;
return new PointHitTestResult(this, pt);
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
if (e.ClickCount == 2)
{
ZoomFit();
}
else
{
draggingPoint = e.GetPosition(this);
CaptureMouse();
}
}
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left && draggingPoint.HasValue)
{
var pos = e.GetPosition(this);
ReleaseMouseCapture();
var v = draggingPoint.Value - pos;
var m = RenderTransform.Value;
v = m.Transform(v);
m.Translate(-v.X, -v.Y);
RenderTransform = new MatrixTransform(m);
draggingPoint = null;
}
}
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
var s = e.Delta > 0 ? 1.04 : 0.96;
var m = RenderTransform.Value;
var porig = m.Transform(e.GetPosition(this));
m.ScaleAt(s, s, porig.X, porig.Y);
RenderTransform = new MatrixTransform(m);
}
private HostVisual visual;
private Dispatcher renderDispatcher;
private VisualTarget vt;
private bool isDrawing = false;
private Point? draggingPoint = null;
private T GetValueInDispacther<T>(DependencyProperty dp, Action<T> freeze)
{
var tcs = new TaskCompletionSource<T>();
Dispatcher.Invoke(() => {
var r = (T)GetValue(dp);
freeze(r);
tcs.SetResult(r);
});
return tcs.Task.Result;
}
private void SetValueInDispacther<T>(DependencyProperty dp, T value, Action<T> freeze)
{
freeze(value);
Dispatcher.Invoke(() => SetValue(dp, value));
}
private void AsyncRenderingCanvas_Loaded(object sender, RoutedEventArgs e)
{
if (Domain == default(Rect))
Domain = new Rect(0, 0, ActualWidth, ActualHeight);
if (Background == default(Brush))
Background = Brushes.Transparent;
}
private void AsyncRenderingCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
{
ZoomFit();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment