diff --git a/ScreenToGif/Controls/InkCanvasExtended.cs b/ScreenToGif/Controls/InkCanvasExtended.cs
index a3e3e6ba..7699bfa8 100644
--- a/ScreenToGif/Controls/InkCanvasExtended.cs
+++ b/ScreenToGif/Controls/InkCanvasExtended.cs
@@ -1,7 +1,12 @@
-using System.Windows;
+using ScreenToGif.ImageUtil;
+using System;
+using System.Reflection;
+using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
+using System.Windows.Input.StylusPlugIns;
using System.Windows.Media;
+using System.Windows.Threading;
namespace ScreenToGif.Controls
{
@@ -11,6 +16,23 @@ namespace ScreenToGif.Controls
///
public class InkCanvasExtended : InkCanvas
{
+ ///
+ /// Base Constructor pluse creation of the Inking Overlay
+ ///
+ public InkCanvasExtended()
+ : base()
+ {
+ // We add a child image to use as an inking overlay because we need
+ // to compose the inking thread's real-time data with the UI data
+ // that contains already-captured strokes.
+ InkingImage = new Image();
+ this.Children.Add(InkingImage);
+ // Inking overlay should be in front, since it is a "newer" pre-stroke
+ // element and once its stroke is brought over to the UI thread it will
+ // be in front of previous strokes.
+ Canvas.SetZIndex(InkingImage, 99);
+
+ }
///
/// Gets or set the eraser shape
///
@@ -25,6 +47,104 @@ public class InkCanvasExtended : InkCanvas
public static readonly DependencyProperty EraserShapeProperty = DependencyProperty.Register("EraserShape", typeof (StylusShape), typeof (InkCanvasExtended),
new UIPropertyMetadata(new RectangleStylusShape(10, 10), OnEraserShapePropertyChanged));
+ ///
+ /// Overlay Image that receives DrawingGroup contents from the real-time inking thread upon UpdateInkOverlay()
+ ///
+ private Image InkingImage { get; }
+
+ ///
+ /// Tiny transparent corner point to prevent whitespace before DrawingGroup.TopLeft being cropped by Image control
+ ///
+ private readonly GeometryDrawing TransparentCornerPoint =
+ new GeometryDrawing(
+ new SolidColorBrush(Color.FromArgb(0, 255, 255, 255)),
+ new Pen(Brushes.White, 0.1),
+ new LineGeometry(new Point(0, 0), new Point(0.1,0.1))
+ ).GetAsFrozen() as GeometryDrawing;
+
+ ///
+ /// Re-initializes InkingImage (the overlay that receives DrawingGroups from the inking thread)
+ ///
+ public void ResetInkOverlay()
+ {
+ InkingImage.Source = new DrawingImage(new DrawingGroup());
+ }
+
+ ///
+ /// Updates the inking overlay from the real-time inking thread
+ ///
+ ///
+ public void UpdateInkOverlay()
+ {
+ // Set the source to nothing at first so we don't see things in the wrong place
+ // momentarily when adjusting the margins later on.
+ // Also good for when returning when no inking exists, but could do immediately
+ // before the return in that case.
+ ResetInkOverlay();
+
+ // The following two HostVisuals are children of the _mainContainerVisual in this thread,
+ // and these won't normally get composed into the UI thread from the inking thread's
+ // element tree until a stroke is complete.
+ var rawInkHostVisuals = new[]
+ {
+ typeof(DynamicRenderer).GetField("_rawInkHostVisual1", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this.DynamicRenderer ),
+ typeof(DynamicRenderer).GetField("_rawInkHostVisual2", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this.DynamicRenderer )
+ };
+
+ // We just need to get a handle on the _renderingThread (inking thread), and its ThreadDispatcher.
+ var lRenderingThread = typeof(DynamicRenderer).GetField("_renderingThread", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this.DynamicRenderer);
+ if(lRenderingThread == null)
+ {
+ return;
+ }
+ PropertyInfo lDynamicDispProperty = lRenderingThread.GetType().GetProperty("ThreadDispatcher", BindingFlags.Instance |
+ BindingFlags.NonPublic |
+ BindingFlags.Public);
+
+ // The ThreadDispatcher derives from System.Windows.Threading.Dispatcher,
+ // which is a little simpler than using more reflection to get the derived type.
+ Dispatcher dispatcher = lDynamicDispProperty.GetValue(lRenderingThread, null) as Dispatcher;
+ if (lRenderingThread == null)
+ {
+ return;
+ }
+
+ // We invoke the inking thread to get the visual targets from the real time host visuals, then
+ // use BFS to grab all the DrawingGroups as frozen into a new DrawingGroup, which we return
+ // as frozen to the UI thread.
+ DrawingGroup inkDrawingGroup = dispatcher.Invoke(DispatcherPriority.Send,
+ (DispatcherOperationCallback)delegate (object rawInkHVs)
+ {
+ // We've got the field references from the other thread now, so we just get their
+ // VisualTarget properties, which is where we'll make a magical RootVisual call.
+ object[] lObjects = rawInkHVs as object[];
+ PropertyInfo vtProperty = lObjects[0].GetType().GetProperty("VisualTarget", BindingFlags.Instance |
+ BindingFlags.NonPublic |
+ BindingFlags.Public);
+ VisualTarget visualTarget1 = vtProperty.GetValue(lObjects[0], null) as VisualTarget;
+ VisualTarget visualTarget2 = vtProperty.GetValue(lObjects[1], null) as VisualTarget;
+
+ // The RootVisual builds the visual when property get is called. We then
+ // all all its descendent DrawingGroups to this DrawingGroup that we are
+ // just using as a collection to return from the thread.
+ DrawingGroup drawingGroups = new DrawingGroup();
+
+ // This is a little jenky:
+ // We add a point in the top left so the DrawingGroup will be
+ // appropriately offset from the top left of the Image container.
+ drawingGroups.Children.Add(TransparentCornerPoint);
+
+ // We try to add both the visualTagets (in case both are active)
+ visualTarget1?.RootVisual?.visualToFrozenDrawingGroup(drawingGroups);
+ visualTarget2?.RootVisual?.visualToFrozenDrawingGroup(drawingGroups);
+
+ return drawingGroups.GetAsFrozen();
+ },
+ rawInkHostVisuals) as DrawingGroup;
+
+ // At this point, just update the image source with the drawing image.
+ InkingImage.Source = new DrawingImage(inkDrawingGroup);
+ }
///
/// Event to handle the property change
///
@@ -39,4 +159,5 @@ private static void OnEraserShapePropertyChanged(DependencyObject d, DependencyP
canvas.RenderTransform = new MatrixTransform();
}
}
+
}
\ No newline at end of file
diff --git a/ScreenToGif/ImageUtil/ImageMethods.cs b/ScreenToGif/ImageUtil/ImageMethods.cs
index bd31793c..ce8faf41 100644
--- a/ScreenToGif/ImageUtil/ImageMethods.cs
+++ b/ScreenToGif/ImageUtil/ImageMethods.cs
@@ -1504,6 +1504,33 @@ public static Icon ToIcon(this ImageSource imageSource)
}
}
+ ///
+ /// Visits all the descendents of the visual using DFS and adds frozen copies
+ /// of their drawing objects as children to the drawingGroup argument.
+ ///
+ /// The visual to convert to a DrawingGroup
+ /// The target DrawingGroup to be populated
+ public static void VisualToFrozenDrawingGroup(this Visual visual, DrawingGroup drawingGroup)
+ {
+ if (visual == null)
+ {
+ return;
+ }
+ Queue visualQueue = new Queue(new[] { visual });
+ while (visualQueue.Count > 0)
+ {
+ Visual visualDescendent = visualQueue.Dequeue();
+ for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visualDescendent); i++)
+ {
+ visualQueue.Enqueue(VisualTreeHelper.GetChild(visualDescendent, i) as Visual);
+ }
+ DrawingGroup vdg = VisualTreeHelper.GetDrawing(visualDescendent);
+ if (vdg != null)
+ {
+ drawingGroup.Children.Add(vdg.GetAsFrozen() as DrawingGroup);
+ }
+ }
+ }
#endregion
}
}
\ No newline at end of file
diff --git a/ScreenToGif/Windows/Board.xaml.cs b/ScreenToGif/Windows/Board.xaml.cs
index 605fcdce..3fc0fc7a 100644
--- a/ScreenToGif/Windows/Board.xaml.cs
+++ b/ScreenToGif/Windows/Board.xaml.cs
@@ -3,7 +3,9 @@
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
+using System.Windows.Controls;
using System.Windows.Input;
+using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using ScreenToGif.ImageUtil;
@@ -51,7 +53,7 @@ public partial class Board
#endregion
- #region Inicialization
+ #region Initialization
public Board(bool hideBackButton = true)
{
@@ -371,6 +373,9 @@ private void AutoFitButtons()
private void Normal_Elapsed(object sender, EventArgs e)
{
var fileName = $"{Project.FullPath}{FrameCount}.png";
+
+ // We call this only when we do a capture for efficiency's sake.
+ MainInkCanvas.UpdateInkOverlay();
//TODO: GetRender fails to create useful image when the control has decimals values as size.
@@ -398,6 +403,8 @@ private void LightWindow_SizeChanged(object sender, SizeChangedEventArgs e)
private void DiscardButton_Click(object sender, RoutedEventArgs e)
{
+ MainInkCanvas.ResetInkOverlay();
+
_capture.Stop();
FrameRate.Stop();
FrameCount = 0;