WPF:箭头绘制程序

环境: 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)
相关推荐
张工在路上18 小时前
WPF 布局基础概述
大数据·hadoop·wpf
迪飞特科技2 天前
分布式事务下 Spring 声明式事务失效的 3 种修复方案
分布式·spring·wpf
2601_962174175 天前
Redis 简介
数据库·redis·wpf
SamChan906 天前
Python+PyMuPDF提取PDF表格并翻译:表格结构检测与双语重建实战
windows·python·ai·pdf·wpf
SamChan906 天前
Python+OpenCV检测PDF翻译后排版偏移:像素级格式一致性验证
python·opencv·ai·pdf·wpf
FuckPatience6 天前
WPF 拿到控件的最初样式,修改获得焦点样式
wpf
主宰者6 天前
【WPF】MainWindow前添加加载窗口,发现加载过程中有白色无内容窗口闪烁一下的解决方案
wpf
Vae_Mars7 天前
WPF中的ControlTemplate(控件模板)
wpf
SamChan907 天前
Python+ReportLab自动生成PDF翻译质量审计报告:从数据到可视化的完整方案
开发语言·python·ai·pdf·wpf