posts - 1, comments - 2, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

ODBC编程

Posted on 2007-11-16 00:13 linuxlove 阅读(2312) 评论(2)  编辑 收藏 引用

 

 

6 MFC ODBC编程

为了简化开发人员编写数据库应用程序,VC++的在其基础类库(MFC)里对ODBC API进行了封装,实现了一个面向对象的数据库编程接口,使VC++的数据库变得更加容易。

本章首先对MFC ODBC的概貌进行简要介绍,然后讲述利用MFC ODBC进行数据库开发的技巧,最后将通过具体数据库开发实例,详细讲述通过MFC ODBC开发数据库应用程序的方法和过程。

6.1 了解MFC ODBC

MFCMicrosoft Foundation Class(微软基础类库)的缩写,它的设计目标是简化开发人员的工作。MFC使开发人员创建基于Windows的应用程序,而不必掌握下层的Windows体系结构。由于数据库应用程序是管理数据的重要方面,Microsoft开发了ODBC API的封装程序,为ODBC编程提供了一个面向对象的方法。

MFCODBC的封装主要是开发了CDatabase类和CRecordSet类。

6.1.1 CDatabase

CDatabase类用于应用程序建立同数据源的连接。CDatabase类包含一个m_hdbc变量,它代表了数据源的连接句柄。如果要建立CDatabase类的实例,应先调用该类的构造函数,再调用Open函数,通过调用,初始化环境变量,并执行与数据源的连接。关闭数据源连接的函数是Close

CDatabase类提供了对数据库进行操作的函数,为了执行事务操作,CDatabase类提供了BeginTrans函数,当全部数据都处理完成后,可以通过调用CommitTrans函数提交事务,或者在特殊情况下通过调用Rollback函数将处理回退。

CDatabase类提供的函数可以用于返回数据源的特定信息,例如通过GetConnect函数返回在使用函数Open连接数据源时的连接字符串,通过调用IsOpen函数返回当前的CDatabase实例是否已经连接到数据源上,通过调用CanUpdate函数返回当前的CDatabase实例是否是可更新的,通过调用CanTransact函数返回当前的CDatabase实例是否支持事务操作,等等。

总之,CDatabase类为C++数据库开发人员提供了ODBC的面向对象的编程接口。

6.1.2 CRecordSet

要实现对结果集的数据操作,就要用到CRecordSet类。CRecordSet类定义了从数据库接收或者发送数据到数据库的成员变量,CRecordSet类定义的记录集可以是表的所有列,也可以是其中的一列,这是由SQL语句决定的。

CRecordSet类的成员变量m_hstmt代表了定义该记录集的SQL语句句柄,m_nFields成员变量保存了记录集中字段的个数,m_nParams成员变量保存了记录集所使用的参数个数。

CRecordSet的记录集通过CDatabase实例的指针实现同数据源的连接,即CRecordSet的成员变量m_pDatabase

如果记录集使用了WHERE子句,m_strFilter成员变量将保存记录集的WHERE子句的内容,如果记录集使用了ORDER BY子句,m_strSort成员变量将保存记录集的ORDER BY子句的内容。

由多种方法可以打开记录集,最常用的方法是使用Open函数执行一个SQL SELECT语句。有如下四种类型的记录集:

·       CRecordset::dynaset
动态记录集,支持双向游标,并保持同所连接的数据源同步,对数据的更新操作可以通过一个fetch操作获取。

·       CRecordset::snapshot
静态快照,一旦形成记录集,此后数据源的所有改变都不能体现在记录集里,应用程序必须重新进行查询,才能获取对数据的更新。该类型记录集也支持双向游标。

·       CRecordset::dynamic
CRecordset::dynaset记录集相比,CRecordset::dynamic记录还能在fetch操作里同步其它用户对数据的重新排序。

·       CRecordset::forwardOnly
除了不支持逆向游标外,其它特征同CRecordset::snapshot相同。

6.2 MFC ODBC数据库访问技术

6.2.1 记录查询

使用CRecordSetOpen()Requery()成员函数可以实现记录查询。需要注意的是,在使用CRecordSet的类对象之前,必须使用CRecordSet的成员函数Open()来获得有效的记录集。一旦使用过Open()函数,再次查询时使用Requery()函数就可以了。在调用Open()函数时,如果已经将一个打开的CDatabase对象指针传递给CRecordSet类对象的m_pDatabase成员变量,那么,CRecordSet类对象将使用该数据库对象建立ODBC连接;否则,如果m_pDatabase为空指针,对象就需要就新建一个CDatabase类对象并使其与缺省的数据源相连,然后进行CRecordSet类对象的初始化。缺省数据源由GetDefaultConnect()函数获得。也可以通过特定的SQL语句为CRecordSet类对象指定数据源,并以它来调用CRecordSet类的Open()函数,例如:

         myRS.Open(AFX_DATABASE_USE_DEFAULT,strSQL);

如果没有指定参数,程序则使用缺省的SQL语句,即对在GetDefaultSQL()函数中指定的SQL语句进行操作,代码如下:

         CString CMyRS::GetDefaultSQL()

        {return _T("[Name],[Age]");}

对于GetDefaultSQL()函数返回的表名,对应的缺省操作是SELECT语句,例如:

        SELECT * FROM BasicData,MainSize

在查询过程中,也可以利用CRecordSet类的成员变量m_strFilterm_strSort来执行条件查询和结果排序。m_strFilter用于指定过滤字符串,存放着SQL语句中关键字WHERE后的条件语句;m_strSort用于指定用于排序的字符串,存放着SQL语句中关键字ORDER BY后的字符串。例如:

         myRS.m_strFilter="Name='刘鹏'";

         myRS.m_strSort="Age";

        myRS.Requery();

数据库查询中对应的SQL语句为:

         SELECT * FROM BasicData,MainSize WHERE Name='刘鹏' ORDER BY Age

除了直接赋值给成员变量m_strFilter以外,还可以通过参数化实现条件查询。利用参化可以更直观、更方便地完成条件查询任务。参数化方法的步骤如下:

(1) 声明参变量,代码如下:

         CString strName;

                  int nAge;

(2) 在构造函数中初始化参变量如下:

        strName =_T("");

        nAge =0;

         m_nParams=2;

(3) 将参变量与对应列绑定,代码如下:

         pFX->SetFieldType(CFieldExchange::param)

         RFX_Text(pFX,_T("Name"), strName);

         RFX_Single(pFX,_T("Age"), nAge);

完成以上步骤之后就可以利用参变量进行条件查询了,代码如下:

         m_pmyRS->m_strFilter="Name=? AND age=?";

         m_ pmyRS -> strName ="刘鹏";

         m_ pmyRS ->nAge=26;

         m_ pmyRS ->Requery();

参变量的值按绑定的顺序替换查询字串中的“?”通配符。

如果查询的结果是多条记录,可以利用CRecordSet类的成员函数Move()MoveNext()MovePrev()MoveFirst()MoveLast()来移动记录光标。

6.2.2 记录添加

使用AddNew()成员函数能够实现记录添加,需要注意的是,在记录添加之前必须保证数据库是以允许添加的方式打开的,代码如下:

         m_ pmyRS ->AddNew();     // 在表的末尾添加新记录

         m_ pmyRS ->SetFieldNull(&(m_pSet->m_type), FALSE);       

      m_ pmyRS ->m_strName="刘鹏";        // 输入新的字段值

      m_ pmyRS ->m_nAge=26; // 输入新的字段值

         m_ pmyRS ->     Update();    // 将新记录存入数据库

         m_ pmyRS ->Requery();      // 重新建立记录集

6.2.3 记录删除

调用Delete()成员函数能够实现记录删除,在调用Delete()函数后不需调用Update()函数,代码如下:

         m_ pmyRS ->Delete();

         if (!m_ pmyRS ->IsEOF())

                   m_ pmyRS ->MoveNext();

         else

                   m_ pmyRS ->MoveLast();

6.2.4 记录修改

调用Edit()成员函数可以实现记录修改,在修改完成后需要调用Update()将修改结果存入数据库,代码如下:

         m_ pmyRS ->Edit();             // 修改当前记录

         m_ pmyRS ->m_strName="刘波";         // 修改当前记录字段值

                   ...

        m_ pmyRS ->Update();                 // 将修改结果存入数据库

         m_ pmyRS ->Requery();

6.2.5 撤销数据库更新操作

如果用户增加或者修改记录后希望放弃当前操作,可以在调用Update()函数之前调 Move()函数,就可以使数据库更新撤销了,代码如下:

        CRecordSet::Move(AFX_MOVE_REFRESH);

该函数用于撤消增加或修改模式,并恢复在增加或修改模式之前的当前记录。其中参 AFX_MOVE_REFRESH的值为零。

6.2.6 直接执行SQL语句

虽然通过CRecordSet类我们可以完成大多数的数据库查询操作,而且在CRecordSet类的Open()成员函数中也可以提供SQL语句,但有的时候我们还想进行一些其他操作, 例如建立新表、删除表、建立新的字段等等,这时就需要用到CDatabase 类的直接执行SQL语句的机制。通过调用CDatabase类的ExecuteSQL()成员函数就能够完成QL语句的直接执行,代码如下:

         BOOL CMyDB::ExecuteSQLWithReport (const CString& strSQL)

         {

                           TRY

                            {

                                     m_pMyDB->ExecuteSQL(strSQL);       // 直接执行SQL语句

                            }

                           CATCH (CDBException,e)

                           {

                                      CString strMsg;

                                     strMsg.LoadString(IDS_EXECUTE_SQL_FAILED);

                                     strMsg+=strSQL;

                                     return FALSE;

                           }

                            END_CATCH

                            return TRUE;

         }

需要注意的是,由于不同DBMS提供的数据操作语句不尽相同,直接执行SQL语句可能会破坏软件的DBMS无关性,因此在应用中应当慎用此类操作。

6.2.7 MFC ODBC的数据库操作过程

ODBC API编程类似,MFCODBC编程也要先建立同ODBC数据源的连接,这个过程由一个CDatabase对象的Open函数实现。然后CDatabase对象的指针将被传递到CRecordSet对象的构造函数里,使CRecordSet对象与当前建立起来的数据源连接结合起来。

完成数据源连接之后,大量的数据库编程操作将集中在记录集的操作上。CRecordSet类的丰富的成员函数可以让开发人员轻松地完成基本的数据库应用程序开发任务。

当然,完成了所有的操作之后,在应用程序退出运行状态的时候,需要将所有的记录集关闭,并关闭所有同数据源的连接。

6.3 MFC ODBC编程实例

6.3.1 实例概述

需求调查与分析

某贸易公司经理需要对公司日常处理的业务通过计算机进行监控,因此需要开发一个浏览数据和报表的数据库应用软件,该软件的主要功能是数据表示和报表。数据的内容包括公司日常的销售报告、产品情况、客户情况、雇员情况以及与公司合作的运货商的情况,数据报表的主要目的是能够将浏览的信息以报表的方式打印出来。

数据库系统及其访问技术

在本实例里,我们采用MFCODBC数据库访问技术,从Access数据库里读取公司销售信息、产品情况、客户信息、雇员信息以及与公司合作的运货商的信息。这是一个小型的数据库应用,使用Access数据库就足够了。在这个实例里,我们借助MFCODBC封装类CRecordset,从该类派生应用程序里使用的CCommonRs类。以CCommonRs类为基础,实现对ODBC数据源的数据访问。

实例实现效果

ODBCDemo2是本书用于阐述MFC ODBC数据库编程的实例应用程序,该应用程序实现了对某销售公司日常销售业务的信息浏览和报表操作。应用程序运行界面如图6-1所示。

6-1 ODBCDemo2实例应用程序的运行界面

6.3.2 实例实现过程

数据库设计

我们利用Microsoft Access工具设计本实例的数据库结构。在本实例里,我们需要利用数据库存放销售公司的如下信息:

·       日常销售信息:指公司日常销售帐单中的客户信息、雇员信息、订购日期、货主信息、运货商信息。

·       产品信息:指公司现经营产品的产品名称、供应商信息、类别信息、单价以及库存量等信息。

·       客户信息:指公司客户的客户名称、联系人信息和客户的地址信息。

·       雇员信息:指公司雇员的雇员姓名、头衔、尊称、出生日期、雇佣日期、联系信息及其上级等信息。

·       供应商信息:指与公司合作的供应商的名称、地址、联系人等信息。

·       运货商信息:指与公司合作的运货商的名称、电话信息。

·       产品类别信息:指公司所经营产品的类别。

我们为数据库设计了八个表,表“订单”和表“订单明细”存放公司日常销售信息,表“产品”存放公司的产品信息,表“客户”存放公司的客户信息,表“雇员”存放公司的雇员信息,表“供应商”存放公司的产品供应商信息,表“运货商”存放公司的运货商信息,表“类别”存放公司经营产品的类别信息。为了便于数据访问,我们还定义了三个视图,“SalesByCustomer”视图管理着以客户为统计方式的销售信息,“SalesByEmployee”视图管理着以雇员为统计方式的销售信息,“SalesByProduct”视图管理着以产品为统计方式的销售信息。

下面的表6-1列出了表“订单”的结构,表6-2列出了表“订单明细”的结构,表6-3列出了表“产品”的结构,表6-4列出了表“客户”的结构,表6-5列出了表“雇员”的结构,表6-6列出了表“供应商”的结构,表6-7列出了表“运货商”的结构,表6-8列出了表“类别”的结构。

6-1 表“订单”的结构

字段名称

类型

字段名称

类型

订单ID(key)

自动编号

运货费

货币

客户ID

文本

货主名称

文本

雇员ID

数字

货主地址

文本

订购日期

日期/时间

货主城市

文本

到货日期

日期/时间

货主地区

文本

发货日期

日期/时间

货主邮政编码

文本

运货商

数字

货主国家

文本

6-2 表“订单明细”的结构

字段名称

类型

字段名称

类型

订单ID

自动编号

数量

数字

产品ID

数字

折扣

数字

单价

货币

 

 

6-3 表“产品”的结构

字段名称

类型

字段名称

类型

产品ID(key)

自动编号

单价

货币

产品名称

文本

库存量

数字

供应商ID

数字

订购量

数字

类别ID

数字

再订购量

数字

单位数量

文本

中止

/

6-4 表“客户”的结构

字段名称

类型

字段名称

类型

客户ID(key)

自动编号

地区

文本

公司名称

文本

邮政编码

文本

联系人姓名

文本

国家

文本

联系人头衔

文本

电话

文本

地址

文本

传真

文本

城市

文本

 

 

6-5 表“雇员”的结构

字段名称

类型

字段名称

类型

名字ID

自动编号

国家

文本

头衔

文本

邮政编码

文本

尊称

文本

家庭电话

文本

出生日期

日期/时间

分机

文本

雇用日期

日期/时间

照片

OLE对象

地址

文本

备注

备注

城市

文本

上级

文本

地区

文本

 

 

6-6 表“供应商”的结构

字段名称

类型

字段名称

类型

供应商ID(key)

自动编号

地区

文本

公司名称

文本

邮政编码

文本

联系人姓名

文本

国家

文本

联系人头衔

文本

电话

文本

地址

文本

传真

文本

城市

文本

主页

超级链接

6-7 表“运货商”的结构

字段名称

类型

字段名称

类型

运货商ID(key)

自动编号

电话

货币类型

公司名称

文本

 

 

6-8 表“类别”的结构

字段名称

类型

字段名称

类型

类别ID

自动编号

说明

备注

类别名称

文本

图片

OLE对象

在实例光盘的Database目录下,stocks.mdb文件是存放公司销售信息的Access数据库文件,读者可以查看这个文件,了解详细信息。

创建ODBCDemo2工程

ODBCDemo2工程是一个基于单文档的应用程序,创建应用程序工程时需要选择基于单文档的应用程序类型。

操作步骤:

1.            (1) 打开VC++的工程创建向导。从VC++的菜单中执行“File>New”命令,将VC++ 6.0工程创建向导显示出来。如果当前的选项标签不是Project,单击Project选项标签将它选中。在左边的列表里选择MFC AppWizardexe)项,在Project Name编辑区里输入工程名称“ODBCDemo2”,并在Location编辑区里调整工程路径,如图6-2所示。

6-2  工程创建向导

2.            (2) 选择应用程序的框架类型。单击“工程创建向导”窗口的OK按钮,进入“MFC AppWizard – Step 1”对话框。首先选择应用程序的框架类型。如图6-3所示。在本工程里,选择“Single document”,保持资源的语言类型为“中文”,单击“Next >”按钮。

6-3 选择应用程序的框架类型

3.            (3) 进入“MFC AppWizard – Step 2 of 6”对话框,设置应用程序数据库特性。在对话框里选择None,如图6-4所示。

4.            (4) 设置应用程序对复杂文档的支持。在“MFC AppWizard – Step 2 of 6”对话框里,单击“Next >”按钮,进入“MFC AppWizard – Step 3 of 6”对话框。在对话框里选择如下两项:

·       None

·       ActiveX Controls

6-4 设置应用程序数据库特性

如图6-5所示,单击“Next >”按钮。

6-5 设置应用程序对复杂文档的支持

5.            (5) 进入“MFC AppWizard – Step 4 of 6”对话框,设置应用程序的特征信息。如图6-6所示对话框是工程的特征信息,在本例中,ODBCDemo2工程有如下特征:

·       Docking toolbar

·       Initial statusbar

·       Printing and print preview

·       3D controls

·       Normal

6.            (6) 在“MFC AppWizard – Step 4 of 6”对话框里单击“Next >”按钮,进入“MFC AppWizard – Step 5 of 6”对话框,选择工程风格和MFC类库的加载方式。在对话框里设置如下三项:

·       MFC Standard

·       Yes, Please

·       As shared DLL

7.         如图6-7所示,单击“Next >”按钮,进入“MFC AppWizard – Step 6 of 6 对话框。

6-7 设置应用程序特征信息

6-8 选择工程风格和MFC类库的加载方式

8.            (7) MFC AppWizard – Step 6 of 6 对话框显示工程创建中的类信息。ODBCDemo2工程包含了四个类:

·       CODBCDemo2View类,工程视图类

·       CODBCDemo2App类,工程的应用类

·       CMainFrame类,工程主框架类

·       CODBCDemo2Doc类,工程文档类

这四个类构成了应用程序工程的主要框架。我们为CODBCDemo2View类选择ClistView基类,如图6-9所示。

6-9 显示工程创建中的类信息

9.            (8) 在对话框“MFC AppWizard – Step 6 of 6”里单击Finish按钮,完成工程创建。工程创建向导将该次工程创建的信息显示在一个“New Project Information”对话框里,如图6-10所示。在对话框里单击“OK”按钮,ODBCDemo2工程创建完成。

6-10 工程创建信息

建立数据源

建立数据源的方法在6.3.2节里已经进行了介绍,这里只说明本实例在建立数据源时的不同之处。

本实例用到的数据源名称是“ODBCDemo2”,其描述为“Data source for ODBC MFC programming”,创建数据源时在“配置创建的新数据源”步骤里需要同ODBCDemo1数据源有所区别。另外,数据源所支持的数据库为stocks.mdb文件,我们已经在6.3.2节里完成了它的设计。

设计应用程序界面

6.3.2节里,我们利用工程创建向导创建了一个基于单文档界面的工程,本节应用程序界面的设计工作主要是菜单和按钮的添加。为了为报表设置报表的内容和格式,我们还需要为应用程序设计一个报表向导,这个向导由三个步骤组成,因此我们需要设计三个对话框以实现这三个步骤的界面。

1. 设计应用程序的主菜单

需要为应用程序设计的菜单包括:销售报告菜单、产品信息菜单、客户信息菜单、雇员信息菜单、供应商信息菜单、运货商信息菜单、类别菜单以及报表操作菜单。这些菜单的标识、标题以及提示信息如表6-9所示。

6-9 工程的菜单资源

标识

标题

提示信息

文件

ID_OPTION

选项

应用程序设置选项

ID_APP_EXIT

退出

退出应用程序;提示保存文档\n退出

销售报告

ID_SALE_PRODUCT

按产品

按照产品显示销售报告。\n产品销售报告

ID_SALE_CUSTOMER

按客户

按照客户显示销售报告。\n客户销售报告

ID_SALE_EMPLOYEE

按雇员

按照雇员显示销售报告。\n雇员销售报告

产品

ID_VIEW_PRODUCT

产品信息

显示产品信息

客户

ID_VIEW_CUSTOMER

客户信息

显示客户信息

雇员

ID_EXPLOYEE

雇员信息

显示雇员信息

运货商

ID_VIEW_TRANSPORTOR

运货商信息

显示运货商信息

供应商

ID_VIEW_PROVIDER

供应商信息

显示供应商信息

报表

ID_REPORT_SETUP

设置

设置报表标题、字段内容、注脚等内容的字体。\n报表设置

ID_FILE_PRINT_PREVIEW

打印预览

显示整页\n打印预览

ID_FILE_PRINT_SETUP

打印设置

改变打印机及打印选项\n打印设置

ID_FILE_PRINT

打印

打印活动文档\n打印

2. 设计应用程序的按钮

需要为应用程序设计的按钮主要是销售报告的三种显示方式。这三个按钮的标识、图标以及提示信息如表6-10所示。

6-10 工程的按钮资源

标识

图标

提示信息

ID_SALE_PRODUCT

按照产品显示销售报告。\n产品销售报告

ID_SALE_EMPLOYEE

按照雇员显示销售报告。\n雇员销售报告

ID_SALE_CUSTOMER

按照客户显示销售报告。\n客户销售报告

3. 设计应用程序的报表向导

需要设计三个对话框:IDD_WZDFIELDIDD_WZDFORMATIDD_WZDPREVIEW。这三个对话框分别代表了报表向导的三个步骤:报表字段选择、格式设置以及设置信息浏览三个步骤。

10.         (1) 设计IDD_WZDFIELD对话框。使用VC++的“Insert>Resource”菜单命令可以将Dialog(对话框)资源加入到工程里。IDD_WZDFIELD对话框供用户选择报表字段,它的标题是“选择报表字段信息”,字段选择对话框的其它资源如表6-11所示。

6-11 IDD_WZDFIELD对话框的资源

资源类型

资源ID

标题

功能

编辑框

IDC_MAINTITLE

 

报表的大标题

编辑框

IDC_FOOTER

 

报表的脚注

编辑框

IDC_FIELDTITLE

 

报表的字段标题

列表框

IDC_LISTPRE

 

候选的报表字段列表

列表框

IDC_LISTSEL

 

选用的报表字段列表

组合框

IDC_CBPNFORMAT

 

页码类型组合框

复选框

IDC_CHKPAGENUM

使用页码

表示是否使用页码

标签

IDC_STATIC

报表标题:

报表标题标签

标签

IDC_STATIC

供选择报表字段:

候选报表字段列表标题

标签

IDC_STATIC

选用的报表字段:

选用报表字段列表标题

标签

IDC_STATIC

字段标题:

字段标题标签

标签

IDC_STATIC

注脚文本:

注脚文本标签

标签

IDC_STATIC

页码格式:

页码格式标签

按钮

IDC_SELECT

选用候选字段按钮

按钮

IDC_DESELECT

删除选用字段按钮

按钮

IDC_SELECTALL

>|

全部选中候选字段按钮

按钮

IDC_DESELECTALL

|<

全部删除选用字段按钮

设计完成后,字段选择对话框如图6-11所示。

6-11  设计完成后的字段选择对话框

11.         (2) 设计IDD_WZDFORMAT对话框。使用VC++的“Insert>Resource”菜单命令可以将Dialog(对话框)资源加入到工程里。IDD_WZDFORMAT对话框供用户选择报表格式,它的标题是“设置报表格式”。报表格式设置对话框的其它资源如表6-12所示。

6-12  IDD_WZDFORMAT对话框的资源

资源类型

资源ID

标题

功能

组合框

IDC_CBTITLEFONT

 

提供标题字体选择

组合框

IDC_CBHEADFONT

 

提供字段标题字体选择

组合框

IDC_CBNORMALFONT

 

提供正文字体选择

组合框

IDC_CBFOOTERFONT

 

提供注脚字体选择

组合框

IDC_CBTITLESIZE

 

提供标题字体大小选择

组合框

IDC_CBHEADSIZE

 

提供字段标题字体大小选择

组合框

IDC_CBNORMALSIZE

 

提供正文字体大小选择

组合框

IDC_CBFOOTERSIZE

 

提供注脚字体大小选择

静态控件

IDC_PREVIEWAREA

 

设置效果预览区域

标签

IDC_STATIC

字体名称:

字体标签

标签

IDC_STATIC

字体大小:

字体大小标签

标签

IDC_STATIC

标题(&T)

标题标签

标签

IDC_STATIC

列头(&M)

列头标签

标签

IDC_STATIC

正文(&M)

正文标签

标签

IDC_STATIC

注脚(&F)

注脚标签

标签

IDC_STATIC

预览:

预览区域标签

设计完成后,报表格式设置对话框如图6-12所示。

6-12  设计完成后的设置报表格式对话框

(3) 设计IDD_WZDPREVIEW对话框。使用VC++的“Insert>Resource”菜单命令可以将Dialog(对话框)资源加入到工程里。IDD_WZDPREVIEW对话框供用户选择报表信息的显示与确认,它的标题是“报表预览”。设置信息浏览对话框的其它资源如表6-13所示。

6-13 IDD_WZDPREVIEW对话框的资源

资源类型

资源ID

标题

功能

标签

IDC_REPORTINFO

Info

信息显示区域

静态控件

IDC_STATIC

信息

标签

设计完成后,设置信息浏览对话框如图6-13所示。

6-13  设计完成后的“报表预览”对话框

编写工程代码

1. 创建CCommonRs

CCommonRs类由CRecordset类派生而来。

创建CCommonRs类的操作步骤:

12.         (1) 执行菜单命令“Insert>New Class…”,VC++弹出“New Class 对话框,如图6-14所示。

6-14  New Class对话框

(2) 在“New Class 对话框里保持Class Type项为MFC Class,在Name编辑框里输入“CCommonRs”,然后在Base class下拉列表框里选择派生的父类CRecordset,如图6-15所示。

6-15  设置派生的类名称与父类

(3) 在“New Class 对话框里单击OK按钮,VC++弹出Database Options对话框,在这里,为CCommonRs设置缺省的数据源。选择ODBC数据源为刚刚创建的ODBCDemo2,保持其它设置,如图6-16所示。

6-16  Database Options对话框

(4) Database Options对话框里按下OK按钮,VC++弹出Select Database Tables对话框,为CCommonRs设置缺省的表,如图6-17所示。

(5) 这里的表并不表示对类CCommonRs的操作只能在这个表上进行,这只是一个缺省的设置,因此可以任意选择一个表,注意,如果不选择任何表,单击OK按钮时操作是不能进行下去的。选择任一个表,单击OK按钮,完成类CCommonRs的创建。

6-17 Select Database Tables对话框

2. 创建全局变量

整个应用程序是通过一个CCommonRs对象对数据库进行操作的,这个对象不仅应用在视图类里,还被报表向导引用,以获取结果集的字段信息。因此有必要将这个CCommonRs对象声明为全局对象。我们在ODBCDemo2View.cpp文件的头部对CCommonRs对象声明如下:

#include "CommonRs.h"

CCommonRs *m_pCommonRS;

第一条语句将类CCommonRs的声明包括到ODBCDemo2View.cpp文件里,第二条语句声明CCommonRs对象的引用指针。

3. CODBCDemo2View类里声明报表信息变量

为了将报表设置信息保存起来,下面为CODBCDemo2View类声明如下的报表信息变量:

public:

// 报表信息设置变量

         // 标题和注脚

         CString m_strReportTitle;

         CString m_strReportFooter;

         BOOL        m_fHavePage;

         UINT         m_uPageType;

         // 报表字段

         CStringArray m_saSelectedFields;

         CStringArray m_saSelectedFieldsTitle;

         // 报表格式

         // 报表字体

         CString       m_strFontTitle;

         CString       m_strFontContentHead;

         CString       m_strFontContent;

         CString       m_strFontFooter;

         // 字体大小

         UINT         m_uSizeTitle;

         UINT         m_uSizeContentHead;

         UINT         m_uSizeContent;

         UINT         m_uSizeFooter;

当报表向导完成了报表信息设置后,这些信息将保存到上述变量里。

4. 编写CODBCDemo2View类的消息响应函数

对所有菜单命令,应用程序都将在CODBCDemo2View类里响应。前面,我们已经介绍了使用VC++ClassWizard工具添加菜单命令响应函数的方法,这里只介绍如何为这些响应函数编写操作代码。

(1) 编写“销售报告>按产品”菜单命令响应函数OnSaleProduct()

OnSaleProduct()函数将销售报告按照产品进行统计,并将结果显示到视图里,它的代码如下:

         CListCtrl& ctrlList = (CListCtrl&) GetListCtrl();

         ctrlList.DeleteAllItems();

         while(ctrlList.DeleteColumn(0));

         UpdateWindow();

 

         CString strSQL;

         strSQL = _T("SELECT * FROM SalesByProduct;");

         if(!ShowInformation(strSQL))       AfxMessageBox("数据获取失败!");

         m_uStatus = TABLE_SPRODUCTS;

SQL语句“SELECT * FROM SalesByProduct”从数据库视图SalesByProduct里将所有数据检索出来,SalesByProduct视图是数据库设计阶段创建的一个数据库视图,它的意义就在于将销售报告(订单)按照产品进行统计。函数ShowInformation()strSQL里的SQL语句检索结果显示到视图里。

(2) 编写“销售报告>按客户”菜单命令响应函数OnSaleCustomer ()

OnSaleProduct()函数将销售报告按照客户进行统计,并将结果显示到视图里,它的代码如下:

         CListCtrl& ctrlList = (CListCtrl&) GetListCtrl();

         ctrlList.DeleteAllItems();

         while(ctrlList.DeleteColumn(0));

         UpdateWindow();

 

         CString strSQL(_T("select * from SalesByCustomer"));

         if(!ShowInformation(strSQL))       AfxMessageBox("数据获取失败!");

         m_uStatus = TABLE_SCUSTOMERS;

该函数操作方法同OnSaleProduct()函数类似,不同的是,它从SalesByCustomer数据库视图里检索数据。SalesByCustomer数据库视图能够将销售报告(订单)按照客户进行统计。

(3) 编写“销售报告>按雇员”菜单命令响应函数OnSaleEmployee ()

OnSaleEmployee ()函数将销售报告按照雇员进行统计,并将结果显示到视图里,它的代码如下:

         CListCtrl& ctrlList = (CListCtrl&) GetListCtrl();

         ctrlList.DeleteAllItems();

         while(ctrlList.DeleteColumn(0));

         UpdateWindow();

 

         CString strSQL(_T("select * from SalesByEmployee"));

         if(!ShowInformation(strSQL))       AfxMessageBox("数据获取失败!");

         m_uStatus = TABLE_SIMPLOYEES;

该函数操作方法同OnSaleProduct()函数类似,不同的是,它从SalesByEmployee数据库视图里检索数据。SalesByEmployee数据库视图能够将销售报告(订单)按照雇员进行统计。

(4) 编写“产品>产品信息”菜单命令响应函数OnViewProduct ()

OnViewProduct ()函数将产品信息显示到视图里,它的代码如下:

         CListCtrl& ctrlList = (CListCtrl&) GetListCtrl();

         ctrlList.DeleteAllItems();

         while(ctrlList.DeleteColumn(0));

         UpdateWindow();

 

         CString strSQL;

         CString strSQL(_T("select * from 产品"));

         if(!ShowInformation(strSQL))       AfxMessageBox("数据获取失败!");

         m_uStatus = TABLE_SPRODUCTS;

该函数从“产品”数据库表里检索数据,并将检索结果显示到视图里。

(5) 编写“客户>客户信息”菜单命令响应函数OnViewCustomer ()

OnViewCustomer ()函数将客户信息显示到视图里,它的代码如下:

         CListCtrl& ctrlList = (CListCtrl&) GetListCtrl();

         ctrlList.DeleteAllItems();

         while(ctrlList.DeleteColumn(0));

         UpdateWindow();

 

         CString strSQL(_T("select * from 客户"));

         if(!ShowInformation(strSQL))       AfxMessageBox("数据获取失败!");

         m_uStatus = TABLE_CUSTOMERS;

该函数从“客户”数据库表里检索数据,并将检索结果显示到视图里。

(6) 编写“雇员>雇员信息”菜单命令响应函数OnExployee ()

OnExployee ()函数将雇员信息显示到视图里,它的代码如下:

         CListCtrl& ctrlList = (CListCtrl&) GetListCtrl();

         ctrlList.DeleteAllItems();

         while(ctrlList.DeleteColumn(0));

         UpdateWindow();

 

         CString strSQL(_T("select * from 雇员"));

         if(!ShowInformation(strSQL))       AfxMessageBox("数据获取失败!");

         m_uStatus = TABLE_IMPLOYEES;

该函数从“雇员”数据库表里检索数据,并将检索结果显示到视图里。

(7) 编写“供应商>供应商信息”菜单命令响应函数OnViewProvider ()

OnViewProvider ()函数将供应商信息显示到视图里,它的代码如下:

         CListCtrl& ctrlList = (CListCtrl&) GetListCtrl();

         ctrlList.DeleteAllItems();

         while(ctrlList.DeleteColumn(0));

         UpdateWindow();

 

         CString strSQL(_T("select * from 供应商"));

         if(!ShowInformation(strSQL))       AfxMessageBox("数据获取失败!");

         m_uStatus = TABLE_PRODUCTS;

该函数从“供应商”数据库表里检索数据,并将检索结果显示到视图里。

(8) 编写“运货商>运货商信息”菜单命令响应函数OnViewTransportor ()

OnViewTransportor ()函数将运货商信息显示到视图里,它的代码如下:

         CListCtrl& ctrlList = (CListCtrl&) GetListCtrl();

         ctrlList.DeleteAllItems();

         while(ctrlList.DeleteColumn(0));

         UpdateWindow();

 

         CString strSQL(_T("select * from 运货商"));

         if(!ShowInformation(strSQL))       AfxMessageBox("数据获取失败!");

         m_uStatus = TABLE_PRODUCTS;

该函数从“运货商”数据库表里检索数据,并将检索结果显示到视图里。

(9) 编写ShowInformation()函数

首先在CODBCDemo2View类的声明里添加该函数的声明:

public:

         BOOL ShowInformation(CString strSQL);

然后在CODBCDemo2View类的实现文件里添加该函数的实现代码:

BOOL CODBCDemo2View::ShowInformation(CString strSQL)

{

         CRect rect;

         CListCtrl& ctrlList = (CListCtrl&) GetListCtrl();

         ctrlList.GetWindowRect(rect);

 

         try{

                            BeginWaitCursor();

                           // 如果结果集已被打开,则关闭它

                            if(m_pCommonRS->IsOpen()) m_pCommonRS->Close(); 

                           // 打开结果集

                            m_pCommonRS->Open(CRecordset::dynaset, strSQL);

                            if(!m_pCommonRS->IsEOF()){

                                     m_pCommonRS->MoveLast();

                                     m_pCommonRS->MoveFirst();

                            }

                           // 取得结果集的字段个数

                            int nFieldCount = m_pCommonRS->GetODBCFieldCount();            

                            CODBCFieldInfo fieldinfo;

                          // 读取字段信息

                            for(int n=0;n<nFieldCount;n++){

                                     m_pCommonRS->GetODBCFieldInfo(n, fieldinfo);

                                     int nWidth = ctrlList.GetStringWidth(fieldinfo.m_strName) + 15;

                                     ctrlList.InsertColumn(n, fieldinfo.m_strName,

                                                                          LVCFMT_LEFT, nWidth);

                           }

                            // 读取记录信息

                           CString strValue;

                            m_pCommonRS->MoveFirst();

                            int nCount = 0;

                           while(!m_pCommonRS->IsEOF()){

                                     ctrlList.InsertItem(nCount, strValue);

                                     for(int j=0;j<nFieldCount;j++){

                                              m_pCommonRS->GetFieldValue(j, strValue);

                                              ctrlList.SetItemText(nCount, j, strValue);

                                     }

                                     m_pCommonRS->MoveNext();

                                     nCount ++;

                           }

                           EndWaitCursor();

                   }

                   catch(CDBException *e){

                            e->ReportError();

                            EndWaitCursor();

                            return FALSE;

                   }

         return TRUE;

}

该函数首先检测m_pCommonRS对象的状态,如果该对象处于打开状态,则关闭这个对象。接下来m_pCommonRS对象打开strSQL所包含的SQL语句结果集,并将结果集游标移动到头部。ShowInformation()函数需要取得结果集的字段信息,作为结果集显示的标题信息,m_pCommonRS对象的GetODBCFieldInfo()能够实现这个功能。最后函数读取结果集中的数据,并显示在视图里。

5. 编写报表向导操作代码

报表向导操作包括三个步骤:报表字段的选择、格式的设置以及设置信息的确认,因此制作向导时需要三个对话框。6.3.2节已经完成了报表向导的界面设计,下面我们首先介绍PropertySheetPropertyPage的创建方法,然后分别介绍三个步骤的实现代码。

·       创建报表向导的CPropertySheet

创建CPropertySheet类的操作步骤:

(1) 执行菜单命令“Insert>New Class…,VC++弹出“New Class 对话框,如图6-18所示。

6-18  New Class对话框

(2) 在“New Class 对话框里保持Class Type项为MFC Class,在Name编辑框里输入“CReportWizard”,然后在Base Class下拉列表框里选择派生的父类CPropertySheet,如图6-19所示。

(3) 单击对话框的OK按钮,VC++CReportWizard类添加到工程里。此后,我们就可以在这个类里添加PropertyPage了。在后面我们完成了所有的PropertyPage创建以后,我们将在该类的实现代码里添加PropertyPage插入操作。

·       创建报表向导的CPropertyPage

6-19  设置派生的类名称与父类

这里需要创建三个CPropertyPage类,分别代表IDD_WZDFIELDIDD_WZDFORMATIDD_WZDPREVIEW对话框。这三个类的名称分别是CReportWzdFieldCReportWzdFormatCReportWzdPreview,它们的创建方法是相同的,这里只介绍CReportWzdField类的创建方法。

操作步骤:

(1) 执行菜单命令“Insert>New Class…,VC++弹出“New Class 对话框,如图6-18所示。

(2) 在“New Class 对话框里保持Class Type项为MFC Class,在Name编辑框里输入“CReportWzdField”,然后在Base Class下拉列表框里选择派生的父类CPropertyPage,在Dialog ID组合框里选择IDD_WZDFIELDS项,如图6-20所示。注意,在创建CReportWzdFormat类时需要选择IDD_WZDFORMAT,在 创建CReportWzdPreview类时需要选择IDD_WZDPREVIEW

6-20  设置派生的类名称与父类

(3) 单击对话框的OK按钮,VC++CReportWzdField类添加到工程里。

·       编写报表向导CPropertySheet类的实现代码

CPropertySheet类的实现代码主要是在类的构造函数里将三个PropertyPage插入到PropertySheet里,同时设置PropertySheet为向导方式。我们首先在ReportWizard.h文件里添加下面的头文件引用代码:

#include "ReportWzdField.h"

#include "PreviewWnd.h"

#include "ReportWzdPreview.h"

然后在类的声明代码里添加如下声明代码:

public:

         CReportWzdField m_Page1;

         CReportWzdFormat m_Page2;

         CReportWzdPreview m_Page3;

在构造函数里,我们添加如下实现代码:

         AddPage(&m_Page1);

         AddPage(&m_Page2);

         AddPage(&m_Page3);

                   SetWizardMode();

前三个函数将三个PropertyPage插入到PropertySheetSetWizardMode()函数设置PropertySheet为向导方式。

·       编写CReportWzdField类的实现代码

(1) 首先需要创建类里与对话框中控件相关联的变量,这些变量可以通过ClassWizard工具添加。通过ClassWizard工具添加变量的方法,本书已经在6.3.2节作了介绍,这里将添加变量的名称、类型、关联的控件ID及其意义罗列出来,如表6-14所示。

6-14 CReportWzdField类的变量

名称

类型

关联的控件ID

意义

m_LBFieldCandi

ClistBox

IDC_LISTPRE

候选的报表字段列表

m_LBSelected

CListBox

IDC_LISTSEL

选用的报表字段列表

m_strReportTitle

CString

IDC_MAINTITLE

报表标题

m_strFieldTitle

CString

IDC_FIELDTITLE

报表字段标题

m_strReportFooter

CString

IDC_FOOTER

报表注脚标题

m_CBPageType

CComboBox

IDC_CBPNFORMAT

页码类型列表

m_fUsePageNum

BOOL

IDC_CHKPAGENUM

是否使用页码复选框

(2) 编写OnSetActive()消息响应函数

该函数在属性页被激活时执行,将该属性页的向导按钮只显示出“下一步”,需要在该函数里添加如下代码:

   CPropertySheet* psheet = (CPropertySheet*) GetParent();  

   psheet->SetWizardButtons(PSWIZB_NEXT);

SetWizardButtons()的作用即是令本属性页的向导按钮只显示“下一步”。

(3) 编写属性页的OnInitDialog()函数

该函数实现本属性页的初始化操作。初始化时,需要将当前显示在视图里的报表信息字段名称罗列在候选列表框里,实现代码如下:

         if(!m_pCommonRS->IsOpen()) return TRUE; 

         int nFieldCount = m_pCommonRS->GetODBCFieldCount();            

         m_LBFieldCandi.ResetContent();

         CODBCFieldInfo fieldinfo;

         for(int n=0;n<nFieldCount;n++){

                 m_pCommonRS->GetODBCFieldInfo(n, fieldinfo);

                 m_LBFieldCandi.InsertString(-1, fieldinfo.m_strName);

         }

         UpdateData(FALSE);

代码通过m_pCommonRS实例的GetODBCFieldInfo函数获取记录集的字段信息。

(4) 编写选择按钮“>”的响应函数OnSelect()

可以通过ClassWizard工具为ID_SELECT按钮添加消息响应函数,使用ClassWizard工具为按钮添加消息响应函数的方法,本书在6.3.2的第5节已经作了介绍,这里不再赘述。该函数实现将候选列表里的字段送到选用字段列表里,它的实现代码如下:

         UpdateData();

         CString strFieldName;

         UINT nSel = m_LBFieldCandi.GetCurSel();

         if(nSel == LB_ERR) return;

         int nSelected;

         m_LBFieldCandi.GetText(nSel, strFieldName);

         nSelected = m_LBSelected.AddString(strFieldName);

         m_LBFieldCandi.DeleteString(nSel);

         m_saFieldTitle.Add(strFieldName);

         UpdateData(FALSE);

代码首先使用UpdateData()函数将界面上输入的变量内容保存到变量里,然后取m_LBFieldCandi控件的当前选择项索引,并通过该索引将项的文本保存到strFieldName变量里,接下来将该文本作为LBSelected`控件的一个项添加进去,并删除m_LBFieldCandi控件的选择项。变量m_saFieldTitle是存放字段标题的CstringArray类型变量,这里需要为该变量添加一个新元素。最后再次调用UpdateData()函数将变量内容显示到界面上。

(5) 编写选择按钮“<”的响应函数OnDeselect()

同样使用ClassWizard工具为ID_DESELECT按钮添加消息响应函数OnDeselect(),该函数将选用字段列表里的选择项“删除”并送回候选字段列表里,它的实现代码如下:

         UpdateData();    

         CString strFieldName;

         UINT nSel = m_LBSelected.GetCurSel();

         if(nSel == LB_ERR) return;

         int nDeselected;

         m_LBSelected.GetText(nSel, strFieldName);

         nDeselected = m_LBFieldCandi.AddString(strFieldName);

         m_LBSelected.DeleteString(nSel);

         int nArrayCount = m_saFieldTitle.GetSize();

         if(nDeselected < nArrayCount)   m_saFieldTitle.RemoveAt(nDeselected);

         m_strFieldTitle.Empty(); 

         UpdateData(FALSE); 

代码同OnSelect()函数大致相似,不同之处是,将m_LBSelected列表里的选择项删除,并将该项恢复到m_LBFieldCandi列表里,同时需要将m_saFieldTitle变量里相应的项删除。

(6) 编写选择按钮“>|”的响应函数OnSelectall ()

该函数将候选字段里的所有字段送到选用字段列表里,它的实现代码如下:

         UpdateData();

         CString strFieldName;

         UINT nCount = m_LBFieldCandi.GetCount();

         if(nCount <= 0) return;

         for(UINT iIdx = 0; iIdx<nCount; iIdx++){

                 strFieldName.Empty();

                            m_LBFieldCandi.GetText(0, strFieldName);

                            m_LBSelected.AddString(strFieldName);

                            m_LBFieldCandi.DeleteString(0); 

                            m_saFieldTitle.Add(strFieldName);

         }

         UpdateData(FALSE);

代码同OnSelect()函数大致相似,不同之处是,使用循环将所有候选字段送到选用字段列表里。

(7) 编写选择按钮“|<”的响应函数OnDeselectall ()

该函数将选用字段里的所有字段恢复到候选字段列表里,它的实现代码如下:

         UpdateData();    

         CString strFieldName;

         UINT nCount = m_LBSelected.GetCount();

         if(nCount <= 0) return;

         for(UINT iIdx = 0; iIdx<nCount; iIdx++){

                   strFieldName.Empty();

                   m_LBSelected.GetText(0, strFieldName);

                   m_LBFieldCandi.AddString(strFieldName);

                   m_LBSelected.DeleteString(0);      

         }

         m_saFieldTitle.RemoveAll();

         m_strFieldTitle.Empty(); 

         UpdateData(FALSE);

代码同OnDeselect()函数大致相似,不同之处是,使用循环将所有选用字段恢复到候选字段列表里。

(8) IDC_LISTSEL列表控件编写LBN_SELCHANGE消息响应函数OnSelchangeListsel(),该函数在IDC_LISTSEL列表控件选择项发生改变时执行,它的执行代码如下:

         UpdateData();

         UINT nSel = m_LBSelected.GetCurSel();

         if(nSel == LB_ERR) return;

         UINT nArrayCount = m_saFieldTitle.GetSize();

         if(nSel >= nArrayCount) return;

         CString strFieldTitle;

         strFieldTitle = m_saFieldTitle.GetAt(nSel);

         m_strFieldTitle = strFieldTitle;

         m_uSelIndex = nSel;

         UpdateData(FALSE);

代码通过取得选用字段的索引,从字段标题变量m_saFieldTitle里读取文本,并显示在IDC_FIELDTITLE编辑控件里。

(9) IDC_FIELDTITLE编辑控件编写EN_KILLFOCUS消息响应函数OnKillfocusFieldtitle (),该函数在IDC_FIELDTITLE编辑控件失去输入焦点时执行,它的执行代码如下:

         UpdateData();

         UINT nArrayCount = m_saFieldTitle.GetSize();

         if(m_uSelIndex < nArrayCount)

                           m_saFieldTitle.SetAt(m_uSelIndex, m_strFieldTitle);

代码将控件当前内容保存到字段标题变量m_saFieldTitle里。

(10) IDC_CHKPAGENUM复选控件编写BN_CLICKED消息响应函数OnChkpagenum (),该函数在IDC_CHKPAGENUM复选控件被单击时执行,它的执行代码如下:

         UpdateData();

         if(m_fUsePageNum){

                 GetDlgItem(IDC_PAGECAP)->EnableWindow(TRUE);

                            GetDlgItem(IDC_CBPNFORMAT)->EnableWindow(TRUE);

         }

         else{

                 GetDlgItem(IDC_PAGECAP)->EnableWindow(FALSE);

                            GetDlgItem(IDC_CBPNFORMAT)->EnableWindow(FALSE);

         }

代码在控件被选中时将IDC_PAGECAP标签和IDC_CBPNFORMAT列表框设置为有效,否则相反。

(11) 编写“下一步”按钮的消息响应函数OnWizardNext()

该函数在单击了“下一步”按钮时执行,它的执行代码如下:

         UpdateData();

         CReportWizard *psheet = (CReportWizard *) GetParent();  

         psheet->m_strReportTitle = m_strReportTitle;

         psheet->m_strReportFooter = m_strReportFooter;

         psheet->m_fHavePage = m_fUsePageNum;

         psheet->m_uPageType = m_CBPageType.GetCurSel();

         CString strFieldName;

         for(int i=0;i<m_LBSelected.GetCount();i++){

                 m_LBSelected.GetText(i, strFieldName);

                           psheet->m_saSelectedFields.Add(strFieldName);

         }

         for(i=0;i<m_saFieldTitle.GetSize();i++)

                            psheet->m_saSelectedFieldsTitle.Add(m_saFieldTitle.GetAt(i));

代码将当前属性页里的设置保存到CReportWizard类的相应变量里。

·       编写CReportWzdFormat类的实现代码

(1) 首先创建类里与对话框中控件相关联的变量,这些变量可以通过ClassWizard工具添加, 如表6-15所示,为添加变量的名称、类型、关联的控件ID及其意义。

6-15 CReportWzdFormat类的变量

名称

类型

关联的控件ID

意义

m_CBFontTitle

CFontCombo

IDC_CBTITLEFONT

标题字体列表

m_CBFontContentHead

CFontCombo

IDC_CBHEADFONT

字段标题字体列表

m_CBFontContent

CFontCombo

IDC_CBNORMALFONT

正文字体列表

m_CBFontFooter

CFontCombo

IDC_CBFOOTERFONT

注脚字体列表

CFontCombo类是一个第三方开发类,它的声明代码以及实现代码分别在文件FontCombo.hFontCombo.cpp里,我们只要将这两个文件添加到工程里就可以使用。使用时,需要在CFontCombo类使用的地方加入头文件,有如下代码:

#include "FontCombo.h"

(2) 编写OnSetActive()消息响应函数

该函数在属性页被激活时执行,需要该属性页的向导按钮显示出“上一步”和“下一步”,在该函数里添加如下代码:

   CPropertySheet* psheet = (CPropertySheet*) GetParent();  

   psheet->SetWizardButtons(PSWIZB_BACK | PSWIZB_NEXT);

(3) 编写属性页的OnInitDialog()函数

该函数实现本属性页的初始化操作。初始化操作包括:四个字体列表框的初始化,四个字号大小组合框的初始化,实现代码如下:

         m_CBFontTitle.SubclassDlgItem(IDC_CBTITLEFONT, this);

         m_CBFontContentHead.SubclassDlgItem(IDC_CBHEADFONT, this);

         m_CBFontContent.SubclassDlgItem(IDC_CBNORMALFONT, this);

         m_CBFontFooter.SubclassDlgItem(IDC_CBFOOTERFONT, this);

         m_CBFontFooter.Initialize();

         m_CBFontContentHead.Initialize();

         m_CBFontContent.Initialize();

         m_CBFontTitle.Initialize();

        

         int nIndex = 0;

         nIndex = m_CBFontFooter.FindStringExact(0, "宋体");

         m_CBFontFooter.SetCurSel(nIndex);

         nIndex = m_CBFontContentHead.FindStringExact(0, "宋体");

         m_CBFontContentHead.SetCurSel(nIndex);

         nIndex = m_CBFontContent.FindStringExact(0, "宋体");

         m_CBFontContent.SetCurSel(nIndex);

         nIndex = m_CBFontTitle.FindStringExact(0, "宋体");

         m_CBFontTitle.SetCurSel(nIndex);

         ((CComboBox *)GetDlgItem(IDC_CBTITLESIZE))->SetCurSel(0);

         ((CComboBox *)GetDlgItem(IDC_CBHEADSIZE))->SetCurSel(0);

         ((CComboBox *)GetDlgItem(IDC_CBNORMALSIZE))->SetCurSel(0);

         ((CComboBox *)GetDlgItem(IDC_CBFOOTERSIZE))->SetCurSel(0);

(4) IDC_CBTITLEFONT(IDC_CBNORMALFONTIDC_CBHEADFONTIDC_CBFOOTERFONTIDC_CBTITLESIZEIDC_CBNORMALSIZEIDC_ CBFOOTERSIZEIDC_CBHEADSIZE组合控件编写CBN_SELCHANGE消息响应函数OnSelchangeCbTitlefont()OnSelchangeCbNormalfont ()OnSelchangeCbheadfont ()OnSelchangeCbFooterfont ()OnSelchangeCbTitlesize ()OnSelchangeCbNormalsize ()OnSelchangeCbFootersize ()OnSelchangeCbheadsize (),这些函数在组合控件选择项发生改变时执行,都是调用RefreshDisplay()函数刷新预览区域的显示,执行代码相同,如下:

         UpdateData();

         RefreshDisplay();                 

(5) 编写RefreshDisplay()函数

该函数根据八个列表框里字体和字体大小的选择,重新显示预览内容,执行代码如下:

        // 取显示设备环境DC

         CDC *pDC = GetDlgItem(IDC_PREVIEWAREA)->GetDC();

         if(!pDC) return; 

        // 填充区域

         CRect rect;

         GetDlgItem(IDC_PREVIEWAREA)->GetClientRect(rect);

         CBrush brush;

         brush.CreateSolidBrush(RGB(255,255,255));

         pDC->FillRect(rect, &brush);

         // 显示

         CString line;        // 显示内容

         TEXTMETRIC metrics;       //Font measurements

         int y = 0;             //Current y position on report

        // 创建字体

         CFont TitleFont;          //Font for Title

         CFont HeadingFont;    //Font for headings

         CFont DetailFont;        //Font for detail lines

         CFont FooterFont;       //Font for footer lines

         // 设置TAB间隔

         int TabStops[] = {100, 275, 650};

         CString strFontName;

         CString strSize;

         UINT         uFontSize;

         CComboBox *pSizeCB;

         // 为标题设置粗体

         m_CBFontTitle.GetLBText(m_CBFontTitle.GetCurSel(), strFontName);

         pSizeCB = (CComboBox *)GetDlgItem(IDC_CBTITLESIZE);

         pSizeCB->GetLBText(pSizeCB->GetCurSel(), strSize);

         uFontSize = atoi(strSize);

         TitleFont.CreateFont(uFontSize, 0, 0, 0, FW_BOLD, FALSE, TRUE, 0,

                           ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,

                            DEFAULT_QUALITY, DEFAULT_PITCH | FF_ROMAN,

                            strFontName);

         // 为字段标题设置粗体和下划线

         m_CBFontContentHead.GetLBText(m_CBFontContentHead.GetCurSel(),

                           strFontName);

         pSizeCB = (CComboBox *)GetDlgItem(IDC_CBHEADSIZE);

         pSizeCB->GetLBText(pSizeCB->GetCurSel(), strSize);

         uFontSize = atoi(strSize);

         HeadingFont.CreateFont(uFontSize, 0, 0, 0, FW_BOLD, FALSE, TRUE, 0,

                   ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,

                   DEFAULT_QUALITY, DEFAULT_PITCH | FF_ROMAN,

                   strFontName);

         // 正文字体

         m_CBFontContent.GetLBText(m_CBFontContent.GetCurSel(), strFontName);

         pSizeCB = (CComboBox *)GetDlgItem(IDC_CBNORMALSIZE);

         pSizeCB->GetLBText(pSizeCB->GetCurSel(), strSize);

         uFontSize = atoi(strSize);

         DetailFont.CreateFont(uFontSize, 0, 0, 0, FW_NORMAL, FALSE, FALSE, 0,

                 ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,

                           DEFAULT_QUALITY, DEFAULT_PITCH | FF_ROMAN,

                           strFontName);   

         // 注脚字体

         m_CBFontFooter.GetLBText(m_CBFontFooter.GetCurSel(), strFontName);

         pSizeCB = (CComboBox *)GetDlgItem(IDC_CBFOOTERSIZE);

         pSizeCB->GetLBText(pSizeCB->GetCurSel(), strSize);

         uFontSize = atoi(strSize);

         FooterFont.CreateFont(uFontSize, 0, 0, 0, FW_NORMAL, FALSE, FALSE, 0,

                            ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,

                            DEFAULT_QUALITY, DEFAULT_PITCH | FF_ROMAN,

                            strFontName);

        // 开始显示

         CFont* OldFont = pDC->SelectObject(&TitleFont);

         // 取显示文本尺寸

         pDC->GetTextMetrics(&metrics);

         // 计算高度

         int LineHeight = metrics.tmHeight + metrics.tmExternalLeading;

         // 显示标题

         y = 2;        

         pDC->TextOut(rect.Width()/2 - 70, y, "   ");

         // 显示列标题

         pDC->GetTextMetrics(&metrics);

         LineHeight = metrics.tmHeight + metrics.tmExternalLeading;

         y += LineHeight + 1;   //Adjust y position

         pDC->SelectObject(&HeadingFont);

         line.Format("%s\t%s\t%s","1","2","3");

         pDC->TabbedTextOut(10, y, line, 2, TabStops, 0);

         // 显示正文

         pDC->GetTextMetrics(&metrics);

         LineHeight = metrics.tmHeight + metrics.tmExternalLeading;

         y += LineHeight + 1;   //Adjust y position

         pDC->SelectObject(&DetailFont);

         line.Format("%s\t%s\t%s", "1","2","3");

         pDC->TabbedTextOut(10, y, line, 2, TabStops, 0);

         // 显示注脚

         pDC->GetTextMetrics(&metrics);

         LineHeight = metrics.tmHeight + metrics.tmExternalLeading;

         y += LineHeight + 1;   //Adjust y position

         pDC->SelectObject(&FooterFont);

         line = _T("注脚");

         pDC->TextOut(rect.Width() - 40, y, line);

         // 恢复设备环境里的字体

         pDC->SelectObject(OldFont);

上述代码首先取得显示的设备环境的显示尺寸,然后,用白色将显示区域填充;接着创建四种字体,并分别用四种字体显示与预览文本,最后恢复显示设备环境里的字体信息。这里要注意的是,TextOut()函数值将文本简单显示在指定区域,而TabbedTextOut ()函数可以显示TAB文本,即在文本里插入了TAB字符。GetTextMetrics()函数用于取得当前显示设备环境的文字尺寸。

(6) 编写“下一步”按钮的消息响应函数OnWizardNext()

该函数在单击“下一步”按钮时执行,它的执行代码如下:

         UpdateData();

         CReportWizard *psheet = (CReportWizard *) GetParent();  

         // 保存字体信息

         CString strFontName;

         m_CBFontTitle.GetLBText(m_CBFontTitle.GetCurSel(), strFontName) ;

         psheet->m_strFontTitle = strFontName;

         m_CBFontContentHead.GetLBText(m_CBFontContentHead.GetCurSel(),

                                                                                          strFontName) ;

         psheet->m_strFontContentHead = strFontName;

         m_CBFontContent.GetLBText(m_CBFontContent.GetCurSel(), strFontName) ;

         psheet->m_strFontContent = strFontName;

         m_CBFontFooter.GetLBText(m_CBFontFooter.GetCurSel(), strFontName) ;

         psheet->m_strFontFooter = strFontName;

         // 保存字体大小信息

         UINT         uFontSize;

         CString strSize;

         CComboBox *pCombo;

         pCombo = (CComboBox *)GetDlgItem(IDC_CBTITLESIZE);

         pCombo->GetLBText(pCombo->GetCurSel(), strSize); 

         uFontSize = atoi(strSize);

         psheet->m_uSizeTitle = uFontSize;

         pCombo = (CComboBox *)GetDlgItem(IDC_CBHEADSIZE);

         pCombo->GetLBText(pCombo->GetCurSel(), strSize); 

         uFontSize = atoi(strSize);

         psheet->m_uSizeContentHead = uFontSize;

         pCombo = (CComboBox *)GetDlgItem(IDC_CBNORMALSIZE);

         pCombo->GetLBText(pCombo->GetCurSel(), strSize); 

         uFontSize = atoi(strSize);

         psheet->m_uSizeContent = uFontSize;

         pCombo = (CComboBox *)GetDlgItem(IDC_CBFOOTERSIZE);

         pCombo->GetLBText(pCombo->GetCurSel(), strSize); 

         uFontSize = atoi(strSize);

         psheet->m_uSizeFooter = uFontSize;

该函数用于保存设定的字体信息。

·       编写CReportWzdPreview类的实现代码

(1) 首先创建类里与对话框中控件相关联的变量,这些变量通过ClassWizard工具添加,如表6-16所示,为添加变量的名称、类型、关联的控件ID及其意义。

6-16 CReportWzdPreview类的变量

名称

类型

关联的控件ID

意义

m_strReportInfo

CString

IDC_REPORTINFO

报表设置信息显示

(2) 编写OnSetActive()消息响应函数

该函数在属性页被激活时执行,将该属性页的向导按钮显示出“上一步”和“完成”,同时还要显示报表设置信息。我们需要在该函数里添加如下代码:

         // 设置属性页的向导按钮

      CReportWizard *psheet = (CReportWizard *) GetParent();  

         psheet->SetWizardButtons(PSWIZB_BACK | PSWIZB_FINISH);

         // 显示报表设置信息

         CString strReportFields;

         strReportFields.Empty();

         for(int i=0;i<psheet->m_saSelectedFields.GetSize();i++){

                 strReportFields += psheet->m_saSelectedFields.GetAt(i);

                            if(i+1 < psheet->m_saSelectedFields.GetSize())

                                     strReportFields += _T("");

         }

         CString strPageInfo;

         if(psheet->m_fHavePage){

                           strPageInfo = _T("使用页码,");

                           switch(psheet->m_uPageType){

                                      case 0: strPageInfo += _T("底部左对齐"); break;

                                      case 1: strPageInfo += _T("底部左居中"); break;

                                      case 2: strPageInfo += _T("底部右对齐"); break;

                            }

         }

         else strPageInfo = _T("不使用页码");

         m_strReportInfo.Format("您设置的报表格式如下:\n报表标题: %s, \

                         字体名称: %s 字体大小: %d\n\

                         报表字段: %s, 字体名称: %s \

                         字体大小: %d\n报表注脚: %s, \

                         字体名称: %s 字体大小: %d\n\n%s",

                                                                psheet->m_strReportTitle,

                                                                psheet->m_strFontTitle,

                                                                 psheet->m_uSizeTitle,

                                                                  strReportFields,psheet->m_strFontContent,

                                                                psheet->m_uSizeContent,

                                                                  psheet->m_strReportFooter,psheet->m_strFontFooter,

                                                                psheet->m_uSizeFooter,

                                                                 strPageInfo);

         UpdateData(FALSE);

·       编写“完成”按钮消息响应函数OnWizardFinish()

该函数在“完成”按钮按下时执行,需要保存所有设置到CODBCDemo2View类的相应变量里。在该函数里添加如下代码:

         CMainFrame *pMainFrm = (CMainFrame *)AfxGetMainWnd();

         CODBCDemo2View *pView =  \

                                              (CODBCDemo2View *)pMainFrm->GetActiveView();

         int nCount = psheet->m_saSelectedFields.GetSize();

         for(int i=0;i<nCount;i++){

                 pView->m_saSelectedFields.Add(psheet->m_saSelectedFields.GetAt(i));

                           pView->m_saSelectedFieldsTitle.Add(

                                                       psheet->m_saSelectedFieldsTitle.GetAt(i));

         }

         pView->m_fHavePage = psheet->m_fHavePage;

         pView->m_uPageType = psheet->m_uPageType;

         pView->m_strReportTitle = psheet->m_strReportTitle;

         pView->m_strFontTitle = psheet->m_strFontTitle;

         pView->m_uSizeTitle = psheet->m_uSizeTitle;

         pView->m_strFontContentHead = psheet->m_strFontContentHead;

         pView->m_uSizeContentHead = psheet->m_uSizeContentHead;

         pView->m_strFontContent = psheet->m_strFontContent;

         pView->m_uSizeContent = psheet->m_uSizeContent;

         pView->m_strReportFooter = psheet->m_strReportFooter;

         pView->m_strFontFooter = psheet->m_strFontFooter;

         pView->m_uSizeFooter = psheet->m_uSizeFooter;

·       编写报表代码

报表代码在菜单项“报表>打印预览”和“报表>打印”里执行,这两个命令都调用OnPrint()函数,我们在该函数里加入如下代码:

         OutputReport(pDC, pInfo);

OutputReport()函数负责报表信息的打印,它的实现代码同前面的RefreshDisplay()函数类似,不同的是,此函数是在打印设备环境下运行的,函数在CODBCDemo2View类里的声明代码如下:

public:

         void OutputReport(CDC* pDC, CPrintInfo* pInfo=NULL);

13.     ODBCDemo2View.cpp文件里,该函数的实现代码如下:

void CODBCDemo2View::OutputReport(CDC* pDC, CPrintInfo* pInfo)

{

        // 检测结果集对象的有效性

         if(!m_pCommonRS) return;

        if(!m_pCommonRS->IsOpen()) return;

        //

         CString line;

         TEXTMETRIC metrics;      

         int y = 0;

         CFont TitleFont, HeadingFont, DetailFont, FooterFont;

         int TabStops[] = {100, 400, 700, 1000, 1300, 1600,

                                                       1900, 2200, 2500, 2800, 3100};

         int FooterTabStops[] = {350};

         if (!pInfo || pInfo->m_nCurPage == 1) {

                          m_pCommonRS->Requery();

         }

         TitleFont.CreateFont(m_uSizeTitle, 0, 0, 0, FW_BOLD, FALSE, FALSE, 0,

                                                        ANSI_CHARSET, OUT_DEFAULT_PRECIS,

                                                       CLIP_DEFAULT_PRECIS,

                                                       DEFAULT_QUALITY, DEFAULT_PITCH | FF_ROMAN,

                                                       m_strFontTitle);

         HeadingFont.CreateFont(m_uSizeContentHead, 0, 0, 0,

                                                                FW_BOLD, FALSE, TRUE, 0,

                                                                 ANSI_CHARSET, OUT_DEFAULT_PRECIS,

                                                                CLIP_DEFAULT_PRECIS,

                                                                 DEFAULT_QUALITY,

                                                                DEFAULT_PITCH | FF_ROMAN,

                                                                 m_strFontContentHead);

         DetailFont.CreateFont(m_uSizeContent, 0, 0, 0, FW_NORMAL,

                                                                FALSE, FALSE, 0,

                                                                 ANSI_CHARSET, OUT_DEFAULT_PRECIS,

                                                                CLIP_DEFAULT_PRECIS,

                                                                  DEFAULT_QUALITY,

                                                                DEFAULT_PITCH | FF_ROMAN,

                                                                 m_strFontContent);

         FooterFont.CreateFont(m_uSizeFooter, 0, 0, 0, FW_NORMAL,

                                                                FALSE, FALSE, 0,

                                                                 ANSI_CHARSET, OUT_DEFAULT_PRECIS,

                                                                CLIP_DEFAULT_PRECIS,

                                                                 DEFAULT_QUALITY,

                                                                DEFAULT_PITCH | FF_ROMAN,

                                                                 m_strFontFooter);

         // 打印标题

         CFont* OldFont = pDC->SelectObject(&TitleFont);

         pDC->GetTextMetrics(&metrics);

         int LineHeight = metrics.tmHeight + metrics.tmExternalLeading;

         y += LineHeight;         

         pDC->TextOut(200, 0, m_strReportTitle);

         // 打印字段标题

         pDC->SelectObject(&HeadingFont);

         if(0<m_saSelectedFieldsTitle.GetSize()){

                           for(int i=0;i<m_saSelectedFieldsTitle.GetSize()-1;i++){

                                     line += m_saSelectedFieldsTitle.GetAt(i);

                                     line += _T("\t");

                           }

                           line += m_saSelectedFieldsTitle.GetAt(i);

         }

         pDC->TabbedTextOut(0, y, line, m_saSelectedFieldsTitle.GetSize(),

                                                                TabStops, 0);

         // 打印正文

         LineHeight = metrics.tmHeight + metrics.tmExternalLeading;

         y += LineHeight;

         pDC->SelectObject(&DetailFont);

         pDC->GetTextMetrics(&metrics);

         LineHeight = metrics.tmHeight + metrics.tmExternalLeading;

         while (!m_pCommonRS->IsEOF()) {

                           if (pInfo && abs(y) > 1000) {

                                     pInfo->SetMaxPage(pInfo->m_nCurPage + 1);

                                     break;

                           }

                           line.Empty();

                            CString strValue;

                           if(0<m_saSelectedFieldsTitle.GetSize()){

                                     for(int j=0;j<m_saSelectedFields.GetSize()-1;j++){

                                              m_pCommonRS->GetFieldValue(m_saSelectedFields.GetAt(j),

                                                                           strValue);

                                              line += strValue;

                                               line += _T("\t");

                                     }

                            m_pCommonRS->GetFieldValue(m_saSelectedFields.GetAt(j), strValue);

                            line += strValue;

                   }

                   pDC->TabbedTextOut(0, y, line, m_saSelectedFields.GetSize(), TabStops, 0);

                   y += LineHeight;          //Adjust y position

                   m_pCommonRS->MoveNext();

         }

         // 打印注脚

         if (pInfo) {

                           pDC->SelectObject(&FooterFont);

                            line.Format("%s \tPage: %d \tJackie", m_strReportFooter,

                                                       pInfo->m_nCurPage);

                           pDC->TabbedTextOut(0, 1025, line, 2, FooterTabStops, 0);

         }

         // 恢复设备环境字体

         pDC->SelectObject(OldFont);

}

6.3.3 编译并运行ODBCDemo2工程

现在我们完成了ODBCDemo2工程代码的编写,可以编译并运行应用程序了。

编译并运行ODBCDemo2工程的操作步骤:

(1) 执行“Build>Build ODBCDemo2.exe”菜单项,或者按下快捷键【F7】,VC++开始编译ODBCDemo2工程,产生ODBCDemo2.exe可执行程序。

(2) 执行“Build>Execute ODBCDemo2.exe”菜单项,或者按下快捷键【Ctrl+F5】,VC++开始运行ODBCDemo2.exe应用程序,启动界面如图6-21所示。

6-21 ODBCDemo2.exe应用程序启动界面

(3) 执行“销售报告>按产品”菜单命令,运行结果如图6-22所示。

6-22 执行“销售报告>按产品”菜单命令

(4) 执行“销售报告>按客户”菜单命令,运行结果如图6-23所示。

6-23 执行“销售报告>按客户”菜单命令

(5) 执行“销售报告>按雇员”菜单命令,运行结果如图6-24所示。

6-24 执行“销售报告>按雇员”菜单命令

(6) 执行“产品>产品信息”菜单命令,运行结果如图6-25所示。

6-25 执行“产品>产品信息”菜单命令

(7) 执行“客户>客户信息”菜单命令,运行结果如图6-26所示。

6-26 执行“客户>客户信息”菜单命令

(8) 执行“雇员>雇员信息”菜单命令,运行结果如图6-27所示。

6-27 执行“雇员>雇员信息”菜单命令

(9) 执行“供应商>供应商信息”菜单命令,运行结果如图6-28所示。

6-28 执行“供应商>供应商信息”菜单命令

(10) 执行“运货商>运货商信息”菜单命令,运行结果如图6-29所示。

6-29 执行“运货商>运货商信息”菜单命令

(11) 执行“产品>产品信息”菜单命令,将产品信息显示在视图里。执行“报表>设置”菜单命令,报表设置向导开始执行第一步,报表字段选择。运行结果如图6-30所示。

6-30 报表设置向导的第一步

(12) 在“报表标题”编辑区域输入“公司产品报表”,将所有字段选择到选用列表里,完成信息输入后,按下“下一步”按钮,报表设置向导开始执行第二步,报表格式设置。运行结果如图6-31所示。

6-31 报表设置向导的第二步

(13) 选择所有字体为“宋体”,选择标题字体大小为48,选择列头字体大小为28,选择正文字体大小为28,选择注脚字体大小为18,完成信息设置后,按下“下一步”按钮,报表设置向导开始执行第三步,显示报表设置信息。运行结果如图6-32所示。

6-32 报表设置向导的第三步

(14) 单击“完成”按钮,完成报表设置,返回系统界面。执行“报表>打印预览”菜单命令,将显示打印预览结果如图6-33所示。

(15) 单击“放大”按钮,将报表预览界面放大,如图6-34所示。

(16) 单击“打印”按钮,即可将报表内容打印出来。

(17) 执行“文件>退出”菜单命令,结束应用程序的运行。

6-33 报表预览

6-34 放大后的报表预览

6.3.4 ODBCDemo2实例小结

本实例着重介绍了MFCCRecordset类实现对数据库的操作,用到的主要函数有:

·       Open(),打开结果集。

·       Close(),关闭结果集。

·       GetODBCFieldCount(),取得结果集的字段数。

·       GetODBCFieldInfo(),取得结果集的字段信息。

·       IsEOF(),判断结果集是否检索到末尾。

·       MoveNext(),移动结果集游标到下一行。

·       Requery(),再次请求结果集。

本实例另一个重点是介绍报表的生成,VC++生成报表没有现成的控件,不如VB容易,必须通过字符打印操作将信息显示在打印设备环境里。这里用到了CDC的两个重要的打印函数:

·       TextOut(),将文本简单打印到指定位置。

·       TabbedTextOut(),将文本格式打印到指定位置。

另外,本实例还演示了PropertySheetPropertyPage的使用方法,对读者设置其信息会有帮助。

本实例源代码在随书光盘的code\ ODBCDemo2目录下。

6.4 本 章 小 结

本章介绍了MFC ODBC编程方法和过程。与ODBC API编程相比,MFC编程更适用于界面型数据库应用程序的开发,由于MFC的广泛支持,ODBC编程可以对数据进行很好地表示。然而MFCCDatabase类和CRecordset类提供的数据库操作函数非常有限,支持的游标类型也很有限,限制了高效的数据库开发。从编程层次上,ODBCMFC编程则属于高级编程。

 

Feedback

# re: ODBC编程  回复  更多评论   

2008-07-09 10:14 by 西西
图呢

# re: ODBC编程  回复  更多评论   

2010-03-22 09:24 by ss
你照做通过没··给我发一份行不· 谢谢·381022214@qq.com

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理