alpha
This commit is contained in:
@@ -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的Guid,2为对应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>Tuple,1为Guid,2为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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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所导致的更改到画布上,需要指定Dispatcher,StrokeCollection
|
||||
/// </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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user