荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: Deny (孤苦伶仃一个人,哈哈), 信区: Program
标  题: 『VC++技术内幕』学习笔记(6)
发信站: 荔园晨风BBS站 (Tue Dec 25 20:00:29 2001), 转信

原作者姓名 雷神
文章原始出处 http://www.ai361.com
正文
『VC++技术内幕』学习笔记(6)
雷神
第六篇:映射模式
在此篇之前我们已经学会了在窗口显示图形,更准确的说是在窗口指定位置显示图
形或文字,我们使用的坐标单位是象素,称之为设备坐标。看下面语句:
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
......

编译运行它,怎么样成功了吧,
■第七步:做一个更复杂的程序,例如屏幕上有多个圆,然后点其中一个,则点中
的变色,其他的不变。

未完待续
下一篇:图形设备接口(GDI)
正文完


--

※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.1.201]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店