很多开发人员会去做类一些简单的矢量图绘制模块,但是,很多人往往最后被坐标变化搞得晕头转向。本文讨论了一下整个设计思路,给出了一些典型操作的公式。
原来的文档中有一些图和公式,实在懒得转了。doc文件和示例代码,可以在这里下载。
http://download.csdn.net/detail/pkrobbie/4189237
转贴请注明出处。
坐标系统定义
绘图软件的坐标系统一般包括两个:(1) 物理设备坐标系统(Deveice Space),比如,屏幕(和窗口)或者打印机。通常坐标的单位是设备像素,坐标轴正方向,采用x向右,y轴向下。;(2) 逻辑坐标系(Page Sapce),主要为了计算或者表达方便,一般采用数学常用的笛卡尔坐标系,坐标的单位,一般使用常用的逻辑单位,比如,毫米、英寸之类。为了编程方便,也可以采用,0.1mm/0.01mm之类的单位。坐标轴正方向,采用x向右,y轴向上。
Windows的各种鼠标消息、窗口参数等都是基于物理坐标系的。屏幕和窗口坐标之间有一个原点偏移,可以通过ScreentoClient或者ClienttoScreen转换。在本文中忽略这个差别,统一使用窗口左上角座位坐标原点。
设计分析
坐标系实现主要取决于几个方面:
1) 每个图元的draw和操作基于那个坐标系统。只考虑坐标变换实现,可以有下面两种方式
a) 如果主要的操作和draw都是基于一个逻辑坐标系实现,就必须依靠DC提供的MapMode和windows的坐标转换。这种方式在将来支持打印和扩展会容易。
b) 如果主要操作和draw都基于屏幕完成,主要的工作就是计算转换矩阵。坐标转换的效率有可能比基于Window API还要简单。但是,想支持打印设备可能需要补充设计。
2) 实际设计中还要考虑一个问题,就是画线线宽、字体的宽高等参数也是基于逻辑坐标系的,和windows mapmode设置相关的。这两种方式,对于同样的参数,效果是不一样的。
3) 对于逻辑形状的各种操作要转换成相应的基本参数变换,不记录操作参数,比如,矩形的旋转、镜像等操作后直接修改4个顶点的值,而不是记录长宽+角度。
4) 整个基础的画布converse只考虑缩放和平移。
相关设计的核心就转化成了这样的问题:
(1) 确定初始参数:(a) 逻辑单位到像素的比例关系,简单说,就是zoom scale=100%的时候。这两者的比例系数,记为dpfactor。这个参数对于进程的生命周期,是一个只和设备有关的常量; (b) 逻辑坐标原点对应的窗口坐标,记为original。(c) zoom scale的初始值,也就是一般用户看到的缩放比例100%,150%之类。
(2) 根据这三个参数建立两个坐标系统之间的转换公式(或者叫转换矩阵)。这样,程序中所有涉及到的坐标只有两种,窗口坐标或者逻辑坐标。所有的操作变换统一体现到对这个矩阵的修改。
(3) 在用户改变整个画布的位置或者缩放比例时,重新计算original的值或者zoom scale的值。设计中会给出主要操作对应的修改公式。后续的用户操作,仍然遵循(2)的方式,坐标变换统一由DPtoLP或者LPtoDP函数完成。不要自己处理任何变化系数。
单个元素的坐标变化推荐在逻辑坐标系中直接通过坐标变换完成就可以。这个坐标系更接近于一般专业书籍中公式的坐标系。
Windows DC版公式推导
下面以Windows DC完成坐标变化的方式,给出上完整的设计方案。
这种方案中,所有和DC相关的操作都是用逻辑坐标完成。通过设置DC的window/viewport/mapmode,使DC可以支持两个坐标系的变换。这样,所有的坐标变化,都直接使用DPtoLP和LPtoDP完成。
坐标变化可以描述成,有两个坐标系,设备坐标系(Device space)和逻辑坐标系(Page space)。坐标下标的p/d代表坐标数值对应的坐标系。
设备坐标系对应屏幕窗口的客户区,其中,原点是窗口左上角,x轴正方向向左,y轴正方向下,x/y的单位对应屏幕像素。逻辑坐标系对应画布数据的实际坐标,其中,原点是位于屏幕某一点(x0d,y0d)d,x轴正方向向左,y轴正方向上,x/y的单位对应某个实际单位,先假定为mm。
需要解决的问题是,一个点A已知一个坐标,求另一个坐标系的值。
三个参数中scale/original需要随时记录,dpfactor只和设备有关,可以直接通过DC参数获得。
为了支持缩放,我们选择MM_ISOTROPIC作为MapMode。这样,就需要相应定义Window Extent和View Extent确定坐标单位之间的比例和缩放。这两个值的相对比例才有实际意义,本身的绝对值没有意义。参考下面的伪代码。这两个值相对比例里面体现了像素和绝对坐标的映射,以及应用程序界面体现的缩放scale。
constfloat presiconfactor = 100.0f; // on logical unit = 1/100mm
constfloat LunitsPerInch = 25.4f * 1; // one logical unit = 1mm
pDC->SetMapMode(MM_ISOTROPIC);
CPointptOldOrigin = pDC->SetViewportOrg(original);
pDC->SetWindowExt(round(LunitsPerInch* presiconfactor),
round(LunitsPerInch * presiconfactor));
pDC->SetViewportExt(round(pDC->GetDeviceCaps(LOGPIXELSX) * scale * presiconfactor),
- round( pDC->GetDeviceCaps(LOGPIXELSY)*scale * presiconfactor));
其中, presiconfactor是为了减小SetWindowExt/ SetViewportExt带来的计算误差。这两个函数接受的参数都是整数。如果LunitsPerInch或者GetDeviceCaps(LOGPIXELSY)比较小,可能带来很明显的计算误差。
如果想实现1个逻辑单位是0.01mm,只要修改LunitsPerInch = 25.4f * 100就可以了(这就可以等价成SetMapMode (MM_HIMETRIC))。
这样,每次坐标变化之前,直接调用新的original和scale设置相应的值,然后,直接调用DLtoLP和LPtoDP就可以实现坐标变换。不需要考虑各种平移或者缩放的影响。
界面操作设计
常用的画布操作基本上可以归纳为下面几种。
1) 画布平移,其实就是改变了逻辑原点坐标original的值。只要把鼠标相对的偏移量加到原来的原点上就可以了。重新计算转换矩阵就可以了。
2) 画布Zoom操作包括两个参数,zoomscale的变化量,和zoom操作的中心点B(xBd,yBd)d。
a) zoom scales变化有三种情况:定义sratio= new_scale / old_scale。
i. 通过滚轮,scale会改变一个固定的比例或者差值。
ii. 通过菜单或者工具条指定一个新的scale值
iii. 通过拖框或者Fit to Window之类缩放,根据现有数据计算新的scale值。
b) 中心点三种情况:
i. 通过滚轮,缩放的中心点就是鼠标所在点。这个点保持不变。
ii. 通过菜单或者工具条制定一个固定比例,缩放相对于窗口客户区的中心点。相当于鼠标放在屏幕中心做滚轮缩放。
iii. 通过拖框或者Fit to Window之类缩放。先把缩放区域的中心移到屏幕中心对应的位置,然后,再按照中心缩放。
对于情况i和ii,可以推导出,新的逻辑坐标系原点对应的设备坐标tN和原来的原点t0对应设备坐标系的关系为
其中所有向量t都是设备坐标系的向量,tB代表缩放鼠标点,t0时缩放变化前的逻辑坐标原点,tN是缩放变化后逻辑坐标原点。由此,可以求解出新的原点tN
(newx0d,newyod)T= sratio* (oldx0d,oldy0d)T +(1- sratio) * (oldxBd,oldyBd)T
其中sratio = new_scale / old_scale,sratio〉1表示zoom scale增大,(oldxBd,oldyBd)T为缩放中心点对应的原逻辑坐标系的坐标。。
对于情况iii,可以推导出,新的逻辑坐标系原点对应的设备坐标和原来的原点对应设备坐标系的关系为
(newx0d,newyod)T= (oldx0d,oldy0d)T+ (Xcd,ycd)T -(Xbd,ybd)T
其中,(Xcd,ycd)是窗口中心点设备坐标,(Xbd,ybd)是拖框区域的中心点设备坐标。
自主变换公式推导
如果不使用DC的映射,我们同样可以自己完成这样的变换。这部分放在这,主要是为了作为手工验证使用。
问题定义不变。已知,有两个坐标系,设备坐标系(Device space)和逻辑坐标系(Page space)。坐标下标的p/d代表坐标数值对应的坐标系。
设备坐标系对应屏幕窗口的客户区,其中,原点是窗口左上角,x轴正方向向左,y轴正方向下,x/y的单位对应屏幕像素。逻辑坐标系对应画布数据的实际坐标,其中,原点是位于屏幕某一点(x0d,y0d)d,x轴正方向向左,y轴正方向上,x/y的单位对应某个实际单位,先假定为mm。
需要解决的问题是,一个点A已知一个坐标,求另一个坐标系的值。
系统中定义Zpd代表一个逻辑单位的像素数,它包括两个因子Zo,也就是scale=100%时一个逻辑单位对应的像素数,scale代表给用户界面的缩放比例。
Zpd = Zo * s
Zdp = 1/Zpd 一个像素对应的逻辑单位长度
先推导,已知P坐标,如何获得D坐标。
Xd = Xp * Zpd - X0d
Yd= -Yp * Zpd - Y0d
简写成(xd,yd)T=M1*M2*M3*(xp,yp)T
其中,(x3,y3)T =M3*(xp,yp)T 产生的坐标系,和P同方向,只是坐标单位变成的像素。
(x2,y2)T = M2*M3*(xp,yp)T 产生的坐标系,和P同方向,坐标单位变成的像素,坐标原点移到了屏幕左上角。
参考下图
反向的转换,只要对矩阵求逆M1*M2*M3就可以了。
(xp,yp)T =(M1*M2*M3)-1*(xd,yd)T
相应的
Xp = (Xd – X0d) / Zpd
Yp = (- Yd + Y0d) / Zpd
界面操作设计
界面操作设计和用DC辅助实现变换是一致的。不再重复了。
Demo
Demo程序里面实现两种方案的技术验证。主要支持的操作包括:
1) 初始情况下画出一个逻辑空间坐标系。
2) 鼠标左键拖拽实现整个画布的移动
3) 滚轮在屏幕任意一点,实现以这点为中心的缩放。
4) 鼠标移动时在状态条实时显示鼠标所在位置的逻辑坐标和窗口坐标。
代码使用VS2008做的,使用Vs 2005或者以前的版本,可以自己坐一下移植。大部分代码都在ChildView.cpp中。