WPF 实现自定义的笔迹橡皮擦
本文来告诉大家使用比较底层的方法来实现 WPF 的笔迹橡皮擦
在 WPF 里面,对于笔迹来说,应该放在 Stroke 类里面,而不是作为点的集合存储。在 Stroke 类里面将作为管理笔迹的类提供笔迹的渲染和橡皮擦等功能。咱下面将从 Stroke 类开始,自己定义笔迹橡皮擦。阅读本文,你将了解如何自定义橡皮擦,如自定义橡皮擦的外观样式,了解如何不依赖 InkCanvas 来实现笔迹的擦除
原本我是想采用 WPF 最简逻辑实现多指顺滑的笔迹书写 的方式来做笔迹的绘制部分的,但是考虑使用上面博客的方法将会让大家需要多了解很多触摸相关的知识,因此我就简单使用 InkCanvas 来做笔迹的绘制。以下只是将 InkCanvas 作为笔迹的绘制,而橡皮擦部分是咱定制的
在 XAML 中添加一个 InkCanvas 的代码很简单,请看代码
<InkCanvas x:Name="InkCanvas"></InkCanvas>
咱可以从这个 InkCanvas 里面获取当前的笔迹,如下面代码
StrokeCollection strokes = InkCanvas.Strokes;
这里拿到的 StrokeCollection 是一个集合,这个集合里面包含了多个 Stroke 类,在 WPF 中,一条笔迹就是一个 Stroke 对象。而多个 Stroke 就放在 StrokeCollection 类里面。可以认为是一个笔画就是一个 Stroke 而一个汉子包含了多个笔画,因此一个汉子的笔迹集合就使用 StrokeCollection 表示
通过上面代码就可以拿到 InkCanvas 里面的所有笔迹,接下来就是自定义橡皮擦部分的逻辑
这里的自定义橡皮擦的核心逻辑就是在 InkCanvas 上再放一个 Canvas 容器,在这个 Canvas 容器里面放自定义的橡皮擦的界面。因为这个 Canvas 容器在 InkCanvas 的上方,因此自定义的橡皮擦界面也将会在 InkCanvas 上
在界面里面放一个 Canvas 和一个用 Rectangle 表示的自定义外观的橡皮擦,大家可以使用自己喜欢的控件来代替 Rectangle 控件
<InkCanvas x:Name="InkCanvas"></InkCanvas>
<Canvas x:Name="EraserCanvas" Grid.Row="0" Background="Transparent" Visibility="Collapsed">
<Rectangle x:Name="EraserShape" HorizontalAlignment="Left"
Width="50" Height="100" Fill="Red" VerticalAlignment="Top">
<Rectangle.RenderTransform>
<TranslateTransform x:Name="TranslateTransform"></TranslateTransform>
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
可以看到在上面代码中,使用了 RenderTransform 来控制自定义的橡皮擦所在的坐标。上面代码有一个细节是需要设置这个自定义橡皮擦就在容器的左上角上,通过 HorizontalAlignment 和 VerticalAlignment 设置。当然了咱因为是放在 Canvas 容器里面,默认就是在左上角上,但是有个好习惯还是不错的。我就怕你抄代码的时候,用的容器和用的控件默认不是在左上角的
在上面代码中,咱默认的 EraserCanvas 是不可见的,而且背景色是透明的。这是为了默认可以在 InkCanvas 上写,而在点击按钮的时候,才设置 EraserCanvas 可见。在 EraserCanvas 设置背景色是透明的,是为了让 EraserCanvas 可以收到命中测试,也就是收到触摸或鼠标消息
在界面添加一个按钮,用于点击按钮的时候进入橡皮擦模式,如下面代码
<StackPanel Grid.Row="1">
<Button Content="进入橡皮擦" Margin="10,10,10,10" Click="Button_OnClick"></Button>
</StackPanel>
现在的整个界面的代码大概如下
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<InkCanvas x:Name="InkCanvas"></InkCanvas>
<Canvas x:Name="EraserCanvas" Grid.Row="0" Background="Transparent" Visibility="Collapsed">
<Rectangle x:Name="EraserShape" HorizontalAlignment="Left"
Width="50" Height="100" Fill="Red" VerticalAlignment="Top">
<Rectangle.RenderTransform>
<TranslateTransform x:Name="TranslateTransform"></TranslateTransform>
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
<StackPanel Grid.Row="1">
<Button Content="进入橡皮擦" Margin="10,10,10,10" Click="Button_OnClick"></Button>
</StackPanel>
</Grid>
进入到咱的后台代码逻辑,在点击按钮的时候,才是进入到核心的逻辑里面
private void Button_OnClick(object sender, RoutedEventArgs e)
{
EraserCanvas.Visibility = Visibility.Visible;
}
其实就是让 EraserCanvas 可见,因为 EraserCanvas 放在 InkCanvas 上方,如果 EraserCanvas 可见,那么 EraserCanvas 将会吃掉在 InkCanvas 上的交互,如鼠标或触摸,都会命中到 EraserCanvas 上。因此 InkCanvas 就不能接收到消息,也就无法进入书写了
在 EraserCanvas 监听输入的事件,如下面代码监听了鼠标事件。那么即可在进入橡皮擦模式的时候,在 EraserCanvas 可以接收到输入消息触发代码
EraserCanvas.MouseDown += EraserCanvas_MouseDown;
EraserCanvas.MouseMove += EraserCanvas_MouseMove;
EraserCanvas.MouseUp += EraserCanvas_MouseUp;
在鼠标按下的时候咱开始进入核心的逻辑,请看代码
private IncrementalStrokeHitTester _incrementalStrokeHitTester;
private bool _isDown;
private void EraserCanvas_MouseDown(object sender, MouseButtonEventArgs e)
{
_isDown = true;
IncrementalStrokeHitTester incrementalStrokeHitTester =
InkCanvas.Strokes.GetIncrementalStrokeHitTester(new RectangleStylusShape(EraserShape.ActualWidth,
EraserShape.ActualHeight));
_incrementalStrokeHitTester = incrementalStrokeHitTester;
_incrementalStrokeHitTester.StrokeHit += IncrementalStrokeHitTester_StrokeHit;
}
在 StrokeCollection 里面有一个方法是 GetIncrementalStrokeHitTester 方法,可以通过这个方法获取这段笔迹的命中测试工具。需要传入的是橡皮擦的形状和大小,可以支持的橡皮擦只有矩形和圆形两个。本文这里使用的是矩形的橡皮擦。如果你需要支持自定义形状的橡皮擦,如三角形等,就需要自己用更底层的方式去实现了,也不在本文范围之内
在获取到 IncrementalStrokeHitTester 工具之后,需要监听他的 StrokeHit 事件,这个事件将会在笔迹被擦到的时候触发,这个事件就是咱的核心逻辑了
在鼠标移动的时候,需要给 IncrementalStrokeHitTester 加上当前的触摸移动的点,请看代码
private void EraserCanvas_MouseMove(object sender, MouseEventArgs e)
{
if (_isDown)
{
var point = e.GetPosition(this);
TranslateTransform.X = point.X - EraserShape.ActualWidth / 2;
TranslateTransform.Y = point.Y - EraserShape.ActualHeight / 2;
_incrementalStrokeHitTester.AddPoint(point);
}
}
上面代码有两个功能,一个是移动橡皮擦的外观,另一个是给命中测试工具加上当前的触摸点
在调用 IncrementalStrokeHitTester 的 AddPoint 方法的时候,如果刚好此时命中到了某个笔迹,那么将会触发 StrokeHit 事件
在 StrokeHit 事件里面包含了两个有用的参数,其中一个参数表示的是当前被命中的笔迹是哪个笔迹。另一个是在进行擦除之后新创建的笔迹。也就是说将原有的笔迹,一个笔迹擦为了多个笔迹,当然多个笔迹肯定也包含了零个笔迹
private void IncrementalStrokeHitTester_StrokeHit(object sender, StrokeHitEventArgs e)
{
InkCanvas.Strokes.Remove(e.HitStroke);
InkCanvas.Strokes.Add(e.GetPointEraseResults());
}
上面代码的逻辑就是将被擦到的笔迹删除掉,添加为擦出之后新建的多个笔迹。这样就能实现出笔迹被擦的效果。也就是说笔迹被插不是在原有的笔迹上删除某些点,而是将一条笔迹修改为多条的方式进行擦掉
这样的设计的好处在于撤销重做的功能很好做,因为原有的笔迹是不动的,是通过替换笔迹的形式,因此只需要保存笔迹的对象即可
在鼠标抬起的时候,可以清理一下橡皮擦的逻辑。当然在我的业务里面,抬起鼠标就是等于橡皮擦结束了
private void EraserCanvas_MouseUp(object sender, MouseButtonEventArgs e)
{
EraserCanvas.Visibility = Visibility.Collapsed;
_isDown = false;
_incrementalStrokeHitTester.EndHitTesting();
_incrementalStrokeHitTester = null;
}
上面代码核心是调用 EndHitTesting 清理一下资源,不调用也可以,不会存在内存泄露
全部的后台代码如下
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
EraserCanvas.MouseDown += EraserCanvas_MouseDown;
EraserCanvas.MouseMove += EraserCanvas_MouseMove;
EraserCanvas.MouseUp += EraserCanvas_MouseUp;
}
private IncrementalStrokeHitTester _incrementalStrokeHitTester;
private bool _isDown;
private void EraserCanvas_MouseDown(object sender, MouseButtonEventArgs e)
{
_isDown = true;
IncrementalStrokeHitTester incrementalStrokeHitTester =
InkCanvas.Strokes.GetIncrementalStrokeHitTester(new RectangleStylusShape(EraserShape.ActualWidth,
EraserShape.ActualHeight));
_incrementalStrokeHitTester = incrementalStrokeHitTester;
_incrementalStrokeHitTester.StrokeHit += IncrementalStrokeHitTester_StrokeHit;
}
private void EraserCanvas_MouseUp(object sender, MouseButtonEventArgs e)
{
EraserCanvas.Visibility = Visibility.Collapsed;
_isDown = false;
_incrementalStrokeHitTester.EndHitTesting();
_incrementalStrokeHitTester = null;
}
private void Button_OnClick(object sender, RoutedEventArgs e)
{
EraserCanvas.Visibility = Visibility.Visible;
TranslateTransform.X = -1000;
TranslateTransform.Y = -1000;
}
private void IncrementalStrokeHitTester_StrokeHit(object sender, StrokeHitEventArgs e)
{
InkCanvas.Strokes.Remove(e.HitStroke);
InkCanvas.Strokes.Add(e.GetPointEraseResults());
}
private void EraserCanvas_MouseMove(object sender, MouseEventArgs e)
{
if (_isDown)
{
var point = e.GetPosition(this);
TranslateTransform.X = point.X - EraserShape.ActualWidth / 2;
TranslateTransform.Y = point.Y - EraserShape.ActualHeight / 2;
_incrementalStrokeHitTester.AddPoint(point);
}
}
}
更多触摸请看 WPF 触摸相关 更多笔迹相关请看
- WPF 渲染原理
- 高性能笔迹原理
- WPF 高性能笔
- WPF 高速书写 StylusPlugIn 原理
- WPF 最小的代码使用 DynamicRenderer 书写
- WPF 使用 Composition API 做高性能渲染
- WPF 使用 Win2d 渲染
- win10 uwp win2d CanvasVirtualControl 与 CanvasAnimatedControl
- WPF 最简逻辑实现多指顺滑的笔迹书写
WPF 实现自定义的笔迹橡皮擦的更多相关文章
- WPF 之 自定义窗体标题栏
在WPF中自定义窗体标题栏,首先需要将窗体的WindowStyle属性设置为None,隐藏掉WPF窗体的自带标题栏.然后可以在窗体内部自定义一个标题栏. 例如,标题栏如下: <WrapPanel ...
- 在WPF中自定义你的绘制(五)
原文:在WPF中自定义你的绘制(五) 在WPF中自定义你的绘制(五) ...
- 在WPF中自定义你的绘制(三)
原文:在WPF中自定义你的绘制(三) 在WPF中自定义你的绘制(三) ...
- 在WPF中自定义你的绘制(四)
原文:在WPF中自定义你的绘制(四) 在WPF中自定义你的绘制(四) ...
- 在WPF中自定义你的绘制(一)
原文:在WPF中自定义你的绘制(一) 在WPF中自定义你的绘制(一) ...
- 在WPF中自定义你的绘制(二)
原文:在WPF中自定义你的绘制(二) 在WPF中自定义你的绘制(二) ...
- 在VS2005中设置WPF中自定义按钮的事件
原文:在VS2005中设置WPF中自定义按钮的事件 上篇讲了如何在Blend中绘制圆角矩形(http://blog.csdn.net/johnsuna/archive/2007/08/13/17407 ...
- WPF绘制自定义窗口
原文:WPF绘制自定义窗口 WPF是制作界面的一大利器,下面就用WPF模拟一下360的软件管理界面,360软件管理界面如下: 界面不难,主要有如下几个要素: 窗体的圆角 自定义标题栏及按钮 自定义状态 ...
- wpf之自定义滚动条
原文:wpf之自定义滚动条 首先我们添加一个带滚动条的textbox控件: <ScrollViewer Height="130" Width="620" ...
- WPF中自定义的DataTemplate中的控件,在Window_Loaded事件中加载机制初探
原文:WPF中自定义的DataTemplate中的控件,在Window_Loaded事件中加载机制初探 最近因为项目需要,开始学习如何使用WPF开发桌面程序.使用WPF一段时间之后,感 ...
随机推荐
- Locust如何实现负载测试?
一.场景要求 我们在使用locust时,有时候默认的场景无法满足我们的要求时,这时后我们需要自定义场景 比如我们要设置每一段时间启动10个用户运行,执行60s后再一次启动10个用户,总共运行10分钟, ...
- 记录--vue组件划分的思考
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 对vue项目来说,组件是构成项目的基本单元,为了方便理解,这里定义两类组件:页面组件,功能组件.为什么需要划分这两类组件是从组件复用来考虑 ...
- 记一次 .NET某半导体CIM系统 崩溃分析
一:背景 1. 讲故事 前些天有一位朋友在公众号上找到我,说他们的WinForm程序部署在20多台机器上,只有两台机器上的程序会出现崩溃的情况,自己找了好久也没分析出来,让我帮忙看下怎么回事,就喜欢这 ...
- #搜索,计算几何#JZOJ 4016 圈地为王
题目 在\(n\)行\(m\)列的网格中,你要圈一些地. 你从左上角出发,最后返回左上角,路径内部的区域视为被你圈住. 你不可以进入网格内部, 只能在边上行走. 你的路径不能在左上角以外自交, 但是边 ...
- 【LGR-069】洛谷 2 月月赛 II & EE Round 2
目录 前言 洛谷 6101 [EER2]出言不逊 分析 代码 洛谷 6102 [EER2]谔运算 分析 代码 洛谷 6103 [EER2] 直接自然溢出啥事没有 分析 代码 洛谷 6105 [Ynoi ...
- 【FAQ】用户访问次数不变,访问时长却突增2倍,分析服务发生数据异常该如何解决?
在产品运营的工作过程中,需要每日关注产品的核心指标变化情况,监控其整体运营状况.华为分析服务提供查看吸引新用户卡片,该卡片展示了新增用户数.人均会话次数.人均访问时长.人均页面访问数.借助该页面运营可 ...
- selenium 关闭浏览--- close 与 quit 的区别
selenium 关闭浏览器,有两种方式 close quit 既然都是关闭浏览器,为什么要写两种方式? 区别 close: close只是关闭浏览器,但是不会退出 webdriver quit: q ...
- 什么是MurmurHash
MurmurHash简介 MurmurHash是一种非加密散列函数,名称来自两个基本操作,乘法(MU)和旋转(R).与加密散列函数不同,它不是专门设计为难以被对手逆转,因此不适用于加密目的.在2018 ...
- 最全能的AI换脸软件,FaceFusion下载介绍(可直播)
FaceFusion是一款多功能的AI换脸软件,它不仅能图片.视频换脸,还可以直播换脸,换脸效果真实.自然 与大多数换脸软件不同的是,FaceFusion不仅支持N卡处理程序(Azure),还额外提供 ...
- CentOS 8 安装 oracle 23c CentOS9 Error deal
1.环境准备 软件准备 序号 软件 下载地址 1 VirtualBox https://www.virtualbox.org/wiki/Downloads 2 CentOS Stream 8 http ...