VC6.0映射模式转换及如何消除坐标误差

 作者:刘 涛 来源:yesky

本文关键词: VC6.0 映射模式 转换 坐标误差

在实际项目的开发过程中,经常需要绘制几何图形,并且要求用户可以与图形进行交互,既用户可以按照自己的思路对图形进行局部的任意调整,这些问题在Visual C++ 6.0可视化编程中可以很容易地解决,但是笔者在处理用户交互问题上,发现在坐标映射模式下,设备坐标和转换后的逻辑坐标有些偏差,不能一致地对应起来,通过仔细研究,最终很好的解决了这个问题,本文将解决该问题的方法拿出来与读者朋友们一起分享。

   一、Visual C++编程映射模式简介

  在一般的情况之下进行绘图操作都是以像素作为绘图单位,也就是所谓的设备坐标。但是为了更好的反映实际设计的对象的尺寸大小,因此多采用与对象的实际尺寸成一定比例的绘图映射模式。

  WINDOWS提供了几种映射方式,可以实现对应的逻辑坐标系。例如,如果我们指定了MM_TEXT方式,那么坐标系的坐标原点就位于屏幕的左上角,X轴和Y轴的正方向分别指向我们面对屏幕的右方和下方,它的绘图单位是像素,这时候逻辑坐标和设备坐标是一致的;如果我们指定了MM_LOENGLISH方式,那么一个逻辑坐标的单位就是百分之一英寸,坐标原点仍然位于屏幕的左上角,但是X轴和Y轴 的方向恰好和MM_TEXT方式下的轴方向相反。我们可以调用CDC的SetMapMode()来设置逻辑坐标系统,也可以调用CDC的SetViewpotOrg()函数来改变逻辑系的坐标原点,另外,结合CDC的SetWindowExt()用户可以定义自己的逻辑坐标系。对于这些函数的用法和Windows提供的一些逻辑坐标映射模式,不是本文的重点,读者可以自己参考微软的MSDN。

  对于坐标的映射模式,Visucal C++中虽然提供的许多MFC库函数,却只接受设备坐标,对于这些函数不能使用逻辑坐标,否则就会发现结果和自己要实现的目标出现误差。

   二、映射模式转换的坐标偏差的消除

  在绘图过程中,首先要设置逻辑坐标模式,例如,为了画一个边长为2厘米的正方形,首先在程序视图类CTestView::OnDraw()函数中将映射模式设置为MM_LOMETRIC,然后用CDC的Rectangle()函数绘制一个矩形,左上角和右下角的坐标分别为(200,-200),(400,-400)(MM_LOMETRIC模式下逻辑坐标的X轴向右为正,Y轴向上为正,坐标原点为屏幕的左上角,逻辑坐标的单位为0.1毫米)。代码如下:

void CTestView::OnDraw(CDC* pDC)
{
CTestDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
pDC->SetMapMode(MM_LOMETRIC);//设置逻辑坐标的模式;
pDC->Rectangle (200,-200,400,-400);//以逻辑坐标为单位;
}

  在实现交互式绘图过程中,要求用户采用鼠标在屏幕上对图形进行拾取,记录拾取点的坐标并进行处理和运算,因为当采用鼠标在逻辑坐标系统绘制出的图形上交互拾取时,其拾取点的坐标只能按照缺省设备坐标表示,也既是以像素单位表示,为了解决这个问题,就需要将拾取点的坐标转换到逻辑坐标下的坐标值。现在希望通过编程实现在该映射模式下点击鼠标后,以该位置为圆心,画一个半径为2厘米的圆。为此,利用ClassWizard创建CdrawingView类的响应鼠标左键按下的函数OnLButtonDown,在其中添加如下语句:

void CTestView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
double scale;
CClientDC *pDC=new CClientDC(this);
pDC->SetMapMode(MM_LOMETRIC);//设置逻辑坐标系统;
scale=12.0/800*25.4*10;//计算坐标转换的系数;
pDC->Ellipse(point.x*scale-200,-point.y*scale+200,point.x*scale+200,-point.y*scale-200);
//以用户拾取的设备坐标点为基础,计算对应的逻辑坐标点,并以该点为圆心作圆;
pDC->SetMapMode(MM_TEXT);//恢复为设备坐标模式;
pDC->MoveTo(point.x-5,point.y);//标记出鼠表标点击的位置;
pDC->LineTo(point.x+5,point.y);
pDC->MoveTo(point.x,point.y-5);
pDC->LineTo(point.x,point.y+5);
}

  在上面的程序段中,CClientDC为CDC的派生类,提供了一个用于画图的设备上下文对象,用来立即响应鼠标事件进行绘图:以鼠标的拾取点为圆心,以2厘米为半径,在屏幕上画一个圆。由于作图在MM_LOMETRIC映射模式下完成,所以它的逻辑单位为0.1毫米。程序中的scale既是屏幕拾取点坐标(以像素为单位)向MM_LOMETRIC映射模式下的逻辑坐标的转换系数。对于它的实现解释如下:要求将以像素表示的拾取点坐标换算成以0.1毫米为单位逻辑坐标系下的坐标,既要找出屏幕上每一个像素点代表多少个0.1毫米,也可以理解为像素之间的距离为多长(以毫米为单位)。

  由于笔者开发的程序运行在Windows 2000下,屏幕显示器设置为800*600像素,显示器的尺寸为15英寸(这个尺寸为屏幕对角线的长度),所以推断出显示器的长宽尺寸为12*9英寸(长宽比为4:3),每英寸相当于25.4毫米,所以每个像素相当于12*25.4*10/800个0.1毫米,这个结果就是我们要得到的转换系数scale。需要提醒的一点是,由于逻辑坐标的Y轴以向上为正,所以在对Y轴坐标进行换算时,需要再乘上个-1。采用该方法作出的圆的实际圆心位置和鼠标按下去的位置有明显的偏差,它的效果图如下:

  

  图中"十"字星表示用户鼠标按下后的位置,圆为经过坐标转换后得到的结果,从效果图可以看出,采用上述办法进行坐标变换存在一定的问题,笔者分析原因可能是显卡上的设备单位的尺寸有一定的增量,不能完全反映相应的映射关系以及屏幕不是纯平造成的。

  笔者经过多次实验,发现使用GetDeviceCaps()函数可以很好的消除这种误差。GetDeviceCaps(int nIndex)函数为CDC类的成员函数,它的参数为整型,根据该参数的不同,它的返回值为设备上下文对应的某个指标参量值,具体参数的设置可以参考MSDN。采用LOGPIXELSX,它的物理含义是在显示屏的水平方向上(X轴方向)每逻辑英寸内的像素数。为了消除上述计算方法产生的偏差,仅需要将上述函数中的scale改为:
scale=25.4*10/pDC->GetDeviceCaps(LOGPIXELSX)。

  经过笔者设置端点调试,得出屏幕上X轴的方向每英寸内有96个像素点,其倒数既为每个像素点所等价的英寸数,再与25.4*10相乘即表示每个像素等价于多少个0.1毫。经过上述改动后,屏幕结果显示正确,效果图如下:

  

  在关于几何图形的编程中,尽可能采用反映实际建模对象的映射模式,本文运用了GetDeviceCaps()函数很好的消除了坐标转换过程中的误差,相信可以对用户开发各类涉及到交互式图形拾取的项目有一定的帮助。

查看本文来源

 

 

映射模式

   在此篇之前我们已经学会了在窗口显示图形,更准确的说是在窗口指定位置显示图形或文字,我们使用的坐标单位是象素,称之为设备坐标。看下面语句:

pDC->Rectangle(CRect(0,0,200,200));

   画一个高和宽均为200个象素的方块,因为采用的是默认的MM_TEXT映射模式,所以在设备环境不一样时,画的方块大小也不一样,在1024*768的显示器上看到的方块会比640*480的显示器上的小(在不同分辨率下的屏幕象素,在WINDOWS程序设计一书中有示例程序可以获得,或者可以用GetClientRect函数获得客户区的矩形大小。在这里就不说了,大家只要知道就行了),在输出到打印机时也会有类似的情况发生。如何做才能保证在不同设备上得到大小一致的方块或者图形、文字呢?就需要我们进行选择模式映射,来转换设备坐标和逻辑坐标。

Windows提供了以下几种映射模式:

MM_TEXT
MM_LOENGLISH
MM_HIENGLISH
MM_LOMETRIC
MM_HIMETRIC
MM_TWIPS
MM_ISOTROPIC
MM_ANISOTROPIC

下面分别讲讲这几种映射模式:

MM_TEXT:

   默认的映射模式,把设备坐标被映射到象素。x值向右方向递增;y值向下方向递增。坐标原点是屏幕左上角(0,0)。但我们可以通过调用CDC的SetViewprotOrg和SetWindowOrg函数来改变坐标原点的位置看下面两个例子:

//************************************************
// 例子6-1
void CMyView::OnDraw(CDC * pDC)
{
pDC->Rectangle(CRect(0,0,200,200));//全部采用默认画一个宽和高为200象素的方块
}

//**************************************************
// 例子6-2
void CMyView::OnDraw(CDC * pDC)
{
pDC->SetMapMode(MM_TEXT);//设定映射模式为MM_TEXT
pDC->SetWindowOrg(CPoint(100,100));//设定逻辑坐标原点为(100,100)
pDC->Rectangle(CRect(100,100,300,300));//画一个宽和高为200象素的方块
}

   这两个例子显示出来的图形是一样的,都是从屏幕左上角开始的宽和高为200象素的方块,可以看出例子2将逻辑坐标(100,100)映射到了设备坐标(0,0)处,这样做有什么用?滚动窗口使用的就是这种变换。

固定比例映射模式:

MM_LOENGLISH、MM_HIENGLISH、MM_LOMETRIC、MM_HIMETRIC、MM_TWIPS这一组是Windows提供的重要的固定比例映射模式。

它们都是x值向右方向递增,y值向下递减,并且无法改变。它们之间的区别在于比例因子见下:(我想书上P53页肯定是印错了,因为通过程序实验x值向右方向也是递增的)

MM_LOENGLISH 0.01英寸
MM_HIENGLISH 0.001英寸
MM_LOMETRIC 0.1mm
MM_HIMETRIC 0.01mm
MM_TWIPS 1/1440英寸 //应用于打印机,一个twip相当于1/20磅,一磅又相当于1/72英寸。

看例3

//**************************************************
// 例子6-3
void CMyView::OnDraw(CDC * pDC)
{
pDC->SetMapMode(MM_HIMETRIC);//设定映射模式为MM_HIMETRIC
pDC->Rectangle(CRect(0,0,4000,-4000));//画一个宽和高为4厘米的方块
}

   还有一种是可变比例映射模式,MM_ISOTROPIC、MM_ANISOTROPIC。用这种映射模式可以做到当窗口大小发生变化时图形的大小也会相应的发生改变,同样当翻转某个轴的伸展方向时图象也会以另外一个轴为轴心进行翻转,并且我们还可以定义任意的比例因子,怎么样很有用吧。
MM_ISOTROPIC、MM_ANISOTROPIC两种映射模式的区别在于MM_ISOTROPIC模式下无论比例因子如何变化纵横比是1:1而M_ANISOTROPIC模式则可以纵横比独立变化。

让我们看例子4

//**************************************************
// 例子6-4
void CMy002View::OnDraw(CDC* pDC)
{
CRect rectClient; //
GetClientRect(rectClient);//返回客户区矩形的大小
pDC->SetMapMode(MM_ANISOTROPIC);//设定映射模式为MM_ANISOTROPIC
pDC->SetWindowExt(1000,1000);
pDC->SetViewportExt (rectClient.right ,-rectClient.bottom );
//用SetWindowExt和SetViewportExt函数设定窗口为1000逻辑单位高和1000逻辑单位宽
pDC->SetViewportOrg(rectClient.right/2,rectClient.bottom/2 );//设定逻辑坐标原点为窗口中心
pDC->Ellipse(CRect(-500,-500,500,500));//画一个撑满窗口的椭圆。
// TODO: add draw code for native data here
}

怎么样,屏幕上有一个能跟随窗口大小改变而改变的椭圆。把 pDC->SetMapMode(MM_ANISOTROPIC);这句改为pDC->SetMapMode(MM_ISOTROPIC)会怎样?大家可以试试。那还有一个问题就是上例的比例因子是多少呢?看下面公式(注意是以例子4为例的)

x比例因子=rectClient.right/1000 //视窗的宽除以窗口范围
y比例因子=-rectClient.bottom/1000 //视窗的高除以窗口范围

   从Windows的鼠标消息可以获得鼠标指针的当前坐标值(point.x和point.y)此坐标值是设备坐标。

很多MFC库函数尤其是CRect的成员函数只能工作在设备坐标下。
还有我们有时需要利用物理坐标,物理坐标的概念就是现实世界的实际尺寸。
设备坐标-逻辑坐标-物理坐标之间如何进行转换便成为我们要考虑的一个问题,物理坐标和逻辑坐标是完全要我们自己来做的,但WINDOWS提供了函数来帮助我们转换逻辑坐标和设备坐标。

CDC的LPtoDP函数可以将逻辑坐标转换成设备坐标
CDC的DPtoLP函数可以将设备坐标转换成逻辑坐标

下面列出我们应该在什么时候使用什么样的坐标系一定要记住:

◎CDC的所有成员函数都以逻辑坐标为参数
◎CWnd的所有成员函数都以设备坐标为参数
◎区域的定义采用设备坐标
◎所有的选中测试操作应考虑使用设备坐标。
◎需要长时间使用的值用逻辑坐标或物理坐标来保存。因设备坐标会因窗口的滚动变化而改变。
用书上的例子作为以前几篇的复习,如果你能够独立完成它说明前面的内容已经掌握。另外有些东西是新的,我会比较详细的做出说明,例如客户区、滚动窗口等。

下面我们来一步步完成例子6-5:

■第一步:用AppWizard创建MyApp6。除了Setp 1 选择单文档视图和Setp 6 选择基类为CScrollView外其余均为确省。

■第二步:在CMyApp6View类中增加m_rectEllipse和m_nColor两个私有数据成员。你可以手工在myapp6View.h添加,不过雷神建议这样做,在ClassView中选中CMyApp6View类,击右键选择Add Member Variable插入它们。

//**************************
// myapp6View.h
private:
int m_nColor; //存放椭圆颜色值
CRect m_rectEllipse; //存放椭圆外接矩形

//***************************************************

问题1: CRect是什么?
CRect是类,是从RECT结构派生的,和它类似的还有从POINT结构派生的CPoint、从SIZE派生的CSize。因此它们继承了结构中定义的公有整数数据成员,并且由于三个类的一些操作符被重载所以可以直接在三个类之间进行类的运算。
//重载operator +
CRect operator +( POINT point ) const;
CRect operator +( LPCRECT lpRect ) const;
CRect operator +( SIZE size ) const;
//重载operator -
CRect operator -( POINT point ) const;
CRect operator -( SIZE size ) const;
CRect operator -( LPCRECT lpRect ) const;
......
更多的请在MSDN中查看

■第三步:修改由AppWizard生成的OnIntitalUpdate函数

void CMyApp6View::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
CSize sizeTotal(20000,30000);
CSize sizePage(sizeTotal.cx /2,sizeTotal.cy /2);
CSize sizeLine(sizeTotal.cx /50,sizeTotal.cy/50);
SetScrollSizes(MM_HIMETRIC,sizeTotal,sizePage,sizeLine);//设置滚动视图的逻辑尺寸和映射模式
}

问题2: 关于void CMyApp6View::OnInitialUpdate()

函数OnInitialUpdate()是一个非常重要的虚函数,在视图窗口完全建立后框架用的第一个函数,框架在第一次调用OnDraw前会调用它。因此这个函数是设置滚动视图的逻辑尺寸和映射模式的最佳地点。

■第四步:编辑CMyApp6View构造函数和OnDraw函数

//*********************************************
// CMyApp6View构造函数
//
CMyApp6View::CMyApp6View():m_rectEllipse(0,0,4000,-4000)//椭圆矩形为4*4厘米。
{
m_nColor=GRAY_BRUSH;//设定刷子颜色
}

//*********************************************
// CMyApp6View的OnDraw函数
//
void CMyApp6View::OnDraw(CDC* pDC)
{
pDC->SelectStockObject (m_nColor);
pDC->Ellipse(m_rectEllipse);
}

问题3:

CMyApp6View::CMyApp6View():m_rectEllipse(0,0,4000,-4000)为什么不能这样写:
CMyApp6View::CMyApp6View()
{
m_rectEllipse(0,0,4000,-4000);
m_nColor=GRAY_BRUSH;
}

我从CSDN上得到的答案:两者实际上没有区别。有两个原因使得我们选择第一种语法,它被称为成员初始化列表:

一个原因是必须的,另一个只是出于效率考虑。
   让我们先看一下第一个原因——必要性。设想你有一个类成员,它本身是一个类或者结构,而且只有一个带一个参数的构造函数。

class CMember {
public:
   CMember(int x) { ... }
};

   因为Cmember有一个显式声明的构造函数, 编译器不产生一个缺省构造函数(不带参数),所以没有一个整数就无法创建Cmember的一个实例。

CMember* pm = new CMember(2);   // OK
   如果Cmember是另一个类的成员, 你怎样初始化它呢?你必须使用成员初始化列表。
class CMyClass {
   CMember m_member;
public:
   CMyClass();
};
//必须使用成员初始化列表
CMyClass::CMyClass() : m_member(2)
{
}

   没有其它办法将参数传递给m_member,如果成员是一个常量对象或者引用也是一样。根据C++的规则,常量对象和引用不能被赋值, 它们只能被初始化。
   第二个原因是出于效率考虑,当成员类具有一个缺省的构造函数和一个赋值操作符时。MFC的Cstring提供了一个完美的例子。假定你有一个类CmyClass具有一个Cstring类型成员m_str,你想把它初始化为"yada yada."。你有两种选择:

CMyClass::CMyClass() {
   // 使用赋值操作符
   // CString::operator=(LPCTSTR);
   m_str = _T("yada yada");
}
//使用类成员列表
// and constructor CString::CString(LPCTSTR)
CMyClass::CMyClass() : m_str(_T("yada yada"))
{
}

   在它们之间有什么不同吗?是的。编译器总是确保所有成员对象在构造函数体执行之前初始化,因此在第一个例子中编译的代码将调用CString::Cstring来初始化m_str,这在控制到达赋值语句前完成。在第二个例子中编译器产生一个对CString:: CString(LPCTSTR)的调用并将"yada yada" 传递给这个函数。结果是在第一个例子中调用了两个Cstring函数(构造函数和赋值操作符),而在第二个例子中只调用了一个函数。在Cstring的例子里这是无所谓的,因为缺省构造函数是内联的,Cstring只是在需要时为字符串分配内存(即,当你实际赋值时)。但是,一般而言,重复的函数调用是浪费资源的,尤其是当构造函数和赋值操作符分配内存的时候。在一些大的类里面,你可能拥有一个构造函数和一个赋值操作符都要调用同一个负责分配大量内存空间的Init函数。在这种情况下,你必须使用初始化列表,以避免不要的分配两次内存。在内部类型如ints或者longs或者其它没有构造函数的类型下,在初始化列表和在构造函数体内赋值这两种方法没有性能上的差别。不管用那一种方法,都只会有一次赋值发生。有些程序员说你应该总是用初始化列表以保持良好习惯,但我从没有发现根据需要在这两种方法之间转换有什么困难。在编程风格上,我倾向于在主体中使用赋值,因为有更多的空间用来格式化和添加注释,你可以写出这样的语句:x=y=z=0; 或者memset(this,0,sizeof(this)); 注意第二个片断绝对是非面向对象的。

   当我考虑初始化列表的问题时,有一个奇怪的特性我应该警告你,它是关于 C++初始化类成员的,它们是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。

class CMyClass {
   CMyClass(int x, int y);
   int m_x;
   int m_y;
};
CMyClass::CMyClass(int i) : m_y(i), m_x(m_y)
{
}

   你可能以为上面的代码将会首先做m_y=I,然后做m_x=m_y,最后它们有相同的值。但是编译器先初始化m_x,然后是m_y,因为它们是按这样的顺序声明的。结果是m_x将有一个不可预测的值。 我的例子设计来说明这一点,然而这种bug会更加自然的出现。有两种方法避免它, 一个是总是按照你希望它们被初始化的顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。

■第五步:映射WM_LBUTTONDOWN消息并编辑OnLButtonDown消息处理函数。在Class Wizard中选择CMyApp6View类,在Message列表中选择WM_LBUTTONDOWN双击,则此消息映射便完成了。用下面代码替换Wizard生成的OnLButtonDown消息处理函数。

void CMyApp6View::OnLButtonDown(UINT nFlags, CPoint point)
{
CClientDC dc(this);
OnPrepareDC(&dc);
CRect rectDevice = m_rectEllipse;
dc.LPtoDP(rectDevice);
if (rectDevice.PtInRect(point)) {
if (m_nColor == GRAY_BRUSH) {
m_nColor = WHITE_BRUSH;
}
else{
m_nColor = GRAY_BRUSH;
}
InvalidateRect(rectDevice);
}
}

问题4:详解此段代码

第1行 CClientDC由CDC派生,它的对象dc是当前窗口的客户区域
第2行 OnPrepareDC是在OnDraw函数前调用的。
第3行 将m_rectEllipse赋给rectDevice矩形区域
第4行 将矩形区域的逻辑坐标转为设备坐标,LP
本篇文章来源于 黑客基地-全球最大的中文黑客站 原文链接:http://www.hackbase.com/lib/2008-07-11/12726.html

 

你可能感兴趣的:(VC6.0映射模式转换及如何消除坐标误差)