[SharpMap] 屏幕坐标和Map坐标转换
1. SharpMap中屏幕坐标和地图Map坐标转换:

1 using System.Drawing;
2 using GeoAPI.Geometries;
3
4 namespace SharpMap.Utilities
5 {
6 /// <summary>
7 /// Class for transforming between world and image coordinate
8 /// </summary>
9 public class Transform
10 {
11 /// <summary>
12 /// Transforms from world coordinate system (WCS) to image coordinates
13 /// 将世界坐标转换为image坐标
14 /// NOTE: This method DOES NOT take the MapTransform property into account (use <see cref="Map.WorldToImage(GeoAPI.Geometries.Coordinate,bool)"/> instead)
15 /// </summary>
16 /// <param name="p">Point in WCS</param>
17 /// <param name="map">Map reference</param>
18 /// <returns>Point in image coordinates</returns>
19 public static PointF WorldtoMap(Coordinate p, Map map)
20 {
21 //if (map.MapTransform != null && !map.MapTransform.IsIdentity)
22 // map.MapTransform.TransformPoints(new System.Drawing.PointF[] { p });
23 if (p.IsEmpty())
24 return PointF.Empty;
25
26 var result = new PointF();
27
28 var height = (map.Zoom * map.Size.Height) / map.Size.Width;
29 var left = map.Center.X - map.Zoom * 0.5;
30 var top = map.Center.Y + height * 0.5 * map.PixelAspectRatio;
31 result.X = (float)((p.X - left) / map.PixelWidth);
32 result.Y = (float)((top - p.Y) / map.PixelHeight);
33 if (double.IsNaN(result.X) || double.IsNaN(result.Y))
34 result = PointF.Empty;
35 return result;
36 }
37
38 /// <summary>
39 /// Transforms from image coordinates to world coordinate system (WCS).
40 /// NOTE: This method DOES NOT take the MapTransform property into account (use <see cref="Map.ImageToWorld(System.Drawing.PointF,bool)"/> instead)
41 /// </summary>
42 /// <param name="p">Point in image coordinate system</param>
43 /// <param name="map">Map reference</param>
44 /// <returns>Point in WCS</returns>
45 public static Coordinate MapToWorld(PointF p, Map map)
46 {
47 if (map.Center.IsEmpty() || double.IsNaN(map.MapHeight))
48 {
49 return new Coordinate(0, 0);
50 }
51 var ul = new Coordinate(map.Center.X - map.Zoom * .5, map.Center.Y + map.MapHeight * .5);
52 return new Coordinate(ul.X + p.X * map.PixelWidth,
53 ul.Y - p.Y * map.PixelHeight);
54 }
55 }
56 }
详细分析: http://www.cnblogs.com/yhlx125/archive/2012/02/10/2342282.html
2. OpenS-CAD中的实现
已知屏幕分辨率每英寸像素点数,一般为96dpi, 定义float m_screenResolution = 96;
1. 初始化CanvasCtrl时,首先调用OnResize()方法。

1 protected override void OnResize(EventArgs e) 2 { 3 base.OnResize(e); 4 5 if (m_lastCenterPoint != UnitPoint.Empty && Width != 0) 6 SetCenterScreen(ToScreen(m_lastCenterPoint), false); 7 m_lastCenterPoint = CenterPointUnit(); 8 m_staticImage = null; 9 DoInvalidate(true); 10 }
由于m_lastCenterPoint是结构体变量,所以首先设置到中心点m_lastCenterPoint,即(0,0),是Unit坐标
if (m_lastCenterPoint != UnitPoint.Empty && Width != 0)
SetCenterScreen(ToScreen(m_lastCenterPoint), false);
接着调用m_lastCenterPoint = CenterPointUnit();
通过直角坐标的左上角点和右下角点计算。其实初始化时候执行该方法没有起到设置基准点的作用。可以跳过这2次,等窗体Resize的时候再看。

1 public UnitPoint CenterPointUnit() 2 { 3 UnitPoint p1 = ScreenTopLeftToUnitPoint(); 4 UnitPoint p2 = ScreenBottomRightToUnitPoint(); 5 UnitPoint center = new UnitPoint(); 6 center.X = (p1.X + p2.X) / 2; 7 center.Y = (p1.Y + p2.Y) / 2; 8 return center; 9 } 10 public UnitPoint ScreenTopLeftToUnitPoint() 11 { 12 return ToUnit(new PointF(0, 0)); 13 } 14 public UnitPoint ScreenBottomRightToUnitPoint() 15 { 16 return ToUnit(new PointF(this.ClientRectangle.Width, this.ClientRectangle.Height)); 17 }
2. 接着打开文档DocumentForm,在构造的过程中设置文档画布的视点中心坐标为(0,0)。这样就实现了绘图画布原点坐标和屏幕(客户区)中心点的对应,形成基准点。默认的数据的长度单位为inch,屏幕坐标的单位为像素。这两者之间存在比例关系,通过Zoom缩放比例来实现尺度的变换,同时结合平移量和偏移距离计算出鼠标点的世界坐标。
m_canvas.SetCenter(new UnitPoint(0, 0));
或者加载完数据,设置画布的视点中心坐标。
m_canvas.SetCenter(m_data.CenterPoint);

1 public DocumentForm(string filename)
2 {
3 InitializeComponent();
4
5 Text = "<New Document>";
6 m_data = new DataModel();
7 if (filename.Length > 0 && File.Exists(filename) && m_data.Load(filename))
8 {
9 Text = filename;
10 m_filename = filename;
11 }
12
13 m_canvas = new CanvasCtrl(this, m_data);
14 m_canvas.Dock = DockStyle.Fill;
15 Controls.Add(m_canvas);
16 m_canvas.SetCenter(new UnitPoint(0, 0));
17 m_canvas.RunningSnaps = new Type[]
18 {
19 typeof(VertextSnapPoint),
20 typeof(MidpointSnapPoint),
21 typeof(IntersectSnapPoint),
22 typeof(QuadrantSnapPoint),
23 typeof(CenterSnapPoint),
24 typeof(DivisionSnapPoint),
25 };
26
27 m_canvas.AddQuickSnapType(Keys.N, typeof(NearestSnapPoint));
28 m_canvas.AddQuickSnapType(Keys.M, typeof(MidpointSnapPoint));
29 m_canvas.AddQuickSnapType(Keys.I, typeof(IntersectSnapPoint));
30 m_canvas.AddQuickSnapType(Keys.V, typeof(VertextSnapPoint));
31 m_canvas.AddQuickSnapType(Keys.P, typeof(PerpendicularSnapPoint));
32 m_canvas.AddQuickSnapType(Keys.Q, typeof(QuadrantSnapPoint));
33 m_canvas.AddQuickSnapType(Keys.C, typeof(CenterSnapPoint));
34 m_canvas.AddQuickSnapType(Keys.T, typeof(TangentSnapPoint));
35 m_canvas.AddQuickSnapType(Keys.D, typeof(DivisionSnapPoint));
36
37 m_canvas.KeyDown += new KeyEventHandler(OnCanvasKeyDown);
38 SetupMenuItems();
39 SetupDrawTools();
40 SetupLayerToolstrip();
41 SetupEditTools();
42 UpdateLayerUI();
43
44 MenuStrip menuitem = new MenuStrip();//创建文档的主菜单
45 menuitem.Items.Add(m_menuItems.GetMenuStrip("edit"));
46 menuitem.Items.Add(m_menuItems.GetMenuStrip("draw"));
47 menuitem.Visible = false;
48 Controls.Add(menuitem);
49 this.MainMenuStrip = menuitem;
50 }
51 protected override void OnLoad(EventArgs e)
52 {
53 base.OnLoad(e);
54 m_canvas.SetCenter(m_data.CenterPoint);
55 }
2.1查看SetCenter(UnitPoint unitPoint)方法,首先调用PointF point = ToScreen(unitPoint);将unitPoint转换PointF point.找到地图原点对应的屏幕坐标,应该是屏幕左下角点偏移(25,-25)。

1 /// <summary> 2 /// 设置画布到屏幕的中心 3 /// </summary> 4 /// <param name="rPoint">直角坐标系坐标</param> 5 public void SetCenter(RPoint unitPoint) 6 { 7 //将unitPoint点对应到屏幕上point 8 PointF point = Transform.ToScreen(unitPoint, this); 9 m_lastCenterPoint = unitPoint; 10 //将unitPoint偏移到屏幕中心 11 SetCenterScreen(point, false); 12 }
这里注意计算Unit坐标到屏幕坐标的ToScreen()方法中,transformedPoint.Y = ScreenHeight() - transformedPoint.Y;//将Unit坐标系转换为屏幕坐标系,Y轴反向
其中ScreenHeight()方法似乎有点问题,修改后如下。

1 /// <summary> 2 /// 将Unit坐标转换到屏幕坐标 3 /// </summary> 4 /// <param name="point"></param> 5 /// <returns></returns> 6 public PointF ToScreen(UnitPoint point) 7 { 8 PointF transformedPoint = Translate(point); 9 transformedPoint.Y = ScreenHeight() - transformedPoint.Y;//将Unit坐标系转换为屏幕坐标系,Y轴反向 10 transformedPoint.Y *= m_screenResolution * m_model.Zoom; 11 transformedPoint.X *= m_screenResolution * m_model.Zoom; 12 13 transformedPoint.X += m_panOffset.X + m_dragOffset.X; 14 transformedPoint.Y += m_panOffset.Y + m_dragOffset.Y; 15 return transformedPoint; 16 }

1 float ScreenHeight() 2 { 3 return (float)(ToUnit(this.ClientRectangle.Height)); 4 //return (float)(ToUnit(this.ClientRectangle.Height) / m_model.Zoom); 5 }
这样就引出了ToUnit(float screenvalue)函数,将屏幕距离转换为英寸数。
1 public double ToUnit(float screenvalue) 2 { 3 return (double)screenvalue / (double)(m_screenResolution * m_model.Zoom); 4 }
3. 接着将unitPoint赋值给m_lastCenterPoint
m_lastCenterPoint = unitPoint;
SetCenterScreen(point, false);
调用了SetCenterScreen()方法,
1 protected void SetCenterScreen(PointF screenPoint, bool setCursor)
2 {
3 float centerX = ClientRectangle.Width / 2;
4 m_panOffset.X += centerX - screenPoint.X;
5
6 float centerY = ClientRectangle.Height / 2;
7 m_panOffset.Y += centerY - screenPoint.Y;
8
9 if (setCursor)
10 Cursor.Position = this.PointToScreen(new Point((int)centerX, (int)centerY));
11 DoInvalidate(true);
12 }
4.理解了public PointF ToScreen(UnitPoint point),那public UnitPoint ToUnit(PointF screenpoint)也好理解了。

1 /// <summary> 2 /// 将屏幕坐标转换到Unit坐标 3 /// </summary> 4 /// <param name="screenpoint"></param> 5 /// <returns></returns> 6 public UnitPoint ToUnit(PointF screenpoint) 7 { 8 float panoffsetX = m_panOffset.X + m_dragOffset.X; 9 float panoffsetY = m_panOffset.Y + m_dragOffset.Y; 10 float xpos = (screenpoint.X - panoffsetX) / (m_screenResolution * m_model.Zoom); 11 float ypos = ScreenHeight() - ((screenpoint.Y - panoffsetY)) / (m_screenResolution * m_model.Zoom); 12 return new UnitPoint(xpos, ypos); 13 }
5.最后单独说一下ToScreen(UnitPoint point)和ToUnit(PointF screenpoint)中的两个变量
PointF m_panOffset = new PointF(25, -25);
PointF m_dragOffset = new PointF(0, 0);
这里m_panOffset控制的是中心点Center的偏移量,是一个累计的量,相对于中心点。
m_dragOffset记录了每次移动过程中的移动量,每次产生一个新值。每次CanvasCtrl控件的OnMouseDown时累积到偏移量上,之后重新初始化,同时在OnMouseUp时的移动命令下重新初始化(如下代码),似乎重复了。
1 if (m_commandType == eCommandType.pan) 2 { 3 m_panOffset.X += m_dragOffset.X; 4 m_panOffset.Y += m_dragOffset.Y; 5 m_dragOffset = new PointF(0, 0); 6 }
在protected override void OnMouseMove(MouseEventArgs e)事件中

1 if (m_commandType == eCommandType.pan && e.Button == MouseButtons.Left)
2 {
3 m_dragOffset.X = -(m_mousedownPoint.X - e.X);
4 m_dragOffset.Y = -(m_mousedownPoint.Y - e.Y);
5 m_lastCenterPoint = CenterPointUnit();
6 DoInvalidate(true);
7 }
可知,m_dragOffset和m_panOffset记录的是偏移的屏幕坐标。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程