第10单元 图形设备接口本单元教学目标介绍Windows的图形设备接口(GDI)和MFC的图形对象。
学习要求了解图形设备接口的概念,掌握画笔、画刷和字体的用法,以及逻辑坐标和设备坐标的概念。
授课内容在Windows程序中,应用程序的输出并不直接面向物理的设备,如显示屏或打印机,而是针对一个称之为设备环境(Device Context)的逻辑设备进行操作,设备环境与实际设备之间的信息传送由Windows直接管理。
在MFC应用程序中,所有的绘制调用均通过相应的设备环境对象实现,设备环境对象封装了相应的Windows API功能,由一个32位的HDC类型句柄标识。在MFC类库中,用CDC类封装设备文本对象。
CPaintDC类是从CDC类派生的设备环境类。CPaintDC类的对象在OnPaint()函数中使用。另外,CClientDC类也是常用的CDC派生类,用于在OnPaint()函数外访问设备环境。
在第9单元的程序举例中,我们已经看到了CPaintDC类的应用。但是,那些举例都很单调,没有颜色、线型和字体的变化。本单元首先介绍一些在设备环境下使用的绘图工具(图形设备接口对象),使用它们可改善应用程序的外观,然后介绍图形设备接口(GDI)的坐标系统。
10.1 画笔与画刷画笔是用来画线的工具,是CPen类的对象。其使用方法为:
// 声明画笔对象,并创建宽度为3的红色实线画笔
CPen penRed;
penRed.CreatePen(PS_SOLID,3,RGB(255,0,0));
// 使用新的画笔,保存原来的画笔以便恢复
CPen *pOldPen;
pOldPen = dc.SelectObject(&pOldPen);
// 以下为作图代码,所画的线均使用新画笔
… …
// 恢复原来的画笔
dc.SelectObject(pOldPen);
保存并恢复原来画笔的原因是,每个图形设备接口对象要占用一个HDC句柄,而可用的句柄数量是有限的,在使用完后要及时释放。否则,每次执行OnPaint()函数时均要重新创建图形接口对象,未被释放的非法句柄会留在设备上下文对象中,积累下去将导致严重的运行错误。
CPen类的成员函数CreatePen()用于创建画笔,其原型为:
BOOL CreatePen (int nPenStyle,int nWidth,COLORREF crColor);
第1个参数是画笔样式,可取
画笔样式 说明
PS_SOLID 创建实线笔
PS_DASH 创建由短线构成的虚线
PS_DOT 创建由点构成的虚线
PS_DASHDOT 创建由短线和点构成的虚线
PS_DASH_DOTDOT 创建由短线、点、点构成的虚线
PS_NULL 创建空(空白)画笔各种虚线只有当线宽为1时有效。第2个参数为线宽,第3个参数为线的颜色,可使用RGB()函数指定。RGB()函数有3个参数,分别代表选取颜色的红、绿、蓝分量,可取0~255之间的整数值。例如RGB(255,255,255)为白色,RGB(0,0,0)为黑色。
画刷是用来填充图形的工具,是CBrush类的对象,使用方法与画笔类似,也要定义画刷对象,创建画刷并保存原来的画刷,在绘图工作结束后恢复原来的画刷。创建画刷的成员函数的原型为:
BOOL CreateSolidBrush ( COLORREF crColor );
参数crColor指定了画刷的颜色。除此而外,还可以创建一个阴影风格的画刷:
BOOL CreateHatchBrush ( int nIndex,COLORREF crColor );
其中参数nIndex指定了阴影风格,可取值为:
阴影风格 说明
HS_BDIAGONAL 从左下角到右上角的45度斜线
HS_CROSS 水平线与垂直线
HS_DIAGCROSS 相互垂直的45度线
HS_FDIAGONAL 从左上角到右下角的45度斜线
HS_HORIZONTAL 水平线
HS_VERTICAL 垂直线
CDC类的SelectObject()函数原型如下:
CPen* SelectObject( CPen* pPen );
CBrush* SelectObject( CBrush* pBrush );
virtual CFont* SelectObject( CFont* pFont );
即SelectObject()是重载的CDC类成员函数。SelectObject()将一个GDI对象选入到设备环境中,新选中的对象将替换原有的同类型对象,然后返回指向被替换的对象的指针。
10.2 绘画模式在Windows中,绘图的最终效果不但取决于画笔和画刷的设置,还可以通过设定绘图模式来修正。屏幕绘图模式可通过CDC的成员函数SetROP2()设定,其原型为:
int SetROP2 ( int nDrawMode );
其中参数nDrawMode为选定的绘图模式,常用模式有:
绘图模式 说明
R2_BLACK 无论画笔色如何,只用黑色绘图;
R2_WHITE 无论画笔色如何,只用白色绘图;
R2_NOP 无论画笔色如何,用无色笔绘图;
R2_NOT 用与背景色相反的颜色绘图;
R2_NOTCOPYPEN 用与画笔色相反的颜色绘图;
R2_COPYPEN 用画笔色绘图;
R2_XORPEN 对画笔色和背景色作异或(XOR)运算。
其中R2_NOT模式可保证所绘图形是可见的,即如果画笔色与背景色相同,则以与背景色相反的颜色作图,避免了所画图形“淹没”在背景中;R2_XORPEN模式有一种特殊效果,即对同一条线画两次会起到擦除作用。
该函数的返回值为原来的绘图模式。
10.3 GDI坐标系
GDI支持两种类型的坐标系,即逻辑坐标系和设备坐标系。逻辑坐标系按坐标设置方式(又称为映射模式)可分为8种,它们的坐标特性如下:
映射模式 逻辑单位 x递增方向 y递增方向
MM_TEXT 像素 向右 向下
MM_LOMETRIC 0.1mm 向右 向上
MM_HIMETRIC 0.01mm 向右 向上
MM_LOENGLISH 0.01inch 向右 向上
MM_HIENGLISH 0.001inch 向右 向上
MM_TWIPS 1/1440inch 向右 向上
MM_ISOTROPIC 可调整 (x = y) 可选择 可选择
MM_ANISOTROPIC 可调整(x != y) 可选择 可选择注意所有映射模式的坐标原点均在设备输出区域(如窗口客户区或打印纸上的打印区域)的左上角。因此,对于y坐标递增方向向下的映射模式(如MM_TEXT),y坐标值均为正值,而对于y坐标递增方向向上的映射模式(如MM_LOMETRIC等),所有的y坐标值均为负值,在编程时要特别注意。
最常用的映射模式是MM_TEXT,这也是缺省设置。在该模式下,坐标原点在客户区左上角,x坐标值是向右递增,y坐标值是向下递增,单位值1代表一个像素,与屏幕坐标系类似。采用除MM_TEXT外的其他映射模式的原因有二:一是欲使程序显示在不同的屏幕分辨率(如640×480、800×600或1024×768等)下有相近的尺度;二是欲使程序的显示和打印比例相近(参看13.2:“打印和打印预览”)。
设置映射模式,可使用CDC类的SetMapMode()成员函数,其原型为
virtual int SetMapMode ( int nMapMode );
其中参数nMapMode为欲设置的映射模式,返回值为原来的映射模式。
MFC绘图函数均使用逻辑坐标作为位置参数。例如
CString str(“Hello,MFC!”);
dc.TextOut(10,10,str,str.GetLength());
这里的(10,10)是逻辑坐标而不是像素点数(只是在缺省映射模式MM_TEXT下,正好与像素点相对应),在输出时GDI函数会将逻辑坐标(10,10)依据当前映射模式转化为“设备坐标”,然后将文字输出在屏幕上。
设备坐标以像素点为单位,且x轴坐标值向右递增,y轴坐标值向下延伸,但原点(0,0)位置却不限定在工作区左上角。依据设备坐标的原点和用途,可以将Windows下使用的设备坐标系统分为三种:客户区坐标系统,窗口坐标系统和屏幕坐标系统。
1.客户区坐标系统:客户区坐标系统是最常见的坐标系统,以窗口客户区左上角为原点(0,0),主要用于窗口客户区绘图输出以及处理窗口的一些消息。鼠标消息WM_LBUTTONDOWN、WM_MOUSEMOVE传给框架的消息参数以及CDC一些用于绘图的成员都是使用客户区坐标。
2.屏幕坐标系统:屏幕坐标系统是另一类常用的坐标系统,以屏幕左上角为原点(0,0)。一些与窗口客户区不相关的函数均以屏幕坐标为单位,例如设置和取得光标位置的函数SetCursorPos()和GetCursorPos();由于光标可以在任何一个窗口之间移动,它不属于任何一个单一的窗口,因此使用屏幕坐标。弹出式菜单使用的也是屏幕坐标。另外,CreateWindow()和MoveWindow()等函数用于设置窗口相对于屏幕的位置,使用的也是屏幕坐标系统。
3.窗口坐标系统:窗口坐标系统以窗口左上角为坐标原点,它包含了窗口控制菜单、标题栏等内容。一般情况下很少在窗口标题栏上绘图,因此这种坐标系统很少使用。
MFC提供ClientToScreen()、ScreenToClient()两个函数用于完成客户区坐标和屏幕坐标之间的转换工作。
void ScreenToClient( LPPOINT lpPoint ) const;
void ScreenToClient( LPRECT lpRect ) const;
void ClientToScreen( LPPOINT lpPoint ) const;
void ClientToScreen( LPRECT lpRect ) const;
如果用户在窗口客户区移动鼠标或按下鼠标按键,就会得到鼠标位置的设备坐标。在使用该数据绘图时,需要将其转化为逻辑坐标。CDC类提供了两个成员函数LPtoDP()和DPtoLP()完成逻辑坐标和设备坐标之间的转换工作,其中LPtoDP用于将逻辑坐标转换为设备坐标,而DPtoLP()用于将设备坐标转换为逻辑坐标:
void LPtoDP ( LPPOINT lpPoints,int nCount = 1 ) const;
void LPtoDP ( LPRECT lpRect ) const;
void DPtoLP ( LPPOINT lpPoints,int nCount = 1 ) const;
void DPtoLP ( LPRECT lpRect ) const;
但如果采用MM_TEXT的映射模式,设备坐标和逻辑坐标一致,就无需转换了。第9单元的例题程序均如此。
[例10-1] 在窗口中显示一个椭圆,并用鼠标切换该椭圆的图形参数。
说 明:建立项目的方法见9.8:“用Visual C++集成开发环境开发Win32应用程序”。
程 序:
// Example 10-1,用鼠标切换图形参数
#include <afxwin.h>
// 框架窗口类
class CMyWnd,public CFrameWnd
{
int m_nColor;
CRect m_rectEllipse;
public:
CMyWnd();
protected:
afx_msg void OnPaint();
afx_msg void OnLButtonDown(UINT nFlags,CPoint point);
DECLARE_MESSAGE_MAP()
};
// 消息映射
BEGIN_MESSAGE_MAP(CMyWnd,CFrameWnd)
ON_WM_PAINT()
ON_WM_LBUTTONDOWN()
END_MESSAGE_MAP()
// 框架窗口类的成员函数
CMyWnd::CMyWnd():m_rectEllipse(100,100,300,250)
{
m_nColor = 0;
}
void CMyWnd::OnPaint()
{
CPaintDC dc(this);
CBrush brushNew,*pbrushOld;
CPen penNew,*ppenOld;
switch(m_nColor)
{
case 0:
brushNew.CreateSolidBrush(RGB(255,0,0));
penNew.CreatePen(PS_DASH,1,RGB(0,0,0));
break;
case 1:
brushNew.CreateHatchBrush(HS_DIAGCROSS,RGB(0,255,0));
penNew.CreatePen(PS_SOLID,1,RGB(255,0,0));
break;
case 2:
brushNew.CreateSolidBrush(RGB(0,0,255));
penNew.CreatePen(PS_SOLID,3,RGB(0,255,0));
break;
}
pbrushOld = dc.SelectObject(&brushNew);
ppenOld = dc.SelectObject(&penNew);
dc.Ellipse(m_rectEllipse);
dc.SelectObject(pbrushOld);
dc.SelectObject(ppenOld);
}
void CMyWnd::OnLButtonDown(UINT nFlags,CPoint point)
{
if(m_rectEllipse.PtInRect(point))
{
m_nColor = (m_nColor+1)%3;
InvalidateRect(m_rectEllipse);
}
}
// 应用程序类
class CMyApp,public CWinApp
{
public:
BOOL InitInstance();
};
// 应用程序类的成员函数
BOOL CMyApp::InitInstance()
{
CMyWnd *pFrame = new CMyWnd;
pFrame->Create(0,_T("Change the Color of a Ellipse"));
pFrame->ShowWindow(m_nCmdShow);
pFrame->UpdateWindow();
this->m_pMainWnd = pFrame;
return TRUE;
}
// 全局应用程序对象
CMyApp ThisApp;
输 出:在窗口客户区显示一个椭圆,用鼠标左键点击可以改变它的颜色等设置。
分 析:该程序的基本结构仍与例9-1相同。在由CFrameWnd类派生的框架窗口类中添了两个数据成员,一个是椭圆的包含矩形,一个是其颜色参数。这两个数据成员的初始化在重载的CMyWnd类的构造函数中进行。
在OnPaint()函数中,根据变量m_nColor的值设置画笔和画刷,然后画出椭圆。
如果用户按下鼠标左键则触发OnLButtonDown()函数。在OnLButtonDown()函数中,首先检测鼠标的位置,如果是在椭圆的包含矩形中,则修改数据成员m_nColor的值。
调用Wnd类的成员函数InvalidateRect()的用途是强制更新窗口客户区的内容。我们知道,OnPaint()函数仅在创建窗口或窗口发生变化的情况下调用,因此为了显示修改过的效果,就必须强行更新窗口客户区。不过,因为被修改的仅仅是客户区的一小块内容(椭圆),所以使用InvalidateRect()函数,该函数仅强制更新指定的矩形区域,这样可以减少因大面积更新带来的图形抖动影响。如果要更新整个客户区,可使用Wnd类的成员函数Invalidate()。
本程序中使用缺省的逻辑坐标系MM_TEXT,其特点是其坐标与工作区坐标系统(设备坐标系)恰好一致。这样,在就避免了在检测鼠标位置时转换坐标,因为鼠标位置用设备坐标给出,而绘图函数(包括PtInRect()函数)使用逻辑坐标系。
自学内容
10.4 字体描述输出文字的字体可用CFont对象。CFont对象的使用方法与画笔、画刷类似,也要定义字体对象,创建字体并保存原来的字体,在文字输出工作结束后恢复原来的字体。创建字体的CFont类成员函数的原型为:
BOOL CreateFont( int nHeight, // 字符逻辑高度
int nWidth, // 字体逻辑宽度
int nEscapement, // 出口矢量与X轴的角度
int nOrientation, // 字符基线与X轴的角度
int nWeight, // 字体磅值
BYTE bItalic, // 非0则为斜体
BYTE bUnderline, // 非0则加下划线
BYTE cStrikeOut, // 非0则加删除线
BYTE nCharSet, // 此字体的字符集
BYTE nOutPrecision, // 输出精度
BYTE nClipPrecision, // 裁剪精度
BYTE nQuality, // 输出质量
BYTE nPitchAndFamily, // 调距和字体族
LPCTSTR lpszFacename ); // 字体的字型名其中各参数的含义已经注明。下面进一步说明几个重要参数的设置。
1.nHeight(字符逻辑高度)
该参数值大于0则高度值转换为设备单位,并与可用字体的单元高度匹配;等于0则使用缺省字体尺寸;小于0则高度值转换为设备单位,其绝对值与可用字体的字符高度匹配。
2.nWidth(字体逻辑宽度)
字体字符的平均宽度(逻辑单位)。如果为0,则宽度由系统确定。
3.nWeight(字体磅值)
可取0到1000间的任何值,或用符号常数:
FW_DONTCARE 0 // 缺省字体磅值
FW_THIN 100
FW_EXTRALIGHT 200
FW_ULTRALIGHT 200
FW_LIGHT 300
FW_NORMAL 400
FW_REGULAR 400
FW_MEDIUM 500
FW_SEMIBOLD 600
FW_DEMIBOLD 600
FW_BOLD 700
FW_EXTRABOLD 800
FW_ULTRABOLD 800
FW_BLACK 900
FW_HEAVY 900
4.nCharSet(字符集)
可用:
ANSI_CHARSET 0
DEFAULT_CHARSET 1 // 缺省字符集
SYMBOL_CHARSET 2
SHIFTJIS_CHARSET 128
OEM_CHARSET 255
5.nOutPrecision(输出精度)
可用:
OUT_CHARACTER_PRECIS
OUT_STRING_PRECIS
OUT_DEFAULT_PRECIS // 缺省输出精度
OUT_STROKE_PRECIS
OUT_DEVICE_PRECIS
OUT_TT_PRECIS
OUT_RASTER_PRECIS
6.nClipPrecision(裁剪精度)
定义在裁减区边界的裁减精度。可用:
CLIP_CHARACTER_PRECIS
CLIP_MASK
CLIP_DEFAULT_PRECIS // 缺省裁减精度
CLIP_STROKE_PRECIS
CLIP_ENCAPSULATE
CLIP_TT_ALWAYS
CLIP_LH_ANGLES
7.nQuality(输出质量)
可用:
DEFAULT_QUALITY // 缺省输出质量
DRAFT_QUALITY
PROOF_QUALITY
8.nPitchAndFamily(调距和字体族)
可用,
DEFAULT_PITCH // 缺省值
VARIABLE_PITCH
FIXED_PITCH
该函数的返回值为原来的字体。
字体设置是一件比较麻烦的事。当然,在设置某种字体时,可能其中大部分参数都可使用缺省值。为了确定某个参数的效果,最好是编一简单的程序验证一下。
可以使用LOGFONT类型的对象存储某字体的各项参数。LOGFONT是一结构体类型,其定义为:
typedef struct tagLOGFONT {
LONG lfHeight; // 字体逻辑高度
LONG lfWidth; // 字体逻辑宽度
LONG lfEscapement; // 输出字符行与X轴的角度
LONG lfOrientation; // 字符基线与X轴的角度
LONG lfWeight; // 字体磅值
BYTE lfItalic; // 非0则为斜体
BYTE lfUnderline; // 非0则加下划线
BYTE lfStrikeOut; // 非0则加删除线
BYTE lfCharSet; // 此字体的字符集
BYTE lfOutPrecision; // 输出精度
BYTE lfClipPrecision; // 裁减精度
BYTE lfQuality; // 输出质量
BYTE lfPitchAndFamily; // 调距和字体族
TCHAR lfFaceName[LF_FACESIZE]; // 字体的字型名
} LOGFONT;
如果已经声明了一个LOGFONT类型的对象,则也可据其建立相应的字体。方法是使用CFont类的成员函数CreateFontIndirect()。其原型为:
BOOL CreateFontIndirect(const LOGFONT* lpLogFont );
其中参数lpLogFont是一指向存有字体参数对象的指针。
10.5 库存图形对象在Windows的GDI中,包含一些预定义的图形对象,无需用户去创建,马上就可以拿来使用。这些对象称作库存(Stock)对象。库存对象包括画笔、画刷和字体等。创建库存对象可使用CGdiObject类的成员函数CreateStockObject(),其原型为:
BOOL CreateStockObject( int nIndex );
其中参数nIndex指定要创建的图形对象。常用的图形对象有:
图形对象 说明
BLACK_PEN 黑笔
NULL_PEN 空笔,不画线或边框
WHITE_PEN 白笔
BLACK_BRUSH 黑色刷子
WHITE_BRUSH 白色刷子
DKGRAY_BRUSH 暗灰刷子
GRAY_BRUSH 灰色刷子
LTGRAY_BRUSH 淡灰色刷子
NULL_BRUSH 空刷子,内部不填充
ANSI_FIXED_FONT Windows固定倾角(单间隔)系统字体
ANSI_VAR_FONT Windows可变倾角(比例间隔)系统字体
DEFAULT_GUI_FONT 缺省GUI字体(如菜单和对话框字体)
SYSTEM_FONT 系统字体创建库存图形对象成功,该函数成功返回非0值,否则返回0。实际上,该函数并不真正创建对象,而只是取得库存对象的句柄,并将该句柄连到调用该函数的GDI对象上。
使用库存图形对象不必存储和恢复原来的图形对象。
编程与调试
10.6 Visual C++的常用调试宏
Visual C++提供了一些宏和成员函数用于调试程序。其中最常用的有:TRACE(),ASSERT(),ASSERT_VALID()和CObject类的Dump()成员函数。
10.6.1 TRACE()宏
TRACE宏的格式如下:
TRACE(<表达式>);
其中参数<表达式> 用于构造输出字符串,构造方式与CString类的Format()成员函数中所用的方法相同。TRACE()的输出结果在调试状态的Output窗口中显示。在TRACE宏中,可以使用各种MFC类。举例如下:
int nSize = 3;
CString sName(“why”);
TRACE(“Name = %s,Size = %d\n”,sName,nSize);
在调试器的Output窗口显示
Name = why,Size = 3
并换行。
10.6.2 ASSERT()宏
ASSERT语法如下:
ASSERT(<表达式>);
如果表达式返回结果为真,则程序继续运行;如果返回结果为假,则在该语句行处终止程序运行,并弹出一个对话框,显示程序终止的行及所在文件信息。如:
CMyFrame *pFrame = (CMyFrame*)AfxGetMainWnd();
ASSERT(pFrame->IsKindOf(RUN_TIMECLASS(CMyFrame)));
pFrame->DoSomeOperation();
又如:
CAge a(21);
ASSERT(a.IsKindOf(RUNTIME_CLASS(CAge)));
ASSERT(a.IsKindOf(RUNTIME_CLASS(CObject)));
10.6.3 ASSERT_VALID()宏
ASSERT_VALID用于检查指针和对象的有效性。对于一般指针,只检查指针是否为空(NULL),如果为空,则终止程序执行。对于MFC对象指针,不但检查对象指针是否为空,还调用对象的AssertValid()成员函数,判断对象合法性。这对于许多对象来说是非常有用的,例如,声明一个描述人的类,就可以在其AssertValid()成员函数中把人的年龄限制在1到150之间,如果超出这一范围,就认为是非法数值。
ASSERT_VALID语法如下:
ASSERT_VALID(<指针>)
10.6.4 CObject::Dump()成员函数
CObject::Dump()成员函数用于输出对象内部数据成员的数值。当程序调试过程中希望检查对象内部状态时,Dump()是非常有用的。Dump()使用“<<”插入操作符输出数据成员数值。例如
void CAge::Dump( CDumpContext &dc ) const
{
CObject::Dump(dc);
dc << "Age = " << m_years;
}
其中,dc是一个CDumpContext类的对象。CDumpContext类支持以字符流形式的动态输出,有关CDumpContext类的详细情况可参看Visual C++帮助文档。
10.7 Developer Studio的输出窗口
Developer Studio在停靠的Output(输出)窗口中显示项目的有关信息,如图10-1所示。如果Output窗口不可见,在View(查看)菜单中单击其名字即可使其出现在屏幕上。Output窗口也可以通过Standard(标准)工具栏上的相应按钮来激活或取消。
Output窗口有5个选项卡:Build(建立)、Debug(调试)、Find In Files 1(在文件1中查找)、Find In Files 2(在文件2中查找)和Results(结果)。
Build选项卡显示编译器、链接器和其他工具的状态消息。
Debug选项卡用于显示来自调试器的提示,这些提示对诸如未处理的异常和内存异常之类的情况提出警告。应用程序通过OutputDebugString API()函数或afxDump类库产生的消息,也显示在Debug选项卡中。
剩下的两个选项卡显示从Edit(编辑)菜单中选中的Find In Files(在文件中查找)命令的执行结果(这个十分有用的特征与UNIX的grep命令相似)。默认情况下,Find In Files搜索结果显示在Output窗口的Find In Files 1选项卡中,但Find In Files对话框中的一个复选项,允许将结果转移到Find In Files 2选项卡中。
程序设计举例
[例10-2] 编一程序,允许用鼠标左键拖动一个椭圆在客户区内移动。
设计思想:所谓“拖动”某物体,即在该物体的图象范围内按下鼠标左键,然后移动鼠标至一新位置。在此期间,物体的图象应随鼠标移动。因此,为了实现“拖动”效果,需要重载框架窗口类的按下鼠标、移动鼠标和放开鼠标等消息处理函数,并在这些消息处理函数中恰当修改被移物体的位置数据。
说 明:建立项目的方法见9.8:“用Visual C++集成开发环境开发Win32应用程序”。
程 序:
// Example 10-2:用鼠标移动图形
#include <afxwin.h>
// 框架窗口类
class CMyWnd,public CFrameWnd
{
CRect m_rectEllipse;
CPoint m_pointMouse;
BOOL m_bCaptured;
public:
CMyWnd();
protected:
afx_msg void OnPaint();
afx_msg void OnLButtonDown(UINT nFlags,CPoint point);
afx_msg void OnLButtonUp(UINT nFlags,CPoint point);
afx_msg void OnMouseMove(UINT nFlags,CPoint point);
DECLARE_MESSAGE_MAP()
};
// 消息映射
BEGIN_MESSAGE_MAP(CMyWnd,CFrameWnd)
ON_WM_PAINT()
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_MOUSEMOVE()
END_MESSAGE_MAP()
// 框架窗口类的成员函数
CMyWnd::CMyWnd(): m_rectEllipse(100,100,300,250)
{
m_bCaptured = FALSE;
}
void CMyWnd::OnPaint()
{
CPaintDC dc(this);
CBrush brushNew,*pbrushOld;
brushNew.CreateHatchBrush(HS_CROSS,RGB(255,0,0));
pbrushOld = dc.SelectObject(&brushNew);
dc.Ellipse(m_rectEllipse);
dc.SelectObject(pbrushOld);
}
void CMyWnd::OnLButtonDown(UINT nFlags,CPoint point)
{
if(m_rectEllipse.PtInRect(point))
{
SetCapture();
m_bCaptured = TRUE;
m_pointMouse = point;
}
}
void CMyWnd::OnLButtonUp(UINT nFlags,CPoint point)
{
if(m_bCaptured)
{
::ReleaseCapture();
m_bCaptured = FALSE;
}
}
void CMyWnd::OnMouseMove(UINT nFlags,CPoint point)
{
if(m_bCaptured)
{
InvalidateRect(m_rectEllipse);
CSize offset(point-m_pointMouse);
m_rectEllipse += offset;
InvalidateRect(m_rectEllipse);
m_pointMouse = point;
}
}
// 应用程序类
class CMyApp,public CWinApp
{
public:
BOOL InitInstance();
};
// 应用程序类的成员函数
BOOL CMyApp::InitInstance()
{
CMyWnd *pFrame = new CMyWnd;
pFrame->Create(0,_T("Using Mouse Move a Ellipse"));
pFrame->ShowWindow(m_nCmdShow);
pFrame->UpdateWindow();
this->m_pMainWnd = pFrame;
return TRUE;
}
// 全局应用程序对象
CMyApp ThisApp;
输入输出:在窗口客户区显示一椭圆,可用鼠标左键拖动。
分 析:为了用鼠标拖动图形,在程序主窗口类中添了两个数据成员:m_bCaptured用来记录鼠标左键是否按下,m_pointMouse用来记录鼠标的位置。在各消息处理成员函数中,主要是针对这两个数据进行操作。为了拖动图形对象,除了WM_PAINT消息和WM_LBUTTONDOWN消息外,还需处理WM_LBUTTONUP消息和WM_MOUSEMOVE消息,因此必须在程序中添加相应的消息映射和消息处理函数。
OnPaint()函数负责根据数据成员m_rectEllipse的值在框架窗口客户区画出一椭圆。
OnLButtonDown()函数的工作流程为,如果用户在椭圆的包含矩形中按下鼠标左键,则记录鼠标位置并置鼠标左键按下标志为TRUE。其中SetCapture()为Wnd类的成员函数,用于“捕获”鼠标。一旦鼠标活动被SetCapture()捕获,则可保证其后的鼠标消息(如鼠标移动等)一直发往该窗口,即使鼠标已经离开该窗口。
OnLButtonUp()函数比较简单,用于清除鼠标左键按下标志。其中的Windows全局函数::ReleaseCapture()用于关闭鼠标捕获。
OnMouseMove()函数负责在移动鼠标时修改椭圆的位置参数。其工作流程为,如果m_bCaptured为真,首先计算鼠标移动的增量,然后据其修改椭圆的包含矩形位置。最后记录鼠标的新位置。在更新过程中,还要通知OnPaint()函数,重绘原来的图形区域(擦除)和新的图形区域(重绘)。
CSize类对象用于存放矩形的大小或位置的偏移,它有两个数据成员cx和cy。CRect类、CPoint类和CSize类的对象可进行四则运算(通过运算符重载),其中主要有: 两点相减得偏移量、点加偏移量得新点和矩形加偏移量移动矩形位置等。
[例10-3] 用定时器控制物体在窗口客户区自行移动。
设计思想:鼠标、键盘等消息由用户控制发送,因而是被动的。定时器消息由系统按指定的时间间隔主动发送,因此通过重载框架函数的定时器消息响应函数可以控制物体在客户区自行移动。
说 明:建立项目的方法见9.8:“用Visual C++集成开发环境开发Win32应用程序”。
程 序:
// Example 10-3:用定时器控制物体在窗口客户区自行移动
#include <afxwin.h>
// 框架窗口类
class CMyWnd,public CFrameWnd
{
CRect m_rectCake;
public:
CMyWnd(){m_rectCake = CRect(0,80,100,160);}
protected:
afx_msg void OnPaint();
afx_msg void OnTimer(UINT nIDEvent);
afx_msg void OnDestroy();
DECLARE_MESSAGE_MAP()
};
// 消息映射
BEGIN_MESSAGE_MAP(CMyWnd,CFrameWnd)
ON_WM_PAINT()
ON_WM_TIMER()
ON_WM_DESTROY()
END_MESSAGE_MAP()
// 框架窗口类的成员函数
void CMyWnd::OnPaint()
{
CPaintDC dc(this);
dc.SelectStockObject(LTGRAY_BRUSH);
dc.Ellipse(m_rectCake);
}
void CMyWnd::OnTimer(UINT nIDEvent) // 定时更新数据
{
CRect rectClient;
GetClientRect(&rectClient);
InvalidateRect(m_rectCake);
if(m_rectCake.left<rectClient.right)
{
m_rectCake.left += 5;
m_rectCake.right += 5;
}
else
{
m_rectCake.left = 0;
m_rectCake.right = 100;
}
InvalidateRect(m_rectCake);
}
void CMyWnd::OnDestroy() // 解除定时器
{
KillTimer(1);
}
// 应用程序类
class CMyTimerApp,public CWinApp
{
public:
BOOL InitInstance();
};
// 应用窗口类的成员函数
BOOL CMyTimerApp::InitInstance()
{
CMyWnd *pFrame = new CMyWnd;
pFrame->Create(0,_T("A Cake Move on Screen"));
pFrame->ShowWindow(m_nCmdShow);
pFrame->UpdateWindow();
pFrame->SetTimer(1,100,NULL); // 设置定时器
this->m_pMainWnd = pFrame;
return TRUE;
}
CMyTimerApp ThisApp;
输入输出:在窗口客户区显示一椭圆,可自动由左至右移动。
分 析:本例讨论定时器消息WM_TIMER。为了使用定时器,首先要设置定时器,这可以通过窗口类的成员函数SetTimer()完成。其原型为:
UINT SetTimer( UINT nIDEvent,UINT nElapse,void *lpfnTimer );
该函数安装一个系统定时器,按预设的时间间隔向应用程序发送WM_TIMER消息。其中参数nIDEvent为定时器标识,可用任一非0整数。如果在程序中设置了若干定时器,则可每个定时器的标识应不同。参数nElapse是定时器的时间间隔,单位为毫秒。参数lpfnTimer可以取值NULL,这时WM_TIMER消息加入应用程序的消息队列中,由CWnd 类对象处理。
在本例中,设置定时器的工作放在应用实例初始化函数InitInstance()中进行。这是因为我们希望该定时器能在程序运行之初就设置好,因此放在框架窗口建立并显示后来做这项工作。当然,设置定时器的工作也可放在其他地方,如根据鼠标、键盘消息,或放在菜单中。
设置好的定时器用完后要及时释放。释放定时器使用窗口类的成员函数KillTimer(),其原型为:
BOOL KillTimer( int nIDEvent );
其中参数nIDEvent为定时器标识。本例中释放定时器的工作放在响应WM_DESTROY消息的框架窗口成员函数OnDestroy()中。只有当应用程序结束,撤销其框架窗口时,才会发送WM_DESTROY消息。这样,该定时器就与应用程序的框架窗口共始终了。
设置的定时器会按其时间间隔向应用程序发送WM_TIMER消息。为了接收和处理该消息,在框架窗口中重载消息处理函数OnTimer(),其原型为:
afx_msg void OnTimer( UINT nIDEvent );
其中参数nIDEvent为定时器标识。若在程序中设置了多个定时器,则在重载的OnTimer()函数中可根据定时器标识来区分不同的定时器周期。由于在本例中仅设置了一个定时器,所以在OnTimer()函数中未使用参数nIDEvent。
本例的工作逻辑很简单,框架窗口派生类的数据成员m_rectCake用来存放物体的位置,OnPaint()函数根据其值画出该物体。在OnTimer()函数不断修改物体的位置并通知OnPaint()函数更新窗口客户区的部分区域。
[例10-4] 可供两人在计算机上对弈的中国象棋程序。
说 明:建立项目的方法见9.8:“用Visual C++集成开发环境开发Win32应用程序”。
程 序:(见附录3:“可供两人对弈的中国象棋程序”)
输入输出:在程序窗口客户区显示中国象棋棋盘和棋子,以及轮哪方行棋的提示(图10-2)。用户可使用鼠标左键选择要移动的棋子以及移到何处。不能将棋子移到已有己方棋子的位置,或移出棋盘。一方移动棋子后自动提示该对方行棋。
分 析:尽管这这只是一个相当粗略的游戏程序框架(例如程序中没有反应各棋子的行棋规则,更不用说利用人工智能,将机器作为一方与人对弈了),但程序长度已达四、五百行。实际上,真正的应用程序规模总是很大。通过仔细阅读和分析本程序,以及对本程序的进一步完善(如增加对各种棋子行棋规则的判断等),可大大提高编写程序的能力。
在程序中,首先声明了一个棋盘类和一个棋子类,用于存放棋盘类和棋子类对象的有关属性。
棋盘类的数据成员包括棋盘的大小、位置以及棋盘格的宽度。棋盘类的成员函数是棋盘类的对外接口,包括棋盘类的构造函数(用于初始化棋盘参数)和显示棋盘。在显示棋盘成员函数ShowPlate()中,又调用了一个用于画兵、炮位标志的DrawConer()函数。ShowPlate()函数有一个CDC类的指针参数,在框架窗口类的OnPaint()成员函数中可以看到,该参数的实参为CPaintDC类的设备环境。另一个成员函数GetPosition()给出了棋盘上某交叉点的实际坐标(用于画棋子或判断鼠标是否指向某交叉点)。
棋子类的数据成员包括棋子的位置、名称、颜色、是否被吃掉(不显示)、是否被选择(被选择的棋子要变颜色,以利识别)等属性。其成员函数包括构造函数(用于设置棋子初值)和显示棋子、将棋子移到棋盘某交叉点、查看棋子的各种属性的函数。其中大部分成员函数比较简单,所以使用了内联函数形式。注意在显示棋子函数中,只显示未被吃掉的棋子,且被选择的棋子颜色有变化(由黄色变为灰色)。
框架窗口类用于实现具体的下棋过程。框架窗口类的数据成员包括一个棋盘对象和一个棋子对象的数组。棋子数组的前一半存放红方棋子,后一半存放黑方棋子。其他数据成员用于存放轮哪方行棋、是否选择了棋子等标志信息。
框架窗口类的构造函数调用InitGame()函数初始化棋局,主要是通过调用棋子类的构造函数来设置各个棋子。
OnPaint()函数根据棋盘的设置和各棋子的当前状况显示棋盘、棋子和提示信息。
消息响应函数OnLButtonDown()用于处理用户的走棋消息。实际上,走一步棋需要点两次鼠标左键,第一次用于选择要移动的棋子,第二次用于指出要将选定的棋子移向何处。因此,当用户按下鼠标左键时,首先要判断是否已选定了一枚棋子。如果是,则应移动该棋子,否则应选定鼠标指向的那枚棋子。
选定棋子的工作实际上是通过逐个检查己方的所有未被吃掉的棋子,看其是否在鼠标下。如果有一枚己方棋子被选中,则设置选中标志,并通知OnPaint()更新窗口客户区该棋子的区域(因为被选中的棋子要变为灰色)。
如果已选择了一枚棋子,则再次点击鼠标左键应该移动被选中的棋子。首先要确定该位置可以走棋(如非棋盘交叉点、棋盘外或已有己方棋子的对方均不能走棋),这是通过逐个检查棋盘上的每个交叉点实现的。如果该点可以走棋,则将选定的棋子移向该处。这包括修改棋子的位置参数(CStone::MoveTo)、分别通知OnPaint()更新棋子原来位置和新位置的图象、还原选择棋子标志检查是否吃了对方棋子等工作。如果吃了对方的“将”或“帅”,则宣布本局结束并重新摆好棋盘,进行下一局。
如果仅仅走一部棋或吃了对方其他棋子,则本方行棋结束,轮对方行棋。在程序中,这项工作包括改变轮谁行棋的标志和显示相应的提示信息。
单元上机练习题目编一程序,在窗口客户区绘制一幅包括太阳、蓝天、草地和房子的彩色图画。
修改例10-2,添加可用鼠标右键画出或擦除椭圆的功能。其思路如下:
(1)在程序主窗口类中添加表示椭圆是否画出的数据成员m_nDraw;
(2)在程序中添加响应WM_RBUTTONDOWN消息的内容,包括消息映射宏和消息处理函数OnRButtonDown();
(3)在OnPaint()函数中,只有当m_nDraw为真时才绘出椭圆;
(4)OnRButtonDown()的处理流程为:检查椭圆是否画出,如已画出且鼠标位于椭圆包含矩形内则置m_nDraw为假,否则置m_nDraw为真;强行更新窗口客户区。
修改例10-4,采用鼠标左键拖动的方式移动棋子。另外,加入一些走棋的规则,如马走“日”,象走田,车直行等。
利用鼠标、键盘、定时器等消息自行设计一游戏程序。