您的位置:寻梦网首页编程乐园C/C++编程Visual C++教程
一、走进Visual C++ 二、MFC程序结构分析 三、深入MFC类库 四、VC程序调试 五、Visual C++与多媒体
Visual C++教程 三 深入MFC类库
  • 三 深入MFC类库
  • 1 处理用户输入
  • 1.1 定义对话框资源
  • 1.2 定义对话框类
  • 2 有关屏幕输出
  • 2.1 设备上下文工作原理
  • 2.2 实例绘图原理剖析
  • 2.3 绘图操作实现
  • 2.4 有关屏幕映射方式
  • 3 文件处理
  • 3.1 对象持续化简述
  • 3.2 实例分析
  • 3.3 与文件处理关系密切的类CFile
  • 4 DAO技术
  • 4.1 DAO与ODBC
  • 4.2 使用MFC实现DAO技术
  • 5 打印
  • 5.1打印和显示
  • 5.2打印分页
  • 5.3 打印工作的开始和结束
  • 5.4 打印程序实例
  • 三 深入MFC类库

    1 处理用户输入

    程序从用户那里得到数据,经过自己的处理,再把计算的结果输出到屏幕、打印机或者其他的输出设备上,这是软件工作的基本模型。消息和键盘消息是最基本的输入信息,除此之外,MFC封装了一系列的使用户可以进行可视化的输入的界面对象,比方说对话框以及可以布置在上面的编辑框、按钮、列表控件、树形控件等等。使程序支持用户输入的手段更加丰富。

     

    图3.1

     

    1.1 定义对话框资源

    下面我们通过一个例子来介绍如何设计一个基于对话框的界面,接受用户输入的数据,并且将它们以图形化的方式显示出来。我们将制作这样的一个有用的程序。它访问一个保存歌曲曲目的数据库,用户可以通过对话框让用户定义一个曲目表,并选定一张背景图,然后在一个没有系统菜单的窗口客户区上滚动显示曲目的名字。在许多的娱乐场所的大屏幕上我们都可以看到类似的东西。在这部分的内容当中,我们着重介绍如何获得和处理用户输入数据的工作,在后面的处理用户输出的内容当中,我们还将使用这个例子来说明如何在屏幕上进行输出工作。

    下面我们来介绍如何定义一个对话框资源。在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。在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按钮。

     

    图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*/)

    : 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");

    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();

    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();

    }

    }

    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中找到其定义:

    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就可以了。

    接下来的这段代码位于响应中曲目列表框当中删去选定曲目的按钮单击事件当中。要实现从列表控件当中删去的条目的操作,只要把需要删去的条目的索引号传递给列表对象的删去条目函数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))

    {

     

    m_selsonglist.DeleteItem(step);

    }

    step++;

    }

    我们在这总结一下对对话框上面的控件进行控制所需的工作:

    首先在对话框类当中定义成员变量,然后调用对话框的成员变量的函数来操纵界面控件。

    2 有关屏幕输出

    2.1 设备上下文工作原理

    在绝大多数的WINDOWS应用都需要在屏幕上显示自己的数据。由于WINDOWS是一个设备无关的操作系统,所以任何向屏幕上进行输出的功能都要间接地通一个叫做设备上下文(device context)的对象来完成。我们向设备上下文提出屏幕输出的要求,然后由WINDOWS自己来调用具体的输出设备的驱动程序来完成实际的输出工作。围绕设备上下文,MFC提供了一系列与其配合使用的绘图对象,这其中包括画笔对象、刷子对象以及字体对象等等。它们的工作模型是这样的:首先对设备上下文对象——我们简称为DC对象——进行设置,然后选择进行屏幕输出所需要的工具,最后用DC对象的输出函数绘制图形。屏幕输出的目标一般都是窗口的客户区,它是一个万能的输出区域,可以接受无论是文本、位图、还是其他类型的数据(比方说OLE对象)。

    2.2 实例绘图原理剖析

    在有关用户输入部分的内容当中,我们曾经介绍过一个实例,它访问一个保存歌曲曲目的数据库,用户可以通过对话框让用户定义一个曲目表,并选定一张背景图,然后在一个没有系统菜单的窗口客户区上滚动显示曲目的名字。我们已经介绍了通过对话框介绍用户输入的方法,接下来就着重介绍如何把用户输入的信息在屏幕上显示出来。

    我们将用户选定的字符串在一张背景图上面滚动输出。前面已经介绍了使用设备上下文进行工作的基本模型。即:首先选择绘图的工具,然后调用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;

    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;

    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对象的输出属性;

    第三 选择绘图工具

    第四 用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的坐标映射方法。

    一般来说,最常用的就是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。

    在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个逻辑单位宽,坐标原点被设为窗口的中心,在这样的设置之下,绘制出一个半径为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操作都应该考虑设备坐标。
    • 以逻辑坐标的形式来保存数据,否则用户对窗口进行滚动操作的时候,这个数据就不再有效了。

     

    3 文件处理

     

    几乎所有的软件都需要将程序当中的信息保存在磁盘存储器上面。这些信息可能是程序运行的初始化数据,或者是程序中计算得到的结果,还可能是程序经常用到的资料。从磁盘存储器上存取数据的工作往往是通过文件操作或者数据库操作来完成的。关于数据库操作的内容,我们将在后面的章节当中进行详细的介绍,在下面的内容中,我们主要讨论VC如何实现一般意义上的数据存取工作。

    VC是面向对象的开发平台,在使用MFC编写的程序中,我们定义和生成了各种各样的对象,通过它们之间的协同工作完成程序的功能。所以在MFC中,程序的存取工作的核心内容就是如何实现这些对象的持续化。一个可以实现持续化的对象知道如何保存和载入它们自己的数据。比方说,在程序使用MFC文档/视图结构的时候,如果为文档对象提供正确的持续化功能,它将在您需要的时候自动地保存和恢复自己的数据,并且保持用户最新的修改结果。需要注意的是对象的持续化同样是将数据保存到磁盘文件中去,它的好处在于MFC对二进制文件的存取过程进行了封装,使用来实现对象存取的程序代码得到了简化。

    当然,如果您更喜欢直接操作文件进行数据的存取工作,MFC也提供了更为直接的渠道。CFile这个类封装了所有对文件的常用操作,使用CFile对象处理文件会比使用API函数简单得多。

     

    3.1 对象持续化简述

    在MFC当中,对象的持续化功能主要是通过文档/视图结构当中文档对象的序列化机制来实现的。下面,我们将详细介绍如何使用序列化机制来实现对象的持续化。

    序列化,简单地说就是向一个持久性的存储媒体——如磁盘文件保存对象或读取对象的过程。可以实现序列化的类——即从CObject继承而来的类,有一个叫做Serialize的成员函数,序列化工作主要是在这个函数当中进行的。我们使用一张示意图来说明序列化的原理。

     

    MFC使用一个类型为CArchive的归档对象充当磁盘文件与程序中的对象的中介。归档对象总是与一个CFile对象相关联,从中获得一些进行序列化所需要的信息,比如说文件名,以及存取标志等。对象的持续化的主要工作就是将自己的成员变量或者当前状态保存起来。我们可以使用经过重载的流入和流出操作符直接向归档对象中保存或者取出变量成员的值,而将这些数据保存到磁盘文件中的工作由CArchive对象指示CFile对象来完成。当用户在打开或保存拥有文档对象数据的文件或者使用文档对象的Open 、

    Save、Save As菜单命令时,MFC便会自动调用Serialize函数。

    使类实现序列化,需要五个步骤:

    1、从Cobject 类或其派生类派生用户类;

    2、在类声明中使用DECLARE_SERIAL宏;

    3、重载Serialize 函数;

    4、定义不用变量的构造函数;

    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)

    {

    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),这个宏不需要加分号。

    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);

     

    if (ar.IsStoring())

    {

    ar<<m_x1<<m_y1;

    ar<<m_x2<<m_y2;

    }

    else

    {

    ar>>m_x1>>m_y1;

    ar>>m_x2>>m_y2;

    }

     

    }

    首先调用基类的Serialize函数,CObject::Serialize(ar);然后判断是保存数据还是载入数据,然后再根据判断的结果进行实际的存取工作。这里ar就是框架程序传递给序列化函数的归档对象指针。

    当调用完基类的序列化函数后,判断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<<m_x1<<m_y1;中的<<是把数据存入ar中。

    4 文档对象序列化

    在对直线对象进行序列化函数处理之后,接下来,我们对文档类进行改写,首先实现文档类的序列化函数。双击WorkSpace中CDrawLineDoc类Serialize函数打开它,文档类的Serialize函数是个虚拟成员函数的,其缺省实现是不做任何工作的。

    我们再来看文档类的序列化函数,CDrawLineDoc类的序列化函数主要是对CLine类对象的序列化函数的调用,其大体情况是这样的:当进行保存时,我们先得到直线的总数——调用GetNumLines函数,然后用一个循环对每一条直线对象调用序列化函数;当进行读取数据时,亦先得到直线总条数——从文件中读出,然后同样用一个循环,每次读出一条直线——调用对象的序列化函数,然后把它加入到文档类的成员变量m_LineArray中去,直到直线读完。这个函数的源代码如下:

    void CDrawLineDoc::Serialize(CArchive& ar)

    {

    int linenum=GetNumLines();

    if (ar.IsStoring())

    {

    ar << linenum;

    for(int i=0;i<linenum;i++)

    m_LineArray.GetAt(i)_>Serialize(ar);

    }

    else

    {

    m_LineArray.RemoveAll();

    ar >> linenum;

    CLine Line;

    for(int i=0;i<linenum;i++)

    {

    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;

    // 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;

     

    // 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_>GetNumLines(); i++)

    {

    (pDoc_>GetLine(i)) _>Draw(pDC);

    }

    }

    至此我们对程序的序列化已经完成,对程序编译运行即可。

    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 打开文件用于读。
    • modeReadWrite 打开文件用于读写。
    • modeWrite 打开文件用于只写。
    • modeNoInberit 阻止文件被子进程继承。
    • shareDenyNone 打开文件,不允许其它进程读或写该文件。如果该文件已由其它任何进程用兼容方式打开,则Create将失败。
    • shareDenyWrite 打开文件,不允许其它进程写该文件。如果该文件已由其它任何进程用兼容方式或写方式打开,则Create将失败。
    • shareDenyRead 打开文件,不允许其它进程读该文件。如果该文件已由其它任何进程用兼容方式或读方式打开,则Create将失败。
    • shareExclusive 以独占方式打开文件,不允许其它进程写该文件。如果该文件已用其它读或写方式打开,即使是当前进程打开,则构造也将失败。
    • shareCompat 以兼容方式打开文件,允许给定机器上任何进程打开该文件任意次。如果该文件已用其它任何共享方式打开,构造将失败。
    • typeText 设置文本方式,对回车换行进行特殊处理,它只用于派生类。
    • typeBinary 设置二进制方式,它只用于派生类。

     

    4 DAO技术

    4.1 DAO与ODBC

    在WINDOWS环境下进行数据库访问工作您有两种选择:使用DAO技术或者使用ODBC技术。ODBC(OPEN DATABASE CONNECTIVITY)即开放式数据库互联,作为WINDOWS开放栍准结构的一个重要部分已经为很多的WINDOWS程序员所熟悉。DAO(DATA ACCESS OBJECTS)即数据访问对象集(DATA ACCESS OBJECTS)是MICROSOFT提供的基于一个数据库对象集合的访问技术。它们都是WINDOWS API的一个部分,可以独立于DBMS进行数据库访问。那么ODBC和DAO的区别在哪里呢?ODBC和DAO访问数据库的机制是完全不同的。ODBC的工作依赖于数据库制造商提供的驱动程序,使用ODBC API的时候,WINDOWS的ODBC管理程序,把数据库访问的请求传递给正确的驱动程序,驱动程序再使用SQL语句指示DBMS完成数据库访问工作。DAO则绕开了中间环节,直接使用MICROSOFT提供的数据库引擎(MICROSOFT JET DATABASE ENGINE)提供的数据库访问对象集进行工作。速度比ODBC快。数据库引擎目前已经达到了3.0版本。它是DAO、MS ACCESS、MS VISUAL BASIC等等WINDOWS应用进行数据库访问的基础。引擎本身的数据库格式为MDB,也支持对目前流行的绝大多数数据库格式的访问,当然MDB是数据库引擎中效率最高的数据库。

    如果您使用客户机/服务器模型的话,建议您使用ODBC方案;如果您希望采用MDB格式的数据库,或者利用数据库引擎的速度,那么DAO是更好的选择。

    4.2 使用MFC实现DAO技术

    MFC对所有的DAO对象都进行了封装。使用MFC进行DAO编程,首先要为每一个打开的数据库文件提供一个数据库对象──CDaoDatabase,由这个对象管理数据库的连接。然生成记录集对象──CDaoRecordset,通过它来进行查询、操作、更新等等的工作。如果需要在程序中管理数据库的结构,则需要使用DAO当中的表结构信息对象CDaoTableInfo及字段定义对象CDaoFieldInfo来进行获得或者改变数据库表结构的工作。CDaoDatabase、CDaoRecordset、CDaoTableDefInfo、CDaoFieldInfo是使用MFC进行DAO编程的最基本也是最常用的类。

    下面,我们通过一个实例来介绍如何使用MFC的DAO类来进行数据库访问的工作。在这个实例当中,我们将在程序当中建立一个学生档案管理数据库,并通过对话框来添加、删除和浏览记录。我们首先看以下程序运行的情况。

    我们将针对程序的功能依次介绍如何生成和使用数据库对象、记录集对象以及如何通过记录集来操纵数据库。我们将通过解释对数据库进行操作的源程序来介绍如何用MFC来实现DAO技术。

    下面介绍如何建库:

    首先新建一个数据库对象。

    newDatabase = new CDaoDatabase;

    newDatabase_>Create(_T("stdfile.mdb"),

    dbLangGeneral, dbVersion30);

    利用数据库引擎在磁盘上建立一个MDB格式的数据库文件。

    stdfile.mdb是在磁盘上面建立的数据库文件的名字,

    dbLangGeneral 是语言选项。

    dbVersion30这是数据库引擎版本选项。

    图3.12

    然后新建一个数据库表定义信息对象。

    CDaoTableDef *TableInfo;

    TableInfo = new CDaoTableDef(newDatabase);

    TableInfo_>Create(_T("student"));

    新建一个字段定义信息对象。

    按要求填写字段定义信息对象。

    定义字段名称:

    FieldInfo_>m_strName = CString("studentName");

    定义字段类型:

    FieldInfo_>m_nType = dbText;

    定义字段所占的字节数大小:

    FieldInfo_>m_lSize = 10;

    定义字段特性:

    FieldInfo_>m_lAttributes = dbVariableField | dbUpdatableField;

    dbVariableField参数的意思是该字段的所占的字节数是可变的。

    dbUpdatableField参数的意思是该字段的值是可变的。

    根据字段定义对象在数据库表对象当中生成字段。

    TableInfo_>CreateField(*FieldInfo);

    在生成了所有的字段之后,将新的数据库表的定义填加到数据库对象当中去。

    TableInfo_>Append();

    下面介绍如何进行数据库操作:

    首先生成记录集对象:

    Recordset = new CDaoRecordset(newDatabase);

    然后使用SQL语句打开记录集对象。首先把SQL语句记入一个字符串:

    CString strQuery = _T("Select * from student");

    使用这个字符串打开记录集。

    Recordset_>Open(dbOpenDynaset , strQuery);

    dbOpenDynaset参数的意思是表示记录集打开的类型。dbOpenDynaset的意思是打开一个可以双向滚动的动态记录集。这个记录集中的记录是使用我们定义的SQL语句对数据库进行查询得到的。这个参数还有另外的两种选择:

    dbOpenTable参数指示打开一个数据表类型的记录集,使用这种类型的记录集只能对单一的数据库中的记录进行操纵。

    如果使用dbOpenSnapshot参数表示打开的是映像记录集,它实际上是所选择的记录集的一个静态的拷贝,在只需要进行查询操作或者希望制作报表的时候,使用这种记录集比较合适,它不会对数据库中的数据进行修改。

    接下来对记录集当中的一个标志位赋值,说明是否要求自动地标记出CACHE当中经改变的记录。使用记录集的时候是DAO把被检索出的记录读入CACHE,所有的操纵都是针对CACHE中的记录进行的,要实现对数据库当中的记录更新必须把CACHE记录中被改变的字段的值写回到数据库文件当中去。这个标志位的作用就是当CACHE中的数据改变的时候,是否需要自动的标记出记录中那些应该被写回的字段。

    下面介绍如何填加一个记录。

     

    m_Recordset _>AddNew();

    m_Recordset_>Update();

    使用AddNew()这个函数可以在数据表记录集或者是动态记录集当中添加新的记录,调用AddNew()之后必须接着调用Update()来确认这个添加动作,将新的记录保存到数据库文件当中去。新的记录在数据库当中的位置取决于当前记录集的类型:如果是动态记录集,新记录都将被插入到记录集的末尾。如果是数据表记录集的话,当数据库表中定义了主键的时候新记录将按照库表的排序规则插入到合适的地方;如果没有定义主键那么新记录也会被插入到记录集的末尾。

    用AddNew()会改变记录集的当前记录。只有将当前记录定位在新记录上,才能填写它的数据。所以我们使用MoveLast函数使刚刚添加的记录成为当前记录,然后调用Edit函数对新记录进行编辑。

    m_Recordset_>MoveLast();

    m_Recordset_>Edit();

    依次给新记录的字段进行赋值:

    COleVariant var1(m_Name , VT_BSTRT);

    m_Recordset_>SetFieldValue(_T("studentName") , var1);

    COleVariant var2(m_ID , VT_BSTRT);

    m_Recordset_>SetFieldValue(_T("studentID") , var2);

    COleVariant var3(m_Class , VT_BSTRT);

    m_Recordset_>SetFieldValue(_T("studentClass") , var3);

    COleVariant var4(m_SID , VT_BSTRT);

    m_Recordset_>SetFieldValue(_T("studentSID") , var4);

    COleVariant 这个类封装了WIN32提供的VARIANT这个结构以及对它的操作。这个类当中可以存储多种类型的数据。需要注意的是这种包容能力是通过C语言当中的UNION提供的,就是说一个COleVariant 对象只能保存一种类型的数据。我们先把字段的值装入OLE变体对象,再使用这个变体对象对记录中的字段进行赋值。VT_BSTRT参数的作用是在生成OLE变体对象的时候指示将要封入的数据的类型为字符串。当对所有的字段都结束赋值后,调用Update 函数来保存刚才的修改。

    m_Recordset_>Update();

    注意,在调用Update函数之前,如果进行了改变当前记录的操作,那么前面进行的所有的赋值工作都将丢失,而且不会给出任何的警告。

    这段代码从记录集中取出一个记录的值,这里同样要用到OLE变体对象。记录集的GetFieldValue将返回一个变体对象,我们首先取得这个变体对象,然后从中取出需要的值。

    这里V_BSTRT指示从变体对象当中取出字符串类型的数据。

    如何从数据库中删去一个记录呢?首先要使该记录成为当前记录。然后使用Delete函数来执行删除操作。

    m_Recordset_>Delete();

    删除之后,我们必须把当前记录更改为其他的记录,以确认这个删除动作。

    以上就是在MFC中使用DAO进行数据库操作的方法。

    了解了前面的内容,相信您对MFC类库已经有了比较深入的认识,可以使用MFC编写出不错的程序了。下面,我们将向您介绍如何在VISUAL C++集成开发环境之下调试自己的程序。

    5 打印

    打印功能是现在几乎所有的应用程序所必须具备的一项基本功能,这一点从APPWizard 生成的应用程序框架的菜单中缺省地包括打印和打印预览功能中可以看出来。但是打印功能的实现如果只依赖于以前的Windows API 调用来实现的话,是一件非常繁杂的事情。相比之下,Microsoft的基本类库应用框架则大大简化了打印功能的实现,并且还提供了打印预览功能。

    5.1打印和显示

    以前我们进行的输出工作都是向屏幕上的一块窗口区域中进行,而打印则是打印机向打印纸上输出一些东西。的确,这两者之间有很大的相似性,比如,它们都能输出文本,也都能输出一些图形。正是基于这些相似性,在 Windows 中,用设备上下文将它们之间的共性统一起来了。你在进行打印和输出时,你可以用相同的输出函数(如TextOut)来往屏幕或打印纸上输出。Windows 会在不同的情况下,将输出联系到相应的设备上。但是这并不意味着我们可以完全不考虑这两者之间的差别,而认为我们只要实现了屏幕输出功能就自动地实现了相应的打印功能。毕竟,它们之间有一些无法统一的差别:打印时,有页和分页的概念,即数据或输出是有条理地组织在一张张有一定大小的纸上;而屏幕输出时,则没有页和分页的功能,同时,可以认为屏幕输出的输出区域是没有大小限制的,超出窗口范围的,我们可以用滚动条来滚动。

    CView 有这样三个虚拟函数:OnPaint(),OnDraw(CDC *pDC),OnPrint()。OnPaint()是当视类窗口需要在屏幕上输出时被调用,负责完成窗口的屏幕输出显示工作;OnPrint()是当打印一页时被调用,负责完成向某页打印纸上打印。这两个函数的缺省实现中包含了对OnDraw(CDC *pDC)的调用。我们以前的程序中,并不考虑打印问题,所以我们被告之在OnDraw 中考虑如何完成屏幕输出工作。但现在不一样了,我们在OnDraw 中添加代码时,必须小心这有可能是在进行打印,并不是向一个窗口中输出。如果这两者在程序中不能统一,就必须分开考虑。分分开考虑有两种方法:一是分别在OnPaint和OnPrint 中完成屏幕输出和打印输出工作,而不必依赖于OnDraw。二是在OnDraw 中,我们可以调用pDC_>IsPrinting() 来识别目前是在进行哪种输出工作。如果是在进行打印,pDC_>IsPrinting()返回true。这样我们就可以在OnDraw 中区别对待了。

    5.2打印分页

    前面我们讲过打印输出一个很特殊的地方在于它的分页这一点上。要输出的的内容是被安排在不同的页上的。这样,当我们有超过一页的内容要显示时,我们就必须考虑分页,要能计算出显示到哪时必须换到下一页上去输出。这种拆页计算是有一定的麻烦的,我们会在后面结合给出的例子来讨论。现在这里要说明的是OnPrint的每一次调用对应着打印某一页。OnPrint(CDC *pDC,CPrintInfo *pInfo )中的第二个参数中包含了我们感兴趣的一些关于页的信息。CPrintInfo 类型的对象有一个m_nCurPage的公共成员属性,它告诉了我们目前是在打印哪一页。我们计算拆页时就显然要利用它。

    应用程序框架在每一次调用OnPrint打印一页之前,都会调用 OnPrepareDC(CDC *pDC,CPrintInfo *pinfo) 这个虚函数。这个虚函数在每次屏幕输出前也会被调用来允许做一些设置。在后一种情况下, 第二个参数pinfo等于NULL。打印时,我们可以在这里做一些针对某一页做一些设备上下文的设置工作。同时还有另一项重要的设置工作,我们可以设置打印工作的结束,即告诉应用框架,所有的页都已打印完了,打印可以结束了。这是通过设置 pinfo 的公共成员属性m_bContinuePrinting为false 来完成的。做此设置后,应用框架便不会再调用OnPrint,从而结束打印工作。

    5.3 打印工作的开始和结束

    前面我们讨论了每一页打印的准备工作,在每一次打印任务之前和之后,应用框架也允许我们进行一些设置工作。在每次打印工作开始时,应用程序会跳出一个打印对话框,如图3.13。

    而应用框架在跳出这个对话框之前,会调用OnPreparePrinting(CPrintInfo *pinfo),我们可以通过调用pinfo 的SetMaxPage和SetMinPage 来设置出现在对话框中的最大最小页。

    当关闭这个对话框之后,OnBeginPrinting 会被调用。

    当打印工作结束后,OnEndPrinting会被调用。

    图3.13

    5.4 打印程序实例

    在这里,我们制作了一个包含了打印功能的例子。它从一个学生信息数据库中抽取所有的记录,然后显示在一个CScrollView中,当用户要将它们打印出来时,程序按每页打印五条记录将这些记录打印出来。这个例子中用到了DAO 来访问数据库,在本书的前面章节中已介绍过,读者可以先复习一下,在本章我们不会详细解释相应的语句。

    在这个例子中,我们先利用APPWizard生成一个应用程序框架。注意在生成的第六步中将CPrintView 的基类从CView 改成

    图3.14

    CScrollView,如图3.14所示。

    在这个例子中,我们就分开考虑打印和显示输出,即在OnPrint中考虑打印,在OnPaint 中考虑显示输出。而在OnDraw 不做任何工作。

    在CPrintView中,我们添加一个虚函数:OnPreparePrinting,其代码如下:
    BOOL CPrintView::OnPreparePrinting(CPrintInfo* pInfo)

    {

    // default preparation

    pInfo_>SetMaxPage((Recnumber+4)/5);

    return DoPreparePrinting(pInfo);

    }

    这其中pInfo_>SetMaxPage((Recnumber+4)/5)用来设置打印的最大页数。Recnumber 是我们通过访问数据库计算出来的记录条数。我们前面说过,每五条记录分一页,(Recnumber+4)/5就可以得到页数。

    下面是OnBeginPrinting中的代码

    m_bPrintEnd=false;

    StuRecSet_>MoveFirst();

    if (StuRecSet_>IsEOF())

    m_bPrintEnd=true;

    m_bPrintEnd是我们往CPrintView 类中添加的一个用来指示是否该结束打印的成员。

    void CPrintView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo)

    {

    CScrollView::OnPrepareDC(pDC, pInfo);

    // TODO: Add your specialized code here and/or call the base class

    if (pInfo!=NULL)

    pInfo_>m_bContinuePrinting=!m_bPrintEnd;

    }

    上面是虚函数OnPrepareDC中的代码,注意if (pInfo!=NULL) 的判断,我们要识别是否是为打印做准备。

    下面是最重要的OnPrint中的代码:

    void CPrintView::OnPrint(CDC* pDC, CPrintInfo* pInfo)

    {

    // TODO: Add your specialized code here and/or

    //call the base class

    int Curpage=pInfo_>m_nCurPage;

    StuRecSet_>SetAbsolutePosition((Curpage_1)*5);

     

    //Assume to print 5 records every page

    TEXTMETRIC tm;

    int nColumnWidth[4];

    int i,j;

    pDC_>SetMapMode(MM_TWIPS);

    pDC_>GetTextMetrics(&tm);

     

    nheight=tm.tmHeight+tm.tmExternalLeading;

    for (i=0;i<4;i++)

    nColumnWidth[i]=(pDC_>GetTextExtent(ColumnName[i])).cx;

    RECT r;

    r.right=r.left=720;

    r.top=_720;

    r.bottom=(r.top_nheight);

     

    //print the column headers

    for (i=0;i<4;i++)

    {

    r.right=r.left+nColumnWidth[i];

    pDC_>ExtTextOut(r.left,r.top,ETO_CLIPPED,

    &r,ColumnName[i],NULL);

    r.left=r.right;

    }

    r.top_=nheight;

    r.bottom_=nheight;

     

    //print next 20 student records

    for (j=0;j<5;j++)

    {

    r.right=r.left=720;

    for (i=0;i<4;i++)

    {

    r.right=r.left+nColumnWidth[i];

    pDC_>ExtTextOut(r.left,r.top,ETO_CLIPPED,&r,

    CString(V_BSTRT(&(StuRecSet_>GetFieldValue(i)))),

    NULL);

    r.left=r.right;

    }

     

    //set for next record printing

    r.top_=nheight;

    r.bottom_=nheight;

    StuRecSet_>MoveNext();

    if (StuRecSet_>IsEOF())

    {

    m_bPrintEnd=true;

    break;

    }

    }

    CScrollView::OnPrint(pDC, pInfo);

    }

     

    pDC_>SetMapMode(MM_TWIPS);

    pDC_>GetTextMetrics(&tm);

    nheight=tm.tmHeight+tm.tmExternalLeading;

     

    这几句语句的作用是设置输出的映射方式,然后获取有关文本输出的一些参数,以便后面进行计算输出。

    //print the column headers

    for (i=0;i<4;i++)

    {

    r.right=r.left+nColumnWidth[i];

    pDC_>ExtTextOut(r.left,r.top,ETO_CLIPPED,

    &r,ColumnName[i],NULL);

    r.left=r.right;

    }

    以上这几句是在每一页上打印记录各个域的名称,以便下面输出各条记录。

    接下来就应该是输出五条记录(如果还有五条的话),它被包含在for(j=0;j<5;j++) 这个循环中。

    for (i=0;i<4;i++)

    {

    r.right=r.left+nColumnWidth[i];

    pDC_>ExtTextOut(r.left,r.top,

    ETO_CLIPPED,&r,

    CString(V_BSTRT(&(StuRecSet_>GetFieldValue(i)))),NULL);

    r.left=r.right;

    }

    这个for 循环是嵌在前面那个循环之中的,它负责将一条记录的每个域(共 4个)输出在某一行上。

    StuRecSet_>MoveNext();

    if (StuRecSet_>IsEOF())

    {

    m_bPrintEnd=true;

    break;

    }

    然后移向数据库的下一条记录,并判断是否已到了数据库中最后一条记录,如是,则跳出循环,并置m_bPrintEnd为true,以便下一次在OnPrepareDC 中能正确地结束打印。

    在我们给出的这个例子中,还是简化了很多的考虑的,特别是在拆页的计算上,我们简单地指定每五条记录一页,而一个真正的实用程序肯定是要通过获知纸张的大小(这可以通过CDC类的GetDeviceCaps来进行),然后计算得出一页上可以输出多少条记录。