中包括画笔对象、刷子对象以及字体对象等等。它们的工作模型是这样的:首先对设备上下文对象——我们简称为DC对象——进行设置,然后选择进行屏幕输出所需要的工具,最后用DC对象的输出函数绘制图形。屏幕输出的目标一般都是窗口的客户区,它是一个万能的输出区域,可以接受无论是文本、位图、还是其他类型的数据(比方说OLE对象)。
2.2 实例绘图原理剖析1 处理用户输入
程序从用户那里得到数据,经过自己的处理,再把计算的结果输出到屏幕、打印机或者其他的输出设备上,这是软件工作的基本模型。消息和键盘消息是最基本的输入信息,除此之外,MFC封装了一系列的使用户可以进行可视化的输入的界面对象,比方说对话框以及可以布置在上面的编辑框、按钮、列表控件、树形控件等等。使程序支持用户输入的手段更加丰富。
图3.1
1.1 定义对话框资源
下面我们通过一个例子来介绍如何设计一个基于对话框的界面,接受用户输入的数据,并且将它们以图形化的方式显示出来。我们将制作这样的一个有用的程序。它访问一个保存歌曲曲目的数据库,用户可以通过对话框让用户定义一个曲目表,并选定一张背景图,然后在一个没有系统菜单的窗口客户区上滚动显示曲目的名字。在许多的娱乐场所的大屏幕上我们都可以看到类似的东西。在这部分的内容当中,我们着重介绍如何获得和处理用户输入数据的工作,在后面的处理用户输出的内容当中,我们还将使用这个例子来说明如何在屏幕上进行输出工作。
下面我们来介绍如何定义一个对话框资源。在#p#分页标题#e#WORKSPACE窗口当中RESOURCE一页,在DIALOG小图标上面单击鼠标右键,弹出菜单,选择INSERT命令,如图3.2所示:
图3.2
接下来在弹出的资源类型对话框中选择DIALOG,表示添加一个对话框资源。单击NEW按钮。
图3.3
接下来我们就开始布置这个对话框。对话框上面已经有了两个按钮:OK和CANCEL。如下图3.4所示。它们的作用分别是确认用户通过对话框进行的输入和取消前面的输入工作。把它们的标题分别改为确定和取消。布置其它的控件。每一个对话框以及对话框上面的每一个控件都有一个ID号码,它们的定义包括在RESOURCE.H 这个头文件当中。比方说,这个对话框的ID号是IDD_DIALOG2,确认按钮的ID号是IDOK,取消按钮的ID号是IDCANCEL,MFC将通过ID号来访问这些资源。单击DIALOG工具条上面的TEST按钮可以测试对话框运行的效果。需要注意的是我们这里我们定义的对话框只是一个资源,如果要使这个对话框真正实现它的功能,必须在程序当中定义一个使用这个资源的对话框类。
图3.4
1.2 定义对话框类
下面我们就定义一个对话框类。在VIEW菜单当中选择CLASS WIZARD命令,单击ADD CLASS按钮,在弹出的菜单当中选择NEW命令,在NAME一栏当中输入新类的名字,在BASE CLASS列表框当中选择需要继承MFC当中的哪一个类。在DIALOG ID列表框当中选择对话框资源的ID号码,在这个实例当中,我们不使用OLE AUTOMATION,所以在这个组框当中选择NONE#p#分页标题#e#。在FILE NAME一栏显示的是这个类的定义写在哪一个文件当中。
图3.5
单击图3.5中所示CHANGE按钮,在HEADER FILE和IMPLEMENTATION FILE当中分别敲入新类的声明和定义分别写在哪个文件当中,单击OK按钮确认,这样我们就完成了对新的对话框类的定义。单击OK 按钮,CLASS WIZARD将按照我们刚才的要求进行对话框类定义的工作。打开WORKSPACE,选择FILE VIEW一页,在SOURCE FILES和HEADER FILES组当中到CLASS WIZARD已经新建了两个文件,并将它们加入了工程当中。SongDlg.h当中内容是CSongDlg这个类的声明,SongDlg.cpp这个文件当中的内容是这个类的实现。但是目前的程序只是包含了实现一个对话框的最基本功能的代码,调用这个对话框类的DoModal函数之后可以运行它。但是用户通过对话框进行的所有的输入工作都不会被接受。
下面,我们就着手完成实现对话框接受用户输入功能的工作。这里核心的工作就是实现对布置在对话框当中的控件的控制。控制又可以分两种类型:第一种是与界面上的控件交换数据,在对话框中的某些响应函数当中编写取出用户在对话框当中输入的数据。比方说在用户单击了确认输入的按钮,触发了该按钮的单击事件的时候,我们就要从输入新歌的编辑框当中取出曲目字符串保存到数据库当中,并将其显示在曲目列表当中。
我们可以使用MFC提供的一种叫做对话框数据交换(DDX)的机制来从编辑控件当中取出数据。在MFC的对话框类CDialog中已经封装了这种机制。它的工作原理就是在对话框资源中的编辑框和对话框类的一个成员变量之间建立连接。然后由MFC自动地完成在成员变量和控件之间的数据交换工作。首先打开CLASSWIZARD,选中MEMBER VARIABLE这一页,在CLASS NAME列表框当中选择CSongDlg,选择曲目编辑框的ID号IDC_EDIT1,单击ADD VARIABLE按钮。
#p#分页标题#e#
图3.6
在MEMBER VARIABLE NAME 一栏当中敲入变量的名字,在CATEGORY列表框当中可以选择变量的类型,VALUE表示生成一个数据变量,CONTROL类型的变量可以被用来对控件资源进行另一种类型的控制。它的类型依赖于前面选中的控件资源,比方说如果为一个编辑框控件成一个CONTROL类型的成员变量,那么它只能是CEdit类型的。我们将在后面的内容当中具体地介绍如何使用CONTROL类型的成员变量。
图3.7
生成一个VALUE变量,它的数据类型是字符串。单击OK按钮。这时WIZARD就自动地添加了进行对话框数据交换所有的代码。打开对话框类的头文件和实现文件,我们发现当中增加了一个CString类型的成员变量:
// Dialog Data
//{{AFX_DATA(CSongDlg)
enum { IDD = IDD_DIALOG2 };
CString m_songname;
//}}AFX_DATA
并且在建构函数当中对这个变量进行了初始化:
CSongDlg::CSongDlg(CWnd* pParent /*=NULL*/)
#p#分页标题#e#
: CDialog(CSongDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CSongDlg)
m_songname = _T("");
//}}AFX_DATA_INIT
}
在新生成的对话框类CSongDlg 中有如下一个虚函数:
virtual void DoDataExchange(CDataExchange* pDX);
//DDX/DDV support
DoDataExchange函数就是对话框类和对话框资源进行DDX数据交换的函数。在对话框初始化的时候或者在程序中调用UpdateData()函数的时候,这个函数将会被调用。DDX_TEXT这个函数可以处理多种类型的数据成员变量与控件资源之间的数据交换。这中间包括int,uint,long,DWORD,CString,float,double等。PDX这个参数是一个指向一个CDataExchange对象的指针通过它我们可以设置进行数据交换的方法。比方说:数据交换的方向。这段代码就可以通过PDX的这个标志志判断数据交换的方向是从变量到控件还是从控件到变量,然后进行不同的处理。进行数据交换之后,程序当中就可以通过成员变量来使用用户输入的数据了。
对控件资源的另外一种类型的控制就是要操纵界面控件的外观。比方说,我们可以通过生成一个CONTROL类型的成员变量来控制对话框当中的列表控件。和VALUE类型变量的添加方法一样,我们可以使用CLASSWIZARD生成一个CListControl 类型的对象,在DoDataExchange当中增加了这样的代码:
DDX_Control(pDX, IDC_LIST1, m_listCtrl1);
DDX_CONTROL也是对话框数据交换机制提供的一个函数,它的作用和DDX_TEXT大致一样。使用刚才定义的控件对象m_listCtrl1,就可以对列表框资源进行操纵了。
当对话框开始运行的时候,我们需要从数据库当中取出已经入库的曲目的名字将其显示在曲目列表框当中。这个工作应该在对话框响应WM_INITDIALOG消息的时候来做。使用CLASS WIZARD来添加这个消息响应函数。在左边的列表框当中选定CSongDlg这个类,在消息列表框当中选定对话框初始化消息,单击ADD FUNCTION按钮,WIZARD就自动地在这个类的声明当中重载了基类的这个成员函数并且在实现文件当中加入了函数体。单击EDIT CODE 按钮,就可以在函数体当中加入我们自己的代码了。
图3.8
在响应WM_INITDIALOG消息的处理函数CSongDlg::OnInitDialog中添加如下一段代码:
COleVariant var;
LV_ITEM lvitem;
CString Name("song_name");
#p#分页标题#e#
char str[50];
lvitem.iItem = 0;
if(globalRS_>IsOpen())
globalRS_>Close();
CString strQuery = _T("Select * from ");
strQuery += "SONGS";
globalRS_>Open(dbOpenDynaset,strQuery);
globalRS_>m_bCheckCacheForDirtyFields = FALSE;
if(globalRS_>IsOpen())
{
if(!globalRS_>GetRecordCount())
return 0;
globalRS_>MoveFirst();#p#分页标题#e#
while(!globalRS_>IsEOF())
{
var =globalRS_>GetFieldValue(_T("[")
+ Name + _T("]"));
lvitem.mask = LVIF_TEXT | LVIF_IMAGE| LVIF_DI_SETITEM;
lvitem.iItem++ ;
lvitem.iSubItem = 0;
strcpy(str , (LPCTSTR)CString(V_BSTRT(&var)));
lvitem.pszText = str;
lvitem.iImage = 0;
m_originsonglist.InsertItem(&lvitem);
globalRS_>MoveNext();
}#p#分页标题#e#
}
if(globalRS_>IsOpen())
globalRS_>Close();
这里使用了DAO技术来访问数据库并使用读出的字符串向列表控件当中添加条目。关于DAO技术的使用方法,我们在其他的章节当中会有详细地介绍。我们关心的是下面这段代码:
lvitem.mask = LVIF_TEXT | LVIF_IMAGE| LVIF_DI_SETITEM;
lvitem.iItem++ ;
lvitem.iSubItem = 0;
strcpy(str , (LPCTSTR)CString(V_BSTRT(&var)));
lvitem.pszText = str;
lvitem.iImage = 0;
m_originsonglist.InsertItem(&lvitem);
它执行了对列表控件添加条目的操作。这里需要用到WIN32提供的一个结构:LV_ITEM。我们可以从VC的HELP中找到其定义:
#p#分页标题#e#
typedef struct _LV_ITEM {
UINT mask;
int iItem;
int iSubItem;
UINT state;
UINT stateMask;
LPTSTR pszText;
int cchTextMax;
int iImage; // index of the list view item抯 icon
LPARAM lParam; // 32_bit value to associate with item
} LV_ITEM;
为了添加一个条目,我们首先在在这个结构当中填写条目的信息,然后把它传给列表对象的添加条目函数InsertItem就可以了。
接下来的这段代码位于响应中曲目列表框当中删去选定曲目的按钮单击事件当中。要实现从列表控件当中删去的条目的操作,只要把需要删去的条目的索引号传递给列表对象的删去条目函数#p#分页标题#e#DeleteItem就可以了。
int totalNum;
totalNum = m_selsonglist.GetItemCount();
int step = 0;
LV_ITEM lvitem;
lvitem.mask = LVIF_TEXT | LVIF_IMAGE| LVIF_DI_SETITEM;
lvitem.iSubItem = 0;
lvitem.iImage = 0;
while(step <= totalNum)
{
if(m_selsonglist.GetItemState(step,LVIS_SELECTED))
{
#p#分页标题#e#
m_selsonglist.DeleteItem(step);
}
step++;
}
我们在这总结一下对对话框上面的控件进行控制所需的工作:
首先在对话框类当中定义成员变量,然后调用对话框的成员变量的函数来操纵界面控件。
2 有关屏幕输出
2.1 设备上下文工作原理
在绝大多数的WINDOWS应用都需要在屏幕上显示自己的数据。由于WINDOWS是一个设备无关的操作系统,所以任何向屏幕上进行输出的功能都要间接地通一个叫做设备上下文(device context)的对象来完成。我们向设备上下文提出屏幕输出的要求,然后由WINDOWS自己来调用具体的输出设备的驱动程序来完成实际的输出工作。围绕设备上下文,MFC提供了一系列与其配合使用的绘图对象,这其
在有关用户输入部分的内容当中,我们曾经介绍过一个实例,它访问一个保存歌曲曲目的数据库,用户可以通过对话框让用户定义一个曲目表,并选定一张背景图,然后在一个没有系统菜单的窗口客户区上滚动显示曲目的名字。我们已经介绍了通过对话框介绍用户输入的方法,接下来就着重介绍如何把用户输入的信息在屏幕上显示出来。
我们将用户选定的字符串在一张背景图上面滚动输出。前面已经介绍了使用设备上下文进行工作的基本模型。即:首先选择绘图的工具,然后调用#p#分页标题#e#DC的绘图函数来进行绘制。在WINDOWS当中,每次窗口的外观发生改变的时候都会发出一个WM_PAINT消息,窗口的重绘工作都是在响应这个消息的处理函数当中进行的。可以使用CLASSWIZARD来添加这个消息响应函数。之后,就可以在这个函数当中进行屏幕输出了。还有什么时候会触发重绘事件呢?在程序中调用CWND的UpdateWindow 和RedrawWindow数的时候都会触发重绘事件。我们还可以直接使用SendMessage函数向一个指定的窗口送出重绘消息。另外调用CWND的Invalidate函数可以指示重绘的时候是否需要擦去背景,如果使用InvalidateRect函数还可以设置客户区的无效区域,系统重绘的时候将只把该区域的内容重新绘制,我们首先在窗口的客户区上帖一张位图,然后滚动输出文本。如何实现滚动输出呢我们的方法是在程序中设置一个定时器,在定时器计时已满事件WM_TIMER触发的时候来,调用REDRAWWINDOW函数,触发重绘事件,我们只要在它的消息响应函数ONPAINT当中重新绘制背景,擦去原来的文本,然后不断的改变文本输出的位置就可以达到目的了。您可能会重绘整个背景的做法会很耗费了过多的系统时间并且可能产背景的闪烁。这种担心是必要的。在WINDOWS95当中,系统对重绘的机制进行了优化,在我们没有指定无效区域的情况之下,系统自己会选择一个最小的无效区域,只对这一区域进行重绘。
2.3 绘图操作实现
下面介绍绘图操作的源程序使您对设备上下文的使用有一个大致的了解。
首先生成一个设备上下文。CPaintDC是MFC提供的一个从CDC继承出来的类。使用它有什么好处呢?如果直接使用CDC的话,我们需要首先调用CWnd的BeginPaint函数为重绘工作做一些准备工作,在完成绘制之还要用EndPaint函数表示结束绘制工作。所有的绘图操作都必须在这两个函数之间完成。CPaintDC封装了这两个函数,自动地对它们进行调用,使用者无须再去进行这些调用。
CPaintDC dc(this);
BITMAP bm;
m_bitmap_>GetBitmap(&bm);
CDC dcImage;
if (!dcImage.CreateCompatibleDC(&dc))
return;
CBitmap* pOldBitmap = dcImage.SelectObject(m_bitmap);
dc.BitBlt(0, 0, bm.bmWidth, bm.bmHeight,
&dcImage, 0, 0, SRCCOPY);
以上这段代码完成向屏幕上面输出位图的工作。首先根据资源生成一个位图对象,然后生成成一个和CPaintDC一致的内存DC对象,在内存DC当中选择这个位图。BITMAP是一个WIN32提供的位图结构,我们将这幅位图的信息保存在这个结构当中。这样做的原因是由于在使用到位图的位置及大小信息。BITMAP结构的定义如下:
typedef struct tagBITMAP { /* bm */
int bmType;
int bmWidth;
int bmHeight;
int bmWidthBytes;
BYTE bmPlanes;#p#分页标题#e#
BYTE bmBitsPixel;
LPVOID bmBits;
} BITMAP;
作好这些准备工作之后。调用DC对象的BitBlt函数把位图从内存DC当中贴到绘图DC当中来。前四个参数指示了位图在目的DC上的位置和大小。第五个参数是原来保存位图的内存DC的地址。接下来的两个参数是从位图的哪一点开始进行拷。最后这个参数设置该位图和屏幕上当前内容的相互关系,SRCCOPY的意思是拷贝过来覆盖原来的内容。这个参数还有其他的许多选择比方说取反操作或者异或操作,设置不同的参数可以获得丰富的效果。
下面介绍如何输出文本。首先对DC对象进行设置:
dc.SetBkMode(TRANSPARENT);
dc.SetTextColor(RGB(0 , 155 , nowX*nowX));
这里把文本输出的背景置为透明,然后设置输出文本的前景颜色。
下面这段程序的意思是为将要输出的文本选择字体。
LOGFONT logfont;
#p#分页标题#e#memset(&logfont, 0, sizeof(logfont));
logfont.lfWeight = 50;
logfont.lfHeight = 50;
lstrcpy(logfont.lfFaceName, "黑体");
nowFont.CreateFontIndirect(&logfont);
dc.SelectObject(&nowFont);
首先声一个WIN32提供的字体信息结构。为其分配内存空间。再按照我们的要求填写这个字体结构,设置字体的宽度,设置字体的高度,选择字体种类,根据这个结构生成一个CFONT字体对象,让DC对象选中这个字体对象,最后使用DC的文本输出函数来输出一个字符串。
总而言之,进行屏幕输出的规则如下:
第一 必须通过CDC对象进行屏幕输出;
第二 设置DC对象的输出属性;
第三 选择绘图工具
#p#分页标题#e#
第四 用CDC对象的绘图函数。
有关屏幕输出的内容就介绍到这里。
2.4 有关屏幕映射方式
在一般的情况之下,我们都以像素作为绘图的单位,我们称之为设备坐标。我们在进行绘图操作的时候,不可避免的要用到设备坐标系。
WINDOWS提供了几种映射方式,或称坐标系。可以通过它们来和设备上下文相联系。比方说不管是什么样的显示设备,如果我们需要在上面显示一个2英寸高,2英寸宽的矩形,该怎样处理呢?这就要依赖于我们所设定的坐标系。如果我们指定了MM_TEXT 方式,这时坐标原点就位于屏幕的左上角,X轴和Y轴的方向分别指向我们面对屏幕的右方和下方,它的绘图单位是像素,如果一英寸对应72个像素的话,我们就需要这样绘制这个矩形:
DC.Rectangle(CRect( 0,0,72*2,72*2));
所以我们如果我们指定了MM_LOENGLISH 方式,那么一个绘图单位就是百分之一英寸,坐标原点仍然位于屏幕的左上角,但是X轴和Y轴 的方向恰好和MM_TEXT方式下的轴方向相反,同样完成绘制上面提到的矩形的工作,我们就需要写出这样的代码:
DC.Rectangle(CRect(0,0,200,_200));
可见,坐标系的选择对我们编写程序有很大的影响。
此外,在有些时候,我们需要在几个不同的坐标系下面工作,那么还需要在进行在这些坐标系之间的转换工作。所以 ,我们有必要在这里详细介绍以下WINDOWS的坐标映射方法。#p#分页标题#e#
一般来说,最常用的就是WM_TEXT方式。在WM_TEXT坐标方式下面,坐标被映射到了像素,X的值向右方递增,Y的值向下递增,并且可以通过调用CDC的SetViewpotOrg函数来改变坐标原点。下面的代码把屏幕映射方式设为MM_TEXT方式,并且把坐标原点设在(300,300)处:
DC.SetMapMode(MM_TEXT);
DC.SetViewportOrg(CPoint(300,300));
另外,WINDOWS提供了一组非常重要的比例固定的映射方式,在这些映射方式下面,我们可以改变它的坐标原点,却无法改变它的比例因子。对于MM_LOENGLISH映射方式,我们已经知道它的X值是向右递减的,Y的值是向下递减的,所有的固定比例的映射方式都遵循这一原则。它们的比例因子也各不相同。我们列表如下:
映射方式 逻辑单位
MM_LOENGLISH 0.01英寸
MM_HIENGLISH 0.001英寸
MM_LOMETRIC 0.1毫米
MM_HIMETRIC 0.01毫米
MM_TWIPS 1/1440英寸
最后一种映射方式MM_TWIPS常常用语打印机,一个’twip’单位相当于1/20个点(一点近似与1/72)英寸。例如,如果指定的MM_TWIPS一个社单位,那么对于12点大小的字模来说,字符的高度为12x20,即240个twip。
除了固定比例的映射方式,WINDOWS还提供了比例可变的映射方式,在这种映射方式下面,我们除了可以改变它们比例因子之外还可以改变比例因子。借助于这样的映射方式,当用户改变窗口的尺寸的时候,绘制的图形的大小也可以根据比例发生相应的变化;同样,当我们翻转某个轴的时候,他们所绘制的图像,也以另外的一个轴为轴心进行翻转。这样的映射方式有两种:MM_ISOTROPIC和MM_ANIOTROPIC。
#p#分页标题#e#在MM_ISOTROPIC方式下,纵横的比例为1:1,换句话说,无论比例因子如何变化,我们画出的图形不会改变自己的形状。但是在MM_ANIOSTROPIC方式下面,X和Y的比例因子可以独立地变化,图形的形状可以发生变化。
我们分析下面这段程序:
void CAView::OnDraw(CDC *pDC)
{
CRect clientDC;
GetClientRect(clientRect);
pDC_>SetMapMode(MM_ANISOTROPIC);
pDC_>SetWindowExt(1000,1000);
pDC_>SetViewportExt(clientRect.right,_clientRect.bottom);
pDC_>SetViewportOrg(clientRect.right/2, clientRect.bottom/2);
pDC_>Ellipse(CRect(_500, _500, 500, 500));
}
这段代码的功能是这样的,首先取得窗口客户区矩形的大小,然后用SetWindowExt和SetViewportExt函数设定比例,结果窗口尺寸的大小被设为1000个逻辑单位高和1000个逻辑单位宽,坐标原点被设为窗口的中心,在这样的设置之下,绘制出一个半径为#p#分页标题#e#500个逻辑单位的椭圆。
在这里如果将映射方式改变为MM_ISOTROPIC那么就将画出一个圆。圆的直径是窗口举行宽和高的最小值。
下面我们给出逻辑单位到设备单位的公式:
X比例因子 = X视口范围/X窗口范围
Y比例因子 = Y视口范围/Y窗口范围
设备X = 逻辑X *X比例因子 + X坐标原点偏移
设备Y = 逻辑Y *Y比例因子 + Y坐标原点偏移
当我们设定了设备上下文的映射方式之后,就可以直接使用逻辑坐标作为其参数了,但是从WM_MOUSEMOVE消息所获得的鼠标的坐标值是设备坐标。许多其他的MFC库函数,尤其是类CRect的成员函数,只接受设备坐标。所以我们有时要利用CDC的LPtoDP和DPtoLP在逻辑坐标和设备坐标之间进行转换的工作。
下面我们列出进行坐标映射工作的时候所要遵循的一些规则:
可以认为CDC的所有成员函数都以逻辑坐标作为参数,但和CRect有关的函数例外。
可以认为CWnd 的成员函数都以设备坐标作为参数。
所有的HIT_TEST操作都应该考虑设备坐标。
#p#分页标题#e#以逻辑坐标的形式来保存数据,否则用户对窗口进行滚动操作的时候,这个数据就不再有效了。
3 文件处理
几乎所有的软件都需要将程序当中的信息保存在磁盘存储器上面。这些信息可能是程序运行的初始化数据,或者是程序中计算得到的结果,还可能是程序经常用到的资料。从磁盘存储器上存取数据的工作往往是通过文件操作或者数据库操作来完成的。关于数据库操作的内容,我们将在后面的章节当中进行详细的介绍,在下面的内容中,我们主要讨论VC如何实现一般意义上的数据存取工作。
VC是面向对象的开发平台,在使用MFC编写的程序中,我们定义和生成了各种各样的对象,通过它们之间的协同工作完成程序的功能。所以在MFC中,程序的存取工作的核心内容就是如何实现这些对象的持续化。一个可以实现持续化的对象知道如何保存和载入它们自己的数据。比方说,在程序使用MFC文档/视图结构的时候,如果为文档对象提供正确的持续化功能,它将在您需要的时候自动地保存和恢复自己的数据,并且保持用户最新的修改结果。需要注意的是对象的持续化同样是将数据保存到磁盘文件中去,它的好处在于MFC对二进制文件的存取过程进行了封装,使用来实现对象存取的程序代码得到了简化。
当然,如果您更喜欢直接操作文件进行数据的存取工作,MFC也提供了更为直接的渠道。CFile这个类封装了所有对文件的常用操作,使用CFile对象处理文件会比使用API函数简单得多。
3.1 对象持续化简述
在MFC当中,对象的持续化功能主要是通过文档/视图结构当中文档对象的序列化机制来实现的。下面,我们将详细介绍如何使用序列化机制来实现对象的持续化。
#p#分页标题#e#
序列化,简单地说就是向一个持久性的存储媒体——如磁盘文件保存对象或读取对象的过程。可以实现序列化的类——即从CObject继承而来的类,有一个叫做Serialize的成员函数,序列化工作主要是在这个函数当中进行的。我们使用一张示意图来说明序列化的原理。
MFC使用一个类型为CArchive的归档对象充当磁盘文件与程序中的对象的中介。归档对象总是与一个CFile对象相关联,从中获得一些进行序列化所需要的信息,比如说文件名,以及存取标志等。对象的持续化的主要工作就是将自己的成员变量或者当前状态保存起来。我们可以使用经过重载的流入和流出操作符直接向归档对象中保存或者取出变量成员的值,而将这些数据保存到磁盘文件中的工作由CArchive对象指示CFile对象来完成。当用户在打开或保存拥有文档对象数据的文件或者使用文档对象的Open 、
Save、Save As菜单命令时,MFC便会自动调用Serialize函数。
使类实现序列化,需要五个步骤:
1、从Cobject 类或其派生类派生用户类;
2、在类声明中使用DECLARE_SERIAL宏;
3、重载Serialize 函数;
4#p#分页标题#e#、定义不用变量的构造函数;
5、在类实现文件中使用宏IMPLEMENT_SERIAL。
在下一节中,我们将以一个具体的实例来详细说明如何实现类的序列化。
3.2 实例分析
在描述了类序列化的基本原理之后,让我们来看一个具体的序列化实例DrawLine。启动这个实例。
图3.9
在这个实例中,我们将完成对直线的简单绘制。在视图按下鼠标左键,然后拖动鼠标到一个新位置松开鼠标,程序就画出一条直线。再来看看这个程序的基本构成。
除了基本的文档类和视类之外,我们还有一个直线类CLine,它有四个int成员变量m_x1,m_y1,m_x2,m_y2,用来记录直线两个端点的X轴和Y轴方向坐标,此外有一个Draw成员函数,Draw是根据直线的以上四个成员变量,在视图客户区中绘出直线。在WorkSpace的ClassView中双击类CLine的Draw函数,则可以看到Draw的实现。
void CLine::Draw (CDC *PDC)#p#分页标题#e#
{
PDC_>MoveTo (m_x1, m_y1);
PDC_>LineTo (m_x2, m_y2);
}
我们对视类CDrawLineView的OnLButtonDown,OnMouseMove,OnLButtonUp 三条消息进行了处理,在WorkSpace的ClassView中同样可看到它们的实现。
此外我们在文档类CDrawLineDoc中有一个成员变量m_LineArray,它是用来记录我们在视图客户区所画的直线。函数GetLine是根据索引取得m_LineArray中的一条直线,GetNumLines则是取得直线的总数的。
在了解了基本的程序结构之后,下面对直线对象进行序列化处理。
1 从Object 类中派生并使用宏DECLARE_SERIAL
打开定义CLine这个类的头文件line.h,可以看到这个类是从CObject 类派生出来的。要对CLine类实现序列化,需要在类的声明中加入宏DECLARE_SERIAL的调用,并在类的实现文件中,加入宏IMPLEMENT_SERIAL 的调用。CObject 类拥有基本的序列化功能,通过对此类的继承实现可以获得这些功能,此外一个无参数的构造函数是不可缺少的。
我们打开Line.h后,在CLine类定义中第一句就可以是DECLARE__SERIAL(CLine),这个宏不需要加分号。
#p#分页标题#e#2 重载Serialize 成员函数
我们要实现序列化,先对其进行改造,在WorkSpace的ClassView中选择CLine类,单击鼠标右键,选择Add Member Function增加一个成员函数:
图3.10
VC将会跳出如3.11所示下添加函数的对话框:
图3.11
在Function Type输入void,在Function Declaretion输入 Serialize(CArchive& ar),然后选择Virtual,按OK即可。然后在ClassView中可以看到这个函数。
下面我们编辑这个函数,双击WorkSpace显示的CLine类的Serialize函数,则转到Line.cpp中其实现处。这个函数的实现如下:
void CLine::Serialize(CArchive & ar)
{
CObject::Serialize(ar);
#p#分页标题#e#
if (ar.IsStoring())
{
ar<
ar<
}
else
{
ar>>m_x1>>m_y1;
ar>>m_x2>>m_y2;
}
}
首先调用基类的Serialize函数,CObject::Serialize(ar);然后判断是保存数据还是载入数据,然后再根据判断的结果进行实际的存取工作。这里ar就是框架程序传递给序列化函数的归档对象指针。
#p#分页标题#e#
当调用完基类的序列化函数后,判断ar的状态,当ar.IsStoring()返回真时,这时进行数据保存;当ar.IsStoring()返回非真时,这时CArchive 对象要求读取数据。
3 使用操作符 存取数据
在上面的代码中我们用到了>>和<<,在这里对它们作一个介绍,>>和<<是一种操作符,用来指示向CArchive对象读取还是保存数据,必要时我们可以重载重定向符。如ar>>m_x1>>m_y1;这一句,其中>>表示从ar中读出数据m_x1,m_y2,这个符号及>>可以连用,亦可以分开来用,如ar>>m_x1;ar>>m_y1;同样ar<中的<<是把数据存入ar中。
4 文档对象序列化
在对直线对象进行序列化函数处理之后,接下来,我们对文档类进行改写,首先实现文档类的序列化函数。双击WorkSpace中CDrawLineDoc类Serialize函数打开它,文档类的Serialize函数是个虚拟成员函数的,其缺省实现是不做任何工作的。
我们再来看文档类的序列化函数,CDrawLineDoc类的序列化函数主要是对CLine类对象的序列化函数的调用,其大体情况是这样的:当进行保存时,我们先得到直线的总数——调用GetNumLines函数,然后用一个循环对每一条直线对象调用序列化函数;当进行读取数据时,亦先得到直线总条数——从文件中读出,然后同样用一个循环,每次读出一条直线——调用对象的序列化函数,然后把它加入到文档类的成员变量m_LineArray中去,直到直线读完。这个函数的源代码如下:
void CDrawLineDoc::Serialize(CArchive& ar)
#p#分页标题#e#
{
int linenum=GetNumLines();
if (ar.IsStoring())
{
ar << linenum;
for(int i=0;i
m_LineArray.GetAt(i)_>Serialize(ar);
}
else
{
m_LineArray.RemoveAll();
ar >> linenum;
CLine Line;
for(int i=0;i
{#p#分页标题#e#
CLine *PLine = new CLine ();
PLine_>Serialize(ar);
m_LineArray.Add (PLine);
}
UpdateAllViews(NULL);
}
}
5 连接视图
接着,我们对文档类和视类进行一些必须的处理:在ClassWizard 中,对CDrawLineDoc增加两个成员函数:OnNewDocument和OnOpenDocument。对OnNewDocument 的调用是当新生成一个文档时,所以此时需要处理文档类的成员变量m_LineArray,我们将它里面的直线全部除去,同时调用UpdateAllViews(NULL),更新视图,我们对这个函数的代码作如下改动:
BOOL CDrawLineDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;#p#分页标题#e#
// TODO: add reinitialization code here
// (SDI documents will reuse this document)
m_LineArray.RemoveAll();
UpdateAllViews(NULL);
return TRUE;
}
框架程序对OnOpenDocument的调用是在我们打开文件时,所以这个时候,在我们读取数据之前,同样要清除m_LineArray所有的直线,我们对这个函数的代码作如下改动:
BOOL CDrawLineDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
m_LineArray.RemoveAll();
UpdateAllViews(NULL);
if (!CDocument::OnOpenDocument(lpszPathName))
return FALSE;
#p#分页标题#e#
// TODO: Add your specialized creation code here
return TRUE;
}
我们还需接着对视类的OnDraw进行改写,让它可以实现重画,在这里我们只是简单地对每一个直线类对象调用Draw成员函数,对每一条直线进行重画,我们对这个函数的代码作如下改动:
void CDrawLineView::OnDraw(CDC* pDC)
{
CDrawLineDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
for (int i = 0; i
{
(pDoc_>GetLine(i)) _>Draw(pDC);#p#分页标题#e#
}
}
至此我们对程序的序列化已经完成,对程序编译运行即可。
3.3 与文件处理关系密切的类CFile
CFile类用来处理正常文件的I/O操作,它直接供无缓冲的、二进制磁盘输入/输出服务,并且通过其派生类间接支持文本文件和内存文件。因为CFile类是基本上封装在CArchive类之中了,所以我们只对这个类作简单介绍。
CFile类有三个构造函数,其原型如图所示。
virtual BOOL Open( LPCTSTR lpszFileName, UINT nOpenFlags,
CFileException* pError = NULL )
其中:hFile为一个已打开文件的句柄。LpszFileName指定所想要文件的路径的字符串。路径可以是相对的或绝对的。NOpenFlags指共享和存取方式,对于这个标志的说明,我们留到后面专门说明。
CFile类用Open来创建和打开文件。使用Open创建新文件,必须有一个文件名,并且选择一定的打开方式:
CFile对磁盘文件的定点读和写是通过函数Read ,Write和Seek进行的。
virtual UINT Read( void* lpBuf, UINT nCount );
virtual void Write( const void* lpBuf, UINT nCount );
virtual LONG Seek( LONG lOff, UINT nFrom );
函数Read:返回传输给缓冲区的字节数。如果读到文件尾,返回值可能小于nCount 值。LpBuf:指向用户定义的缓冲区的指针,用来接收数据;nCount:要从文件中读出的最大字节数;函数Write:写缓冲区数据到文件中;Seek用于定位文件指针位置,如果所请求的位置合法,则返回距离文件头的新字节偏移。
文件的打开和关闭是相对的,打开一个文件之后,必须把它关闭,文件的关闭是相当简单的,用CFile对象调用Close函数即可。
下面描述文件共享和存取标志。下列标志指定打开文件时可执行的动作。可以用OR来组合下面所列的选项。一个存取许可和一个共享选项是必需的;modeCreate 和modeNoInberit方式是可选的。
modeCreate指示构造函数创建一个新文件,如果该文件已存在,则该文件截短为0。
modeRead #p#分页标题#e#打开文件用于读。
modeReadWrite 打开文件用于读写。
modeWrite 打开文件用于只写。
modeNoInberit 阻止文件被子进程继承。
shareDenyNone 打开文件,不允许其它进程读或写该文件。如果该文件已由其它任何进程用兼容方式打开,则Create将失败。
shareDenyWrite 打开文件,不允许其它进程写该文件。如果该文件已由其它任何进程用兼容方式或写方式打开,则Create将失败。
shareDenyRead 打开文件,不允许其它进程读该文件。如果该文件已由其它任何进程用兼容方式或读方式打开,则Create将失败。
shareExclusive 以独占方式打开文件,不允许其它进程写该文件。如果该文件已用其它读或写方式打开,即使是当前进程打开,则构造也将失败。
shareCompat 以兼容方式打开文件,允许给定机器上任何进程打开该文件任意次。如果该文件已用其它任何共享方式打开,构造将失败。
typeText 设置文本方式,对回车换行进行特殊处理,它只用于派生类。
typeBinary 设置二进制方式,它只用于派生类。
评论 {{userinfo.comments}}
{{child.content}}
{{question.question}}
提交