当前位置: 首页 > news >正文

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一下

demo

刚好趁 这个机会,把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>

显示效果如下:

image

首先我们获取一下布局中的固定位置

1、Image在Canvas中的位置

image

这里我们可以用CanvasGetLeftGetTop函数获取

1 var pos1 = new Point(Canvas.GetLeft(this.image), Canvas.GetTop(this.image));

2、Image在Grid中的位置

image

这里我们可以用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);

TransformToAncestorTranslatePoint的区别总结如下表所示:

特性TransformToAncestorTranslatePoint
返回值GeneralTransform 对象(转换器)Point(转换后的坐标)
用途获取从一个元素到其祖先的完整坐标转换关系将一个具体的点从一个坐标系转换到另一个坐标系
性能计算转换器有开销,但可复用每次调用都可能重新计算转换,适合单次使用
灵活性高,可对多个点重复使用同一个转换器低,每次转换都需要调用方法
考虑变换是,会考虑 RenderTransform 等是,内部使用了正确的转换逻辑
调用方式element.TransformToAncestor(ancestor)element.TranslatePoint(point, relativeTo)

3、Image在Window中的位置

image

这里跟步骤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的位置

image

RectImage分别处于不同的Grid当中,没有子/父级关系。

我们可以使用Visual.TransformToVisual函数,它的作用是返回一个转换,该转换可用于将 Visual中的坐标转换为指定的可视对象。

实现步骤如下:

1    var transform4 = this.rect.TransformToVisual(this.image);
2    var pos5 = transform4.Transform(new Point(0, 0));

TransformToVisualTransformToAncestor/TransformToDescendant的区别总结如下:

特性TransformToVisualTransformToAncestor/TransformToDescendant
目标范围任意 Visual 元素仅限当前元素的祖先/后代
异常情况目标不在视觉树时返回 null目标不是祖先/后代时抛出异常
典型用途任意两个元素间的坐标转换元素到其容器 / 窗口的坐标转换
 

二、鼠标的动态位置

1、鼠标位置相对于窗口中某个元素的位置

例如,鼠标位置相对于Window的位置

image

对于在鼠标相关事件的处理函数中, 我们可以使用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范围内

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、鼠标位置相对于整个屏幕的位置

image

这里我们可以直接使用前面的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>

层级结构如下:

image

image

这里的位置计算方法跟前面的一样,但前提是我们要获取它的控件模板,可以通过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

http://www.dtcms.com/a/494804.html

相关文章:

  • 重庆学校网站建设html入门网页制作
  • 词向量:开启自然语言处理的奇妙之旅
  • MySQL 5.7 和 8.0 基于kubernetes的yaml部署方案-单实例和高可用
  • 如何给Windows云主机进行加固
  • binLog、redoLog和undoLog的区别
  • 如何做医美机构网站观察分析电商素材网站
  • k8s localpath csi原理
  • 如何解决在xml中传入Integer整型参数为0时条件失效问题?
  • wordpress建什么站希音跨境电商
  • python爬虫学习
  • MySQL 8.0.29 及以上版本中 SSL/TLS 会话复用(Session Reuse)
  • 【项目-】Qt + QCustomPlot 实现频谱监测仪:四图联动、高频信号注入、鼠标交互全解析
  • 用于博客美化的测试(后面再更新)
  • 【一文了解】正则表达式
  • MySQL中表操作
  • 中国建设银行大学助学贷款网站网站备案对网站负责人的要求
  • 江门云建站模板东城企业网站开发
  • 使用Selenium Server 4连接已经运行的Firefox
  • 普蓝机器人PlanRobot-DR200:基于多传感融合的全天候电力巡检自主导航技术与实践
  • PHPCMS V9 自定义证书查询模块(Ajax+防刷+倒计时)
  • 一体化运维平台:当下运维体系的核心支柱
  • HarmonyOS后台任务管理:短时任务与长驻任务实战
  • Unity游戏基础-6(跨平台生成游戏作品,针对安卓教程)
  • Luminex xMAP技术原理与应用概述
  • Http基础协议和解析
  • 官方网站页面尺寸html网页设计作品中国传统文化
  • h5游戏免费下载:激射神经猫
  • 商业航天与数字经济(二):商业航天重构全球数字经济的底层逻辑
  • 免费社区建站系统vue做的商城网站
  • 中电金信:首个金融信创中试平台揭牌,架设国产软硬件落地应用的“高速通道”