You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF Canvas绘制元素上MouseMove/MouseWheel事件无法触发问题求助

WPF Canvas绘图程序:鼠标在线条上时平移/缩放功能失效问题

现象

  • 鼠标位于Canvas空白区域:平移和缩放功能正常,侧边面板实时更新鼠标及模型坐标
  • 鼠标位于绘制的线条上:MouseWheel和MouseMove事件失效,平移、缩放功能无法触发,坐标信息停止更新

相关代码

XAML代码

<Window x:Class="CanvasCad.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CanvasCad"
        mc:Ignorable="d"
        Title="MainWindow" 
        Height="450" Width="800"
        Loaded="Window_Loaded"
        >
    <Grid Margin="5">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="200"/>
        </Grid.ColumnDefinitions>

        <StackPanel Grid.Column="0" Margin="5">
            <Label Content="Model origin in canvas"/>
            <Label Name="lblOriginInCanvasX" Content="X="/>
            <Label Name="lblOriginInCanvasY" Content="Y="/>
            <Separator Height="1" />

            <Label Content="Model scale in canvas"/>
            <Label Name="lblScaleModelInCanvas" Content="z="/>
            <Separator Height="1" />

            <Label Content="Mouse in canvas"/>
            <Label Name="lblMouseInCanvasX" Content="X="/>
            <Label Name="lblMouseInCanvasY" Content="Y="/>
            <Separator Height="1" />

            <Label Content="Mouse in model"/>
            <Label Name="lblMouseInModelX" Content="X="/>
            <Label Name="lblMouseInModelY" Content="Y="/>
            <Separator Height="1" />
        </StackPanel>

        <Canvas
            Name="CanvasCad"
            Grid.Column="1"
            >
        </Canvas>

        <StackPanel
            Grid.Column="2">
            <Button Name="btTest">Test</Button>
        </StackPanel>
    </Grid>
</Window>

C# MainWindow代码

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

namespace CanvasCad
{
    public partial class MainWindow : Window
    {
        private Point _originModelInCanvas;
        private double _scaleModelInCanvas;
        private double _zoomFactor;

        private bool _IsModelMove;
        private Point _vectorMouseToOriginInCanvas;

        private List<Entity> _entities;

        public MainWindow()
        {
            InitializeComponent();

            CanvasCad.Background = new SolidColorBrush(Colors.Black);
            CanvasCad.ClipToBounds = true;

            CanvasCad.MouseMove += CanvasCad_MouseMove;
            CanvasCad.MouseDown += CanvasCad_MouseDown;
            CanvasCad.MouseUp += CanvasCad_MouseUp;
            CanvasCad.MouseWheel += CanvasCad_MouseWheel;

            _IsModelMove = false;
            _originModelInCanvas = new Point(0, 0);
            _scaleModelInCanvas = 1;
            _zoomFactor = 0.1;

            _entities = new List<Entity>();
            // X轴
            _entities.Add(new Entity(new Point(-500, 0), new Point(500, 0), new SolidColorBrush(Colors.Blue), 2));
            // Y轴
            _entities.Add(new Entity(new Point(0, -500), new Point(0, 500), new SolidColorBrush(Colors.Blue), 2));
            // 线条1
            _entities.Add(new Entity(new Point(50, 70), new Point(100, 30), new SolidColorBrush(Colors.Red), 4));
            // 线条2
            _entities.Add(new Entity(new Point(200, -83), new Point(-96, 300), new SolidColorBrush(Colors.Orange), 4));
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            _originModelInCanvas = new Point(75, 130);
            _scaleModelInCanvas = 1;

            lblOriginInCanvasX.Content = $"X={_originModelInCanvas.X.ToString()}";
            lblOriginInCanvasY.Content = $"Y={_originModelInCanvas.Y.ToString()}";
            lblScaleModelInCanvas.Content = $"Z={_scaleModelInCanvas.ToString()}";

            Redraw();
        }

        private void CanvasCad_MouseMove(object sender, MouseEventArgs e)
        {
            Point ptMouseInCanvas = e.GetPosition(CanvasCad);
            Point ptMouseInModel = ConvertPoint.MouseCanvasToModel(ptMouseInCanvas, _originModelInCanvas, _scaleModelInCanvas);

            if (_IsModelMove)
            {
                _originModelInCanvas = new Point(ptMouseInCanvas.X - _vectorMouseToOriginInCanvas.X, ptMouseInCanvas.Y - _vectorMouseToOriginInCanvas.Y);
            }

            lblMouseInCanvasX.Content = $"X={ptMouseInCanvas.X.ToString()}";
            lblMouseInCanvasY.Content = $"Y={ptMouseInCanvas.Y.ToString()}";
            lblMouseInModelX.Content = $"X={ptMouseInModel.X.ToString()}";
            lblMouseInModelY.Content = $"Y={ptMouseInModel.Y.ToString()}";

            Redraw();
        }

        private void CanvasCad_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            Point ptMouseInCanvas = e.GetPosition(CanvasCad);

            if (!_IsModelMove)
            {
                double zoom = 0;
                if (e.Delta > 0) { zoom = 1 + _zoomFactor; }
                if (e.Delta < 0) { zoom = 1 - _zoomFactor; }

                _vectorMouseToOriginInCanvas = new Point(ptMouseInCanvas.X - _originModelInCanvas.X, ptMouseInCanvas.Y - _originModelInCanvas.Y);

                _originModelInCanvas = new Point(ptMouseInCanvas.X - _vectorMouseToOriginInCanvas.X * zoom, ptMouseInCanvas.Y - _vectorMouseToOriginInCanvas.Y * zoom);
                _scaleModelInCanvas = _scaleModelInCanvas * zoom;

                Redraw();
            }
        }

        private void CanvasCad_MouseDown(object sender, MouseButtonEventArgs e)
        {
            Point ptMouseInCanvas = e.GetPosition(CanvasCad);

            switch (e.ChangedButton)
            {
                case MouseButton.Middle:
                    _IsModelMove = true;
                    _vectorMouseToOriginInCanvas = new Point(ptMouseInCanvas.X - _originModelInCanvas.X, ptMouseInCanvas.Y - _originModelInCanvas.Y);
                    break;
            }
        }

        private void CanvasCad_MouseUp(object sender, MouseButtonEventArgs e)
        {
            switch (e.ChangedButton)
            {
                case MouseButton.Middle:
                    _IsModelMove = false;
                    break;
            }
        }

        private void Redraw()
        {
            CanvasCad.Children.Clear();
            foreach (Entity e in _entities)
            {
                CanvasCad.Children.Add(e.ToModel(_originModelInCanvas, _scaleModelInCanvas));
            }
        }
    }
}

C# Entity类代码

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace CanvasCad
{
    internal class Entity
    {
        private Point _pointStart;
        public Point pointStart
        {
            get { return _pointStart; }
            set { _pointStart = value; }
        }

        private Point _pointEnd;
        public Point pointEnd
        {
            get { return _pointEnd; }
            set { _pointEnd = value; }
        }

        private Brush _brush;
        public Brush brush
        {
            get { return _brush; }
            set { _brush = value; }
        }

        private int _thickness;
        public int thickness
        {
            get { return _thickness; }
            set { _thickness = value; }
        }

        public Entity()
        {
            _pointStart = new Point(0, 0);
            _pointEnd = new Point(0, 0);
            _brush = Brushes.Black;
            _thickness = 1;
        }

        public Entity(Point pointStart, Point pointEnd, Brush brush, int thickness)
        {
            _pointStart = pointStart;
            _pointEnd = pointEnd;
            _brush = brush;
            _thickness = thickness;
        }

        public Path ToCanvas()
        {
            Path p = new Path();
            p.Stroke = _brush;
            p.StrokeThickness = _thickness;

            LineGeometry lg = new LineGeometry(_pointStart, _pointEnd);
            GeometryGroup gg = new GeometryGroup();
            gg.Children.Add(lg);
            p.Data = gg;

            return p;
        }

        public Path ToModel(Point origin, double scale)
        {
            Path p = new Path();
            p.Stroke = _brush;
            p.StrokeThickness = _thickness;

            LineGeometry lg = new LineGeometry(ConvertPoint.CanvasToModel(_pointStart, origin, scale), ConvertPoint.CanvasToModel(_pointEnd, origin, scale));
            GeometryGroup gg = new GeometryGroup();
            gg.Children.Add(lg);
            p.Data = gg;

            return p;
        }
    }
}

C# ConvertPoint类代码

using System;
using System.Windows;

namespace CanvasCad
{
    internal class ConvertPoint
    {
        public static Point CanvasToModel(Point point, Point origine, double scale)
        {
            Point p = new Point(point.X, point.Y);
            // Y轴对称
            p = new Point(p.X, -p.Y);
            // 基于原点缩放
            p = new Point(p.X * scale, p.Y * scale);
            // 原点偏移
            p = new Point(p.X + origine.X, p.Y + origine.Y);

            return p;
        }

        public static Point MouseCanvasToModel(Point mouse, Point origine, double scale)
        {
            Point p = new Point(mouse.X, mouse.Y);
            // 原点偏移
            p = new Point(p.X - origine.X, p.Y - origine.Y);
            // 基于原点缩放
            p = new Point(p.X / scale, p.Y / scale);
            // Y轴对称
            p = new Point(p.X, -p.Y);

            return p;
        }
    }
}

解决方案

方法1:禁用线条的命中测试

在创建线条Path时,设置IsHitTestVisible="False",让WPF忽略线条的鼠标命中测试,事件直接传递给Canvas。

修改Entity类的ToModel方法,添加一行代码:

public Path ToModel(Point origin, double scale)
{
    Path p = new Path();
    p.Stroke = _brush;
    p.StrokeThickness = _thickness;
    // 禁用命中测试
    p.IsHitTestVisible = false;

    LineGeometry lg = new LineGeometry(ConvertPoint.CanvasToModel(_pointStart, origin, scale), ConvertPoint.CanvasToModel(_pointEnd, origin, scale));
    GeometryGroup gg = new GeometryGroup();
    gg.Children.Add(lg);
    p.Data = gg;

    return p;
}

方法2:使用Preview事件

WPF的Preview事件属于隧道事件,会从父元素(Canvas)开始向下传递,因此即使鼠标在子元素上,Canvas也能先捕获到事件。

修改MainWindow构造函数中的事件绑定,替换为Preview版本:

// 替换原来的事件绑定
CanvasCad.PreviewMouseMove += CanvasCad_MouseMove;
CanvasCad.PreviewMouseDown += CanvasCad_MouseDown;
CanvasCad.PreviewMouseUp += CanvasCad_MouseUp;
CanvasCad.PreviewMouseWheel += CanvasCad_MouseWheel;

方法3:保留线条交互时的事件冒泡(可选)

如果后续需要对线条做点击、选中之类的交互,可以在Path的事件处理中允许事件继续冒泡:

public Path ToModel(Point origin, double scale)
{
    Path p = new Path();
    p.Stroke = _brush;
    p.StrokeThickness = _thickness;
    
    // 让鼠标事件继续冒泡到Canvas
    p.MouseMove += (s, e) => e.Handled = false;
    p.MouseWheel += (s, e) => e.Handled = false;
    p.MouseDown += (s, e) => e.Handled = false;
    p.MouseUp += (s, e) => e.Handled = false;

    LineGeometry lg = new LineGeometry(ConvertPoint.CanvasToModel(_pointStart, origin, scale), ConvertPoint.CanvasToModel(_pointEnd, origin, scale));
    GeometryGroup gg = new GeometryGroup();
    gg.Children.Add(lg);
    p.Data = gg;

    return p;
}

这种方式适合需要同时保留线条交互和Canvas全局操作的场景。

原因说明

WPF的事件路由分为冒泡和隧道两种:

  • 冒泡事件:从子元素向上传递到父元素
  • 隧道事件(Preview前缀):从父元素向下传递到子元素

默认情况下,MouseMove、MouseWheel等属于冒泡事件,当鼠标在子元素(线条Path)上时,事件会先被Path接收,如果Path没有明确标记事件未处理,就不会继续冒泡到Canvas,导致Canvas的事件处理函数无法触发。

内容的提问来源于stack exchange,提问作者Alex33

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 06:55:16