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)
相关推荐
samble2 天前
从零搭建灌装监控系统(四):深色主题与样式系统,ResourceDictionary 统一管理
c#·wpf·mvvm·样式·工业控制
_Jonas2 天前
.net framework开发环境正式环境配置文件的切换
.net·wpf
dalong104 天前
WPF:Modbus 状态监控
wpf·modbus
He BianGu5 天前
【WPF-VisionMaster】机器视觉通用平台V5.0版本发行说明
opencv·c#·wpf·halcon·机器视觉·visionmaster
fogota8 天前
Emoji与Segoe MDL2 Assets的比较
c#·wpf
fogota8 天前
C# WPF 按钮加载字体图标 / 动物头像
c#·.net·wpf
fogota8 天前
C# WPF 按钮加载系统图标 / 动物头像
c#·.net·wpf
军训猫猫头8 天前
C# 封装 MySQL 自动写入与查询工具类(基于 MySqlConnector)
mysql·c#·.net·wpf
gis开发之家9 天前
Spring Boot 4 定时任务与异步线程池:从 @Scheduled 到高并发任务编排(生产级实战)
java·spring boot·后端·wpf·spring boot4
clz131452110 天前
如何设计一个优雅可靠的金融级联机接口
wpf