如何实现可拖拽移动、缩放、旋转的选择矩形?解决位置异常
问题:旋转后缩放偏移、二次旋转跳变的选择矩形修复
我正在实现一个可通过鼠标拖拽完成移动、缩放和旋转的选择矩形。目前移动功能正常,但旋转与缩放存在问题:
- 旋转前缩放功能正常,旋转后缩放时矩形位置会偏移;
- 首次旋转表现正常,再次旋转时矩形会跳变移位。

原代码
XAML部分
<Window x:Class="SelectionRect.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:local="clr-namespace:SelectionRect" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="MainWindow" Width="400" Height="400" d:DataContext="{d:DesignInstance Type=local:RectModel}" mc:Ignorable="d"> <Window.Resources> <Style x:Key="ThumbStyle" TargetType="Thumb"> <Setter Property="Focusable" Value="True" /> <Setter Property="Height" Value="7" /> <Setter Property="Width" Value="7" /> <Setter Property="BorderBrush" Value="Blue" /> <Setter Property="Background" Value="LightBlue" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Thumb}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1" /> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <Grid> <Canvas x:Name="Canvas1"> <Grid Canvas.Left="{Binding Left}" Name="RectGrid" Canvas.Top="{Binding Top}" Width="{Binding Width}" Height="{Binding Height}"> <Grid.LayoutTransform> <RotateTransform Angle="{Binding Angle}" /> </Grid.LayoutTransform> <Border Background="#4C0000FF" BorderBrush="Blue" BorderThickness="1" /> <Thumb Name="LeftTopThumb" Margin="-5,-5,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Cursor="SizeNWSE" DragDelta="LeftTopThumb_DragDelta" Style="{StaticResource ThumbStyle}" /> <Thumb Name="RightTopThumb" Margin="0,-5,-5,0" HorizontalAlignment="Right" VerticalAlignment="Top" Cursor="SizeNESW" DragDelta="RightTopThumb_DragDelta" Style="{StaticResource ThumbStyle}" /> <Thumb Name="LeftBottomThumb" Margin="-5,0,0,-5" HorizontalAlignment="Left" VerticalAlignment="Bottom" Cursor="SizeNESW" DragDelta="LeftBottomThumb_DragDelta" Style="{StaticResource ThumbStyle}" /> <Thumb Name="RightBottomThumb" Margin="0,0,-5,-5" HorizontalAlignment="Right" VerticalAlignment="Bottom" Cursor="SizeNWSE" DragDelta="RightBottomThumb_DragDelta" Style="{StaticResource ThumbStyle}" /> <Thumb Name="CenterBottomThumb" Margin="0,0,0,-5" HorizontalAlignment="Center" VerticalAlignment="Bottom" Cursor="SizeNS" DragDelta="CenterBottomThumb_DragDelta" Style="{StaticResource ThumbStyle}" /> <Thumb Name="CenterTopThumb" Margin="0,-5,0,0" HorizontalAlignment="Center" VerticalAlignment="Top" Cursor="SizeNS" DragDelta="CenterTopThumb_DragDelta" Style="{StaticResource ThumbStyle}" /> <Thumb Name="RotateThumb" Margin="0,-25,0,0" HorizontalAlignment="Center" VerticalAlignment="Top" Cursor="Cross" DragDelta="RotateThumb_DragDelta" DragStarted="RotateThumb_DragStarted" Style="{StaticResource ThumbStyle}" /> <Thumb Name="CenterLeftThumb" Margin="-5,0,0,0" HorizontalAlignment="Left" VerticalAlignment="Center" Cursor="SizeWE" DragDelta="CenterLeftThumb_DragDelta" Style="{StaticResource ThumbStyle}" /> <Thumb Name="CenterRightThumb" Margin="0,0,-5,0" HorizontalAlignment="Right" VerticalAlignment="Center" Cursor="SizeWE" DragDelta="CenterRightThumb_DragDelta" Style="{StaticResource ThumbStyle}" /> <Thumb x:Name="CenterThumb" Width="Auto" Height="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Cursor="SizeAll" DragDelta="CenterThumb_DragDelta" Focusable="True"> <Thumb.Template> <ControlTemplate> <Border Name="CenterThumbBorder" Margin="10" Background="Transparent" MouseDown="CenterThumbBorder_MouseDown" /> </ControlTemplate> </Thumb.Template> </Thumb> </Grid> </Canvas> </Grid> </Window>
C#部分
using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using Linq; namespace SelectionRect { public partial class MainWindow : Window { private double _initialLeft; private double _initialTop; public MainWindow() { InitializeComponent(); DataContext = new RectModel() { Height = 200, Width = 100 , Left = 20, Top = 50}; } private void CenterBottomThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { var thumb = sender as Thumb; var model = thumb.DataContext as RectModel; if (model.Height + e.VerticalChange >= 1) { model.Height += e.VerticalChange; } } private void CenterTopThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { } private void RotateThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { var thumb = sender as Thumb; var model = thumb.DataContext as RectModel; var initialAngle = model.Angle; var center = new Point(model.CenterX, model.CenterY); center = RectGrid.TranslatePoint(center, Canvas1); var currentPos = Mouse.GetPosition(Canvas1); model.Angle = center.GetAngle(currentPos) + 90; var Rect = new Rect(_initialLeft, _initialTop, model.Width, model.Height); var cornerPoints = new[] { Rect.TopLeft, Rect.TopRight, Rect.BottomRight, Rect.BottomLeft }; var m = new Matrix(); m.RotateAt(model.Angle, center.X, center.Y); m.Transform(cornerPoints); model.Left = cornerPoints.Min(p => p.X); model.Top = cornerPoints.Min(p => p.Y); } private void RotateThumb_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e) { var thumb = sender as Thumb; var model = thumb.DataContext as RectModel; _initialLeft = model.Left; _initialTop = model.Top; } private void CenterLeftThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { } private void CenterRightThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { } private void CenterThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { var thumb = sender as Thumb; var model = thumb.DataContext as RectModel; model.Top += e.VerticalChange; model.Left += e.HorizontalChange; } private void CenterThumbBorder_MouseDown(object sender, MouseButtonEventArgs e) { } private void LeftBottomThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { } private void LeftTopThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { } private void RightTopThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { } private void RightBottomThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { } } public static class Ex { public static double GetAngle(this System.Windows.Point p1, System.Windows.Point p2) { var xDiff = p2.X - p1.X; var yDiff = p2.Y - p1.Y; return Math.Atan2(yDiff, xDiff) * 180.0 / Math.PI; } } class RectModel : INotifyPropertyChanged { private double _Angle; public double Angle { get { return _Angle; } set { if (value == _Angle) return; _Angle = value; OnPropertyChanged(nameof(Angle)); } } private double _Height; public double Height { get { return _Height; } set { if (value == _Height) return; _Height = value; OnPropertyChanged(nameof(Height)); } } private double _Width; public double Width { get { return _Width; } set { if (value == _Width) return; _Width = value; OnPropertyChanged(nameof(Width)); } } private double _Top; public double Top { get { return _Top; } set { if (value == _Top) return; _Top = value; OnPropertyChanged(nameof(Top)); } } private double _Left; public double Left { get { return _Left; } set { if (value == _Left) return; _Left = value; OnPropertyChanged(nameof(Left)); } } public double CenterX => Width / 2; public double CenterY => Height / 2; public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }
解决方案
1. 修复二次旋转跳变问题
原代码中旋转时使用的是旋转后的Left/Top作为基准,导致二次旋转时基准错误。需要记录拖拽开始时的未旋转原始矩形和初始角度,基于增量计算旋转。
修改MainWindow中的变量和旋转相关方法:
// 添加新的成员变量 private Rect _initialUnrotatedRect; private double _initialAngle; // 修改RotateThumb_DragStarted private void RotateThumb_DragStarted(object sender, DragStartedEventArgs e) { var model = (RectModel)DataContext; // 记录未旋转的原始矩形和初始角度 _initialUnrotatedRect = new Rect(model.Left, model.Top, model.Width, model.Height); _initialAngle = model.Angle; } // 修改RotateThumb_DragDelta private void RotateThumb_DragDelta(object sender, DragDeltaEventArgs e) { var model = (RectModel)DataContext; // 获取矩形在Canvas中的实际中心点 var rectCenterInCanvas = RectGrid.TranslatePoint(new Point(model.CenterX, model.CenterY), Canvas1); var currentMousePos = Mouse.GetPosition(Canvas1); // 计算角度增量,避免直接设置绝对角度导致跳变 var targetAngle = rectCenterInCanvas.GetAngle(currentMousePos) + 90; var angleDelta = targetAngle - _initialAngle; model.Angle = _initialAngle + angleDelta; // 基于原始未旋转矩形应用旋转增量,计算新边界 var matrix = new Matrix(); matrix.RotateAt(angleDelta, rectCenterInCanvas.X, rectCenterInCanvas.Y); var cornerPoints = new[] { _initialUnrotatedRect.TopLeft, _initialUnrotatedRect.TopRight, _initialUnrotatedRect.BottomRight, _initialUnrotatedRect.BottomLeft }; matrix.Transform(cornerPoints); model.Left = cornerPoints.Min(p => p.X); model.Top = cornerPoints.Min(p => p.Y); }
2. 修复旋转后缩放偏移问题
旋转后鼠标拖拽的坐标系与模型本地坐标系不一致,需要将拖拽Delta转换为本地坐标系增量,并保持中心点位置稳定。
修改CenterBottomThumb_DragDelta方法:
private void CenterBottomThumb_DragDelta(object sender, DragDeltaEventArgs e) { var model = (RectModel)DataContext; // 获取矩形中心点在Canvas中的位置 var centerInCanvas = RectGrid.TranslatePoint(new Point(model.CenterX, model.CenterY), Canvas1); // 将鼠标拖拽的Delta转换为模型本地坐标系的增量(抵消旋转影响) var transform = RectGrid.LayoutTransform.Value; var inverseTransform = transform.Inverse; var deltaInLocal = inverseTransform.Transform(new Point(0, e.VerticalChange)); var newHeight = model.Height + deltaInLocal.Y; if (newHeight >= 1) { // 调整Top以保持中心点位置不变,避免偏移 var heightDelta = newHeight - model.Height; model.Top -= heightDelta / 2; model.Height = newHeight; } }
3. 补充中心点属性通知
修改RectModel的Width和Height属性,在值变化时通知CenterX和CenterY的变化:
private double _Height; public double Height { get { return _Height; } set { if (value == _Height) return; _Height = value; OnPropertyChanged(nameof(Height)); OnPropertyChanged(nameof(CenterY)); // 添加该行 } } private double _Width; public double Width { get { return _Width; } set { if (value == _Width) return; _Width = value; OnPropertyChanged(nameof(Width)); OnPropertyChanged(nameof(CenterX)); // 添加该行 } }
内容的提问来源于stack exchange,提问作者codeDom
相关产品推荐
相关产品推荐

