WIN32和MFC
2010年10月11日
2. 1. MFC Object和Windows Object的关系 MFC中最重要的封装是对Win32 API的封装,因此,理解Windows Object和MFC Object (C++对象,一个C++类的实例)之间的关系是理解MFC的关键之一。所谓Windows Object(Windows对象)是Win32下用句柄表示的Windows操作系统对象;所谓MFC Object (MFC对象)是C++对象,是一个C++类的实例,这里(本书范围内)MFC Object是有特定含义的,指封装Windows Object的C++ Object,并非指任意的C++ Object。 MFC Object 和Windows Object是不一样的,但两者紧密联系。以窗口对象为例: 一个MFC窗口对象是一个C++ CWnd类(或派生类)的实例,是程序直接创建的。在程序执行中它随着窗口类构造函数的调用而生成,随着析构函数的调用而消失。而Windows窗口则是 Windows系统的一个内部数据结构的实例,由一个“窗口句柄”标识,Windows系统创建它并给它分配系统资源。Windows窗口在MFC窗口对象创建之后,由CWnd类的Create成员函数创建,“窗口句柄”保存在窗口对象的m_hWnd成员变量中。Windows窗口可以被一个程序销毁,也可以被用户的动作销毁。MFC窗口对象和Windows窗口对象的关系如图2-1所示。其他的Windows Object和对应的MFC Object也有类似的关系。 下面,对MFC Object和Windows Object作一个比较。有些论断对设备描述表(MFC类是CDC,句柄是HDC)可能不适用,但具体涉及到时会指出。 1. 从数据结构上比较 MFC Object是相应C++类的实例,这些类是MFC或者程序员定义的; Windows Object是Windows系统的内部结构,通过一个句柄来引用; MFC给这些类定义了一个成员变量来保存MFC Object对应的Windows Object的句柄。对于设备描述表CDC类,将保存两个HDC句柄。 2. 从层次上讲比较 MFC Object是高层的,Windows Object是低层的; MFC Object封装了Windows Object的大部分或全部功能,MFC Object的使用者不需要直接应用Windows Object的HANDLE(句柄)使用Win32 API,代替它的是引用相应的MFC Object的成员函数。 3. 从创建上比较 MFC Object通过构造函数由程序直接创建;Windows Object由相应的SDK函数创建。 MFC中,使用这些MFC Object,一般分两步: 首先,创建一个MFC Object,或者在STACK中创建,或者在HEAP中创建,这时,MFC Object的句柄实例变量为空,或者说不是一个有效的句柄。 然后,调用MFC Object的成员函数创建相应的Windows Object,MFC的句柄变量存储一个有效句柄。 CDC(设备描述表类)的创建有所不同,在后面的2.3节会具体说明CDC及其派生类的创建和使用。 当然,可以在MFC Object的构造函数中创建相应的Windows对象,MFC的GDI类就是如此实现的,但从实质上讲,MFC Object的创建和Windows Object的创建是两回事。 4. 从转换上比较 可以从一个MFC Object得到对应的Windows Object的句柄;一般使用MFC Object的成员函数GetSafeHandle得到对应的句柄。 可以从一个已存在的Windows Object创建一个对应的MFC Object; 一般使用MFC Object的成员函数Attach或者FromHandle来创建,前者得到一个永久性对象,后者得到的可能是一个临时对象。 5. 从使用范围上比较 MFC Object对系统的其他进程来说是不可见、不可用的;而Windows Object一旦创建,其句柄是整个Windows系统全局的。一些句柄可以被其他进程使用。典型地,一个进程可以获得另一进程的窗口句柄,并给该窗口发送消息。 对同一个进程的线程来说,只可以使用本线程创建的MFC Object,不能使用其他线程的MFC Object。 6. 从销毁上比较 MFC Object随着析构函数的调用而消失;但Windows Object必须由相应的Windows系统函数销毁。 设备描述表CDC类的对象有所不同,它对应的HDC句柄对象可能不是被销毁,而是被释放。 当然,可以在MFC Object的析构函数中完成Windows Object的销毁,MFC Object的GDI类等就是如此实现的,但是,应该看到:两者的销毁是不同的。 每类Windows Object都有对应的MFC Object,下面用表格的形式列出它们之间的对应关系,如表2-1所示: 表2-1 MFC Object和Windows Object的对应关系 描述 Windows句柄 MFC Object 窗口 HWND CWnd and CWnd-derived classes 设备上下文 HDC CDC and CDC-derived classes 菜单 HMENU CMenu 笔 HPEN CGdiObject类,CPen和CPen-derived classes 刷子 HBRUSH CGdiObject类,CBrush和CBrush-derived classes 字体 HFONT CGdiObject类,CFont和CFont-derived classes 位图 HBITMAP CGdiObject类,CBitmap和CBitmap-derived classes 调色板 HPALETTE CGdiObject类,CPalette和CPalette-derived classes 区域 HRGN CGdiObject类,CRgn和CRgn-derived classes 图像列表 HimageLIST CimageList和CimageList-derived classes 套接字 SOCKET CSocket,CAsynSocket及其派生类 表2-1中的OBJECT分以下几类: Windows对象, 设备上下文对象, GDI对象(BITMAP,BRUSH,FONT,PALETTE,PEN,RGN), 菜单, 图像列表, 网络套接字接口。 从广义上来看,文档对象和文件可以看作一对MFC Object和Windows Object,分别用CDocument类和文件句柄描述。 后续几节分别对前四类作一个简明扼要的论述。 1. Windows Object 用SDK的Win32 API编写各种Windows应用程序,有其共同的规律:首先是编写WinMain函数,编写处理消息和事件的窗口过程WndProc,在WinMain里头注册窗口(Register Window),创建窗口,然后开始应用程序的消息循环。 MFC应用程序也不例外,因为MFC是一个建立在SDK API基础上的编程框架。对程序员来说所不同的是:一般情况下,MFC框架自动完成了Windows登记、创建等工作。 下面,简要介绍MFC Window对Windows Window的封装。 1. Windows的注册 一个应用程序在创建某个类型的窗口前,必须首先注册该“窗口类”(Windows Class)。注意,这里不是C++类的类。Register Window把窗口过程、窗口类型以及其他类型信息和要登记的窗口类关联起来。 1. “窗口类”的数据结构 “窗口类”是Windows系统的数据结构,可以把它理解为Windows系统的类型定义,而Windows窗口则是相应“窗口类”的实例。Windows使用一个结构来描述“窗口类”,其定义如下: typedef struct _WNDCLASSEX { UINT cbSize; //该结构的字节数 UINT style; //窗口类的风格 WNDPROC lpfnWndProc; //窗口过程 int cbClsExtra; int cbWndExtra; HANDLE hInstance; //该窗口类的窗口过程所属的应用实例 HICON hIcon; //该窗口类所用的像标 HCURSOR hCursor; //该窗口类所用的光标 HBRUSH hbrBackground; //该窗口类所用的背景刷 LPCTSTR lpszMenuName; //该窗口类所用的菜单资源 LPCTSTR lpszClassName; //该窗口类的名称 HICON hIconSm; //该窗口类所用的小像标 } WNDCLASSEX; 从“窗口类”的定义可以看出,它包含了一个窗口的重要信息,如窗口风格、窗口过程、显示和绘制窗口所需要的信息,等等。关于窗口过程,将在后面消息映射等有关章节作详细论述。 Windows系统在初始化时,会注册(Register)一些全局的“窗口类”,例如通用控制窗口类。应用程序在创建自己的窗口时,首先必须注册自己的窗口类。在MFC环境下,有几种方法可以用来注册“窗口类”,下面分别予以讨论。 2. 调用AfxRegisterClass注册 AfxRegisterClass函数是MFC全局函数。AfxRegisterClass的函数原型: BOOL AFXAPI AfxRegisterClass(WNDCLASS *lpWndClass); 参数lpWndClass是指向WNDCLASS结构的指针,表示一个“窗口类”。 首先,AfxRegisterClass检查希望注册的“窗口类”是否已经注册,如果是则表示已注册,返回TRUE,否则,继续处理。 接着,调用::RegisterClass(lpWndClass)注册窗口类; 然后,如果当前模块是DLL模块,则把注册“窗口类”的名字加入到模块状态的域 m_szUnregisterList中。该域是一个固定长度的缓冲区,依次存放模块注册的“窗口类”的名字(每个名字是以“\n\0”结尾的字符串)。之所以这样做,是为了DLL退出时能自动取消(Unregister)它注册的窗口类。至于模块状态将在后面第9章详细的讨论。 最后,返回TRUE表示成功注册。 3. 调用AfxRegisterWndClass注册 AfxRegisterWndClass函数也是MFC全局函数。AfxRegisterWndClass的函数原型: LPCTSTR AFXAPI AfxRegisterWndClass(UINT nClassStyle, HCURSOR hCursor, HBRUSH hbrBackground, HICON hIcon) 参数1指定窗口类风格; 参数2、3、4分别指定该窗口类使用的光标、背景刷、像标的句柄,缺省值是0。 此函数根据窗口类属性动态地产生窗口类的名字,然后,判断是否该类已经注册,是则返回窗口类名;否则用指定窗口类的属性(窗口过程指定为缺省窗口过程),调用AfxRegisterCalss注册窗口类,返回类名。 动态产生的窗口类名字由以下几部分组成(包括冒号分隔符): 如果参数2、3、4全部为NULL,则由三部分组成。 “Afx”+“:”+模块实例句柄”+“:”+“窗口类风格” 否则,由六部分组成: “Afx”+“:”+模块实例句柄+“:”+“窗口类风格”+“:”+光标句柄+“:”+背景刷句柄+“:”+像标句柄。比如:“Afx:400000:b:13de:6:32cf”。 该函数在MFC注册主边框或者文档边框“窗口类”时被调用。具体怎样用在5.3.3.3节会指出。 4. 隐含的使用MFC预定义的的窗口类 MFC4.0以前的版本提供了一些预定义的窗口类,4.0以后不再预定义这些窗口类。但是,MFC仍然沿用了这些窗口类,例如: 用于子窗口的“AfxWnd”; 用于边框窗口(SDI主窗口或MDI子窗口)或视的“AfxFrameOrView”; 用于MDI主窗口的“AfxMDIFrame”; 用于标准控制条的“AfxControlBar”。 这些类的名字就 是“AfxWnd”、“AfxFrameOrView”、“AfxMdiFrame”、 “AfxControlBar”加上前缀和后缀(用来标识版本号或是否调试版等)。它们使用标准应用程序像标、标准文档像标、标准光标等标准资源。为了使用这些“窗口类”,MFC会在适当的时候注册这些类:或者要创建该类的窗口时,或者创建应用程序的主窗口时,等等。 MFC内部使用了函数 BOOL AFXAPI AfxEndDeferRegisterClass(short fClass) 来帮助注册上述原MFC版本的预定义“窗口类”。参数fClass区分了那些预定义窗口的类型。根据不同的类型,使用不同的窗口类风格、窗口类名字等填充WndClass的域,然后调用AfxRegisterClass注册窗口类。并且注册成功之后,通过模块状态的m_fRegisteredClasses记录该窗口类已经注册,这样该模块在再次需要注册这些窗口类之前可以查一下 m_fRegisteredClasses,如果已经注册就不必浪费时间了。为此,MFC内部使用宏 AfxDeferRegisterClass(short fClass) 来注册“窗口类”,如果m_fRegisteredClasses记录了注册的窗口类,返回TRUE,否则,调用AfxEndDeferRegisterClass注册。 注册这些窗口类的例子: MFC在加载边框窗口时,会自动地注册“AfxFrameOrView”窗口类。在创建视时,就会使用该“窗口类”创建视窗口。当然,如果创建视窗口时,该“窗口类”还没有注册,MFC将先注册它然后使用它创建视窗口。 不过,MFC并不使用”AfxMDIFrame”来创建MDI主窗口,因为在加载主窗口时一般都指定了主窗口的资源,MFC使用指定的像标注册新的MDI主窗口类(通过函数AfxRegisterWndClass完成,因此“窗口类”的名字是动态产生的)。 MDI子窗口类似于上述MDI主窗口的处理。 在MFC创建控制窗口时,如工具栏窗口,如果“AfxControlBar”类还没有注册,则注册它。注册过程很简单,就是调用::InitCommonControl加载通用控制动态连接库。 5. 调用::RegisterWndClass。 直接调用Win32的窗口注册函数::RegisterWndClass注册“窗口类”,这样做有一个缺点:如果是DLL模块,这样注册的“窗口类”在程序退出时不会自动的被取消注册(Unregister)。所以必须记得在DLL模块退出时取消它所注册的窗口类。 6. 子类化 子类化(Subclass)一个“窗口类”,可自动地得到它的“窗口类”属性。 1. MFC窗口类CWnd 在Windows系统里,一个窗口的属性分两个地方存放:一部分放在“窗口类”里头,如上所述的在注册窗口时指定;另一部分放在Windows Object本身,如:窗口的尺寸,窗口的位置(X,Y轴),窗口的Z轴顺序,窗口的状态(ACTIVE,MINIMIZED,MAXMIZED,RESTORED…),和其他窗口的关系(父窗口,子窗口…),窗口是否可以接收键盘或鼠标消息,等等。 为了表达所有这些窗口的共性,MFC设计了一个窗口基类CWnd。有一点非常重要,那就是CWnd提供了一个标准而通用的MFC窗口过程,MFC下所有的窗口都使用这个窗口过程。至于通用的窗口过程却能为各个窗口实现不同的操作,那就是MFC消息映射机制的奥秘和作用了。这些,将在后面有关章节详细论述。 CWnd提供了一系列成员函数,或者是对Win32相关函数的封装,或者是CWnd新设计的一些函数。这些函数大致如下。 (1)窗口创建函数 这里主要讨论函数Create和CreateEx。它们封装了Win32窗口创建函数::CreateWindowEx。Create的原型如下: BOOL CWnd::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext) Create是一个虚拟函数,用来创建子窗口(不能创建桌面窗口和POP UP窗口)。CWnd的基类可以覆盖该函数,例如边框窗口类等覆盖了该函数以实现边框窗口的创建,视类则使用它来创建视窗口。 Create调用了成员函数CreateEx。CWnd::CreateEx的原型如下: BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam) CreateEx有11个参数,它将调用::CreateWindowEx完成窗口的创建,这11个参数对应地传递给::CreateWindowEx。参数指定了窗口扩展风格、“窗口类”、窗口名、窗口大小和位置、父窗口句柄、窗口菜单和窗口创建参数。 CreateEx的处理流程将在后面4.4.1节讨论窗口过程时分析。 窗口创建时发送WM_CREATE消息,消息参数lParam指向一个CreateStruct结构的变量,该结构有11个域,其描述见后面4.4.1节对窗口过程的分析,Windows使用和CreateEx参数一样的内容填充该变量。 (2)窗口销毁函数 例如: DestroyWindow函数 销毁窗口 PostNcDestroy( ),销毁窗口后调用,虚拟函数 (3)用于设定、获取、改变窗口属性的函数,例如: SetWindowText(CString tiltle) 设置窗口标题 GetWindowText() 得到窗口标题 SetIcon(HICON hIcon, BOOL bBigIcon);设置窗口像标 GetIcon( BOOL bBigIcon ) ;得到窗口像标 GetDlgItem( int nID);得到窗口类指定ID的控制子窗口 GetDC(); 得到窗口的设备上下文 SetMenu(CMenu *pMenu); 设置窗口菜单 GetMenu();得到窗口菜单… (4)用于完成窗口动作的函数 用于更新窗口,滚动窗口,等等。一部分成员函数设计成或可重载(Overloaded)函数,或虚拟(Overridden)函数,或MFC消息处理函数。这些函数或者实现了一部分功能,或者仅仅是一个空函数。如: * 有关消息发送的函数: SendMessage( UINT message,WPARAM wParam = 0, LPARAM lParam = 0 ); 给窗口发送发送消息,立即调用方式 PostMessage(( UINT message,WPARAM wParam = 0, LPARAM lParam = 0 ); 给窗口发送消息,放进消息队列… * 有关改变窗口状态的函数 MoveWindow( LPCRECT lpRect, BOOL bRepaint = TRUE ); 移动窗口到指定位置 ShowWindow(BOOL );显示窗口,使之可见或不可见…. * 实现MFC消息处理机制的函数: virtual LRESULT WindowProc( UINT message, WPARAM wParam, LPARAM lParam ); 窗口过程,虚拟函数 virtual BOOL OnCommand( WPARAM wParam, LPARAM lParam );处理命令消息… * 消息处理函数: OnCreate( LPCREATESTRUCT lpCreateStruct );MFC窗口消息处理函数,窗口创建时由MFC框架调用 OnClose();MFC窗口消息处理函数,窗口创建时由MFC框架调用… * 其他功能的函数 CWnd的导出类是类型更具体、功能更完善的窗口类,它们继承了CWnd的属性和方法,并提供了新的成员函数(消息处理函数、虚拟函数、等等)。 常用的窗口类及其层次关系见图1-1。 1. 在MFC下创建一个窗口对象 MFC下创建一个窗口对象分两步,首先创建MFC窗口对象,然后创建对应的Windows窗口。在内存使用上,MFC窗口对象可以在栈或者堆(使用new创建)中创建。具体表述如下: * 创建MFC窗口对象。通过定义一个CWnd或其派生类的实例变量或者动态创建一个MFC窗口的实例,前者在栈空间创建一个MFC窗口对象,后者在堆空间创建一个MFC窗口对象。 * 调用相应的窗口创建函数,创建Windows窗口对象。 例如:在前面提到的AppWizard产生的源码中,有CMainFrame(派生于CMDIFrame(SDI)或者CMDIFrameWnd(MDI))类。它有两个成员变量定义如下: CToolBar m_wndToolBar; CStatusBar m_wndStatusBar; 当创建CMainFrame类对象时,上面两个MFC Object也被构造。 CMainFrame还有一个成员函数 OnCreate(LPCREATESTRUCT lpCreateStruct), 它的实现包含如下一段代码,调用CToolBar和CStatusBar的成员函数Create来创建上述两个MFC对象对应的工具栏HWND窗口和状态栏HWND窗口: int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { … if (!m_wndToolBar.Create(this) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0("Failed to create toolbar\n"); return -1; // fail to create } if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Failed to create status bar\n"); return -1; // fail to create } … } 关于工具栏、状态栏将在后续有关章节作详细讨论。 在MFC中,还提供了一种动态创建技术。动态创建的过程实际上也如上所述分两步,只不过MFC使用这个技术是由框架自动地完成整个过程的。通常框架窗口、文档框架窗口、视使用了动态创建。介于MFC的结构,CFrameWnd和CView及其派生类的实例即使不使用动态创建,也要用new在堆中分配。理由见窗口的销毁(2.2.5节)。 至于动态创建技术,将在下一章具体讨论。 在Windows窗口的创建过程中,将发送一些消息,如: 在创建了窗口的非客户区(Nonclient area)之后,发送消息WM_NCCREATE; 在创建了窗口的客户区(client area)之后,发送消息WM_CREATE; 窗口的窗口过程在窗口显示之前收到这两个消息。 如果是子窗口,在发送了上述两个消息之后,还给父窗口发送WM_PARENATNOTIFY消息。其他类或风格的窗口可能发送更多的消息,具体参见SDK开发文档。 1. MFC窗口的使用 MFC提供了大量的窗口类,其功能和用途各异。程序员应该选择哪些类来使用,以及怎么使用他们呢? 直接使用MFC提供的窗口类或者先从MFC窗口类派生一个新的C++类然后使用它,这些在通常情况下都不需要程序员提供窗口注册的代码。是否需要派生新的C++类,视MFC已有的窗口类是否能满足使用要求而定。派生的C++类继承了基类的特性并改变或扩展了它的功能,例如增加或者改变对消息、事件的特殊处理等。 主要使用或继承以下一些MFC窗口类(其层次关系图见图1-1): 框架类CFrameWnd,CMdiFrameWnd; 文档框架CMdiChildWnd; 视图CView和CView派生的有特殊功能的视图如:列表CListView,编辑CEditView,树形列表CTreeView,支持RTF的CRichEditView,基于对话框的视CFormView等等。 对话框CDialog。 通常,都要从这些类派生应用程序的框架窗口和视窗口或者对话框。 工具条CToolBar 状态条CStatusBar 其他各类控制窗口,如列表框CList,编辑框CEdit,组合框CComboBox,按钮Cbutton等。 通常,直接使用这些类。 2. 在MFC下窗口的销毁 窗口对象使用完毕,应该销毁。在MFC下,一个窗口对象的销毁包括HWND窗口对象的销毁和MFC窗口对象的销毁。一般情况下,MFC编程框架自动地处理了这些。 (1)对CFrameWnd和CView的派生类 这些窗口的关闭导致销毁窗口的函数DestroyWindow被调用。销毁Windows窗口时,MFC框架调用的最后一个成员函数是OnNcDestroy函数,该函数负责Windows清理工作,并在最后调用虚拟成员函数PostNcDestroy。 CFrameWnd和CView的PostNcDestroy调用delete this删除自身这个MFC窗口对象。 所以,对这些窗口,如前所述,应在堆(Heap)中分配,而且,不要对这些对象使用delete操作。 (2)对Windows Control窗口 在它们的析构函数中,将调用DestroyWidnow来销毁窗口。如果在栈中分配这样的窗口对象,则在超出作用范围的时候,随着析构函数的调用,MFC窗口对象和它的Windows window对象都被销毁。如果在堆(Heap)中分配,则显式调用delete操作符,导致析构函数的调用和窗口的销毁。 所以,这种类型的窗口应尽可能在栈中分配,避免用额外的代码来销毁窗口。如前所述的CMainFrame的成员变量m_wndStatusBar和m_wndToolBar就是这样的例子。 (3)对于程序员直接从CWnd派生的窗口 程序员可以在派生类中实现上述两种机制之一,然后,在相应的规范下使用。 后面章节将详细的讨论应用程序退出时关闭、清理窗口的过程。 1. 设备描述表 1. 设备描述表概述 当一个应用程序使用GDI函数时,必须先装入特定的设备驱动程序,然后为绘制窗口准备设备描述表,比如指定线的宽度和颜色、刷子的样式和颜色、字体、剪裁区域等等。不像其他Win32结构,设备描述表不能被直接访问,只能通过系列Win32函数来间接地操作。 如同Windows“窗口类”一样,设备描述表也是一种Windows数据结构,用来描述绘制窗口所需要的信息。它定义了一个坐标映射模式、一组GDI图形对象及其属性。这些GDI对象包括用于画线的笔,绘图、填图的刷子,位图,调色板,剪裁区域,及路径(Path)。 表2-2列出了设备描述表的结构和各项缺省值,表2-3列出了设备描述表的类型,表2-4显示设备描述表的类型。 表2-2 设备描述表的结构 属性 缺省值 Background color Background color setting from Windows Control Panel (typically, white) Background mode OPAQUE Bitmap None Brush WHITE_BRUSH Brush origin (0,0) Clipping region Entire window or client area with the update region clipped, as appropriate. Child and pop-up windows in the client area may also be clipped Palette DEFAULT_PALETTE Current pen position (0,0) Device origin Upper left corner of the window or the client area Drawing mode R2_COPYPEN Font SYSTEM_FONT (SYSTEM_FIXED_FONT for applications written to run with Windows versions 3.0 and earlier) Intercharacter spacing 0 Mapping mode MM_TEXT Pen BLACK_PEN Polygon-fill mode ALTERNATE Stretch mode BLACKONWHITE Text color Text color setting from Control Panel (typically, black) Viewport extent (1,1) Viewport origin (0,0) Window extent (1,1) Window origin (0,0) 表2-3 设备描述表的分类 Display 显示设备描述表,提供对视频显示设备上的绘制操作的支持 Printer 打印设备描述表,提供对打印机、绘图仪设备上的绘制操作的支持 Memory 内存设备描述表,提供对位图操作的支持 Information 信息设备描述表,提供对操作设备信息获取的支持 表2-3中的显示设备描述表又分三种类型,如表2-4所示。 表2-4 显示设备描述表的分类 名称 特点 功能 Class Device Contexts 提供对Win16的向后兼容 Common Device Contexts 在Windows系统的高速缓冲区,数量有限 Applicaion获取设备描述表时,Windows用缺省值初始化该设备描述表,Application使用它完成绘制操作,然后释放 Private Device Contexts 没有数量限制,用完不需释放一次获取,多次使用 多次使用过程中,每次设备描述表属性的任何修改或变化都会被保存,以支持快速绘制 (1)使用设备描述表的步骤 要使用设备描述表,一般有如下步骤: * 获取或者创建设备描述表; * 必要的话,改变设备描述表的属性; * 使用设备描述表完成绘制操作; * 释放或删除设备描述表。 Common设备描述表通过::GetDC,::GetDCEx,::BeginPaint来获得一个设备描述表,用毕,用::ReleaseDC或::EndPaint释放设备描述表; Printer设备描述表通过::CreateDC创建设备描述表,用::DeleteDC删除设备描述表。 Memory设备描述表通过::CreateCompatibleDC创建设备描述表,用::DeleteDC删除。 Information设备描述表通过::CreateIC创建设备描述表,用::DeleteDC删除。 (2)改变设备描述表属性的途径 要改变设备描述表的属性,可通过以下途径:用::SelectObject选入新的除调色板以外的GDI Object到设备描述表中; 对于调色板,使用::SelectPalette函数选入逻辑调色板,并使用::RealizePalette把逻辑调色板的入口映射到物理调色板中。 用其他API函数改变其他属性,如::SetMapMode改变映射模式。 1. 设备描述表在MFC中的实现 MFC提供了CDC类作为设备描述表类的基类,它封装了Windows的HDC设备描述表对象和相关函数。 1. CDC类 CDC类包含了各种类型的Windows设备描述表的全部功能,封装了所有的Win32 GDI 函数和设备描述表相关的SDK函数。在MFC下,使用CDC的成员函数来完成所有的窗口绘制工作。 CDC 类的结构示意图2-2所示。 CDC类有两个成员变量:m_hDC,m_hAttribDC,它们都是Windows设备描述表句柄。CDC的成员函数作输出操作时,使用m_Hdc;要获取设备描述表的属性时,使用m_hAttribDC。 在创建一个CDC类实例时,缺省的m_hDC等于m_hAttribDC。如果需要的话,程序员可以分别指定它们。例如,MFC框架实现CMetaFileDC类时,就是如此:CMetaFileDC从物理设备上读取设备信息,输出则送到元文件(metafile)上,所以m_hDC和m_hAttribDC是不同的,各司其责。还有一个类似的例子:打印预览的实现,一个代表打印机模拟输出,一个代表屏幕显示。 CDC封装::SelectObject(HDC hdc,HGDIOBJECT hgdiobject)函数时,采用了重载技术,即它针对不同的GDI对象,提供了名同而参数不同的成员函数: SelectObject(CPen *pen)用于选入笔; SelectObject(CBitmap* pBitmap)用于选入位图; SelectObject(CRgn *pRgn)用于选入剪裁区域; SelectObject(CBrush *pBrush)用于选入刷子; SelectObject(CFont *pFont)用于选入字体; 至于调色板,使用SelectPalette(CPalette *pPalette,BOOL bForceBackground )选入调色板到设备描述表,使用RealizePalletter()实现逻辑调色板到物理调色板的映射。 2. 从CDC派生出功能更具体的设备描述表 从CDC 派生出四个功能更具体的设备描述表类。层次如图2-3所示。 下面,分别讨论派生出的四种设备描述表。 * CCientDC 代表窗口客户区的设备描述表。其构造函数CClientDC(CWnd *pWin)通过::GetDC获取指定窗口的客户区的设备描述表HDC,并且使用成员函数Attach把它和CClientDC对象捆绑在一起;其析构函数使用成员函数Detach把设备描述表句柄HDC分离出来,并调用::ReleaseDC释放设备描述表HDC。 * CPaintDC 仅仅用于响应WM_PAINT消息时绘制窗口,因为它的构造函数调用了::BeginPaint获取设备描述表 HDC,并且使用成员函数Attach把它和CPaintDC对象捆绑在一起;析构函数使用成员函数Detach把设备描述表句柄HDC分离出来,并调用::EndPaint释放设备描述表HDC,而::BeginPaint和::EndPaint仅仅在响应WM_PAINT时使用。 * CMetaFileDC 用于生成元文件。 * CWindowDC 代表整个窗口区(包括非客户区)的设备描述表。其构造函数CWindowDC(CWnd *pWin)通过::GetWindowDC获取指定窗口的客户区的设备描述表HDC,并使用Attach把它和CWindowDC对象捆绑在一起;其析构函数使用Detach把设备描述表HDC分离出来,调用::ReleaseDC释放设备描述表HDC。 1. MFC设备描述表类的使用 1. 使用CPaintDC、CClientDC、CWindowDC的方法 首先,定义一个这些类的实例变量,通常在栈中定义。然后,使用它。 例如,MFC中CView对WM_PAINT消息的实现方法如下: void CView::OnPaint() { // standard paint routine CPaintDC dc(this); OnPrepareDC(&dc); OnDraw(&dc); } 在栈中定义了CPaintDC类型的变量dc,随着构造函数的调用获取了设备描述表;设备描述表使用完毕,超出其有效范围就被自动地清除,随着析构函数的调用,其获取的设备描述表被释放。 如果希望在堆中创建,例如 CPaintDC *pDC; pDC = new CPaintDC(this) 则在使用完毕时,用delete删除pDC: delete pDC; 2. 直接使用CDC 需要注意的是:在生成CDC对象的时候,并不像它的派生类那样,在构造函数里获取相应的Windows设备描述表。最好不要使用::GetDC等函数来获取一个设备描述表,而是创建一个设备描述表。其构造函数如下: CDC::CDC() { m_hDC = NULL; m_hAttribDC = NULL; m_bPrinting = FALSE; } 其析构函数如下: CDC::~CDC() { if (m_hDC != NULL) ::DeleteDC(Detach()); } 在CDC析构函数中,如果设备描述表句柄不空,则调用DeleteDC删除它。这是直接使用CDC时最好创建 Windows设备描述表的理由。如果设备描述表不是创建的,则应该在析构函数被调用前分离出设备描述表句柄并用::RealeaseDC释放它,释放后 m_hDC为空,则在析构函数调用时不会执行::DeleteDC。当然,不用担心CDC的派生类的析构函数调用CDC的析构函数,因为 CDC::~CDC()不是虚拟析构函数。 直接使用CDC的例子是内存设备上下文,例如: CDC dcMem; //声明一个CDC对象 dcMem.CreateCompatibleDC(&dc); //创建设备描述表 pbmOld = dcMem.SelectObject(&m_bmBall);//更改设备描述表属性…//作一些绘制操作 dcMem.SelectObject(pbmOld);//恢复设备描述表的属性 dcMem.DeleteDC(); //可以不调用,而让析构函数去删除设备描述表 1. GDI对象 在讨论设备描述表时,已经多次涉及到GDI对象。这里,需强调一下:GDI对象要选入Windows 设备描述表后才能使用;用毕,要恢复设备描述表的原GDI对象,并删除该GDI对象。 一般按如下步骤使用GDI对象: Create or get a GDI OBJECT hNewGdi; hOldGdi = ::SelectObject(hdc, hNewGdi) …… ::SelectObject(hdc, hOldGdi) ::DeleteObject(hNewGdi) 先创建或得到一个GDI对象,然后把它选入设备描述表并保存它原来的GDI对象;用毕恢复设备描述表原来的GDI对象并删除新创建的GDI对象。 需要指出的是,如果hNewGdi是一个Stock GDI对象,可以不删除(删除也可以)。通过 HGDIOBJ GetStockObject( int fnObject // type of stock object ); 来获取Stock GDI对象。 1. MFC GDI对象 MFC用一些类封装了Windows GDI对象和相关函数,层次结构如图2-4所示: CGdiObject封装了Windows GDI Object共有的特性。其派生类在继承的基础上,主要封装了各类GDI的创建函数以及和具体GDI对象相关的操作。 CGdiObject的构造函数仅仅让m_hObject为空。如果m_hObject不空,其析构函数将删除对应的Windows GDI对象。MFC GDI对象和Windows GDI对象的关系如图2-5所示。 2. 使用MFC GDI类的使用 首先创建GDI对象,可分一步或两步创建。一步创建就是构造MFC对象和Windows GDI对象一步完成;两步创建则先构造MFC对象,接着创建Windows GDI对象。然后,把新创建的GDI对象选进设备描述表,取代原GDI对象并保存。最后,恢复原GDI对象。例如: void CMyView::OnDraw(CDC *pDC) { CPen penBlack; //构造MFC CPen对象 if (penBlack.CreatePen(PS_SOLID, RGB(0, 0, 0))) { CPen *pOldPen = pDC->SelectObject(&penBlack)); //选进设备表,保存原笔… pDC->SelectObject(pOldPen); //恢复原笔 }else { … } } 和在SDK下有一点不同的是:这里没有DeleteObject。因为执行完OnDraw后,栈中的penBlack被销毁,它的析构函数被调用,导致DeleteObject的调用。 还有一点要说明: pDC->SelectObject(&penBlack)返回了一个CPen *指针,也就是说,它根据原来PEN的句柄创建了一个MFC CPen对象。这个对象是否需要删除呢?不必要,因为它是一个临时对象,MFC框架会自动地删除它。当然,在本函数执行完毕把控制权返回给主消息循环之前,该对象是有效的。 关于临时对象及MFC处理它们的内部机制,将在后续章节详细讨论。 至此,Windows编程的核心概念:窗口、GDI界面(设备描述表、GDI对象等)已经陈述清楚,特别揭示了MFC对这些概念的封装机制,并简明讲述了与这些Windows Object对应的MFC类的使用方法。还有其他Windows概念,可以参见SDK开发文档。在MFC的实现上,基本上仅仅是对和这些概念相关的Win32函数的封装。如果明白了MFC的窗口、GDI界面的封装机制,其他就不难了。
2010年10月11日
2. 1. MFC Object和Windows Object的关系 MFC中最重要的封装是对Win32 API的封装,因此,理解Windows Object和MFC Object (C++对象,一个C++类的实例)之间的关系是理解MFC的关键之一。所谓Windows Object(Windows对象)是Win32下用句柄表示的Windows操作系统对象;所谓MFC Object (MFC对象)是C++对象,是一个C++类的实例,这里(本书范围内)MFC Object是有特定含义的,指封装Windows Object的C++ Object,并非指任意的C++ Object。 MFC Object 和Windows Object是不一样的,但两者紧密联系。以窗口对象为例: 一个MFC窗口对象是一个C++ CWnd类(或派生类)的实例,是程序直接创建的。在程序执行中它随着窗口类构造函数的调用而生成,随着析构函数的调用而消失。而Windows窗口则是 Windows系统的一个内部数据结构的实例,由一个“窗口句柄”标识,Windows系统创建它并给它分配系统资源。Windows窗口在MFC窗口对象创建之后,由CWnd类的Create成员函数创建,“窗口句柄”保存在窗口对象的m_hWnd成员变量中。Windows窗口可以被一个程序销毁,也可以被用户的动作销毁。MFC窗口对象和Windows窗口对象的关系如图2-1所示。其他的Windows Object和对应的MFC Object也有类似的关系。 下面,对MFC Object和Windows Object作一个比较。有些论断对设备描述表(MFC类是CDC,句柄是HDC)可能不适用,但具体涉及到时会指出。 1. 从数据结构上比较 MFC Object是相应C++类的实例,这些类是MFC或者程序员定义的; Windows Object是Windows系统的内部结构,通过一个句柄来引用; MFC给这些类定义了一个成员变量来保存MFC Object对应的Windows Object的句柄。对于设备描述表CDC类,将保存两个HDC句柄。 2. 从层次上讲比较 MFC Object是高层的,Windows Object是低层的; MFC Object封装了Windows Object的大部分或全部功能,MFC Object的使用者不需要直接应用Windows Object的HANDLE(句柄)使用Win32 API,代替它的是引用相应的MFC Object的成员函数。 3. 从创建上比较 MFC Object通过构造函数由程序直接创建;Windows Object由相应的SDK函数创建。 MFC中,使用这些MFC Object,一般分两步: 首先,创建一个MFC Object,或者在STACK中创建,或者在HEAP中创建,这时,MFC Object的句柄实例变量为空,或者说不是一个有效的句柄。 然后,调用MFC Object的成员函数创建相应的Windows Object,MFC的句柄变量存储一个有效句柄。 CDC(设备描述表类)的创建有所不同,在后面的2.3节会具体说明CDC及其派生类的创建和使用。 当然,可以在MFC Object的构造函数中创建相应的Windows对象,MFC的GDI类就是如此实现的,但从实质上讲,MFC Object的创建和Windows Object的创建是两回事。 4. 从转换上比较 可以从一个MFC Object得到对应的Windows Object的句柄;一般使用MFC Object的成员函数GetSafeHandle得到对应的句柄。 可以从一个已存在的Windows Object创建一个对应的MFC Object; 一般使用MFC Object的成员函数Attach或者FromHandle来创建,前者得到一个永久性对象,后者得到的可能是一个临时对象。 5. 从使用范围上比较 MFC Object对系统的其他进程来说是不可见、不可用的;而Windows Object一旦创建,其句柄是整个Windows系统全局的。一些句柄可以被其他进程使用。典型地,一个进程可以获得另一进程的窗口句柄,并给该窗口发送消息。 对同一个进程的线程来说,只可以使用本线程创建的MFC Object,不能使用其他线程的MFC Object。 6. 从销毁上比较 MFC Object随着析构函数的调用而消失;但Windows Object必须由相应的Windows系统函数销毁。 设备描述表CDC类的对象有所不同,它对应的HDC句柄对象可能不是被销毁,而是被释放。 当然,可以在MFC Object的析构函数中完成Windows Object的销毁,MFC Object的GDI类等就是如此实现的,但是,应该看到:两者的销毁是不同的。 每类Windows Object都有对应的MFC Object,下面用表格的形式列出它们之间的对应关系,如表2-1所示: 表2-1 MFC Object和Windows Object的对应关系 描述 Windows句柄 MFC Object 窗口 HWND CWnd and CWnd-derived classes 设备上下文 HDC CDC and CDC-derived classes 菜单 HMENU CMenu 笔 HPEN CGdiObject类,CPen和CPen-derived classes 刷子 HBRUSH CGdiObject类,CBrush和CBrush-derived classes 字体 HFONT CGdiObject类,CFont和CFont-derived classes 位图 HBITMAP CGdiObject类,CBitmap和CBitmap-derived classes 调色板 HPALETTE CGdiObject类,CPalette和CPalette-derived classes 区域 HRGN CGdiObject类,CRgn和CRgn-derived classes 图像列表 HimageLIST CimageList和CimageList-derived classes 套接字 SOCKET CSocket,CAsynSocket及其派生类 表2-1中的OBJECT分以下几类: Windows对象, 设备上下文对象, GDI对象(BITMAP,BRUSH,FONT,PALETTE,PEN,RGN), 菜单, 图像列表, 网络套接字接口。 从广义上来看,文档对象和文件可以看作一对MFC Object和Windows Object,分别用CDocument类和文件句柄描述。 后续几节分别对前四类作一个简明扼要的论述。 1. Windows Object 用SDK的Win32 API编写各种Windows应用程序,有其共同的规律:首先是编写WinMain函数,编写处理消息和事件的窗口过程WndProc,在WinMain里头注册窗口(Register Window),创建窗口,然后开始应用程序的消息循环。 MFC应用程序也不例外,因为MFC是一个建立在SDK API基础上的编程框架。对程序员来说所不同的是:一般情况下,MFC框架自动完成了Windows登记、创建等工作。 下面,简要介绍MFC Window对Windows Window的封装。 1. Windows的注册 一个应用程序在创建某个类型的窗口前,必须首先注册该“窗口类”(Windows Class)。注意,这里不是C++类的类。Register Window把窗口过程、窗口类型以及其他类型信息和要登记的窗口类关联起来。 1. “窗口类”的数据结构 “窗口类”是Windows系统的数据结构,可以把它理解为Windows系统的类型定义,而Windows窗口则是相应“窗口类”的实例。Windows使用一个结构来描述“窗口类”,其定义如下: typedef struct _WNDCLASSEX { UINT cbSize; //该结构的字节数 UINT style; //窗口类的风格 WNDPROC lpfnWndProc; //窗口过程 int cbClsExtra; int cbWndExtra; HANDLE hInstance; //该窗口类的窗口过程所属的应用实例 HICON hIcon; //该窗口类所用的像标 HCURSOR hCursor; //该窗口类所用的光标 HBRUSH hbrBackground; //该窗口类所用的背景刷 LPCTSTR lpszMenuName; //该窗口类所用的菜单资源 LPCTSTR lpszClassName; //该窗口类的名称 HICON hIconSm; //该窗口类所用的小像标 } WNDCLASSEX; 从“窗口类”的定义可以看出,它包含了一个窗口的重要信息,如窗口风格、窗口过程、显示和绘制窗口所需要的信息,等等。关于窗口过程,将在后面消息映射等有关章节作详细论述。 Windows系统在初始化时,会注册(Register)一些全局的“窗口类”,例如通用控制窗口类。应用程序在创建自己的窗口时,首先必须注册自己的窗口类。在MFC环境下,有几种方法可以用来注册“窗口类”,下面分别予以讨论。 2. 调用AfxRegisterClass注册 AfxRegisterClass函数是MFC全局函数。AfxRegisterClass的函数原型: BOOL AFXAPI AfxRegisterClass(WNDCLASS *lpWndClass); 参数lpWndClass是指向WNDCLASS结构的指针,表示一个“窗口类”。 首先,AfxRegisterClass检查希望注册的“窗口类”是否已经注册,如果是则表示已注册,返回TRUE,否则,继续处理。 接着,调用::RegisterClass(lpWndClass)注册窗口类; 然后,如果当前模块是DLL模块,则把注册“窗口类”的名字加入到模块状态的域 m_szUnregisterList中。该域是一个固定长度的缓冲区,依次存放模块注册的“窗口类”的名字(每个名字是以“\n\0”结尾的字符串)。之所以这样做,是为了DLL退出时能自动取消(Unregister)它注册的窗口类。至于模块状态将在后面第9章详细的讨论。 最后,返回TRUE表示成功注册。 3. 调用AfxRegisterWndClass注册 AfxRegisterWndClass函数也是MFC全局函数。AfxRegisterWndClass的函数原型: LPCTSTR AFXAPI AfxRegisterWndClass(UINT nClassStyle, HCURSOR hCursor, HBRUSH hbrBackground, HICON hIcon) 参数1指定窗口类风格; 参数2、3、4分别指定该窗口类使用的光标、背景刷、像标的句柄,缺省值是0。 此函数根据窗口类属性动态地产生窗口类的名字,然后,判断是否该类已经注册,是则返回窗口类名;否则用指定窗口类的属性(窗口过程指定为缺省窗口过程),调用AfxRegisterCalss注册窗口类,返回类名。 动态产生的窗口类名字由以下几部分组成(包括冒号分隔符): 如果参数2、3、4全部为NULL,则由三部分组成。 “Afx”+“:”+模块实例句柄”+“:”+“窗口类风格” 否则,由六部分组成: “Afx”+“:”+模块实例句柄+“:”+“窗口类风格”+“:”+光标句柄+“:”+背景刷句柄+“:”+像标句柄。比如:“Afx:400000:b:13de:6:32cf”。 该函数在MFC注册主边框或者文档边框“窗口类”时被调用。具体怎样用在5.3.3.3节会指出。 4. 隐含的使用MFC预定义的的窗口类 MFC4.0以前的版本提供了一些预定义的窗口类,4.0以后不再预定义这些窗口类。但是,MFC仍然沿用了这些窗口类,例如: 用于子窗口的“AfxWnd”; 用于边框窗口(SDI主窗口或MDI子窗口)或视的“AfxFrameOrView”; 用于MDI主窗口的“AfxMDIFrame”; 用于标准控制条的“AfxControlBar”。 这些类的名字就 是“AfxWnd”、“AfxFrameOrView”、“AfxMdiFrame”、 “AfxControlBar”加上前缀和后缀(用来标识版本号或是否调试版等)。它们使用标准应用程序像标、标准文档像标、标准光标等标准资源。为了使用这些“窗口类”,MFC会在适当的时候注册这些类:或者要创建该类的窗口时,或者创建应用程序的主窗口时,等等。 MFC内部使用了函数 BOOL AFXAPI AfxEndDeferRegisterClass(short fClass) 来帮助注册上述原MFC版本的预定义“窗口类”。参数fClass区分了那些预定义窗口的类型。根据不同的类型,使用不同的窗口类风格、窗口类名字等填充WndClass的域,然后调用AfxRegisterClass注册窗口类。并且注册成功之后,通过模块状态的m_fRegisteredClasses记录该窗口类已经注册,这样该模块在再次需要注册这些窗口类之前可以查一下 m_fRegisteredClasses,如果已经注册就不必浪费时间了。为此,MFC内部使用宏 AfxDeferRegisterClass(short fClass) 来注册“窗口类”,如果m_fRegisteredClasses记录了注册的窗口类,返回TRUE,否则,调用AfxEndDeferRegisterClass注册。 注册这些窗口类的例子: MFC在加载边框窗口时,会自动地注册“AfxFrameOrView”窗口类。在创建视时,就会使用该“窗口类”创建视窗口。当然,如果创建视窗口时,该“窗口类”还没有注册,MFC将先注册它然后使用它创建视窗口。 不过,MFC并不使用”AfxMDIFrame”来创建MDI主窗口,因为在加载主窗口时一般都指定了主窗口的资源,MFC使用指定的像标注册新的MDI主窗口类(通过函数AfxRegisterWndClass完成,因此“窗口类”的名字是动态产生的)。 MDI子窗口类似于上述MDI主窗口的处理。 在MFC创建控制窗口时,如工具栏窗口,如果“AfxControlBar”类还没有注册,则注册它。注册过程很简单,就是调用::InitCommonControl加载通用控制动态连接库。 5. 调用::RegisterWndClass。 直接调用Win32的窗口注册函数::RegisterWndClass注册“窗口类”,这样做有一个缺点:如果是DLL模块,这样注册的“窗口类”在程序退出时不会自动的被取消注册(Unregister)。所以必须记得在DLL模块退出时取消它所注册的窗口类。 6. 子类化 子类化(Subclass)一个“窗口类”,可自动地得到它的“窗口类”属性。 1. MFC窗口类CWnd 在Windows系统里,一个窗口的属性分两个地方存放:一部分放在“窗口类”里头,如上所述的在注册窗口时指定;另一部分放在Windows Object本身,如:窗口的尺寸,窗口的位置(X,Y轴),窗口的Z轴顺序,窗口的状态(ACTIVE,MINIMIZED,MAXMIZED,RESTORED…),和其他窗口的关系(父窗口,子窗口…),窗口是否可以接收键盘或鼠标消息,等等。 为了表达所有这些窗口的共性,MFC设计了一个窗口基类CWnd。有一点非常重要,那就是CWnd提供了一个标准而通用的MFC窗口过程,MFC下所有的窗口都使用这个窗口过程。至于通用的窗口过程却能为各个窗口实现不同的操作,那就是MFC消息映射机制的奥秘和作用了。这些,将在后面有关章节详细论述。 CWnd提供了一系列成员函数,或者是对Win32相关函数的封装,或者是CWnd新设计的一些函数。这些函数大致如下。 (1)窗口创建函数 这里主要讨论函数Create和CreateEx。它们封装了Win32窗口创建函数::CreateWindowEx。Create的原型如下: BOOL CWnd::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext) Create是一个虚拟函数,用来创建子窗口(不能创建桌面窗口和POP UP窗口)。CWnd的基类可以覆盖该函数,例如边框窗口类等覆盖了该函数以实现边框窗口的创建,视类则使用它来创建视窗口。 Create调用了成员函数CreateEx。CWnd::CreateEx的原型如下: BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam) CreateEx有11个参数,它将调用::CreateWindowEx完成窗口的创建,这11个参数对应地传递给::CreateWindowEx。参数指定了窗口扩展风格、“窗口类”、窗口名、窗口大小和位置、父窗口句柄、窗口菜单和窗口创建参数。 CreateEx的处理流程将在后面4.4.1节讨论窗口过程时分析。 窗口创建时发送WM_CREATE消息,消息参数lParam指向一个CreateStruct结构的变量,该结构有11个域,其描述见后面4.4.1节对窗口过程的分析,Windows使用和CreateEx参数一样的内容填充该变量。 (2)窗口销毁函数 例如: DestroyWindow函数 销毁窗口 PostNcDestroy( ),销毁窗口后调用,虚拟函数 (3)用于设定、获取、改变窗口属性的函数,例如: SetWindowText(CString tiltle) 设置窗口标题 GetWindowText() 得到窗口标题 SetIcon(HICON hIcon, BOOL bBigIcon);设置窗口像标 GetIcon( BOOL bBigIcon ) ;得到窗口像标 GetDlgItem( int nID);得到窗口类指定ID的控制子窗口 GetDC(); 得到窗口的设备上下文 SetMenu(CMenu *pMenu); 设置窗口菜单 GetMenu();得到窗口菜单… (4)用于完成窗口动作的函数 用于更新窗口,滚动窗口,等等。一部分成员函数设计成或可重载(Overloaded)函数,或虚拟(Overridden)函数,或MFC消息处理函数。这些函数或者实现了一部分功能,或者仅仅是一个空函数。如: * 有关消息发送的函数: SendMessage( UINT message,WPARAM wParam = 0, LPARAM lParam = 0 ); 给窗口发送发送消息,立即调用方式 PostMessage(( UINT message,WPARAM wParam = 0, LPARAM lParam = 0 ); 给窗口发送消息,放进消息队列… * 有关改变窗口状态的函数 MoveWindow( LPCRECT lpRect, BOOL bRepaint = TRUE ); 移动窗口到指定位置 ShowWindow(BOOL );显示窗口,使之可见或不可见…. * 实现MFC消息处理机制的函数: virtual LRESULT WindowProc( UINT message, WPARAM wParam, LPARAM lParam ); 窗口过程,虚拟函数 virtual BOOL OnCommand( WPARAM wParam, LPARAM lParam );处理命令消息… * 消息处理函数: OnCreate( LPCREATESTRUCT lpCreateStruct );MFC窗口消息处理函数,窗口创建时由MFC框架调用 OnClose();MFC窗口消息处理函数,窗口创建时由MFC框架调用… * 其他功能的函数 CWnd的导出类是类型更具体、功能更完善的窗口类,它们继承了CWnd的属性和方法,并提供了新的成员函数(消息处理函数、虚拟函数、等等)。 常用的窗口类及其层次关系见图1-1。 1. 在MFC下创建一个窗口对象 MFC下创建一个窗口对象分两步,首先创建MFC窗口对象,然后创建对应的Windows窗口。在内存使用上,MFC窗口对象可以在栈或者堆(使用new创建)中创建。具体表述如下: * 创建MFC窗口对象。通过定义一个CWnd或其派生类的实例变量或者动态创建一个MFC窗口的实例,前者在栈空间创建一个MFC窗口对象,后者在堆空间创建一个MFC窗口对象。 * 调用相应的窗口创建函数,创建Windows窗口对象。 例如:在前面提到的AppWizard产生的源码中,有CMainFrame(派生于CMDIFrame(SDI)或者CMDIFrameWnd(MDI))类。它有两个成员变量定义如下: CToolBar m_wndToolBar; CStatusBar m_wndStatusBar; 当创建CMainFrame类对象时,上面两个MFC Object也被构造。 CMainFrame还有一个成员函数 OnCreate(LPCREATESTRUCT lpCreateStruct), 它的实现包含如下一段代码,调用CToolBar和CStatusBar的成员函数Create来创建上述两个MFC对象对应的工具栏HWND窗口和状态栏HWND窗口: int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { … if (!m_wndToolBar.Create(this) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0("Failed to create toolbar\n"); return -1; // fail to create } if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Failed to create status bar\n"); return -1; // fail to create } … } 关于工具栏、状态栏将在后续有关章节作详细讨论。 在MFC中,还提供了一种动态创建技术。动态创建的过程实际上也如上所述分两步,只不过MFC使用这个技术是由框架自动地完成整个过程的。通常框架窗口、文档框架窗口、视使用了动态创建。介于MFC的结构,CFrameWnd和CView及其派生类的实例即使不使用动态创建,也要用new在堆中分配。理由见窗口的销毁(2.2.5节)。 至于动态创建技术,将在下一章具体讨论。 在Windows窗口的创建过程中,将发送一些消息,如: 在创建了窗口的非客户区(Nonclient area)之后,发送消息WM_NCCREATE; 在创建了窗口的客户区(client area)之后,发送消息WM_CREATE; 窗口的窗口过程在窗口显示之前收到这两个消息。 如果是子窗口,在发送了上述两个消息之后,还给父窗口发送WM_PARENATNOTIFY消息。其他类或风格的窗口可能发送更多的消息,具体参见SDK开发文档。 1. MFC窗口的使用 MFC提供了大量的窗口类,其功能和用途各异。程序员应该选择哪些类来使用,以及怎么使用他们呢? 直接使用MFC提供的窗口类或者先从MFC窗口类派生一个新的C++类然后使用它,这些在通常情况下都不需要程序员提供窗口注册的代码。是否需要派生新的C++类,视MFC已有的窗口类是否能满足使用要求而定。派生的C++类继承了基类的特性并改变或扩展了它的功能,例如增加或者改变对消息、事件的特殊处理等。 主要使用或继承以下一些MFC窗口类(其层次关系图见图1-1): 框架类CFrameWnd,CMdiFrameWnd; 文档框架CMdiChildWnd; 视图CView和CView派生的有特殊功能的视图如:列表CListView,编辑CEditView,树形列表CTreeView,支持RTF的CRichEditView,基于对话框的视CFormView等等。 对话框CDialog。 通常,都要从这些类派生应用程序的框架窗口和视窗口或者对话框。 工具条CToolBar 状态条CStatusBar 其他各类控制窗口,如列表框CList,编辑框CEdit,组合框CComboBox,按钮Cbutton等。 通常,直接使用这些类。 2. 在MFC下窗口的销毁 窗口对象使用完毕,应该销毁。在MFC下,一个窗口对象的销毁包括HWND窗口对象的销毁和MFC窗口对象的销毁。一般情况下,MFC编程框架自动地处理了这些。 (1)对CFrameWnd和CView的派生类 这些窗口的关闭导致销毁窗口的函数DestroyWindow被调用。销毁Windows窗口时,MFC框架调用的最后一个成员函数是OnNcDestroy函数,该函数负责Windows清理工作,并在最后调用虚拟成员函数PostNcDestroy。 CFrameWnd和CView的PostNcDestroy调用delete this删除自身这个MFC窗口对象。 所以,对这些窗口,如前所述,应在堆(Heap)中分配,而且,不要对这些对象使用delete操作。 (2)对Windows Control窗口 在它们的析构函数中,将调用DestroyWidnow来销毁窗口。如果在栈中分配这样的窗口对象,则在超出作用范围的时候,随着析构函数的调用,MFC窗口对象和它的Windows window对象都被销毁。如果在堆(Heap)中分配,则显式调用delete操作符,导致析构函数的调用和窗口的销毁。 所以,这种类型的窗口应尽可能在栈中分配,避免用额外的代码来销毁窗口。如前所述的CMainFrame的成员变量m_wndStatusBar和m_wndToolBar就是这样的例子。 (3)对于程序员直接从CWnd派生的窗口 程序员可以在派生类中实现上述两种机制之一,然后,在相应的规范下使用。 后面章节将详细的讨论应用程序退出时关闭、清理窗口的过程。 1. 设备描述表 1. 设备描述表概述 当一个应用程序使用GDI函数时,必须先装入特定的设备驱动程序,然后为绘制窗口准备设备描述表,比如指定线的宽度和颜色、刷子的样式和颜色、字体、剪裁区域等等。不像其他Win32结构,设备描述表不能被直接访问,只能通过系列Win32函数来间接地操作。 如同Windows“窗口类”一样,设备描述表也是一种Windows数据结构,用来描述绘制窗口所需要的信息。它定义了一个坐标映射模式、一组GDI图形对象及其属性。这些GDI对象包括用于画线的笔,绘图、填图的刷子,位图,调色板,剪裁区域,及路径(Path)。 表2-2列出了设备描述表的结构和各项缺省值,表2-3列出了设备描述表的类型,表2-4显示设备描述表的类型。 表2-2 设备描述表的结构 属性 缺省值 Background color Background color setting from Windows Control Panel (typically, white) Background mode OPAQUE Bitmap None Brush WHITE_BRUSH Brush origin (0,0) Clipping region Entire window or client area with the update region clipped, as appropriate. Child and pop-up windows in the client area may also be clipped Palette DEFAULT_PALETTE Current pen position (0,0) Device origin Upper left corner of the window or the client area Drawing mode R2_COPYPEN Font SYSTEM_FONT (SYSTEM_FIXED_FONT for applications written to run with Windows versions 3.0 and earlier) Intercharacter spacing 0 Mapping mode MM_TEXT Pen BLACK_PEN Polygon-fill mode ALTERNATE Stretch mode BLACKONWHITE Text color Text color setting from Control Panel (typically, black) Viewport extent (1,1) Viewport origin (0,0) Window extent (1,1) Window origin (0,0) 表2-3 设备描述表的分类 Display 显示设备描述表,提供对视频显示设备上的绘制操作的支持 Printer 打印设备描述表,提供对打印机、绘图仪设备上的绘制操作的支持 Memory 内存设备描述表,提供对位图操作的支持 Information 信息设备描述表,提供对操作设备信息获取的支持 表2-3中的显示设备描述表又分三种类型,如表2-4所示。 表2-4 显示设备描述表的分类 名称 特点 功能 Class Device Contexts 提供对Win16的向后兼容 Common Device Contexts 在Windows系统的高速缓冲区,数量有限 Applicaion获取设备描述表时,Windows用缺省值初始化该设备描述表,Application使用它完成绘制操作,然后释放 Private Device Contexts 没有数量限制,用完不需释放一次获取,多次使用 多次使用过程中,每次设备描述表属性的任何修改或变化都会被保存,以支持快速绘制 (1)使用设备描述表的步骤 要使用设备描述表,一般有如下步骤: * 获取或者创建设备描述表; * 必要的话,改变设备描述表的属性; * 使用设备描述表完成绘制操作; * 释放或删除设备描述表。 Common设备描述表通过::GetDC,::GetDCEx,::BeginPaint来获得一个设备描述表,用毕,用::ReleaseDC或::EndPaint释放设备描述表; Printer设备描述表通过::CreateDC创建设备描述表,用::DeleteDC删除设备描述表。 Memory设备描述表通过::CreateCompatibleDC创建设备描述表,用::DeleteDC删除。 Information设备描述表通过::CreateIC创建设备描述表,用::DeleteDC删除。 (2)改变设备描述表属性的途径 要改变设备描述表的属性,可通过以下途径:用::SelectObject选入新的除调色板以外的GDI Object到设备描述表中; 对于调色板,使用::SelectPalette函数选入逻辑调色板,并使用::RealizePalette把逻辑调色板的入口映射到物理调色板中。 用其他API函数改变其他属性,如::SetMapMode改变映射模式。 1. 设备描述表在MFC中的实现 MFC提供了CDC类作为设备描述表类的基类,它封装了Windows的HDC设备描述表对象和相关函数。 1. CDC类 CDC类包含了各种类型的Windows设备描述表的全部功能,封装了所有的Win32 GDI 函数和设备描述表相关的SDK函数。在MFC下,使用CDC的成员函数来完成所有的窗口绘制工作。 CDC 类的结构示意图2-2所示。 CDC类有两个成员变量:m_hDC,m_hAttribDC,它们都是Windows设备描述表句柄。CDC的成员函数作输出操作时,使用m_Hdc;要获取设备描述表的属性时,使用m_hAttribDC。 在创建一个CDC类实例时,缺省的m_hDC等于m_hAttribDC。如果需要的话,程序员可以分别指定它们。例如,MFC框架实现CMetaFileDC类时,就是如此:CMetaFileDC从物理设备上读取设备信息,输出则送到元文件(metafile)上,所以m_hDC和m_hAttribDC是不同的,各司其责。还有一个类似的例子:打印预览的实现,一个代表打印机模拟输出,一个代表屏幕显示。 CDC封装::SelectObject(HDC hdc,HGDIOBJECT hgdiobject)函数时,采用了重载技术,即它针对不同的GDI对象,提供了名同而参数不同的成员函数: SelectObject(CPen *pen)用于选入笔; SelectObject(CBitmap* pBitmap)用于选入位图; SelectObject(CRgn *pRgn)用于选入剪裁区域; SelectObject(CBrush *pBrush)用于选入刷子; SelectObject(CFont *pFont)用于选入字体; 至于调色板,使用SelectPalette(CPalette *pPalette,BOOL bForceBackground )选入调色板到设备描述表,使用RealizePalletter()实现逻辑调色板到物理调色板的映射。 2. 从CDC派生出功能更具体的设备描述表 从CDC 派生出四个功能更具体的设备描述表类。层次如图2-3所示。 下面,分别讨论派生出的四种设备描述表。 * CCientDC 代表窗口客户区的设备描述表。其构造函数CClientDC(CWnd *pWin)通过::GetDC获取指定窗口的客户区的设备描述表HDC,并且使用成员函数Attach把它和CClientDC对象捆绑在一起;其析构函数使用成员函数Detach把设备描述表句柄HDC分离出来,并调用::ReleaseDC释放设备描述表HDC。 * CPaintDC 仅仅用于响应WM_PAINT消息时绘制窗口,因为它的构造函数调用了::BeginPaint获取设备描述表 HDC,并且使用成员函数Attach把它和CPaintDC对象捆绑在一起;析构函数使用成员函数Detach把设备描述表句柄HDC分离出来,并调用::EndPaint释放设备描述表HDC,而::BeginPaint和::EndPaint仅仅在响应WM_PAINT时使用。 * CMetaFileDC 用于生成元文件。 * CWindowDC 代表整个窗口区(包括非客户区)的设备描述表。其构造函数CWindowDC(CWnd *pWin)通过::GetWindowDC获取指定窗口的客户区的设备描述表HDC,并使用Attach把它和CWindowDC对象捆绑在一起;其析构函数使用Detach把设备描述表HDC分离出来,调用::ReleaseDC释放设备描述表HDC。 1. MFC设备描述表类的使用 1. 使用CPaintDC、CClientDC、CWindowDC的方法 首先,定义一个这些类的实例变量,通常在栈中定义。然后,使用它。 例如,MFC中CView对WM_PAINT消息的实现方法如下: void CView::OnPaint() { // standard paint routine CPaintDC dc(this); OnPrepareDC(&dc); OnDraw(&dc); } 在栈中定义了CPaintDC类型的变量dc,随着构造函数的调用获取了设备描述表;设备描述表使用完毕,超出其有效范围就被自动地清除,随着析构函数的调用,其获取的设备描述表被释放。 如果希望在堆中创建,例如 CPaintDC *pDC; pDC = new CPaintDC(this) 则在使用完毕时,用delete删除pDC: delete pDC; 2. 直接使用CDC 需要注意的是:在生成CDC对象的时候,并不像它的派生类那样,在构造函数里获取相应的Windows设备描述表。最好不要使用::GetDC等函数来获取一个设备描述表,而是创建一个设备描述表。其构造函数如下: CDC::CDC() { m_hDC = NULL; m_hAttribDC = NULL; m_bPrinting = FALSE; } 其析构函数如下: CDC::~CDC() { if (m_hDC != NULL) ::DeleteDC(Detach()); } 在CDC析构函数中,如果设备描述表句柄不空,则调用DeleteDC删除它。这是直接使用CDC时最好创建 Windows设备描述表的理由。如果设备描述表不是创建的,则应该在析构函数被调用前分离出设备描述表句柄并用::RealeaseDC释放它,释放后 m_hDC为空,则在析构函数调用时不会执行::DeleteDC。当然,不用担心CDC的派生类的析构函数调用CDC的析构函数,因为 CDC::~CDC()不是虚拟析构函数。 直接使用CDC的例子是内存设备上下文,例如: CDC dcMem; //声明一个CDC对象 dcMem.CreateCompatibleDC(&dc); //创建设备描述表 pbmOld = dcMem.SelectObject(&m_bmBall);//更改设备描述表属性…//作一些绘制操作 dcMem.SelectObject(pbmOld);//恢复设备描述表的属性 dcMem.DeleteDC(); //可以不调用,而让析构函数去删除设备描述表 1. GDI对象 在讨论设备描述表时,已经多次涉及到GDI对象。这里,需强调一下:GDI对象要选入Windows 设备描述表后才能使用;用毕,要恢复设备描述表的原GDI对象,并删除该GDI对象。 一般按如下步骤使用GDI对象: Create or get a GDI OBJECT hNewGdi; hOldGdi = ::SelectObject(hdc, hNewGdi) …… ::SelectObject(hdc, hOldGdi) ::DeleteObject(hNewGdi) 先创建或得到一个GDI对象,然后把它选入设备描述表并保存它原来的GDI对象;用毕恢复设备描述表原来的GDI对象并删除新创建的GDI对象。 需要指出的是,如果hNewGdi是一个Stock GDI对象,可以不删除(删除也可以)。通过 HGDIOBJ GetStockObject( int fnObject // type of stock object ); 来获取Stock GDI对象。 1. MFC GDI对象 MFC用一些类封装了Windows GDI对象和相关函数,层次结构如图2-4所示: CGdiObject封装了Windows GDI Object共有的特性。其派生类在继承的基础上,主要封装了各类GDI的创建函数以及和具体GDI对象相关的操作。 CGdiObject的构造函数仅仅让m_hObject为空。如果m_hObject不空,其析构函数将删除对应的Windows GDI对象。MFC GDI对象和Windows GDI对象的关系如图2-5所示。 2. 使用MFC GDI类的使用 首先创建GDI对象,可分一步或两步创建。一步创建就是构造MFC对象和Windows GDI对象一步完成;两步创建则先构造MFC对象,接着创建Windows GDI对象。然后,把新创建的GDI对象选进设备描述表,取代原GDI对象并保存。最后,恢复原GDI对象。例如: void CMyView::OnDraw(CDC *pDC) { CPen penBlack; //构造MFC CPen对象 if (penBlack.CreatePen(PS_SOLID, RGB(0, 0, 0))) { CPen *pOldPen = pDC->SelectObject(&penBlack)); //选进设备表,保存原笔… pDC->SelectObject(pOldPen); //恢复原笔 }else { … } } 和在SDK下有一点不同的是:这里没有DeleteObject。因为执行完OnDraw后,栈中的penBlack被销毁,它的析构函数被调用,导致DeleteObject的调用。 还有一点要说明: pDC->SelectObject(&penBlack)返回了一个CPen *指针,也就是说,它根据原来PEN的句柄创建了一个MFC CPen对象。这个对象是否需要删除呢?不必要,因为它是一个临时对象,MFC框架会自动地删除它。当然,在本函数执行完毕把控制权返回给主消息循环之前,该对象是有效的。 关于临时对象及MFC处理它们的内部机制,将在后续章节详细讨论。 至此,Windows编程的核心概念:窗口、GDI界面(设备描述表、GDI对象等)已经陈述清楚,特别揭示了MFC对这些概念的封装机制,并简明讲述了与这些Windows Object对应的MFC类的使用方法。还有其他Windows概念,可以参见SDK开发文档。在MFC的实现上,基本上仅仅是对和这些概念相关的Win32函数的封装。如果明白了MFC的窗口、GDI界面的封装机制,其他就不难了。