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;