环境: VS2022+.Net Framework4.8+Windows.Ink
程序名:WpfA_DrawArrow
一、程序功能
该程序是一个基于WPF InkCanvas 的箭头绘制工具,主要功能包括:
| 功能 | 操作方式 | 说明 |
|---|---|---|
| 绘制箭头 | 鼠标左键按下并拖动 | 从起点到终点绘制带箭头的线段 |
| 平移画布 | 鼠标右键按下并拖动 | 整体平移所有已绘制的箭头 |
| 缩放画布 | 鼠标滚轮滚动 | 以鼠标位置为中心缩放所有箭头 |
| 清空画布 | 鼠标右键双击 | 清除所有已绘制的箭头 |

二、InkCanvas
- InkCanvas 是一个用于在应用程序中实现手写输入、绘图和触摸操作的控件。它可以与 InkToolbar 控件配合使用,以提供更丰富的墨迹书写功能和设置。
1、基本概念
- InkCanvas 控件允许用户在其上进行手写输入、绘制图形或执行触摸操作。它是 Ink 命名空间的一部分,支持手写笔输入和多点触控。InkCanvas 控件本身是一个透明的覆盖层,不包括任何用于更改默认墨迹笔划属性的内置 UI。
2、关键属性
-
InkPresenter: 用于指定 InkCanvas 使用的 InkPresenter 类型。
-
EditingMode: 定义了 InkCanvas 的编辑模式,比如是否允许涂鸦、选择或橡皮擦。
-
Ink: 存储 InkCanvas 上的所有墨迹的集合。
-
Strokes: 表示 InkCanvas 上的笔画集合,每条笔画由多个点组成。
三、技术分析
- WPF (Windows Presentation Foundation):UI框架
- InkCanvas:手写笔迹画布控件,支持笔迹的绘制、变换和渲染
- 自定义Stroke:继承Stroke类实现自定义箭头绘制
1、自定义Stroke模式
- 通过重写DrawCore方法自定义渲染
- 使用DrawingContext进行低级图形绘制
2、变换矩阵技术
Matrix matrix = new Matrix();
matrix.Translate(dx, dy); // 平移
matrix.ScaleAt(scale, scale, centerX, centerY); // 缩放
DrawingCanvas.Strokes.Transform(matrix, false);
- 使用Matrix矩阵实现坐标变换
- false参数表示变换矩阵应用于Stroke坐标点而非渲染
3、箭头几何计算
double angleOri = Math.Atan((y2 - y1) / (x2 - x1)); // 线段方向角
double angleDown = angleOri - arrowAngle; // 下箭头角度
double angleUp = angleOri + arrowAngle; // 上箭头角度
- 使用三角函数计算箭头尖端两侧点
- 根据方向标识directionFlag处理不同方向的箭头
##4、鼠标交互事件处理
-
MouseLeftButtonDown/Move/Up:绘制模式
-
MouseRightButtonDown:平移模式(单击)和清空(双击)
-
MouseWheel:缩放
四、程序
1、项目结构
- WpfA_DrawArrow/
├── App.xaml
├── App.xaml.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── CustomArrow.cs
├── InkCanvasMethod.cs
└── WpfA_DrawArrow.csproj
2、MainWindow.xaml
<Window x:Class="WpfA_DrawArrow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPFA_DrawArrow - 箭头绘制工具" Height="600" Width="900"
WindowStartupLocation="CenterScreen"
LostFocus="Window_LostFocus">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 顶部工具栏 -->
<Border Grid.Row="0" Background="#2C3E50" Padding="10,8">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="✏️ 绘制箭头" FontSize="18" Foreground="White" FontWeight="Bold"/>
<TextBlock Text=" | " FontSize="18" Foreground="#7F8C8D"/>
<TextBlock Text="左键拖动绘制" FontSize="14" Foreground="#BDC3C7"/>
<TextBlock Text=" | " FontSize="18" Foreground="#7F8C8D"/>
<TextBlock Text="右键拖动平移" FontSize="14" Foreground="#BDC3C7"/>
<TextBlock Text=" | " FontSize="18" Foreground="#7F8C8D"/>
<TextBlock Text="滚轮缩放" FontSize="14" Foreground="#BDC3C7"/>
<TextBlock Text=" | " FontSize="18" Foreground="#7F8C8D"/>
<TextBlock Text="右键双击清空" FontSize="14" Foreground="#BDC3C7"/>
</StackPanel>
</Border>
<!-- 绘图区域 -->
<Grid Grid.Row="1">
<Grid.Background>
<DrawingBrush Viewport="0,0,30,30" ViewportUnits="Absolute" TileMode="Tile">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="#F5F6FA">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,30,30"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#E8ECF1">
<GeometryDrawing.Geometry>
<LineGeometry StartPoint="0,0" EndPoint="0,30"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#E8ECF1">
<GeometryDrawing.Geometry>
<LineGeometry StartPoint="0,0" EndPoint="30,0"/>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Grid.Background>
<!-- InkCanvas 绘图图层 -->
<InkCanvas x:Name="DrawingCanvas" Background="Transparent"
EditingMode="None"
MouseLeftButtonDown="DrawingCanvas_MouseLeftButtonDown"
MouseMove="DrawingCanvas_MouseMove"
MouseLeftButtonUp="DrawingCanvas_MouseLeftButtonUp"
MouseWheel="DrawingCanvas_MouseWheel"
MouseRightButtonDown="DrawingCanvas_MouseRightButtonDown"
MouseRightButtonUp="DrawingCanvas_MouseRightButtonUp"/>
</Grid>
<!-- 底部状态栏 -->
<Border Grid.Row="1" VerticalAlignment="Bottom" Background="#2C3E50" Opacity="0.8"
Padding="10,4" CornerRadius="8" Margin="10,0,10,10" Height="28">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock x:Name="StatusText" Text="就绪 - 左键拖动绘制箭头" Foreground="#BDC3C7" FontSize="12"/>
</StackPanel>
</Border>
</Grid>
</Window>
3、MainWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace WpfA_DrawArrow
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
private bool CanMove { get; set; }
private Point PointMoveOri { get; set; }
private bool IsPanning { get; set; }
public MainWindow()
{
InitializeComponent();
}
/// <summary>
/// 更新状态栏信息
/// </summary>
private void UpdateStatus(string message)
{
StatusText.Text = message;
}
/// <summary>
/// 鼠标左键按下 - 开始绘制
/// </summary>
private void DrawingCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
CanMove = true;
PointMoveOri = e.GetPosition(DrawingCanvas);
UpdateStatus($"绘制起点: ({PointMoveOri.X:F0}, {PointMoveOri.Y:F0})");
}
/// <summary>
/// 鼠标移动 - 绘制箭头或平移
/// </summary>
private void DrawingCanvas_MouseMove(object sender, MouseEventArgs e)
{
Point curPoint = e.GetPosition(DrawingCanvas);
// 绘制模式:实时更新箭头
if (CanMove && !IsPanning)
{
DrawingCanvas.Strokes.Clear();
DrawingCanvas.Strokes.Add(InkCanvasMethod.CreateArrow(PointMoveOri, curPoint));
UpdateStatus($"绘制中: 起点({PointMoveOri.X:F0},{PointMoveOri.Y:F0}) → 终点({curPoint.X:F0},{curPoint.Y:F0})");
}
// 平移模式
if (CanMove && IsPanning)
{
Matrix matrixMove = new Matrix();
matrixMove.Translate(curPoint.X - PointMoveOri.X, curPoint.Y - PointMoveOri.Y);
DrawingCanvas.Strokes.Transform(matrixMove, false);
PointMoveOri = curPoint;
UpdateStatus($"平移中: 位置 ({curPoint.X:F0}, {curPoint.Y:F0})");
}
}
/// <summary>
/// 鼠标左键释放 - 结束绘制
/// </summary>
private void DrawingCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (CanMove && !IsPanning)
{
Point curPoint = e.GetPosition(DrawingCanvas);
UpdateStatus($"绘制完成: 起点({PointMoveOri.X:F0},{PointMoveOri.Y:F0}) → 终点({curPoint.X:F0},{curPoint.Y:F0}) | 箭头数: {DrawingCanvas.Strokes.Count}");
}
CanMove = false;
}
/// <summary>
/// 鼠标滚轮 - 缩放
/// </summary>
private void DrawingCanvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
Point curPoint = e.GetPosition(DrawingCanvas);
Matrix matrix = new Matrix();
if (e.Delta > 0)
{
matrix.ScaleAt(1.25, 1.25, curPoint.X, curPoint.Y);
UpdateStatus($"放大: 中心({curPoint.X:F0},{curPoint.Y:F0})");
}
else
{
matrix.ScaleAt(0.8, 0.8, curPoint.X, curPoint.Y);
UpdateStatus($"缩小: 中心({curPoint.X:F0},{curPoint.Y:F0})");
}
DrawingCanvas.Strokes.Transform(matrix, false);
}
/// <summary>
/// 鼠标右键按下 - 平移或清空
/// </summary>
private void DrawingCanvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
// 双击清空
if (e.ClickCount > 1)
{
DrawingCanvas.Strokes.Clear();
IsPanning = false;
UpdateStatus("已清空所有箭头");
return;
}
// 单击开始平移
IsPanning = true;
PointMoveOri = e.GetPosition(DrawingCanvas);
UpdateStatus($"平移模式: 开始平移");
}
/// <summary>
/// 鼠标右键释放 - 结束平移
/// </summary>
private void DrawingCanvas_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
if (IsPanning)
{
IsPanning = false;
UpdateStatus($"平移完成 | 箭头数: {DrawingCanvas.Strokes.Count}");
}
}
/// <summary>
/// 窗口失去焦点时重置状态
/// </summary>
private void Window_LostFocus(object sender, RoutedEventArgs e)
{
CanMove = false;
IsPanning = false;
}
}
}
4、 CustomArrow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows;
namespace WpfA_DrawArrow
{
/// <summary>
/// 自定义箭头笔迹
/// </summary>
public class CustomArrow : Stroke
{
public CustomArrow(StylusPointCollection points) : base(points)
{
StylusPoints = points.Clone();
}
protected override void DrawCore(DrawingContext drawingContext, DrawingAttributes drawingAttributes)
{
// 获取起点和终点
double x1 = StylusPoints[0].X;
double y1 = StylusPoints[0].Y;
double x2 = StylusPoints[1].X;
double y2 = StylusPoints[1].Y;
// 计算距离
double dist = Math.Sqrt(((x1 - x2) * (x1 - x2)) + ((y1 - y2) * (y1 - y2)));
// 如果距离太小,只画一个点
if (dist < 1)
{
drawingContext.DrawEllipse(
InkCanvasMethod.StrokeBrushDefault,
null,
new Point(x1, y1),
3, 3);
return;
}
// 箭头参数
double arrowLength = Math.Min(20, dist * 0.4);
double arrowAngle = Math.PI / 8;
// 线段方向角
double angleOri = Math.Atan2(y2 - y1, x2 - x1);
// 箭头两侧点坐标
Point pt3 = new Point(
x2 - arrowLength * Math.Cos(angleOri - arrowAngle),
y2 - arrowLength * Math.Sin(angleOri - arrowAngle));
Point pt4 = new Point(
x2 - arrowLength * Math.Cos(angleOri + arrowAngle),
y2 - arrowLength * Math.Sin(angleOri + arrowAngle));
// 获取画笔
Pen pen = InkCanvasMethod.SetPenSolid();
// 绘制主线
drawingContext.DrawLine(pen, new Point(x1, y1), new Point(x2, y2));
// 绘制箭头两侧
drawingContext.DrawLine(pen, new Point(x2, y2), pt3);
drawingContext.DrawLine(pen, new Point(x2, y2), pt4);
// 填充箭头内部
PathGeometry arrowHead = new PathGeometry();
PathFigure figure = new PathFigure
{
StartPoint = new Point(x2, y2),
IsClosed = true,
IsFilled = true
};
figure.Segments.Add(new LineSegment(pt3, true));
figure.Segments.Add(new LineSegment(pt4, true));
arrowHead.Figures.Add(figure);
drawingContext.DrawGeometry(InkCanvasMethod.StrokeBrushDefault, pen, arrowHead);
}
}
}
5、 InkCanvasMethod.cs
using System.Windows;
using System.Windows.Media;
using System.Windows.Ink;
using System.Windows.Input;
namespace WpfA_DrawArrow
{
public static class InkCanvasMethod
{
// 默认颜色:橙色
public static Color ColorDefault = Color.FromArgb(0xFF, 0xFF, 0xA5, 0x00);
public static Brush StrokeBrushDefault = new SolidColorBrush(ColorDefault);
/// <summary>
/// 设置笔迹属性
/// </summary>
public static DrawingAttributes SetInkAttributes(double thickness = 2)
{
DrawingAttributes attributes = new DrawingAttributes
{
FitToCurve = false,
Width = thickness,
Height = thickness,
Color = ColorDefault,
StylusTip = StylusTip.Rectangle,
IsHighlighter = false,
IgnorePressure = true,
};
return attributes;
}
/// <summary>
/// 创建实线画笔
/// </summary>
public static Pen SetPenSolid(int thickness = 3)
{
Pen pen = new Pen
{
Brush = StrokeBrushDefault,
Thickness = thickness,
DashCap = PenLineCap.Round,
LineJoin = PenLineJoin.Round,
MiterLimit = 0.0
};
// 冻结画笔以提高性能
pen.Freeze();
return pen;
}
/// <summary>
/// 创建箭头笔迹
/// </summary>
public static CustomArrow CreateArrow(Point point1, Point point2)
{
StylusPointCollection points = new StylusPointCollection()
{
new StylusPoint(point1.X, point1.Y),
new StylusPoint(point2.X, point2.Y),
};
CustomArrow stroke = new CustomArrow(new StylusPointCollection(points))
{
DrawingAttributes = SetInkAttributes(),
};
return stroke;
}
}
}
五、资料
【1.4 使用InkCanvas绘制箭头:缩放、平移】WPF案例代码解析 - 知乎](https://zhuanlan.zhihu.com/p/441887897)