WPF中的坐标转换
问题起因
最近在做一个Dicom查看工具时,就遇到了鼠标拖动时,坐标计算错误的问题。
我这里的层级结构如下:
1 <Canvas x:Name="outer" Background="Black" ClipToBounds="True"> 13 <Image x:Name="image" Width="{Binding ElementName=canvas,Path=Width,UpdateSourceTrigger=PropertyChanged}" 14 Height="{Binding ElementName=canvas,Path=Height,UpdateSourceTrigger=PropertyChanged}" Panel.ZIndex="0"> 16 </Image> 17 18 <Canvas x:Name="overlayCanvas" IsHitTestVisible="False" 19 Width="{Binding ElementName=canvas,Path=Width,UpdateSourceTrigger=PropertyChanged}" 20 Height="{Binding ElementName=canvas,Path=Height,UpdateSourceTrigger=PropertyChanged}" Panel.ZIndex="1"> 26 </Canvas> 27 </Canvas> 43 </Canvas>
我想在鼠标移动时,对图像实现放大镜的功能
1 private void Canvas_MouseMove(object sender, MouseEventArgs e) 2 { 3 //鼠标位置 4 var center = e.GetPosition(this.image); 5 }
在外部容器和内部显示区域大小一样时,这种方式是可以的。
但是如果当显示区域和外部 容器大小不一样时,这种计算方式就会出错。
应该更新为下面的方式
1 private void Canvas_MouseMove(object sender, MouseEventArgs e) 2 { 3 //鼠标位置 4 var center = e.GetPosition(this.image); 5 6 //重新转换后的位置 7 Point posInOuter = canvas.TransformToAncestor(outer).Transform(center); 8 }
最终运行效果如下:
项目地址:https://github.com/zhaotianff/ImageViewer.git
感兴趣的小伙伴可以帮忙star一下
刚好趁 这个机会,把WPF中所有坐标转换相关的知识点总结一下。
给大家分享一下,让后面学习的小伙伴少走弯路。
一、控件外部的坐标转换
我们先建立一个如下的窗口
1 <Grid>2 <Grid.ColumnDefinitions>3 <ColumnDefinition/>4 <ColumnDefinition Width="150"/>5 </Grid.ColumnDefinitions>6 7 <Grid Name="grid" Margin="50" Background="White">8 <!--信息显示-->9 <Label Content="Window" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="20" Foreground="White" Margin="-50,-50,0,0"></Label> 10 <Label Content="Grid1" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="20"></Label> 11 12 <Canvas Name="canvas" Width="400" Height="400" Background="Pink"> 13 <!--信息显示--> 14 <Label Content="Canvas" FontSize="20" Foreground="White"></Label> 15 <Label Content="Image" FontSize="20" Foreground="White" Canvas.Left="50" Canvas.Top="120"></Label> 16 17 <Image Name="image" Width="300" Height="300" Canvas.Left="50" Canvas.Top="50" Source="1.jpg" Stretch="UniformToFill"></Image> 18 19 </Canvas> 20 </Grid> 21 22 <Grid Name="grid2" Margin="0,50,50,50" Background="White" Grid.Column="1"> 23 <!--信息显示--> 24 <Label Content="Grid2" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="20"></Label> 25 <Label Content="Rect" HorizontalAlignment="Left" VerticalAlignment="Center" FontSize="20" Foreground="White" Panel.ZIndex="1" Margin="20,0,0,0"></Label> 26 27 <Rectangle Width="80" Height="80" Fill="Pink"></Rectangle> 28 </Grid> 29 </Grid>
显示效果如下:
首先我们获取一下布局中的固定位置
1、Image在Canvas中的位置
这里我们可以用Canvas
的GetLeft
和GetTop
函数获取
1 var pos1 = new Point(Canvas.GetLeft(this.image), Canvas.GetTop(this.image));
2、Image在Grid中的位置
这里我们可以用Visual.TransformToAncestor
函数来获取Image
的相对位置
步骤如下:
TransformToAncestor(grid)
:创建一个转换,用于将 Image 的坐标转换为相对于 Grid 的坐标Transform(new Point(0, 0))
:将 Image 自身坐标系的原点 (左上角) 转换为 Grid 坐标系中的点
Visual.TransformToAncestor
函数的作用是返回一个转换,该转换可用于将 Visual
中的坐标转换为指定的可视对象上级。
1 var transform1 = this.image.TransformToAncestor(this.grid); 2 var pos2 = transform1.Transform(new Point(0, 0));
我们也可以简化上面的调用,直接使用UIElement.TranslatePoint(Point, UIElement)
方法,这个方法的作用是将相对于此元素的点转换为相对于指定元素的坐标。
实现步骤如下:
1 //可以合并上面的两步如下: 2 var pos2t = this.image.TranslatePoint(new Point(0, 0), this.grid);
TransformToAncestor
和TranslatePoint
的区别总结如下表所示:
特性 | TransformToAncestor | TranslatePoint |
---|---|---|
返回值 | GeneralTransform 对象(转换器) | Point (转换后的坐标) |
用途 | 获取从一个元素到其祖先的完整坐标转换关系 | 将一个具体的点从一个坐标系转换到另一个坐标系 |
性能 | 计算转换器有开销,但可复用 | 每次调用都可能重新计算转换,适合单次使用 |
灵活性 | 高,可对多个点重复使用同一个转换器 | 低,每次转换都需要调用方法 |
考虑变换 | 是,会考虑 RenderTransform 等 | 是,内部使用了正确的转换逻辑 |
调用方式 | element.TransformToAncestor(ancestor) | element.TranslatePoint(point, relativeTo) |
3、Image在Window中的位置
这里跟步骤2一样的获取方法即可
1 var transform2 = this.image.TransformToAncestor(this.window); 2 var pos3 = transform2.Transform(new Point(0, 0));
4、Grid相对于Image的位置
这里我们可以直接使用步骤2中的位置并进行取反即可。
但是我们也可以使用Visual.TransformToDescendant
函数,它的作用是返回一个转换,该转换可用于将Visual
中的坐标转换为指定的可视对象后代。
实现步骤如下:
1 var transform3 = this.grid.TransformToDescendant(this.image); 2 var pos4 = transform3.Transform(new Point(0, 0));
5、Rect相对于Image的位置
Rect
和Image
分别处于不同的Grid
当中,没有子/父级关系。
我们可以使用Visual.TransformToVisual
函数,它的作用是返回一个转换,该转换可用于将 Visual
中的坐标转换为指定的可视对象。
实现步骤如下:
1 var transform4 = this.rect.TransformToVisual(this.image); 2 var pos5 = transform4.Transform(new Point(0, 0));
TransformToVisual
和TransformToAncestor/TransformToDescendant
的区别总结如下:
特性 | TransformToVisual | TransformToAncestor/TransformToDescendant |
---|---|---|
目标范围 | 任意 Visual 元素 | 仅限当前元素的祖先/后代 |
异常情况 | 目标不在视觉树时返回 null | 目标不是祖先/后代时抛出异常 |
典型用途 | 任意两个元素间的坐标转换 | 元素到其容器 / 窗口的坐标转换 |
二、鼠标的动态位置
1、鼠标位置相对于窗口中某个元素的位置
例如,鼠标位置相对于Window
的位置
对于在鼠标相关事件的处理函数中, 我们可以使用MouseEventArgs.GetPosition
函数来获取
MouseEventArgs.GetPosition
的作用是返回相对于指定元素的鼠标指针位置。
1 private void OutputPosInfo(MouseEventArgs e) 2 { 3 //鼠标位置相对于window的位置 4 var pos2 = e.GetPosition(this.window); 5 WritePoint($"鼠标位置相对于Window的位置:{pos2}"); 6 }
在其它地方需要获取时,可以使用Mouse.GetPosition
函数
Mouse.GetPosition
函数的作用是获取与指定元素相对的鼠标位置。
1 var pos3 = Mouse.GetPosition(this.window);
2、判断鼠标是否在Image范围内
方法1、借助步骤1中的方法
1 var pos1 = e.GetPosition(this.image); 2 if(pos1.X >= 0 && pos1.Y >=0 && pos1.X <= this.image.Width && pos1.Y <= this.image.Height) 3 { 4 //当前鼠标在Image范围内 5 } 6 else 7 { 8 //当前鼠标不在Image范围内 9 }
方法2、借助Visual.PointFromScreen函数
Visual.PointFromScreen
函数的作用是将屏幕坐标中的点转换为当前元素坐标系中的点。通俗点来说,就是把屏幕上的某个点转换为相对于WPF元素的点。
首先我们来获取一下鼠标位置,这里我选择是直接调用win32 api
1 public struct POINT2 {3 public int x;4 public int y;5 }6 7 public struct RECT8 {9 public int left; 10 public int top; 11 public int right; 12 public int bottom; 13 } 14 public class User32 15 { 16 [DllImport("User32.dll")] 17 public static extern int GetCursorPos(ref POINT point); 18 19 [DllImport("User32.dll")] 20 public static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect); 21 }
实现步骤如下:
1 POINT pp = new POINT();2 //获取鼠标位置3 User32.GetCursorPos(ref pp);4 var mousePos = new Point(pp.x, pp.y);5 //鼠标位置相对于Image的位置6 var imageRelativePos = this.image.PointFromScreen(mousePos);7 8 if (imageRelativePos.X >= 0 && imageRelativePos.Y >= 0 && imageRelativePos.X <= this.image.Width && imageRelativePos.Y <= this.image.Height)9 { 10 //当前鼠标在Image范围内 11 } 12 else 13 { 14 //当前鼠标不在Image范围内 15 }
3、鼠标位置相对于整个屏幕的位置
这里我们可以直接使用前面的win32 api函数获取。
但这里我们重点介绍一下另外一种方式,那就是使用Visual.PointToScreen
函数
Visual.PointToScreen
函数的作用是将当前元素坐标系中的点转换为屏幕坐标中的点。
也就是跟前面Visual.PointFromScreen
函数的功能刚好反过来
实现步骤如下:
1 //获取基于window的相对位置 2 var pos2 = e.GetPosition(this.window); 3 //将window的位置转换为屏幕位置 4 var mousePos2 = this.PointToScreen(pos2); 5 6 //如果是使用 var pos = e.GetPosition(this.image); 7 //那后面就是使用image.PointToScreen(pos);
三 、控件内部的坐标转换
1、以Button控件为例
我们创建一个Button
的样式,如下所示
1 <Style TargetType="Button" x:Key="ButtonStyle">2 <Setter Property="SnapsToDevicePixels"3 Value="true" />4 <Setter Property="OverridesDefaultStyle"5 Value="true" />6 <Setter Property="FocusVisualStyle"7 Value="{StaticResource ButtonFocusVisual}" />8 <Setter Property="MinHeight"9 Value="23" /> 10 <Setter Property="MinWidth" 11 Value="75" /> 12 <Setter Property="Template"> 13 <Setter.Value> 14 <ControlTemplate TargetType="Button"> 15 <Border TextBlock.Foreground="{TemplateBinding Foreground}" 16 x:Name="Border" 17 CornerRadius="2" 18 BorderThickness="1" Background="Silver" BorderBrush="Black"> 19 <Grid Margin="20,3" Background="Pink"> 20 <ContentPresenter Margin="2" 21 HorizontalAlignment="Center" 22 VerticalAlignment="Center" 23 RecognizesAccessKey="True" /> 24 </Grid> 25 </Border> 26 </ControlTemplate> 27 </Setter.Value> 28 </Setter> 29 </Style>
层级结构如下:
这里的位置计算方法跟前面的一样,但前提是我们要获取它的控件模板,可以通过VisualTreeHelper
类的GetChild
函数来获取
这里 我封装了一个方法,可以递归获取控件模板里的所有元素
1 public static IEnumerable<DependencyObject> GetVisualChildren(DependencyObject source)2 {3 List<DependencyObject> children = new List<DependencyObject>();4 5 if (source == null)6 return children;7 8 children.Add(source);9 10 int count = VisualTreeHelper.GetChildrenCount(source); 11 12 for (int i = 0; i < count; i++) 13 { 14 children.AddRange(GetVisualChildren(VisualTreeHelper.GetChild(source, i))); 15 } 16 17 return children; 18 }
例如我要得到TextBlock
相对于Border
的位置
1 var children = TreeHelpers.GetVisualChildren(this.button); 2 Border border = children.ElementAt(1) as Border; 3 TextBlock tb = children.ElementAt(4) as TextBlock; 4 var pos1 = tb.TranslatePoint(new Point(0, 0), border);
TextBlock
相对于整个窗口的位置
1 var pos2 = tb.TransformToVisual(this).Transform(new Point(0, 0));
这个示例来说的话,并没有什么实际性的用处,只是一个演示。接下来我们看一下实际场景会用到的。
2、以ListBox为例
这里为什么要讲控件内部的位置转换,是因为有时候我们需要自定义控件或对原生控件进行增强。
如ListBox
拖动排序
ListBox
的拖动排序涉及到如下问题
1、鼠标点击时选中项的问题
2、选中项在鼠标移动时的其它项的移动问题
3、选中项在鼠标松开时的定位问题
因为本文只介绍位置计算相关的知识点,如果需要学习支持排序的ListBox,可以参考
https://github.com/thinkpixellab/bot/blob/master/net40-client/Bot/ReorderListBox.cs
如何在鼠标移动时通过鼠标位置获取UI元素
可以通过命中测试函数来获取,方法如下:
1 private void Window_MouseMove(object sender, MouseEventArgs e) 2 { 3 var element = this.InputHitTest(new Point(e.GetPosition(this).X, e.GetPosition(this).Y)) as UIElement; 4 }
其它的位置计算参考前面的步骤即可。
示例代码
下载
参考资料:
https://learn.microsoft.com/en-us/dotnet/desktop/wpf/graphics-multimedia/how-to-get-the-offset-of-a-visual