Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #453 ink strokes not captured until stylus up #540

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
120 changes: 119 additions & 1 deletion ScreenToGif/Controls/InkCanvasExtended.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -11,6 +16,22 @@ namespace ScreenToGif.Controls
/// </summary>
public class InkCanvasExtended : InkCanvas
{
/// <summary>
/// Base Constructor pluse creation of the Inking Overlay
/// </summary>
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);
}
/// <summary>
/// Gets or set the eraser shape
/// </summary>
Expand All @@ -25,6 +46,102 @@ public class InkCanvasExtended : InkCanvas
public static readonly DependencyProperty EraserShapeProperty = DependencyProperty.Register("EraserShape", typeof (StylusShape), typeof (InkCanvasExtended),
new UIPropertyMetadata(new RectangleStylusShape(10, 10), OnEraserShapePropertyChanged));

/// <summary>
/// Overlay Image that receives DrawingGroup contents from the real-time inking thread upon UpdateInkOverlay()
/// </summary>
private Image InkingImage { get; }

/// <summary>
/// Re-initializes InkingImage (the overlay that receives DrawingGroups from the inking thread)
/// </summary>
public void ResetInkOverlay()
{
InkingImage.Source = new DrawingImage(new DrawingGroup());
}

/// <summary>
/// Updates the inking overlay from the real-time inking thread
/// </summary>
/// <returns></returns>
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();
visualTarget1?.RootVisual?.visualToFrozenDrawingGroup(drawingGroups);
visualTarget2?.RootVisual?.visualToFrozenDrawingGroup(drawingGroups);
return drawingGroups.GetAsFrozen();
},
rawInkHostVisuals) as DrawingGroup;


// This is a little jenky, but we need to set the image margins otherwise
// the drawing content will be cropped and aligned top-left despite inking
// very far from origin. Because we set image to an empty drawingImage
// earlier, we don't need to worry about things visibly jumping on screen.
// Important note: Negatives are okay. If we set X,Y = (0,0), then the
// DrawingGroup.Bounds.TopLeft = (-3,0) [because user inked off the canvas
// for instance], then the point starting at (-3,0) will be shifted to (0,0)
// and our entire drawing will be moved over by 3 pixels, so here we just
// set the margin to whatever *negative* number the TopLeft has in that case,
// and we care only about going over the actual width/height (e.g. infinity)
var bounds = inkDrawingGroup.Bounds.TopLeft;
var w = System.Math.Min(bounds.X, this.ActualWidth);
var h = System.Math.Min(bounds.Y, this.ActualHeight);
InkingImage.Margin = new System.Windows.Thickness(w, h, 0, 0);

// At this point, just update the image source with the drawing image.
InkingImage.Source = new DrawingImage(inkDrawingGroup);
}
/// <summary>
/// Event to handle the property change
/// </summary>
Expand All @@ -39,4 +156,5 @@ private static void OnEraserShapePropertyChanged(DependencyObject d, DependencyP
canvas.RenderTransform = new MatrixTransform();
}
}

}
27 changes: 27 additions & 0 deletions ScreenToGif/ImageUtil/ImageMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,33 @@ public static Icon ToIcon(this ImageSource imageSource)
}
}

/// <summary>
/// Visits all the descendents of the visual using DFS and adds frozen copies
/// of their drawing objects as children to the drawingGroup argument.
/// </summary>
/// <param name="visual">The visual to convert to a DrawingGroup</param>
/// <param name="drawingGroup">The target DrawingGroup to be populated</param>
static public void visualToFrozenDrawingGroup(this Visual visual, DrawingGroup drawingGroup)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, just a minor tweak in here, but I can do later:

public static void VisualToFrozenDrawingGroup

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be updated now. Sorry, we have an awful coding standard at 🏭 to which I am strongly habituated. I think I fixed the flicker as well.

{
if (visual == null)
{
return;
}
Queue<Visual> visualQueue = new Queue<Visual>(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
}
}
9 changes: 8 additions & 1 deletion ScreenToGif/Windows/Board.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,7 +53,7 @@ public partial class Board

#endregion

#region Inicialization
#region Initialization

public Board(bool hideBackButton = true)
{
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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;
Expand Down