作者:雷神 转载请联系作者 第六篇:映射模式 在此篇之前我们已经学会了在窗口显示图形,更准确的说是在窗口指定位置显示图形或文字,我们使用的坐标单位是象素,称之为设备坐标。看下面语句: 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行 将矩形区域的逻辑坐标转为设备坐标,LPtoDP是CDC类的成员函数,且是多态的,函数声明如下: void LPtoDP( LPPOINT lpPoints, int nCount = 1 ) const; void LPtoDP( LPRECT lpRect ) const; void LPtoDP( LPSIZE lpSize ) const; 第5-11行 CRect的成员函数PtInRect(point)用来判断鼠标当前位置(point)是否在当前矩形(rectDevice)内 第12行 InvalidateRect函数可以触发WM_PAINT消息,改消息又被映射,引起调用OnDraw调用。 ■第六步:映射WM_KEYDOWN消息并编辑OnKeyDown消息处理函数。在Class Wizard中选择CMyApp6View类,在Message列表中选择WM_KEYDOWN双击。用下面代码替换Wizard生成的OnKeyDown消息处理函数。 void CMyApp6View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { switch (nChar) { case VK_HOME: OnVScroll(SB_TOP, 0, NULL); OnHScroll(SB_LEFT, 0, NULL); break; case VK_END: OnVScroll(SB_BOTTOM, 0, NULL); OnHScroll(SB_RIGHT, 0, NULL); break; case VK_UP: OnVScroll(SB_LINEUP, 0, NULL); break; case VK_DOWN: OnVScroll(SB_LINEDOWN, 0, NULL); break; case VK_PRIOR: OnVScroll(SB_PAGEUP, 0, NULL); break; case VK_NEXT: OnVScroll(SB_PAGEDOWN, 0, NULL); break; case VK_LEFT: OnHScroll(SB_LINELEFT, 0, NULL); break; case VK_RIGHT: OnHScroll(SB_LINERIGHT, 0, NULL); break; default: break; } } 问题5:此段代码详解: 先看OnVScroll和OnHScroll的函数原型 afx_msg void OnVScroll( UINT nSBCode, UINT nPos, CScrollBar* pScrollBar ); afx_msg void OnHScroll( UINT nSBCode, UINT nPos, CScrollBar* pScrollBar ); 主要参数 nSBCode是指滚动条移动方向。 再看OnKeyDown函数原型 afx_msg void OnKeyDown( UINT nChar, UINT nRepCnt, UINT nFlags ); 主要参数nChar是指Virtual Keys code 虚拟键码你可以在winuser.h文件中看到更多,这里只列出很小一部分。 #define VK_ESCAPE 0x1B #define VK_SPACE 0x20 #define VK_PRIOR 0x21 #define VK_NEXT 0x22 #define VK_END 0x23 #define VK_HOME 0x24 ...... /* VK_0 thru VK_9 are the same as ASCII '0' thru '9' (0x30 - 0x39) */ /* VK_A thru VK_Z are the same as ASCII 'A' thru 'Z' (0x41 - 0x5A) */ #define VK_LWIN 0x5B ...... 编译运行它,怎么样成功了吧, ■第七步:做一个更复杂的程序,例如屏幕上有多个圆,然后点其中一个,则点中的变色,其他的不变。 未完待续 |