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
相关产品推荐
相关产品推荐

