This commit is contained in:
2025-08-23 21:39:21 +08:00
parent 7bac32e3c4
commit f67b4db4ba
562 changed files with 37981 additions and 38280 deletions
@@ -0,0 +1,113 @@
using InkCanvasForClass.IccInkCanvas.Utils.Threading;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
namespace InkCanvasForClass.IccInkCanvas {
/// <summary>
/// 基于 InkPresenter 优化和魔改的 InkBoard 控件
/// </summary>
public class InkBoard : FrameworkElement {
private readonly VisualCollection _children;
// Tuple 1为Dispatcher的Guid2为对应Dispatcher
private List<Tuple<Guid, Dispatcher>> _asyncDispatchers;
private const int HalfIntMax = 1073741823;
private const float HalfFloatMax = 1073741823f;
private const int QuarterSInt = 536756397;
private const float QuarterSFloat = 536756397f;
public InkBoard() {
VisualCacheMode = new BitmapCache(0);
ClipToBounds = true;
_children = new VisualCollection(this);
test();
}
#region Dispatchers
/// <summary>
/// 创建一个 Dispatcher
/// </summary>
/// <param name="customWorkerDispatcherName">如果可用,会使用这个名字创建Dispatcher(但是不会忽略GUID</param>
/// <returns>Tuple1为Guid2为Dispatcher</returns>
private async Task<Tuple<Guid, Dispatcher>> CreateDispathcer(string customWorkerDispatcherName = "IccInkCanvas") {
var guid = Guid.NewGuid();
var dispatcher = await UIDispatcher.RunNewAsync($"{customWorkerDispatcherName}_" + guid);
return new Tuple<Guid, Dispatcher>(guid, dispatcher);
}
/// <summary>
/// 注册一个新的 Dispatcher,这将会把 Dispatcher 给记录到 _asyncDispatchers 中
/// </summary>
/// <param name="customWorkerDispatcherName">如果可用,会使用这个名字创建Dispatcher(但是不会忽略GUID</param>
private async void RegisterNewDispatcher(string customWorkerDispatcherName = "IccInkCanvas") {
var dispatcher = await CreateDispathcer(customWorkerDispatcherName);
_asyncDispatchers.Add(dispatcher);
}
/// <summary>
/// 让一个 Dispatcher 停止工作并销毁
/// </summary>
/// <param name="dispatcher"></param>
private void DisposeDispatcher(Dispatcher dispatcher) {
dispatcher.BeginInvokeShutdown(DispatcherPriority.Send);
GC.Collect();
}
/// <summary>
/// 判断一个 Dispatcher 是否被挂起
/// </summary>
/// <param name="dispatcher"></param>
/// <returns></returns>
private static async Task<bool> CheckDispatcherHangAsync(Dispatcher dispatcher)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
_ = dispatcher.InvokeAsync(() => taskCompletionSource.TrySetResult(true));
await Task.WhenAny(taskCompletionSource.Task, Task.Delay(TimeSpan.FromMilliseconds(1500)));
return taskCompletionSource.Task.IsCompleted is false;
}
#endregion
/// <summary>
/// 创建跨线程 InkPresenter,使用传入的 Dispatcher
/// </summary>
/// <returns></returns>
private async Task CreateInkPresenterWithDispatcher() {
await UIDispatcher.RunNewAsync("IccInkCanvas_" + Guid.NewGuid());
}
private async Task test() {
var di = new IccDispatcherInkCanvasInfo();
await di.InitDispatcher();
var control = await di.Dispatcher.InvokeAsync(() => new TextBlock() {
Text = "Helloworld!",
Foreground = new SolidColorBrush(Colors.Black),
FontSize = 24,
});
var dc = new DispatcherContainer();
await dc.SetChildAsync(control);
_children.Add(dc);
InvalidateVisual();
}
protected override int VisualChildrenCount {
get { return _children.Count; }
}
protected override Visual GetVisualChild(int index) {
if (index < 0 || index >= _children.Count) new ArgumentOutOfRangeException();
return _children[index];
}
}
}
@@ -0,0 +1,94 @@
<UserControl x:Class="InkCanvasForClass.IccInkCanvas.IccBoard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:InkCanvasForClass.IccInkCanvas"
mc:Ignorable="d" d:DesignHeight="512" d:DesignWidth="512">
<UserControl.Resources>
<DrawingImage x:Key="RectangleEraserImageSource">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V56 H38 V0 H0 Z">
<GeometryDrawing Brush="#FFF2EEEB" Geometry="F1 M38,56z M0,0z M0,4C0,1.79086,1.79086,0,4,0L34,0C36.2091,0,38,1.79086,38,4L38,52C38,54.2091,36.2091,56,34,56L4,56C1.79086,56,0,54.2091,0,52L0,4z" />
<GeometryDrawing Brush="#FFCDCDCD" Geometry="F0 M38,56z M0,0z M34,1L4,1C2.34315,1,1,2.34315,1,4L1,52C1,53.6569,2.34315,55,4,55L34,55C35.6569,55,37,53.6569,37,52L37,4C37,2.34315,35.6569,1,34,1z M4,0C1.79086,0,0,1.79086,0,4L0,52C0,54.2091,1.79086,56,4,56L34,56C36.2091,56,38,54.2091,38,52L38,4C38,1.79086,36.2091,0,34,0L4,0z" />
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M38,56z M0,0z M12,19.5C12,18.1193,13.1193,17,14.5,17L14.5,17C15.8807,17,17,18.1193,17,19.5L17,36.5C17,37.8807,15.8807,39,14.5,39L14.5,39C13.1193,39,12,37.8807,12,36.5L12,19.5z" />
<GeometryDrawing Geometry="F0 M38,56z M0,0z M11.5,19.5C11.5,17.8431 12.8431,16.5 14.5,16.5 16.1569,16.5 17.5,17.8431 17.5,19.5L17.5,36.5C17.5,38.1569 16.1569,39.5 14.5,39.5 12.8431,39.5 11.5,38.1569 11.5,36.5L11.5,19.5z M14.5,17.5C13.3954,17.5,12.5,18.3954,12.5,19.5L12.5,36.5C12.5,37.6046 13.3954,38.5 14.5,38.5 15.6046,38.5 16.5,37.6046 16.5,36.5L16.5,19.5C16.5,18.3954,15.6046,17.5,14.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M38,56z M0,0z M21,19.5C21,18.1193,22.1193,17,23.5,17L23.5,17C24.8807,17,26,18.1193,26,19.5L26,36.5C26,37.8807,24.8807,39,23.5,39L23.5,39C22.1193,39,21,37.8807,21,36.5L21,19.5z" />
<GeometryDrawing Geometry="F0 M38,56z M0,0z M20.5,19.5C20.5,17.8431 21.8431,16.5 23.5,16.5 25.1569,16.5 26.5,17.8431 26.5,19.5L26.5,36.5C26.5,38.1569 25.1569,39.5 23.5,39.5 21.8431,39.5 20.5,38.1569 20.5,36.5L20.5,19.5z M23.5,17.5C22.3954,17.5,21.5,18.3954,21.5,19.5L21.5,36.5C21.5,37.6046 22.3954,38.5 23.5,38.5 24.6046,38.5 25.5,37.6046 25.5,36.5L25.5,19.5C25.5,18.3954,24.6046,17.5,23.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="EllipseEraserImageSource">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V56 H56 V0 H0 Z">
<GeometryDrawing Brush="#FFF2EEEB" Geometry="F1 M56,56z M0,0z M0,28C0,12.536,12.536,0,28,0L28,0C43.464,0,56,12.536,56,28L56,28C56,43.464,43.464,56,28,56L28,56C12.536,56,0,43.464,0,28L0,28z" />
<GeometryDrawing Brush="#FFCDCDCD" Geometry="F0 M56,56z M0,0z M1,28C1,42.9117 13.0883,55 28,55 42.9117,55 55,42.9117 55,28 55,13.0883 42.9117,1 28,1 13.0883,1 1,13.0883 1,28z M28,0C12.536,0 0,12.536 0,28 0,43.464 12.536,56 28,56 43.464,56 56,43.464 56,28 56,12.536 43.464,0 28,0z" />
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M56,56z M0,0z M21,19.5C21,18.1193,22.1193,17,23.5,17L23.5,17C24.8807,17,26,18.1193,26,19.5L26,36.5C26,37.8807,24.8807,39,23.5,39L23.5,39C22.1193,39,21,37.8807,21,36.5L21,19.5z" />
<GeometryDrawing Geometry="F0 M56,56z M0,0z M20.5,19.5C20.5,17.8431 21.8431,16.5 23.5,16.5 25.1569,16.5 26.5,17.8431 26.5,19.5L26.5,36.5C26.5,38.1569 25.1569,39.5 23.5,39.5 21.8431,39.5 20.5,38.1569 20.5,36.5L20.5,19.5z M23.5,17.5C22.3954,17.5,21.5,18.3954,21.5,19.5L21.5,36.5C21.5,37.6046 22.3954,38.5 23.5,38.5 24.6046,38.5 25.5,37.6046 25.5,36.5L25.5,19.5C25.5,18.3954,24.6046,17.5,23.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD1CFCD" Geometry="F1 M56,56z M0,0z M30,19.5C30,18.1193,31.1193,17,32.5,17L32.5,17C33.8807,17,35,18.1193,35,19.5L35,36.5C35,37.8807,33.8807,39,32.5,39L32.5,39C31.1193,39,30,37.8807,30,36.5L30,19.5z" />
<GeometryDrawing Geometry="F0 M56,56z M0,0z M29.5,19.5C29.5,17.8431 30.8431,16.5 32.5,16.5 34.1569,16.5 35.5,17.8431 35.5,19.5L35.5,36.5C35.5,38.1569 34.1569,39.5 32.5,39.5 30.8431,39.5 29.5,38.1569 29.5,36.5L29.5,19.5z M32.5,17.5C31.3954,17.5,30.5,18.3954,30.5,19.5L30.5,36.5C30.5,37.6046 31.3954,38.5 32.5,38.5 33.6046,38.5 34.5,37.6046 34.5,36.5L34.5,19.5C34.5,18.3954,33.6046,17.5,32.5,17.5z">
<GeometryDrawing.Brush>
<SolidColorBrush Color="#FF6F6F6F" Opacity="0.25" />
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</UserControl.Resources>
<Grid Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualHeight}">
<Canvas Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualHeight}"
ClipToBounds="True" x:Name="InkCanvasHostCanvas" x:FieldModifier="private" Loaded="HostCanvas_Loaded" SizeChanged="HostCanvas_SizeChanged">
<Canvas.Clip>
<CombinedGeometry GeometryCombineMode="Exclude" x:Name="HostCanvasClipGeometry" x:FieldModifier="private">
<CombinedGeometry.Geometry1>
<RectangleGeometry x:Name="HostCanvasClipGeometry1" x:FieldModifier="private"/>
</CombinedGeometry.Geometry1>
</CombinedGeometry>
</Canvas.Clip>
</Canvas>
<Canvas Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualHeight}"
ClipToBounds="True" x:Name="InkCanvasWrapperCanvas" x:FieldModifier="private" Visibility="Visible">
<local:IccInkCanvas x:Name="WrapperInkCanvas" Background="Transparent" x:FieldModifier="private" Loaded="WrapperInkCanvas_Loaded">
<local:IccInkCanvas.RenderTransform>
<MatrixTransform x:Name="WrapperInkCanvasMatrixTransform" x:FieldModifier="private"/>
</local:IccInkCanvas.RenderTransform>
</local:IccInkCanvas>
</Canvas>
<Canvas Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualHeight}"
ClipToBounds="True" Loaded="EraserOverlayCanvas_Loaded" x:Name="EraserCanvas" Background="Transparent" x:FieldModifier="private">
<Image Canvas.Left="0" Canvas.Top="0" RenderTransformOrigin="0,0" Width="0" x:Name="EraserFeedback" x:FieldModifier="private">
<Image.RenderTransform>
<TranslateTransform x:Name="EraserFeedbackTranslateTransform" x:FieldModifier="private"/>
</Image.RenderTransform>
</Image>
</Canvas>
<Canvas Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualHeight}"
ClipToBounds="True" Loaded="RectangleAreaEraserCanvas_Loaded" x:Name="RectangleAreaEraserCanvas" Background="Transparent" x:FieldModifier="private">
<Border Visibility="Collapsed" x:Name="AreaErasingFeedback" x:FieldModifier="private" Width="128" Height="128" BorderBrush="#9971717a" BorderThickness="1" Background="#3371717a">
<Border VerticalAlignment="Center" HorizontalAlignment="Center" Width="68" Height="22" CornerRadius="10" Background="#b91c1c">
<TextBlock Margin="8,2" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="White">区域擦除</TextBlock>
</Border>
</Border>
</Canvas>
<Border Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Path=ActualHeight}"
x:Name="RoamingHitTestBorder" x:FieldModifier="private" Background="Transparent" Loaded="RoamingHitTestBorder_Loaded"/>
</Grid>
</UserControl>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
using InkCanvasForClass.IccInkCanvas.Utils.Threading;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
namespace InkCanvasForClass.IccInkCanvas {
public class IccBoardPage {
public Guid GUID { get; set; }
public Dispatcher Dispatcher { get; set; }
public DispatcherContainer Container { get; set; }
public InkCanvas InkCanvas { get; set; }
public TimeMachine TimeMachine { get; set; } = new TimeMachine();
public long LastStrokeID { get; set; } = 0;
public Matrix TransformMatrix { get; set; } = new Matrix();
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Threading;
using InkCanvasForClass.IccInkCanvas.Utils.Threading;
namespace InkCanvasForClass.IccInkCanvas {
public class IccDispatcherInkCanvasInfo {
public Dispatcher Dispatcher { get; private set; }
public Guid GUID { get; private set; } = Guid.NewGuid();
public async Task InitDispatcher() {
Dispatcher = await UIDispatcher.RunNewAsync("IccInkCanvas_" + GUID.ToString());
}
}
}
@@ -0,0 +1,311 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Input.StylusPlugIns;
using System.Windows.Media;
using InkCanvasForClass.IccInkCanvas.Settings;
namespace InkCanvasForClass.IccInkCanvas {
class CustomDynamicRenderer : DynamicRenderer {
private class StylusProcessedCallbackData {
public int StylusDeviceID;
public int TabletDeviceID;
}
private Point prevPoint;
private IccInkCanvas _inkCanvas;
private List<StylusPoint> pointsList = new List<StylusPoint>();
private StylusProcessedCallbackData _stylusProcessedCallbackData;
public InputtingDeviceType InputType = InputtingDeviceType.None;
private void ClearPointsList() {
pointsList.Clear();
}
private void PushPoint(StylusPoint point) {
pointsList.Add(point);
if (pointsList.Count > 15) {
pointsList.RemoveRange(0,pointsList.Count - 15);
}
}
public CustomDynamicRenderer(IccInkCanvas iccInkCanvas) {
_inkCanvas = iccInkCanvas;
}
protected override void OnStylusDown(RawStylusInput rawStylusInput) {
prevPoint = new Point(double.NegativeInfinity, double.NegativeInfinity);
ClearPointsList();
// 判断当前设备类型
var styDevice = rawStylusInput.StylusDeviceId;
var tabletDevice = rawStylusInput.TabletDeviceId;
var pts = rawStylusInput.GetStylusPoints();
if (pts.Count == 1) {
PushPoint(pts.Single());
} else if (pts.Count > 1) {
PushPoint(pts[pts.Count-1]);
}
base.OnStylusDown(rawStylusInput);
_stylusProcessedCallbackData = new StylusProcessedCallbackData() {
StylusDeviceID = styDevice,
TabletDeviceID = tabletDevice,
};
}
protected override void OnStylusDownProcessed(object callbackData, bool targetVerified) {
var data = _stylusProcessedCallbackData;
var tabletDevices = Tablet.TabletDevices;
if (data.StylusDeviceID == 0 && data.TabletDeviceID == 0) InputType = InputtingDeviceType.Mouse;
foreach (TabletDevice tabletDevice in tabletDevices) {
if (tabletDevice.Id == data.TabletDeviceID && tabletDevice.Type == TabletDeviceType.Stylus)
InputType = InputtingDeviceType.Stylus;
if (tabletDevice.Id == data.TabletDeviceID && tabletDevice.Type == TabletDeviceType.Touch)
InputType = InputtingDeviceType.Touch;
}
base.OnStylusDownProcessed(callbackData, targetVerified);
}
protected override void OnStylusMove(RawStylusInput rawStylusInput) {
base.OnStylusMove(rawStylusInput);
var pts = rawStylusInput.GetStylusPoints();
if (pts.Count == 1) {
PushPoint(pts.Single());
} else if (pts.Count > 1) {
if (InputType != InputtingDeviceType.Stylus) PushPoint(pts[pts.Count-1]);
}
}
protected override void OnStylusUpProcessed(object callbackData, bool targetVerified) {
//InputType = InputtingDeviceType.None;
// TODO: 触摸支持
base.OnStylusUpProcessed(callbackData, targetVerified);
}
private void DrawBeautifulNibStroke(DrawingContext drawingContext) {
try {
var sp = new StylusPointCollection();
var n = pointsList.Count - 1;
var pressure = 0.1;
var x = 10;
if (n == 1) return;
if (n >= x) {
for (var i = 0; i < n - x; i++) {
var point = new StylusPoint();
point.PressureFactor = (float)0.5;
point.X = pointsList[i].X;
point.Y = pointsList[i].Y;
sp.Add(point);
}
for (var i = n - x; i <= n; i++) {
var point = new StylusPoint();
point.PressureFactor = (float)((0.5 - pressure) * (n - i) / x + pressure);
point.X = pointsList[i].X;
point.Y = pointsList[i].Y;
sp.Add(point);
}
} else {
for (var i = 0; i <= n; i++) {
var point = new StylusPoint();
point.PressureFactor = (float)(0.4 * (n - i) / n + pressure);
point.X = pointsList[i].X;
point.Y = pointsList[i].Y;
sp.Add(point);
}
}
var da = DrawingAttributes.Clone();
da.Width -= 0.5;
da.Height -= 0.5;
var stk = new Stroke(sp, da);
stk.Draw(drawingContext);
} catch {}
}
private void DrawSolidNibStroke(DrawingContext drawingContext) {
try {
var sp = new StylusPointCollection();
foreach (var pt in pointsList) {
var point = new StylusPoint();
point.PressureFactor = (float)0.5;
point.X = pt.X;
point.Y = pt.Y;
sp.Add(point);
}
var da = DrawingAttributes.Clone();
da.Width -= 0.5;
da.Height -= 0.5;
var stk = new Stroke(sp, da);
stk.Draw(drawingContext);
}
catch {}
}
private void DrawSolidNibStrokeForStylus(StylusPointCollection stylusPoints, DrawingContext drawingContext) {
try {
var sp = new StylusPointCollection();
foreach (var pt in stylusPoints) {
var point = new StylusPoint();
point.PressureFactor = (float)0.5;
point.X = pt.X;
point.Y = pt.Y;
sp.Add(point);
}
var da = DrawingAttributes.Clone();
var stk = new Stroke(sp, da);
stk.Draw(drawingContext);
}
catch {}
}
protected override void OnDraw(DrawingContext drawingContext,
StylusPointCollection stylusPoints,
Geometry geometry, Brush fillBrush) {
if (_inkCanvas.BoardSettings.StrokeNibStyle == StrokeNibStyle.Beautiful && InputType != InputtingDeviceType.Stylus) {
DrawBeautifulNibStroke(drawingContext);
} else if (_inkCanvas.BoardSettings.StrokeNibStyle == StrokeNibStyle.Solid && InputType != InputtingDeviceType.Stylus) {
DrawSolidNibStroke(drawingContext);
} else if (_inkCanvas.BoardSettings.StrokeNibStyle == StrokeNibStyle.Solid && InputType == InputtingDeviceType.Stylus) {
DrawSolidNibStrokeForStylus(stylusPoints, drawingContext);
} else if (_inkCanvas.BoardSettings.StrokeNibStyle == StrokeNibStyle.Default) {
base.OnDraw(drawingContext, stylusPoints, geometry, fillBrush);
} else {
base.OnDraw(drawingContext, stylusPoints, geometry, fillBrush);
}
}
}
internal class IccInkCanvas : InkCanvas {
CustomDynamicRenderer customDynamicRenderer;
public BoardSettings BoardSettings { get; set; } = new BoardSettings();
public IccInkCanvas() {
customDynamicRenderer = new CustomDynamicRenderer(this);
DynamicRenderer = customDynamicRenderer;
// 通过反射移除InkCanvas自带的默认 Delete按键事件
var commandBindingsField =
typeof(CommandManager).GetField("_classCommandBindings", BindingFlags.NonPublic | BindingFlags.Static);
var bnds = commandBindingsField.GetValue(null) as HybridDictionary;
var inkCanvasBindings = bnds[typeof(InkCanvas)] as CommandBindingCollection;
var enumerator = inkCanvasBindings.GetEnumerator();
while (enumerator.MoveNext()) {
var item = (CommandBinding)enumerator.Current;
if (item.Command == ApplicationCommands.Delete) {
var executedField =
typeof(CommandBinding).GetField("Executed", BindingFlags.NonPublic | BindingFlags.Instance);
var canExecuteField =
typeof(CommandBinding).GetField("CanExecute", BindingFlags.NonPublic | BindingFlags.Instance);
executedField.SetValue(item, new ExecutedRoutedEventHandler((sender, args) => { }));
canExecuteField.SetValue(item, new CanExecuteRoutedEventHandler((sender, args) => { }));
}
}
// 为IccInkCanvas注册自定义的 Delete按键Command并Invoke OnDeleteCommandFired。
CommandManager.RegisterClassCommandBinding(typeof(IccInkCanvas), new CommandBinding(ApplicationCommands.Delete,
(sender, args) => {
DeleteKeyCommandFired?.Invoke(this, new RoutedEventArgs());
}, (sender, args) => {
args.CanExecute = GetSelectedStrokes().Count != 0;
}));
}
protected override void OnStrokeCollected(InkCanvasStrokeCollectedEventArgs e) {
IccStroke customStroke = new IccStroke(e.Stroke.StylusPoints, e.Stroke.DrawingAttributes);
if (e.Stroke is IccStroke) {
Strokes.Add(e.Stroke);
} else {
Strokes.Remove(e.Stroke);
Strokes.Add(customStroke);
}
if (BoardSettings.StrokeNibStyle == StrokeNibStyle.Beautiful && customDynamicRenderer.InputType != InputtingDeviceType.Stylus) {
try {
var stylusPoints = new StylusPointCollection();
var n = customStroke.StylusPoints.Count - 1;
var pressure = 0.1;
var x = 10;
if (n == 1) return;
if (n >= x) {
for (var i = 0; i < n - x; i++) {
var point = new StylusPoint();
point.PressureFactor = (float)0.5;
point.X = customStroke.StylusPoints[i].X;
point.Y = customStroke.StylusPoints[i].Y;
stylusPoints.Add(point);
}
for (var i = n - x; i <= n; i++) {
var point = new StylusPoint();
point.PressureFactor = (float)((0.5 - pressure) * (n - i) / x + pressure);
point.X = customStroke.StylusPoints[i].X;
point.Y = customStroke.StylusPoints[i].Y;
stylusPoints.Add(point);
}
}
else {
for (var i = 0; i <= n; i++) {
var point = new StylusPoint();
point.PressureFactor = (float)(0.4 * (n - i) / n + pressure);
point.X = customStroke.StylusPoints[i].X;
point.Y = customStroke.StylusPoints[i].Y;
stylusPoints.Add(point);
}
}
customStroke.StylusPoints = stylusPoints;
} catch { }
} else if (BoardSettings.StrokeNibStyle == StrokeNibStyle.Solid) {
try {
var sp = new StylusPointCollection();
foreach (var pt in customStroke.StylusPoints) {
var point = new StylusPoint();
point.PressureFactor = (float)0.5;
point.X = pt.X;
point.Y = pt.Y;
sp.Add(point);
}
customStroke.StylusPoints = sp;
} catch { }
}
InkCanvasStrokeCollectedEventArgs args =
new InkCanvasStrokeCollectedEventArgs(customStroke);
base.OnStrokeCollected(args);
Trace.WriteLine("dfsffdsdfdfsfds");
}
public event EventHandler<RoutedEventArgs> DeleteKeyCommandFired;
public InputtingDeviceType InputtingDeviceType { get; private set; }
}
}
+214
View File
@@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
namespace InkCanvasForClass.IccInkCanvas {
static class ToPointsHelper {
public static Point[] ToPoints(this IEnumerable<StylusPoint> stylusPoints)
{
List<Point> pointList = new List<Point>();
foreach (StylusPoint stylusPoint in stylusPoints)
pointList.Add(stylusPoint.ToPoint());
return pointList.ToArray();
}
}
public enum ShapeDrawingType {
Line,
DottedLine,
DashedLine,
ArrowOneSide,
ArrowTwoSide,
Rectangle,
Ellipse,
PieEllipse,
Triangle,
RightTriangle,
Diamond,
Parallelogram,
FourLine,
Staff,
Axis2D,
Axis2DA,
Axis2DB,
Axis2DC,
Axis3D,
Hyperbola,
HyperbolaF,
Parabola,
ParabolaA,
ParabolaAF,
Cylinder,
Cube,
Cone,
EllipseC,
RectangleC
}
public static class ShapeDrawingHelper {
/// <summary>
/// 根据给定的两个点计算角度
/// </summary>
/// <param name="firstPoint"></param>
/// <param name="lastPoint"></param>
/// <returns></returns>
public static double CaculateRotateAngleByGivenTwoPoints(Point firstPoint, Point lastPoint) {
var vec1 = new double[] {
lastPoint.X - firstPoint.X ,
lastPoint.Y - firstPoint.Y
};
var vec_base = new double[] { 0, firstPoint.Y };
var cosine = (vec_base[0] * vec1[0] + vec_base[1] * vec1[1]) /
(Math.Sqrt(Math.Pow(vec_base[0],2) + Math.Pow(vec_base[1],2)) *
Math.Sqrt(Math.Pow(vec1[0],2) + Math.Pow(vec1[1],2)));
var angle = Math.Acos(cosine);
var isIn2And3Quadrant = lastPoint.X <= firstPoint.X;
var rotateAngle = Math.Round(180 + 180 * (angle / Math.PI) * (isIn2And3Quadrant ? 1 : -1), 0);
return rotateAngle;
}
public static List<Point> DistributePointsOnLine(Point start, Point end, double interval=16) {
List<Point> points = new List<Point>();
double dx = end.X - start.X;
double dy = end.Y - start.Y;
double distance = Math.Sqrt(dx * dx + dy * dy);
int numPoints = (int)(distance / interval);
for (int i = 0; i <= numPoints; i++) {
double ratio = (interval * i) / distance;
double x = start.X + ratio * dx;
double y = start.Y + ratio * dy;
points.Add(new Point(x, y));
}
return points;
}
public class ArrowLineConfig {
public int ArrowWidth { get; set; } = 20;
public int ArrowHeight { get; set; } = 7;
}
}
public class IccStroke : Stroke {
public IccStroke(StylusPointCollection stylusPoints, DrawingAttributes drawingAttributes)
: base(stylusPoints, drawingAttributes) { }
public static Guid StrokeShapeTypeGuid = new Guid("6537b29c-557f-487f-800b-cb30a8f1de78");
public static Guid StrokeIsShapeGuid = new Guid("40eff5db-9346-4e42-bd46-7b0eb19d0018");
public StylusPointCollection RawStylusPointCollection { get; set; }
public ShapeDrawingHelper.ArrowLineConfig ArrowLineConfig { get; set; } =
new ShapeDrawingHelper.ArrowLineConfig();
/// <summary>
/// 根据这个属性判断当前 Stroke 是否是原始输入
/// </summary>
public bool IsRawStylusPoints = true;
/// <summary>
/// 根据这个属性决定在绘制 Stroke 时是否需要在直线形状中,在两点构成直线上分布点,用于墨迹的范围框选。
/// </summary>
public bool IsDistributePointsOnLineShape = true;
/// <summary>
/// 指示该墨迹是否来自一个完整墨迹被擦除后的一部分墨迹,仅用于形状墨迹。
/// </summary>
public bool IsErasedStrokePart = false;
// 自定义的墨迹渲染
protected override void DrawCore(DrawingContext drawingContext,
DrawingAttributes drawingAttributes) {
if (!(this.ContainsPropertyData(StrokeIsShapeGuid) &&
(bool)this.GetPropertyData(StrokeIsShapeGuid) == true)) {
base.DrawCore(drawingContext, drawingAttributes);
return;
}
if ((int)this.GetPropertyData(StrokeShapeTypeGuid) == (int)ShapeDrawingType.DashedLine ||
(int)this.GetPropertyData(StrokeShapeTypeGuid) == (int)ShapeDrawingType.Line ||
(int)this.GetPropertyData(StrokeShapeTypeGuid) == (int)ShapeDrawingType.DottedLine ||
(int)this.GetPropertyData(StrokeShapeTypeGuid) == (int)ShapeDrawingType.ArrowOneSide ||
(int)this.GetPropertyData(StrokeShapeTypeGuid) == (int)ShapeDrawingType.ArrowTwoSide) {
if (StylusPoints.Count < 2) {
base.DrawCore(drawingContext, drawingAttributes);
return;
}
var pts = new List<Point>(this.StylusPoints.ToPoints());
if (IsDistributePointsOnLineShape && (
(int)this.GetPropertyData(StrokeShapeTypeGuid) == (int)ShapeDrawingType.DashedLine ||
(int)this.GetPropertyData(StrokeShapeTypeGuid) == (int)ShapeDrawingType.Line ||
(int)this.GetPropertyData(StrokeShapeTypeGuid) == (int)ShapeDrawingType.DottedLine) && IsRawStylusPoints) {
IsRawStylusPoints = false;
RawStylusPointCollection = StylusPoints.Clone();
var pointList = new List<Point> { new Point(StylusPoints[0].X, StylusPoints[0].Y) };
pointList.AddRange(ShapeDrawingHelper.DistributePointsOnLine(new Point(StylusPoints[0].X, StylusPoints[0].Y),new Point(StylusPoints[1].X, StylusPoints[1].Y)));
pointList.Add(new Point(StylusPoints[1].X, StylusPoints[1].Y));
StylusPoints = new StylusPointCollection(pointList);
}
StreamGeometry geometry = new StreamGeometry();
using (StreamGeometryContext ctx = geometry.Open()) {
ctx.BeginFigure(pts[0], false , false);
pts.RemoveAt(0);
ctx.PolyLineTo(pts,true, true);
}
var pen = new Pen(new SolidColorBrush(DrawingAttributes.Color),
(drawingAttributes.Width + drawingAttributes.Height) / 2) {
DashCap = PenLineCap.Round,
StartLineCap = PenLineCap.Round,
EndLineCap = PenLineCap.Round,
};
if ((int)this.GetPropertyData(StrokeShapeTypeGuid) != (int)ShapeDrawingType.Line &&
(int)this.GetPropertyData(StrokeShapeTypeGuid) != (int)ShapeDrawingType.ArrowOneSide &&
(int)this.GetPropertyData(StrokeShapeTypeGuid) != (int)ShapeDrawingType.ArrowTwoSide)
pen.DashStyle = (int)this.GetPropertyData(StrokeShapeTypeGuid) == (int)ShapeDrawingType.DottedLine ? DashStyles.Dot : DashStyles.Dash;
if ((int)this.GetPropertyData(StrokeShapeTypeGuid) == (int)ShapeDrawingType.ArrowOneSide && IsRawStylusPoints) {
IsRawStylusPoints = false;
pts = new List<Point>(this.StylusPoints.ToPoints());
RawStylusPointCollection = StylusPoints.Clone();
double w = ArrowLineConfig.ArrowWidth, h = ArrowLineConfig.ArrowHeight;
var theta = Math.Atan2(pts[0].Y - pts[1].Y, pts[0].X - pts[1].X);
var sint = Math.Sin(theta);
var cost = Math.Cos(theta);
var pointList = new List<Point> {
new Point(pts[0].X, pts[0].Y),
};
if (IsDistributePointsOnLineShape) pointList.AddRange(ShapeDrawingHelper.DistributePointsOnLine(new Point(pts[0].X, pts[0].Y),new Point(pts[1].X, pts[1].Y)));
pointList.AddRange(new List<Point> {
new Point(pts[1].X, pts[1].Y),
new Point(pts[1].X + (w * cost - h * sint), pts[1].Y + (w * sint + h * cost)),
new Point(pts[1].X, pts[1].Y),
new Point(pts[1].X + (w * cost + h * sint), pts[1].Y - (h * cost - w * sint)),
});
StylusPoints = new StylusPointCollection(pointList);
var _pts = new List<Point>(this.StylusPoints.ToPoints());
using (StreamGeometryContext ctx = geometry.Open()) {
ctx.BeginFigure(_pts[0], false , false);
_pts.RemoveAt(0);
ctx.PolyLineTo(_pts,true, true);
}
drawingContext.DrawGeometry(new SolidColorBrush(DrawingAttributes.Color),pen, geometry);
return;
}
drawingContext.DrawGeometry(new SolidColorBrush(Colors.Transparent),pen, geometry);
}
}
}
}
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{43929D8F-5630-4786-B75D-E203EA3E992F}</ProjectGuid>
<OutputType>library</OutputType>
<RootNamespace>InkCanvasForClass.IccInkCanvas</RootNamespace>
<AssemblyName>InkCanvasForClass.IccInkCanvas</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<Compile Include="IccBoardPage.cs" />
<Compile Include="IccDispatcherInkCanvasInfo.cs" />
<Compile Include="Icc\InkBoard.cs" />
<Compile Include="Settings\BoardSettings.cs" />
<Compile Include="IccBoard.xaml.cs">
<DependentUpon>IccBoard.xaml</DependentUpon>
</Compile>
<Compile Include="IccInkCanvas.cs" />
<Compile Include="IccStroke.cs" />
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<Compile Include="Settings\EditingMode.cs" />
<Compile Include="Settings\Enums.cs" />
<Compile Include="Settings\Nib.cs" />
<Compile Include="TimeMachine.cs" />
<Compile Include="TimeMachineV2.cs" />
<Compile Include="Utils\Threading\AwaiterInterfaces.cs" />
<Compile Include="Utils\Threading\DispatcherAsyncOperation.cs" />
<Compile Include="Utils\Threading\DispatcherContainer.cs" />
<Compile Include="Utils\Threading\InteractiveHostVisual.cs" />
<Compile Include="Utils\Threading\UIDispatcher.cs" />
<Compile Include="Utils\Threading\VisualTargetPresentationSource.cs" />
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<Page Include="IccBoard.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations">
<Version>2024.2.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -0,0 +1,55 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("InkCanvasForClass.IccInkCanvas")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("InkCanvasForClass.IccInkCanvas")]
[assembly: AssemblyCopyright("Copyright © 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
//若要开始生成可本地化的应用程序,请设置
//.csproj 文件中的 <UICulture>CultureYouAreCodingWith</UICulture>
//在 <PropertyGroup> 中。例如,如果你使用的是美国英语。
//使用的是美国英语,请将 <UICulture> 设置为 en-US。 然后取消
//对以下 NeutralResourceLanguage 特性的注释。 更新
//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly:ThemeInfo(
ResourceDictionaryLocation.None, //主题特定资源词典所处位置
//(未在页面中找到资源时使用,
//或应用程序资源字典中找到时使用)
ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置
//(未在页面中找到资源时使用,
//、应用程序或任何主题专用资源字典中找到时使用)
)]
// 程序集的版本信息由下列四个值组成:
//
// 主版本
// 次版本
// 生成号
// 修订号
//
//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
//通过使用 "*",如下所示:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本: 4.0.30319.42000
//
// 对此文件的更改可能导致不正确的行为,如果
// 重新生成代码,则所做更改将丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace InkCanvasForClass.IccInkCanvas.Properties {
/// <summary>
/// 强类型资源类,用于查找本地化字符串等。
/// </summary>
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// 返回此类使用的缓存 ResourceManager 实例。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if ((resourceMan == null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("InkCanvasForClass.IccInkCanvas.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 重写当前线程的 CurrentUICulture 属性,对
/// 使用此强类型资源类的所有资源查找执行重写。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
@@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace InkCanvasForClass.IccInkCanvas.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
return defaultInstance;
}
}
}
}
@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>
+6
View File
@@ -0,0 +1,6 @@
# IccInkCanvas
`InkCanvasForClass.IccInkCanvas`
## 方法
@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
namespace InkCanvasForClass.IccInkCanvas.Settings {
public class BoardSettings {
public BoardSettings() {}
private double _NibWidth { get; set; } = 4.00;
/// <summary>
/// 笔尖长度
/// </summary>
public double NibWidth {
get => _NibWidth;
set {
if (Math.Abs(_NibWidth - value) < 0.0001) return;
_NibWidth = value;
NibWidthChanged?.Invoke(this,EventArgs.Empty);
}
}
private double _NibHeight { get; set; } = 4.00;
/// <summary>
/// 笔尖高度
/// </summary>
public double NibHeight {
get => _NibWidth;
set {
if (Math.Abs(_NibHeight - value) < 0.0001) return;
_NibHeight = value;
NibHeightChanged?.Invoke(this,EventArgs.Empty);
}
}
private NibType _NibType { get; set; } = NibType.Default;
public NibType NibType {
get => _NibType;
set {
if (_NibType == value) return;
_NibType = value;
NibTypeChanged?.Invoke(this,EventArgs.Empty);
}
}
private Color _NibColor { get; set; } = Colors.Black;
/// <summary>
/// 笔尖颜色
/// </summary>
public Color NibColor {
get => _NibColor;
set {
if (_NibColor.Equals(value)) return;
_NibColor = value;
NibColorChanged?.Invoke(this,EventArgs.Empty);
}
}
private StrokeNibStyle _StrokeNibStyle { get; set; } = StrokeNibStyle.Beautiful;
/// <summary>
/// 笔锋样式类型,默认有笔锋
/// </summary>
public StrokeNibStyle StrokeNibStyle {
get => _StrokeNibStyle;
set {
if (_StrokeNibStyle == value) return;
_StrokeNibStyle = value;
StrokeNibStyleChanged?.Invoke(this,EventArgs.Empty);
}
}
private bool _IsForceIgnoreStylusPressure { get; set; } = false;
/// <summary>
/// 强制忽略支持压力传感的输入设备返回的真实压力值(比如支持压感的手写笔)
/// </summary>
public bool IsForceIgnoreStylusPressure {
get => _IsForceIgnoreStylusPressure;
set {
if (_IsForceIgnoreStylusPressure == value) return;
_IsForceIgnoreStylusPressure = value;
IsForceIgnoreStylusPressureChanged?.Invoke(this,EventArgs.Empty);
}
}
private EraserType _EraserType { get; set; } = EraserType.Rectangle;
/// <summary>
/// 指定橡皮擦的形状,支持矩形和圆形
/// </summary>
public EraserType EraserType {
get => _EraserType;
set {
if (_EraserType == value) return;
_EraserType = value;
EraserTypeChanged?.Invoke(this,EventArgs.Empty);
}
}
private double _EraserSize { get; set; } = 32D;
/// <summary>
/// 指定橡皮擦的大小,矩形橡皮擦为宽度(高度自动确定),圆形橡皮擦为直径
/// </summary>
public double EraserSize {
get => _EraserSize;
set {
if (_EraserSize == value) return;
_EraserSize = value;
EraserSizeChanged?.Invoke(this,EventArgs.Empty);
}
}
#region Events
public event EventHandler<EventArgs> NibWidthChanged;
public event EventHandler<EventArgs> NibHeightChanged;
public event EventHandler<EventArgs> NibColorChanged;
public event EventHandler<EventArgs> NibTypeChanged;
public event EventHandler<EventArgs> StrokeNibStyleChanged;
public event EventHandler<EventArgs> IsForceIgnoreStylusPressureChanged;
public event EventHandler<EventArgs> EraserTypeChanged;
public event EventHandler<EventArgs> EraserSizeChanged;
#endregion
}
}
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace InkCanvasForClass.IccInkCanvas.Settings {
/// <summary>
/// IccBoard的编辑模式
/// </summary>
public enum EditingMode {
/// <summary>
/// 仅显示墨迹,不会接收任何输入事件,没有HitTest
/// </summary>
None,
/// <summary>
/// 仅显示墨迹,不会接收任何输入事件,有HitTest
/// </summary>
NoneWithHitTest,
/// <summary>
/// 书写模式,该模式下允许临时切换到橡皮擦模式
/// </summary>
Writing,
/// <summary>
/// 墨迹擦模式
/// </summary>
StrokeErasing,
/// <summary>
/// 板擦模式
/// </summary>
GeometryErasing,
/// <summary>
/// 区域擦除模式
/// </summary>
AreaErasing,
/// <summary>
/// 墨迹选择模式
/// </summary>
Select,
/// <summary>
/// 仅显示墨迹,仅接受手势输入
/// </summary>
Gestures,
/// <summary>
/// 形状绘制模式,该模式不能被用户直接设置
/// </summary>
ShapeDrawing,
/// <summary>
/// 漫游模式
/// </summary>
RoamingMode
}
}
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace InkCanvasForClass.IccInkCanvas.Settings {
public enum BoardPageAppendMode {
/// <summary>
/// 添加到所有页面的最后去
/// </summary>
AppendToListEnd,
/// <summary>
/// 添加为当前页面的下一页
/// </summary>
AppendAfterItem,
/// <summary>
/// 添加为当前页面的上一页
/// </summary>
AppendBeforeItem,
/// <summary>
/// 添加到所有页面的最前面去
/// </summary>
AppendToListStart
}
public enum PageSwitchMode {
SwitchToPreviousPage,
SwitchToNextPage,
}
public enum InputtingDeviceType {
None,
Mouse,
Touch,
Stylus
}
public enum EraserType {
Rectangle,
Ellipse,
}
internal enum CommitReason {
/// <summary>
/// 由用户输入
/// </summary>
UserInput,
/// <summary>
/// 由代码手动添加
/// </summary>
CodeInput,
/// <summary>
/// 形状绘制
/// </summary>
ShapeDrawing,
/// <summary>
/// 墨迹识别
/// </summary>
ShapeRecognition,
/// <summary>
/// 清空画布
/// </summary>
ClearingCanvas,
/// <summary>
/// 已过时,不再直接对Stroke进行Transform来实现Manipulation
/// </summary>
[Obsolete]
Manipulation
}
}
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace InkCanvasForClass.IccInkCanvas.Settings {
/// <summary>
/// 定义了笔尖类型
/// </summary>
public enum NibType {
/// <summary>
/// 默认笔
/// </summary>
Default,
/// <summary>
/// 荧光笔
/// </summary>
Highlighter
}
/// <summary>
/// 笔锋样式
/// </summary>
public enum StrokeNibStyle {
/// <summary>
/// 默认,直接根据输入设备提供的压力值正常渲染
/// </summary>
Default,
/// <summary>
/// 强制无笔锋
/// </summary>
Solid,
/// <summary>
/// 有笔锋,基于固定点集算法计算,触笔设备不套用算法
/// </summary>
Beautiful
}
}
@@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
namespace InkCanvasForClass.IccInkCanvas {
/// <summary>
/// 时光机代码,基于 Ink Canvas 项目修改
/// </summary>
public class TimeMachine
{
private readonly List<TimeMachineHistory> _currentStrokeHistory = new List<TimeMachineHistory>();
private int _currentIndex = -1;
public delegate void OnUndoRedoStateChanged(TimeMachine timeMachine);
public event OnUndoRedoStateChanged UndoRedoStateChanged;
/// <summary>
/// 提交历史记录的通用方法
/// </summary>
/// <param name="history">TimeMachineHistory</param>
private void CommitHistory(TimeMachineHistory history) {
Trace.WriteLine("History Commited");
// 删除当前索引后的所有记录
if (_currentIndex + 1 < _currentStrokeHistory.Count)
_currentStrokeHistory.RemoveRange(_currentIndex + 1, (_currentStrokeHistory.Count - 1) - _currentIndex);
// 添加历史记录
_currentStrokeHistory.Add(history);
_currentIndex = _currentStrokeHistory.Count - 1;
// 通知撤销和重做的状态变化
NotifyUndoRedoState();
}
/// <summary>
/// 提交由用户绘制的墨迹添加到历史记录
/// </summary>
/// <param name="stroke"></param>
public void CommitStrokeUserInputHistory(StrokeCollection stroke) {
var history = new TimeMachineHistory(stroke, TimeMachineHistoryType.UserInput, false);
CommitHistory(history);
}
/// <summary>
/// 提交由墨迹纠正替换的墨迹添加到历史记录
/// </summary>
/// <param name="strokeToBeReplaced"></param>
/// <param name="generatedStroke"></param>
public void CommitStrokeShapeHistory(StrokeCollection strokeToBeReplaced, StrokeCollection generatedStroke) {
var history = new TimeMachineHistory(generatedStroke, TimeMachineHistoryType.ShapeRecognition, false, strokeToBeReplaced);
CommitHistory(history);
}
/// <summary>
/// 提交墨迹点变更到历史记录,Dictionary中每一项的Key对应一条墨迹的引用,Value为元组,第一个是变化前,第二个变化后
/// </summary>
/// <param name="stylusPointDictionary"></param>
public void CommitStylusPointsHistory(Dictionary<Stroke, Tuple<StylusPointCollection, StylusPointCollection>> stylusPointDictionary) {
var history = new TimeMachineHistory(stylusPointDictionary, TimeMachineHistoryType.StylusPoints);
CommitHistory(history);
}
/// <summary>
/// 提交墨迹的墨迹属性变更到历史记录
/// </summary>
/// <param name="drawingAttributes"></param>
public void CommitStrokeDrawingAttributesHistory(Dictionary<Stroke, Tuple<DrawingAttributes, DrawingAttributes>> drawingAttributes) {
var history = new TimeMachineHistory(drawingAttributes, TimeMachineHistoryType.DrawingAttributes);
CommitHistory(history);
}
/// <summary>
/// 提交墨迹擦被擦除的变更到历史记录中
/// </summary>
/// <param name="stroke"></param>
/// <param name="sourceStroke"></param>
public void CommitStrokeEraseHistory(StrokeCollection stroke, StrokeCollection sourceStroke = null) {
var history = new TimeMachineHistory(stroke, TimeMachineHistoryType.Erased, true, sourceStroke);
CommitHistory(history);
}
/// <summary>
/// 清空所有历史记录
/// </summary>
public void ClearHistory()
{
_currentStrokeHistory.Clear();
_currentIndex = -1;
NotifyUndoRedoState();
}
/// <summary>
/// 撤销
/// </summary>
/// <returns></returns>
public TimeMachineHistory Undo(bool doNotNotifyStateUpdate = false) {
if (!(_currentIndex > -1)) return null;
// 如果当前的墨迹是被创建了,就修改为指示被清除
var item = _currentStrokeHistory[_currentIndex];
item.StrokeHasBeenCleared = !item.StrokeHasBeenCleared;
var index = Math.Min(Math.Max(_currentIndex - 1, -1), _currentStrokeHistory.Count - 1);
_currentIndex = index;
if (!doNotNotifyStateUpdate) NotifyUndoRedoState();
return item;
}
/// <summary>
/// 撤销,指定步长
/// </summary>
/// <param name="steps"></param>
/// <returns></returns>
public TimeMachineHistory[] Undo(int steps = 1) {
var histories = new List<TimeMachineHistory>();
for (int i = 0; i < steps; i++) {
var item = Undo(true);
if (item != null) histories.Add(item);
}
NotifyUndoRedoState();
return histories.ToArray();
}
/// <summary>
/// 重做
/// </summary>
/// <returns></returns>
public TimeMachineHistory Redo(bool doNotNotifyStateUpdate = false) {
if (!(_currentStrokeHistory.Count - _currentIndex - 1 > 0)) return null;
var index = Math.Min(Math.Max(_currentIndex + 1, -1), _currentStrokeHistory.Count - 1);
// 如果当前的墨迹是被清除了,就修改为指示已创建
var item = _currentStrokeHistory[index];
item.StrokeHasBeenCleared = !item.StrokeHasBeenCleared;
_currentIndex = index;
if (!doNotNotifyStateUpdate) NotifyUndoRedoState();
return item;
}
/// <summary>
/// 重做,指定步长
/// </summary>
/// <param name="steps"></param>
/// <returns></returns>
public TimeMachineHistory[] Redo(int steps = 1) {
var histories = new List<TimeMachineHistory>();
for (int i = 0; i < steps; i++) {
var item = Redo(true);
if (item != null) histories.Add(item);
}
NotifyUndoRedoState();
return histories.ToArray();
}
public TimeMachineHistory[] ExportTimeMachineHistory()
{
if (_currentIndex + 1 < _currentStrokeHistory.Count)
{
_currentStrokeHistory.RemoveRange(_currentIndex + 1, (_currentStrokeHistory.Count - 1) - _currentIndex);
}
return _currentStrokeHistory.ToArray();
}
public bool ImportTimeMachineHistory(TimeMachineHistory[] sourceHistory)
{
_currentStrokeHistory.Clear();
_currentStrokeHistory.AddRange(sourceHistory);
_currentIndex = _currentStrokeHistory.Count - 1;
NotifyUndoRedoState();
return true;
}
private void NotifyUndoRedoState() {
Application.Current.Dispatcher.InvokeAsync(() => {
UndoRedoStateChanged?.Invoke(this);
});
}
public bool CanUndo => _currentIndex > -1;
public bool CanRedo => _currentStrokeHistory.Count - _currentIndex - 1 > 0;
public int CurrentHistoriesCount => _currentStrokeHistory.Count;
}
public class TimeMachineHistory
{
public TimeMachineHistoryType CommitType;
public bool StrokeHasBeenCleared = false;
public StrokeCollection CurrentStroke;
public StrokeCollection ReplacedStroke;
// Tuple的 Value1 是初始值 ; Value 2 是改变值
public Dictionary<Stroke, Tuple<StylusPointCollection, StylusPointCollection>> StylusPointsDictionary;
public Dictionary<Stroke, Tuple<DrawingAttributes, DrawingAttributes>> DrawingAttributes;
public TimeMachineHistory(StrokeCollection currentStroke, TimeMachineHistoryType commitType, bool strokeHasBeenCleared)
{
CommitType = commitType;
CurrentStroke = currentStroke;
StrokeHasBeenCleared = strokeHasBeenCleared;
ReplacedStroke = null;
}
public TimeMachineHistory(Dictionary<Stroke, Tuple<StylusPointCollection, StylusPointCollection>> stylusPointDictionary, TimeMachineHistoryType commitType)
{
CommitType = commitType;
StylusPointsDictionary = stylusPointDictionary;
}
public TimeMachineHistory(Dictionary<Stroke, Tuple<DrawingAttributes, DrawingAttributes>> drawingAttributes, TimeMachineHistoryType commitType)
{
CommitType = commitType;
DrawingAttributes = drawingAttributes;
}
public TimeMachineHistory(StrokeCollection currentStroke, TimeMachineHistoryType commitType, bool strokeHasBeenCleared, StrokeCollection replacedStroke)
{
CommitType = commitType;
CurrentStroke = currentStroke;
StrokeHasBeenCleared = strokeHasBeenCleared;
ReplacedStroke = replacedStroke;
}
}
public enum TimeMachineHistoryType
{
UserInput,
ShapeRecognition,
Erased,
StylusPoints,
DrawingAttributes
}
}
@@ -0,0 +1,302 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Ink;
using System.Windows.Threading;
namespace InkCanvasForClass.IccInkCanvas {
/// <summary>
/// 指示时光机的一个Action的类型
/// </summary>
public enum TimeMachineActionType {
/// <summary>
/// 用户绘制的墨迹被添加
/// </summary>
UserInputStrokeAdded,
/// <summary>
/// 墨迹被添加,这可能是由软件添加的
/// </summary>
StrokeAdded,
/// <summary>
/// 墨迹被移除
/// </summary>
StrokeRemoved,
/// <summary>
/// 用户使用了面积擦进行擦除
/// </summary>
UserGeometryErased,
/// <summary>
/// 用户使用了墨迹擦进行擦除
/// </summary>
UserStrokeErased,
/// <summary>
/// 用户使用了墨迹擦进行擦除
/// </summary>
UserAreaErased,
Test
}
public class TimeMachineAction {
/// <summary>
/// Action的类型
/// </summary>
public TimeMachineActionType ActionType { get; protected set ; }
private bool _isReversed = false;
public event EventHandler ActionIsReversedChanged;
/// <summary>
/// 指示该Action是否被逆操作了。
/// 比如现在如果这是一个StrokeAdded的Action,当执行撤销后,墨迹消失,此时IsReversed应该为True。
/// </summary>
public bool IsReversed {
get => _isReversed;
set {
if (value == _isReversed) return;
_isReversed = value;
ActionIsReversedChanged?.Invoke(this,null);
}
}
}
/// <summary>
/// 墨迹操作的Action
/// </summary>
public class TimeMachineStrokeAction : TimeMachineAction {
public event EventHandler AsyncPerformerLocked;
public event EventHandler AsyncPerformerUnlocked;
public bool IsLocked { get; protected set; } = false;
public TimeMachineStrokeAction(TimeMachineActionType type, IccStroke stroke) {
ActionType = (new TimeMachineActionType[] {
TimeMachineActionType.UserInputStrokeAdded,
TimeMachineActionType.StrokeAdded,
TimeMachineActionType.StrokeRemoved,
}).Contains(type)
? TimeMachineActionType.StrokeAdded
: type;
Stroke = stroke;
}
public TimeMachineStrokeAction(TimeMachineActionType type, IccStroke stroke, long timeStamp) {
ActionType = (new TimeMachineActionType[] {
TimeMachineActionType.UserInputStrokeAdded,
TimeMachineActionType.StrokeAdded,
TimeMachineActionType.StrokeRemoved,
}).Contains(type)
? TimeMachineActionType.StrokeAdded
: type;
Stroke = stroke;
TimeStamp = timeStamp;
}
/// <summary>
/// 墨迹。如果为UserInputStrokeAdded或StrokeAdded则为添加的墨迹,否则,StrokeRemoved就是被移除的墨迹。
/// </summary>
public IccStroke Stroke { get; private set; }
/// <summary>
/// 执行这个操作的时间戳,-1为没有时间。
/// </summary>
public long TimeStamp { get; private set; } = -1;
/// <summary>
/// 应用该Action所导致的更改到画布上,需要指定DispatcherStrokeCollection
/// </summary>
public void Perform(Dispatcher dispatcher, StrokeCollection strokes) {
IsLocked = true;
AsyncPerformerLocked?.Invoke(this,null);
var task = dispatcher.InvokeAsync(() => {
if (IsReversed) {
if (ActionType == TimeMachineActionType.StrokeAdded ||
ActionType == TimeMachineActionType.UserInputStrokeAdded) strokes.Remove(Stroke);
else if (ActionType == TimeMachineActionType.StrokeRemoved)
try {
strokes.Add(Stroke);
}
catch { }
} else {
if (ActionType == TimeMachineActionType.StrokeAdded ||
ActionType == TimeMachineActionType.UserInputStrokeAdded)
try {
strokes.Add(Stroke);
}
catch { }
else if (ActionType == TimeMachineActionType.StrokeRemoved) strokes.Remove(Stroke);
}
});
task.Completed += (_, __) => {
IsLocked = false;
AsyncPerformerUnlocked?.Invoke(this, null);
};
}
}
/// <summary>
/// only internal usage
/// </summary>
internal class TimeMachineTestAction : TimeMachineAction {
private long timeStamp;
public TimeMachineTestAction() {
ActionType = TimeMachineActionType.Test;
timeStamp = new DateTimeOffset(DateTime.Now).ToUnixTimeMilliseconds();
}
public void Perform() {
Trace.WriteLine($"helloworld! reversed:{IsReversed} timestamp: {timeStamp}");
}
}
/// <summary>
/// 基于 icc 修改的 时光机 V2 版本,提供了更精简的设计
/// </summary>
public class TimeMachineV2 {
/// <summary>
/// 当前的Index
/// </summary>
private int _currentIndex = 0;
public int CurrentIndex => _currentIndex;
private List<TimeMachineAction> _actions = new List<TimeMachineAction>();
public TimeMachineAction[] Actions => _actions.ToArray();
public int ActionsCount => _actions.Count;
/// <summary>
/// 在撤销重做状态发生改变时触发
/// </summary>
public event EventHandler UndoRedoStateChanged;
/// <summary>
/// 执行action的perform的时候用于锁住时光机
/// </summary>
private bool _asyncPerformerLocker = false;
private void asyncPerformerLockedOrUnLocked(object sender, EventArgs e) {
if (sender is TimeMachineStrokeAction) {
_asyncPerformerLocker = (sender as TimeMachineStrokeAction).IsLocked;
}
}
/// <summary>
/// 丢弃指针Index后的所有Actions并销毁锁事件
/// </summary>
private void DropActionsAfterCursorIndex() {
var range = _actions.GetRange(_currentIndex, _actions.Count - _currentIndex);
foreach (var a in range) {
if (a is TimeMachineStrokeAction) {
(a as TimeMachineStrokeAction).AsyncPerformerUnlocked -= asyncPerformerLockedOrUnLocked;
(a as TimeMachineStrokeAction).AsyncPerformerLocked -= asyncPerformerLockedOrUnLocked;
}
}
_actions.RemoveRange(_currentIndex, _actions.Count - _currentIndex);
}
public void PushStrokeAction(TimeMachineActionType type, IccStroke stroke, long timeStamp) {
var action = new TimeMachineStrokeAction(type, stroke, timeStamp);
_actions.RemoveRange(_currentIndex, _actions.Count - _currentIndex);
_actions.Add(action);
_currentIndex++;
UndoRedoStateChanged?.Invoke(this,null);
}
/// <summary>
/// only internal usage
/// </summary>
public void PushTestAction() {
var action = new TimeMachineTestAction();
_actions.RemoveRange(_currentIndex, _actions.Count - _currentIndex);
_actions.Add(action);
_currentIndex++;
UndoRedoStateChanged?.Invoke(this,null);
}
public void Undo() {
Undo(false);
}
/// <summary>
/// 撤销
/// </summary>
public void Undo(bool doNotNotifyStateUpdate = false) {
if (!(_currentIndex > 0)) return;
if (_asyncPerformerLocker) return;
var item = _actions[Math.Max(_currentIndex-1,0)];
if ((new TimeMachineActionType[] {
TimeMachineActionType.Test,
}).Contains(item.ActionType)) {
(item as TimeMachineTestAction).IsReversed = true;
(item as TimeMachineTestAction).Perform();
}
var index = Math.Min(Math.Max(_currentIndex - 1, 0), _actions.Count);
_currentIndex = index;
if (!doNotNotifyStateUpdate) UndoRedoStateChanged?.Invoke(this,null);
}
/// <summary>
/// 多步撤销
/// </summary>
/// <param name="steps">步长</param>
public void Undo(bool doNotNotifyStateUpdate = false, int steps = 1) {
for (int i = 0; i < steps; i++) {
Undo(true);
}
if (!doNotNotifyStateUpdate) UndoRedoStateChanged?.Invoke(this,null);
}
/// <summary>
/// 多步重做
/// </summary>
/// <param name="steps">步长</param>
public void Redo(bool doNotNotifyStateUpdate = false, int steps = 1) {
for (int i = 0; i < steps; i++) {
Redo(true);
}
if (!doNotNotifyStateUpdate) UndoRedoStateChanged?.Invoke(this,null);
}
public void Redo() {
Redo(false);
}
/// <summary>
/// 重做
/// </summary>
public void Redo(bool doNotNotifyStateUpdate = false) {
if (!(_actions.Count - _currentIndex > 0)) return;
if (_asyncPerformerLocker) return;
var item = _actions[Math.Min(_currentIndex, _actions.Count - 1)];
if ((new TimeMachineActionType[] {
TimeMachineActionType.Test,
}).Contains(item.ActionType)) {
(item as TimeMachineTestAction).IsReversed = false;
(item as TimeMachineTestAction).Perform();
}
var index = Math.Min(Math.Max(_currentIndex + 1, 0), _actions.Count);
_currentIndex = index;
if (!doNotNotifyStateUpdate) UndoRedoStateChanged?.Invoke(this,null);
}
}
}
@@ -0,0 +1,84 @@
using System.Runtime.CompilerServices;
namespace InkCanvasForClass.IccInkCanvas.Utils.Threading
{
/// <summary>
/// 表示一个可等待对象,如果一个方法返回此类型的实例,则此方法可以使用 `await` 异步等待。
/// </summary>
/// <typeparam name="TAwaiter">用于给 await 确定返回时机的 IAwaiter 的实例。</typeparam>
public interface IAwaitable<out TAwaiter> where TAwaiter : IAwaiter
{
/// <summary>
/// 获取一个可用于 await 关键字异步等待的异步等待对象。
/// 此方法会被编译器自动调用。
/// </summary>
TAwaiter GetAwaiter();
}
/// <summary>
/// 表示一个包含返回值的可等待对象,如果一个方法返回此类型的实例,则此方法可以使用 `await` 异步等待返回值。
/// </summary>
/// <typeparam name="TAwaiter">用于给 await 确定返回时机的 IAwaiter{<typeparamref name="TResult"/>} 的实例。</typeparam>
/// <typeparam name="TResult">异步返回的返回值类型。</typeparam>
public interface IAwaitable<out TAwaiter, out TResult> where TAwaiter : IAwaiter<TResult>
{
/// <summary>
/// 获取一个可用于 await 关键字异步等待的异步等待对象。
/// 此方法会被编译器自动调用。
/// </summary>
TAwaiter GetAwaiter();
}
/// <summary>
/// 用于给 await 确定异步返回的时机。
/// </summary>
public interface IAwaiter : INotifyCompletion
{
/// <summary>
/// 获取一个状态,该状态表示正在异步等待的操作已经完成(成功完成或发生了异常);此状态会被编译器自动调用。
/// 在实现中,为了达到各种效果,可以灵活应用其值:可以始终为 true,或者始终为 false。
/// </summary>
bool IsCompleted { get; }
/// <summary>
/// 此方法会被编译器在 await 结束时自动调用以获取返回状态(包括异常)。
/// </summary>
void GetResult();
}
/// <summary>
/// 当执行关键代码(此代码中的错误可能给应用程序中的其他状态造成负面影响)时,
/// 用于给 await 确定异步返回的时机。
/// </summary>
public interface ICriticalAwaiter : IAwaiter, ICriticalNotifyCompletion
{
}
/// <summary>
/// 用于给 await 确定异步返回的时机,并获取到返回值。
/// </summary>
/// <typeparam name="TResult">异步返回的返回值类型。</typeparam>
public interface IAwaiter<out TResult> : INotifyCompletion
{
/// <summary>
/// 获取一个状态,该状态表示正在异步等待的操作已经完成(成功完成或发生了异常);此状态会被编译器自动调用。
/// 在实现中,为了达到各种效果,可以灵活应用其值:可以始终为 true,或者始终为 false。
/// </summary>
bool IsCompleted { get; }
/// <summary>
/// 获取此异步等待操作的返回值,此方法会被编译器在 await 结束时自动调用以获取返回值(包括异常)。
/// </summary>
/// <returns>异步操作的返回值。</returns>
TResult GetResult();
}
/// <summary>
/// 当执行关键代码(此代码中的错误可能给应用程序中的其他状态造成负面影响)时,
/// 用于给 await 确定异步返回的时机,并获取到返回值。
/// </summary>
/// <typeparam name="TResult">异步返回的返回值类型。</typeparam>
public interface ICriticalAwaiter<out TResult> : IAwaiter<TResult>, ICriticalNotifyCompletion
{
}
}
@@ -0,0 +1,151 @@
using System;
using System.Runtime.ExceptionServices;
using System.Windows.Threading;
using JetBrains.Annotations;
namespace InkCanvasForClass.IccInkCanvas.Utils.Threading
{
/// <summary>
/// 表示可以等待一个主要运行在 UI 线程的异步操作。
/// </summary>
/// <typeparam name="T">异步等待 UI 操作结束后的返回值类型。</typeparam>
public class DispatcherAsyncOperation<T> : DispatcherObject,
IAwaitable<DispatcherAsyncOperation<T>, T>, IAwaiter<T>
{
/// <summary>
/// 创建 <see cref="DispatcherAsyncOperation{T}"/> 的新实例。
/// </summary>
private DispatcherAsyncOperation()
{
}
/// <summary>
/// 获取一个可用于 await 关键字异步等待的异步等待对象。
/// 此方法会被编译器自动调用。
/// </summary>
/// <returns>返回自身,用于异步等待返回值。</returns>
public DispatcherAsyncOperation<T> GetAwaiter()
{
return this;
}
/// <summary>
/// 获取一个状态,该状态表示正在异步等待的操作已经完成(成功完成或发生了异常)。
/// 此状态会被编译器自动调用。
/// </summary>
public bool IsCompleted { get; private set; }
/// <summary>
/// 获取此异步等待操作的返回值。
/// 与 <see cref="System.Threading.Tasks.Task{TResult}"/> 不同的是,
/// 如果操作没有完成或发生了异常,此实例会返回 <typeparamref name="T"/> 的默认值,
/// 而不是阻塞线程直至任务完成。
/// </summary>
public T Result { get; private set; }
/// <summary>
/// 获取此异步等待操作的返回值,此方法会被编译器在 await 结束时自动调用以获取返回值。
/// 与 <see cref="System.Threading.Tasks.Task{TResult}"/> 不同的是,
/// 如果操作没有完成,此实例会返回 <typeparamref name="T"/> 的默认值,而不是阻塞线程直至任务完成。
/// 但是,如果异步操作中发生了异常,调用此方法会抛出这个异常。
/// </summary>
/// <returns>
/// 异步操作的返回值。
/// </returns>
public T GetResult()
{
if (_exception != null)
{
ExceptionDispatchInfo.Capture(_exception).Throw();
}
return Result;
}
/// <summary>
/// 使用 Builder 模式配置此异步操作执行完后,后续任务执行采用的优先级。
/// 不配置时,使用的是 <see cref="DispatcherPriority.Normal"/>。
/// </summary>
/// <param name="priority">使用 <see cref="Dispatcher"/> 调度的后续任务的优先级。</param>
/// <returns>实例自身。</returns>
public DispatcherAsyncOperation<T> ConfigurePriority(DispatcherPriority priority)
{
_priority = priority;
return this;
}
/// <summary>
/// 当使用此类型执行异步任务的方法执行完毕后,编译器会自动调用此方法。
/// 也就是说,此方法会在调用方所在的线程执行,用于通知调用方所在线程的代码已经执行完毕,请求执行 await 后续任务。
/// 在此类型中,后续任务是通过 <see cref="Dispatcher.InvokeAsync(Action, DispatcherPriority)"/> 来执行的。
/// </summary>
/// <param name="continuation">
/// 被异步任务状态机包装的后续任务。当执行时,会让状态机继续往下走一步。
/// </param>
public void OnCompleted(Action continuation)
{
if (IsCompleted)
{
// 如果 await 开始时任务已经执行完成,则直接执行 await 后面的代码。
// 注意,即便 _continuation 有值,也无需关心,因为报告结束的时候就会将其执行。
continuation?.Invoke();
}
else
{
// 当使用多个 await 关键字等待此同一个 awaitable 实例时,此 OnCompleted 方法会被多次执行。
// 当任务真正结束后,需要将这些所有的 await 后面的代码都执行。
_continuation += continuation;
}
}
/// <summary>
/// 调用此方法以报告任务结束,并指定返回值和异步任务中的异常。
/// 当使用 <see cref="Create"/> 静态方法创建此类型的实例后,调用方可以通过方法参数中传出的委托来调用此方法。
/// </summary>
/// <param name="result">异步返回值。</param>
/// <param name="exception">异步操作中的异常。</param>
private void ReportResult(T result, Exception exception)
{
Result = result;
_exception = exception;
IsCompleted = true;
// _continuation 可能为 null,说明任务已经执行完毕,但没有任何一处 await 了这个任务。
if (_continuation != null)
{
// 无论此方法执行时所在线程关联的 Dispatcher 是否等于此类型创建时的 Dispatcher;
// 都 Invoke 到创建时的 Dispatcher 上,以便对当前执行上下文造成影响在不同线程执行下都一致(如异常)。
Dispatcher.InvokeAsync(_continuation, _priority);
}
}
/// <summary>
/// 临时保存 await 后后续任务的包装,用于报告任务完成后能够继续执行。
/// </summary>
private Action _continuation;
/// <summary>
/// 临时保存异步任务执行过程中发生的异常。它会在异步等待结束后抛出,以报告异步执行过程中发生的错误。
/// </summary>
private Exception _exception;
/// <summary>
/// 储存恢复 await 后续任务时需要使用的优先级。
/// </summary>
private DispatcherPriority _priority = DispatcherPriority.Normal;
/// <summary>
/// 创建 <see cref="DispatcherAsyncOperation{T}"/> 的新实例,并得到一个可以用于报告操作执行完毕的委托。
/// </summary>
/// <param name="reportResult">一个委托。调用此委托可以报告任务已经执行完毕,并给定返回值和异常信息。</param>
/// <returns>
/// 创建好的 <see cref="DispatcherAsyncOperation{T}"/> 的新实例,将此返回值作为方法的返回值可以让方法支持 await 异步等待。
/// </returns>
public static DispatcherAsyncOperation<T> Create([NotNull] out Action<T, Exception> reportResult)
{
var asyncOperation = new DispatcherAsyncOperation<T>();
reportResult = asyncOperation.ReportResult;
return asyncOperation;
}
}
}
@@ -0,0 +1,162 @@
using JetBrains.Annotations;
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Threading;
namespace InkCanvasForClass.IccInkCanvas.Utils.Threading
{
[ContentProperty(nameof(Child))]
public sealed class DispatcherContainer : FrameworkElement
{
public DispatcherContainer()
{
_hostVisual = new InteractiveHostVisual();
}
private readonly HostVisual _hostVisual;
private VisualTargetPresentationSource _targetSource;
#region Child
private bool _isUpdatingChild;
[CanBeNull] private UIElement _child;
public UIElement Child => _child;
public async Task SetChildAsync<T>([CanBeNull] Dispatcher dispatcher = null)
where T : UIElement, new()
{
await SetChildAsync(() => new T(), dispatcher);
}
public async Task SetChildAsync<T>(Func<T> @new, [CanBeNull] Dispatcher dispatcher = null)
where T : UIElement
{
dispatcher = dispatcher ?? await UIDispatcher.RunNewAsync($"{typeof(T).Name}");
var child = await dispatcher.InvokeAsync(@new);
await SetChildAsync(child);
}
public async Task SetChildAsync(UIElement value)
{
if (_isUpdatingChild)
{
throw new InvalidOperationException("Child property should not be set during Child updating.");
}
_isUpdatingChild = true;
try
{
await SetChildAsync();
}
finally
{
_isUpdatingChild = false;
}
async Task SetChildAsync()
{
var oldChild = _child;
var visualTarget = _targetSource;
if (Equals(oldChild, value))
return;
_targetSource = null;
if (visualTarget != null)
{
RemoveVisualChild(oldChild);
await visualTarget.Dispatcher.InvokeAsync(visualTarget.Dispose);
}
_child = value;
if (value == null)
{
_targetSource = null;
}
else
{
await value.Dispatcher.InvokeAsync(() =>
{
_targetSource = new VisualTargetPresentationSource(_hostVisual)
{
RootVisual = value,
};
});
AddVisualChild(_hostVisual);
}
InvalidateMeasure();
}
}
#endregion
#region Tree & Layout
protected override Visual GetVisualChild(int index)
{
if (index != 0)
throw new ArgumentOutOfRangeException(nameof(index));
return _hostVisual;
}
protected override int VisualChildrenCount => _child != null ? 1 : 0;
protected override Size MeasureOverride(Size availableSize)
{
var child = _child;
if (child == null)
return default(Size);
child.Dispatcher.InvokeAsync(
() => child.Measure(availableSize),
DispatcherPriority.Loaded);
return default(Size);
}
protected override Size ArrangeOverride(Size finalSize)
{
var child = _child;
if (child == null)
return finalSize;
child.Dispatcher.InvokeAsync(
() => child.Arrange(new Rect(finalSize)),
DispatcherPriority.Loaded);
return finalSize;
}
#endregion
#region HitTest
protected override HitTestResult HitTestCore(PointHitTestParameters htp)
{
var child = _child;
var element = child?.Dispatcher.Invoke(() =>
{
double offsetX = 0d, offsetY = 0d;
if (child is FrameworkElement fe)
{
offsetX = fe.Margin.Left;
offsetY = fe.Margin.Top;
}
return _child.InputHitTest(new Point(htp.HitPoint.X - offsetX, htp.HitPoint.Y - offsetY));
}, DispatcherPriority.Normal);
if (element == null)
{
return null;
}
return new PointHitTestResult(this, htp.HitPoint);
}
#endregion
}
}
@@ -0,0 +1,8 @@
using System.Windows.Media;
namespace InkCanvasForClass.IccInkCanvas.Utils.Threading
{
public class InteractiveHostVisual : HostVisual
{
}
}
@@ -0,0 +1,134 @@
using System;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Windows.Threading;
using JetBrains.Annotations;
namespace InkCanvasForClass.IccInkCanvas.Utils.Threading
{
/// <summary>
/// 包含扩展 <see cref="Dispatcher"/> 的一些方法。
/// </summary>
public static class UIDispatcher
{
/// <summary>
/// 创建一个可以运行 <see cref="Dispatcher"/> 的后台 UI 线程,并返回这个线程的调度器 <see cref="Dispatcher"/>。
/// </summary>
/// <param name="name">线程的名称,如果不指定,将使用 “BackgroundUI”。</param>
/// <returns>一个可以异步等待的 <see cref="Dispatcher"/>。</returns>
public static DispatcherAsyncOperation<Dispatcher> RunNewAsync([CanBeNull] string name = null)
{
// 创建一个可等待的异步操作。
var awaiter = DispatcherAsyncOperation<Dispatcher>.Create(out var reportResult);
// 记录原线程关联的 Dispatcher,以便在意外时报告异常。
var originDispatcher = Dispatcher.CurrentDispatcher;
// 创建后台线程。
var thread = new Thread(() =>
{
try
{
// 获取关联此后台线程的 Dispatcher。
var dispatcher = Dispatcher.CurrentDispatcher;
// 设置此线程的 SynchronizationContext,以便此线程上 await 之后能够返回此线程。
SynchronizationContext.SetSynchronizationContext(
new DispatcherSynchronizationContext(dispatcher));
// 报告 Dispatcher 已创建完毕,使用 await 异步等待 Dispatcher 创建的地方可以继续执行了。
reportResult(dispatcher, null);
}
catch (Exception ex)
{
// 报告创建过程中发生的异常。
// 不需要担心其内部发生的异常,因为会被异步状态机捕获后重新在原线程上抛出。
reportResult(null, ex);
}
// 此线程的以下代码将脱离异步状态机的控制,需要自己处理异常。
try
{
// 启动 Dispatcher,开始此线程上消息的调度。
Dispatcher.Run();
}
catch (Exception ex)
{
// 如果新的 Dispatcher 线程上出现了未处理的异常,则将其抛到原调用线程上。
originDispatcher.InvokeAsync(() => ExceptionDispatchInfo.Capture(ex).Throw());
}
})
{
Name = name ?? "BackgroundUI",
IsBackground = true,
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
return awaiter;
}
/// <summary>
/// 创建一个可以运行 <see cref="Dispatcher"/> 的后台 UI 线程,并返回这个线程的调度器 <see cref="Dispatcher"/>。
/// </summary>
/// <param name="name">线程的名称,如果不指定,将使用 “BackgroundUI”。</param>
/// <returns>后台线程创建并启动的 <see cref="Dispatcher"/>。</returns>
public static Dispatcher RunNew([CanBeNull] string name = null)
{
var resetEvent = new AutoResetEvent(false);
// 记录原线程关联的 Dispatcher,以便在意外时报告异常。
var originDispatcher = Dispatcher.CurrentDispatcher;
Exception innerException = null;
Dispatcher dispatcher = null;
// 创建后台线程。
var thread = new Thread(() =>
{
try
{
// 获取关联此后台线程的 Dispatcher。
dispatcher = Dispatcher.CurrentDispatcher;
// 设置此线程的 SynchronizationContext,以便此线程上 await 之后能够返回此线程。
SynchronizationContext.SetSynchronizationContext(
new DispatcherSynchronizationContext(dispatcher));
// 报告 Dispatcher 已创建完毕,使用 ResetEvent 同步等待 Dispatcher 创建的地方可以继续执行了。
resetEvent.Set();
}
catch (Exception ex)
{
// 报告创建过程中发生的异常。
innerException = ex;
resetEvent.Set();
}
// 此线程的以下代码将脱离异步状态机的控制,需要自己处理异常。
try
{
// 启动 Dispatcher,开始此线程上消息的调度。
Dispatcher.Run();
}
catch (Exception ex)
{
// 如果新的 Dispatcher 线程上出现了未处理的异常,则将其抛到原调用线程上。
originDispatcher.InvokeAsync(() => ExceptionDispatchInfo.Capture(ex).Throw());
}
})
{
Name = name ?? "BackgroundUI",
IsBackground = true,
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
resetEvent.WaitOne();
resetEvent.Dispose();
resetEvent = null;
if (innerException != null)
{
ExceptionDispatchInfo.Capture(innerException).Throw();
}
return dispatcher;
}
}
}
@@ -0,0 +1,167 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace InkCanvasForClass.IccInkCanvas.Utils.Threading
{
/// <summary>
/// The VisualTargetPresentationSource represents the root
/// of a visual subtree owned by a different thread that the
/// visual tree in which is is displayed.
/// </summary>
/// <remarks>
/// A HostVisual belongs to the same UI thread that owns the
/// visual tree in which it resides.
///
/// A HostVisual can reference a VisualTarget owned by another
/// thread.
///
/// A VisualTarget has a root visual.
///
/// VisualTargetPresentationSource wraps the VisualTarget and
/// enables basic functionality like Loaded, which depends on
/// a PresentationSource being available.
/// </remarks>
public class VisualTargetPresentationSource : PresentationSource, IDisposable
{
public VisualTargetPresentationSource(HostVisual hostVisual)
{
_visualTarget = new VisualTarget(hostVisual);
}
public override Visual RootVisual
{
get => _visualTarget.RootVisual;
set
{
if (IsDisposed)
{
throw new ObjectDisposedException("VisualTarget");
}
var oldRoot = _visualTarget.RootVisual;
// Set the root visual of the VisualTarget. This visual will
// now be used to visually compose the scene.
_visualTarget.RootVisual = value;
// Hook the SizeChanged event on framework elements for all
// future changed to the layout size of our root, and manually
// trigger a size change.
if (oldRoot is FrameworkElement oldRootFe)
{
oldRootFe.SizeChanged -= root_SizeChanged;
}
if (value is FrameworkElement rootFe)
{
rootFe.SizeChanged += root_SizeChanged;
rootFe.DataContext = _dataContext;
if (_propertyName != null)
{
var myBinding = new Binding(_propertyName)
{
Source = _dataContext
};
rootFe.SetBinding(TextBlock.TextProperty, myBinding);
}
}
// Tell the PresentationSource that the root visual has
// changed. This kicks off a bunch of stuff like the
// Loaded event.
RootChanged(oldRoot, value);
// Kickoff layout...
if (value is UIElement rootElement)
{
rootElement.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
rootElement.Arrange(new Rect(rootElement.DesiredSize));
}
}
}
public object DataContext
{
get => _dataContext;
set
{
if (IsDisposed)
{
throw new ObjectDisposedException("VisualTarget");
}
if (_dataContext == value)
{
return;
}
_dataContext = value;
if (_visualTarget.RootVisual is FrameworkElement rootElement)
{
rootElement.DataContext = _dataContext;
}
}
}
public string PropertyName
{
get => _propertyName;
set
{
if (IsDisposed)
{
throw new ObjectDisposedException("VisualTarget");
}
_propertyName = value;
if (_visualTarget.RootVisual is TextBlock rootElement)
{
if (!rootElement.CheckAccess())
{
throw new InvalidOperationException("What?");
}
var myBinding = new Binding(_propertyName)
{
Source = _dataContext
};
rootElement.SetBinding(TextBlock.TextProperty, myBinding);
}
}
}
public event SizeChangedEventHandler SizeChanged;
public override bool IsDisposed => _isDisposed;
protected override CompositionTarget GetCompositionTargetCore()
{
return _visualTarget;
}
private void root_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (IsDisposed)
{
return;
}
SizeChanged?.Invoke(this, e);
}
private readonly VisualTarget _visualTarget;
private object _dataContext;
private string _propertyName;
private bool _isDisposed;
public void Dispose()
{
_visualTarget?.Dispose();
_isDisposed = true;
}
}
}