Created
October 7, 2016 13:16
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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