|
10.7 学习Enroll例程 Visual C++提供了一个名为Enroll的例子来作为学习MFC数据库编程的教程.Enroll分为四步,本节的任务就是指导读者完成前三步的Enroll例程,并对其进行较彻底的剖析.通过学习这三步例程,读者将掌握用AppWizard和ClassWizard创建MFC数据库应用程序的方法. 在开始学习Enroll例程时,读者也许会感到用AppWizard创建数据库应用很容易,似乎不用学习前面几节的内容.诚然,AppWizard自动地为应用程序加入了许多与数据库有关的代码,大大简化了数据库应用的开发.但AppWizard不是万能的,它建立的数据库应用往往不能满足用户的需要.用户真正想知道的是如何不依赖AppWizard而编写自己的数据库应用程序,这也正是本章的宗旨所在.事实上,前面几节的分析以及后面进行的对Enroll例程的分析正是为这一宗旨服务的. 在学习Enroll以前,请读者先从Visual C++ 5.0的光盘上将Enroll(在samples \ mfc \ tutorial \ enroll目录下)在前三步的例程拷到硬盘上,以供参考.另外,Enroll要用到Access数据库STDRED32.MDB,该文件在VC5.0的Stdreg例程中(在samples \ mfc \ database \ stdreg目录下),请读者将该例子也拷贝到硬盘上. 10.7.1 注册数据源 ODBC应用程序不能直接使用数据库,用户必需为要使用的数据库注册数据源.注册数据源的工作由ODBC管理器完成,该管理器位于Windows 95控制面板的32位ODBC内.现在让我们为Access数据库STDREG32.MDB注册数据源. 打开控制面板,双击“32位ODBC”图标,则会显示一个“ODBC数据源管理器”,如图10.5所示。在管理器中选择“用户DSN”页,用户DSN只对用户可见而且只能用户当前机器。
图10.5 ODBC数据源管理器 点击“添加”按钮,则会弹出一个“创建新数据源”对话框。在该对话框中选择Microsoft Access Driver(*.mdb),然后按完成按钮。接下来会显示一个ODBC Microsoft Access 97 Setup对话框,如图10.6所示,该对话框用来把数据库与一个数据源名连接起来。在Data Source Name:栏中输入Student Registration,然后点击Select...按钮,在随后弹出的对话框中找到并选择STDREG32.MDB。连按两个OK按钮后,一个名为Student Registration的新数据源就被注册到了管理器中。
图10.6 ODBC Microsoft Access 97 Setup对话框
10.7.2 Enroll的第一个版本 Enroll第一个版本如图10.3所示,该程序具有浏览记录集和修改记录这两个基本功能。在修改了表单中的记录后,移动到一个新的记录上就可以保存对原记录的修改。注意在表单中Course和Section编辑框都是只读的,这是因为这两个字段的内容较重要,用户不能随便修改。Enroll使用了STDREG32.MDB的Section表,该表是一张课程表,其内容如表10.2所示。 现在让我们来建立Enroll应用程序。首先,用AppWizard来完成Enroll程序框架的建立,请读者按下面几步进行:
图10.7 Database Options对话框
图10.8 Select Database Table对话框 打开工作区的类视图,可以发现AppWizard自动创建了一个记录集类CSectionSet和一个记录视图类CSectionForm,这两个类分别是CRecordset和CRecordView的派生类。AppWizard也为CSectionSet类自动创建了域数据成员。 打开工作区的资源视图,读者不难找到一个ID为IDD_ENROLL_FORM的对话框模板,该模板将被记录视图用来显示表单。清除该模板中的所有控件,并把模板的尺寸扩大到183×110,然后按图10.3的式样放置控件。这里可以采取一个偷懒的方法:打开VC5.0已作好的第一个版本Enroll的资源文件(Enroll.rc),找到并打开IDD_ENROLL_FORM对话框模板,按住Ctrl键并用鼠标选择模板中的所有控件,然后按Ctrl+Insert键拷贝所选的控件。切换到自己的IDD_ENROLL_FORM对话框模板,然后按Shift+Insert键把刚才拷贝的控件粘贴到模板中。 接下来的任务是用ClassWizard把表单中的控件与记录集的域数据成员连接起来,以实现控件与当前记录的DDX数据交换。请读者按如下步骤操作:
图10.9 Add Member Variable对话框
在CSectionForm类的定义内可以找到下面一行: CSectionSet* m_pSet; 可见m_pSet是CSectionForm类的成员,它指向一个CSectionSet对象。用ClassWizard可以把控件与象记录集这样的“外部数据”连接起来,这是ClassWizard在数据库编程方面的一个特殊应用。 编译并运行Enroll,读者会惊奇的发现Enroll居然是一个相当不错的记录浏览器,并且用户可以对记录进行修改。 现在,让我们来分析一下AppWizard和ClassWizard为Enroll干了哪些事情。 在文档类CEnrollDoc的定义中,有如下一行: CSectionSet m_sectionSet; 可见AppWizard在CEnrollDoc类中嵌入了一个CSectionSet对象。这相当于调用了构造函数CSectionSet(NULL),CSectionSet类的构造函数的声明如下: 函数的定义在清单10.5中列出。可以看出,构造函数调用了基类的构造函数,并对域数据成员进行了初始化。通过10.5.4我们知道,若传递NULL参数给CRecordset的构造函数,那么CRecordset::Open函数将自动构建一个CDatabase对象,并根据CRecordset:: GetDefaultConnect返回的连接字符串建立与数据源的连接。CSectionSet提供了虚拟函数GetDefaultConnect的新版本,如清单10.6所示,在该函数中提供了数据源Student Registration。
清单10.5 CSectionSet的构造函数 CSectionSet::CSectionSet(CDatabase* pdb) : CRecordset(pdb) { //{{AFX_FIELD_INIT(CSectionSet) m_CourseID = _T(""); m_SectionNo = _T(""); m_InstructorID = _T(""); m_RoomNo = _T(""); m_Schedule = _T(""); m_Capacity = 0; m_nFields = 6; //}}AFX_FIELD_INIT m_nDefaultType = snapshot; }
清单10.6 派生类的GetDefaultConnect函数 CString CSectionSet::GetDefaultConnect() { return _T("ODBC;DSN=Student Registration"); }
至于记录集的建立,实际上是在CRecordView:: OnInitialUpdate中完成的,这部分代码对用户是透明的,这里在清单10.7中列出。在该函数中调用CRecordset::Open来建立记录集。在函数的开头调用了OnGetRecordset函数来获取与记录视图相连的记录集对象。CSectionForm提供了虚拟函数OnGetRecordset的新版本,如清单10.8所示,该函数把m_pSet提交给调用者。至于m_pSet的初始化,则是在CSectionForm::OnInitialUpdate函数中完成的,如清单10.9所示。
清单10.7 CRecordView:: OnInitialUpdate函数 void CRecordView::OnInitialUpdate() { CRecordset* pRecordset = OnGetRecordset(); // recordset must be allocated already ASSERT(pRecordset != NULL);
if (!pRecordset->IsOpen()) { CWaitCursor wait; pRecordset->Open(); }
CFormView::OnInitialUpdate(); }
清单10.8 派生类的OnGetRecordset函数 CRecordset* CSectionForm::OnGetRecordset() { return m_pSet; }
清单10.9 派生类的OnInitialUpdate函数 void CSectionForm::OnInitialUpdate() { m_pSet = &GetDocument()->m_sectionSet; CRecordView::OnInitialUpdate(); }
注意到在CRecordView:: OnInitialUpdate中调用CRecordset::Open时未提供任何参数,这意味着Open函数将从CRecordset::GetDefaultSQL中获取SQL信息。CSectionSet提供了虚拟函数GetDefaultSQL的新版本,如清单10.10所示,该函数返回了“Section”表名。
清单10.10 派生类的GetDefaultSQL函数 CString CSectionSet::GetDefaultSQL() { return _T("[Section]"); }
至于记录的滚动和修改的实现,请参看10.6。而与DDX和DFX有关的代码已在清单10.3和10.2中列出。 如果读者对上面的分析还有不明白的地方,那么请再把本章的前几节内容再仔细阅读一遍。
10.7.3 Enroll的第二个版本 Enroll的第二个版本向读者演示了在一个记录视图中使用两个相关联的记录集,以及记录的过滤和排序技术,该版本使读者真正接触到了关系数据库。本小节还将向读者介绍如何用ClassWizard建立记录集类,以及参数化记录集的方法。 读者可以先运行VC 5.0提供的Enroll例子的第二步看看。Enroll的界面有了一个变化,原来的Course编辑框被替换成了组合框,如图10.10所示。组合框中的内容来自同一数据源的另一张表Course的CourseID字段。
图10.10 Enroll的第二个版本
Course表的内容如表10.5所示,与表10.2相对照,读者可以发现Course表和Section表有一个公共字段CourseID。记录视图程序正是利用这个公共字段把两张表联系起来的。例如,当用户在Course组合框中选择了MATH202时,程序将选择Section表中所有CourseID为MATH202的记录并建立新的记录集。 事实上,在STDREG32.MDB的大部分表都共享了CourseID。在主表Course表中,每个记录的CourseID是唯一的,我们称其为主关键字(Primary key)。在Course的相关表中,CourseID不一定唯一,如Section表,我们称相关表中的CourseID为外关键字(Foreign key)。通过关键字可以把多张表联系到一起,这样的数据库就是关系数据库。SectionNo也是一个关键字,在Section表中,SectioinNo字段是主关键字,它的值是唯一的。
表10.5 Course表
现在就让我们开始在上一小节Enroll的基础上制作新版本。若当前工程不是Enroll,请读者打开上一小节创建的Enroll工程。 首先要把Course编辑框替换成组合框,这包括下面几步:
接下来要用ClassWizard做一些与新加的组合框有关的工作:
接着,需要为Course表创建一个名为CCourseSet的记录集类,这个工作可由ClassWizard完成,请读者按下面几步操作:
看看新建的CCourseSet类,读者会发现ClassWizard自动为CCourseSet类创建了与Course表的字段相对应的域数据成员,并且建立了DoFieldExchange函数。ClassWizard也为记录集类提供了新的GetDefaultConnect和GetDefaultSQL函数。 接着,在CEnrollDoc类的定义中,紧接着m_sectionSet成员,加入下面一行: CCourseSet m_courseSet; 这样CEnrollDoc就包含了两个记录集。由于CEnrollDoc类用到了CCourseSet类,所以要在所有含有#include “EnrolDoc.h”语句的CPP文件中,在#include “EnrolDoc.h”语句的前面加上如下的include语句。这些CPP文件包括CEnrollApp、CSectionForm和CEnrollDoc类所在的CPP文件。 #include "CourseSet.h" 在CSectionSet类的定义中,紧接着域数据成员,在“//}}AFX_FIELD”注释外加入下面一行。 CString m_strCourseIDParam; m_strCourseIDParam是记录集的参数数据成员,其作用将在后面说明。
最后,请读者按清单10.11和10.12修改程序。清单10.11列出的是CSectionSet类的部分源代码,清单10.12列出的是CSectionForm类的部分代码。
清单10.11 CSectionSet类的部分代码 CSectionSet::CSectionSet(CDatabase* pdb) : CRecordset(pdb) { . . . m_nParams = 1; //只有一个参数数据成员 m_strCourseIDParam = ""; }
void CSectionSet::DoFieldExchange(CFieldExchange* pFX) { . . . pFX->SetFieldType(CFieldExchange::param); RFX_Text(pFX, "CourseIDParam", m_strCourseIDParam); //替换参数 }
清单10.12 CSectionForm类的部分代码 void CSectionForm::OnInitialUpdate() { m_pSet = &GetDocument()->m_sectionSet;
CEnrollDoc* pDoc = GetDocument(); pDoc->m_courseSet.m_strSort = "CourseID"; if (!pDoc->m_courseSet.Open()) return;
m_pSet->m_strFilter = "CourseID = ?"; //使用参数 m_pSet->m_strCourseIDParam = pDoc->m_courseSet.m_CourseID; m_pSet->m_strSort = "SectionNo"; m_pSet->m_pDatabase = pDoc->m_courseSet.m_pDatabase; //共享CDatabase
CRecordView::OnInitialUpdate();
m_ctlCourseList.ResetContent(); if (pDoc->m_courseSet.IsOpen()) { while (!pDoc->m_courseSet.IsEOF()) { m_ctlCourseList.AddString( pDoc->m_courseSet.m_CourseID); //向表中加入CourseID字段 pDoc->m_courseSet.MoveNext(); } } m_ctlCourseList.SetCurSel(0); }
void CSectionForm::OnSelendokCourselist() {
if (!m_pSet->IsOpen()) return; m_ctlCourseList.GetLBText(m_ctlCourseList.GetCurSel(), m_pSet->m_strCourseIDParam); m_pSet->Requery(); //重新查询 if (m_pSet->IsEOF()) { m_pSet->SetFieldNull(&(m_pSet->m_CourseID), FALSE); m_pSet->m_CourseID = m_pSet->m_strCourseIDParam; } UpdateData(FALSE); }
在CSectionForm::OnInitialUpdate函数的开头部分,调用了CRecordset::Open建立CCourseSet记录集,在调用Open函数之前,指定了按CourseID字段排序记录集。关于调用Open函数的一些前因后果在前面已经解释过了,读者应该不难分析。接下来的代码读者可能就看不懂了,为什么在记录集的m_strFilter过滤字符串中会有一个“?”号呢。 这是因为在本例中使用了“参数化记录集”技术。在记录集的m_strFilter和m_strSort中,可以用“?”号作为参数使用,这样在指定过滤器和排序时可以更具灵活性。例如,在OnInitialUpdate函数中,是这样指定过滤器的: m_pSet->m_strFilter = "CourseID = ?"; 在调用Open或Requery时,“?”将会被CSectionSet::m_strCourseIDParam中的内容取代。例如,如果指定m_strCourseIDParam为“MATH101”,则m_strFilter将变成"CourseID = MATH101"。这样用户只要指定了m_strCourseIDParam,就可以定制过滤器。象m_strCourseIDParam这样的成员被称作参数数据成员,同域数据成员一样,它是记录集所特有的。ClassWizard不支持参数数据成员,用户只能手工加入之,且其名字由用户自己确定。 参数替换的工作实际上是由CSectionSet::DoFieldExchange中的RFX函数完成的,在DoFieldExchange函数的末尾,我们加入了下面两行: pFX->SetFieldType(CFieldExchange::param); RFX_Text(pFX, "CourseIDParam", m_strCourseIDParam); DoFieldExchange可以识别域数据成员和参数数据成员。第一行调用用来表明随后的RFX函数用于参数替换。第二行是一个用于m_strCourseIDParam参数的RFX函数。RFX第二个参数的名字可由用户确定,这里指定其为“CourseIDParam”。 用户可以在m_strFilter和m_strSort中使用一个或多个参数。在有多个参数的情况下,要注意RFX函数的调用次序应与参数出现的次序相对应。框架规定,用户应该先在m_strFilter中安排参数,然后才是m_strSort。 CRecordset有两个数据成员m_nFields和m_nParams分别用来统计域数据成员和参数数据成员的数目。前者由ClassWizard自动计数,而后者必需由用户来维护。在CSectionSet的构造函数中,m_nParams被置为1,因为只有一个参数数据成员。 现在让我们继续研究OnInitialUpdate。在调用基类的OnInitialUpdate以前,在程序中有这样一行代码: m_pSet->m_pDatabase = pDoc->m_courseSet.m_pDatabase; m_pDatabase是CRecordset的公共成员,它是指向CDatabase对象的指针。如果应用程序使用了两个以上的记录集,在缺省情况下,每个记录集都会创建一个代表同一数据源的CDatabase对象(参见10.5.4),这显然没有必要。上面一行代码使CSectionSet记录集共享由CCourseSet记录集创建的CDatabase对象,这样,当在CRecordView:: OnInitialUpdate中调用CRecordset::Open打开CSectionSet记录集时,就不会再创建CDatabase对象了。 接下来,程序把CCourseSet的每一个记录的CourseID字段都加入到组合框中。 当用户在组合框中选择了新的CourseID时,在CSectionForm::OnSelendokCourselist中,就会把用户选择的CourseID设置到m_strCourseIDParam中,然后调用CRecordset::Requery按照定制的过滤器和排序重新查询和建立记录集。如果重新建立的记录集为空,则说明Section表中没有与指定的CourseID相对应的记录,这时记录集被自动设置为NULL。在程序中,调用了CRecordset:: SetFieldNull把m_CourseID字段设置为非空。最后,调用CRecordView::UpdateData更新表单中的控件。 从程序中不难看出,使用参数化记录集的最大好处是可以在程序运行期间方便地指定过滤器和排序,这大大提高了程序的用户定制查询能力。 编译并运行Enroll,试试新增加的功能。
10.7.4 Enroll的第三个版本 Enroll的第三个版本支持记录的添加和删除,该版本也演示了对数据库异常的处理。请读者先运行VC5.0提供的第三步Enroll程序看一看。第三版Enroll的Record菜单中多了三个命令:Add、Delete和Refresh,它们分别用来添加、删除和刷新记录。 当用户选择Add命令后,就进入了添加模式。这时除Course组合框外,所有的字段都被清除。用户可以在各编辑框内输入新记录的字段值,然后移动到别的记录上,这样就把新的记录保存到数据源中。用户也可以通过再次选择Add命令来保存新加的记录。 当用户选择Delete命令后,当前记录被删除,程序会自动滚动到下一个记录上。 Refresh命令用来放弃记录的修改或添加操作,同时,恢复原记录的内容。 现在就让我们开始在上一小节Enroll的基础上制作新版本。若当前工程不是Enroll,请读者打开上一小节创建的Enroll工程。 首先要为Record菜单添加三个菜单项,菜单项的各项属性在表10.6中列出。请把这三个菜单项加到Record菜单的开头,并且用一个分隔线和后面的命令隔开。
表10.6
接着,用ClassWizard为上面三个命令创建处理函数,函数名为缺省的。另外,需要为记录视图编写新的OnMove函数来处理滚动命令,这是因为原来的OnMove函数没有添加记录的功能。在ClassWizard的CSectionForm类的Messages列表中可以找到OnMove函数,请读者双击并建立该函数。 在CSectionForm::OnMove函数处理滚动命令时,必需要有一个标志来判断当前是否处于添加模式,以便向数据源中加入新记录或进行普通的滚动处理。请读者在CSectionForm类的定义中的适当位置加入下面两行: protected: BOOL m_bAddMode; 在前两个版本的Enroll中,Section编辑框都是只读的,但在添加记录时,必需允许用户修改Section编辑框。这是因为SectionNo字段是Section表的主关键字,它的值必需唯一,如果在加入新记录时不改变原来的SectionNo字段,那么将会因为主关键字重复而导致异常产生。显然,我们需要一个CEdit对象来控制IDC_SECTION编辑框,请读者用ClassWizard为CSectionForm类加入一个与IDC_SECTION对应的CEdit型成员变量,变量的名字为m_ctlSection。 最后,请读者按清单10.13修改程序。
清单10.13 CSectionForm类的部分源代码 CSectionForm::CSectionForm() : CRecordView(CSectionForm::IDD) { . . . m_bAddMode = FALSE; }
void CSectionForm::OnSelendokCourselist() { if (!m_pSet->IsOpen()) return; m_ctlCourseList.GetLBText(m_ctlCourseList.GetCurSel(), m_pSet->m_strCourseIDParam);
if (!m_bAddMode) { m_pSet->Requery(); if (m_pSet->IsEOF()) { m_pSet->SetFieldNull(&(m_pSet->m_CourseID), FALSE); m_pSet->m_CourseID = m_pSet->m_strCourseIDParam; } UpdateData(FALSE);
} }
void CSectionForm::OnRecordAdd() {
if (m_bAddMode) //如果已处于添加模式,则完成添加操作 OnMove(ID_RECORD_FIRST);
CString strCurrentCourse = m_pSet->m_CourseID; m_pSet->AddNew(); m_pSet->SetFieldNull(&(m_pSet->m_CourseID), FALSE); m_pSet->m_CourseID = strCurrentCourse; m_bAddMode = TRUE; m_ctlSection.SetReadOnly(FALSE); UpdateData(FALSE); //更新表单视图 }
void CSectionForm::OnRecordDelete() {
TRY { m_pSet->Delete(); } CATCH(CDBException, e) { AfxMessageBox(e->m_strError); return; } END_CATCH
m_pSet->MoveNext(); //滚动到下一个记录 if (m_pSet->IsEOF()) //如果滚出了记录集的边界,则滚动到最后一个记录 m_pSet->MoveLast(); if (m_pSet->IsBOF()) //如果记录变空了,则清除域数据成员 m_pSet->SetFieldNull(NULL); UpdateData(FALSE); //更新表单视图 }
void CSectionForm::OnRecordRefresh() {
if (m_bAddMode == TRUE) { m_pSet->Move(AFX_MOVE_REFRESH); //取消添加模式 m_ctlSection.SetReadOnly(TRUE); m_bAddMode = FALSE; } UpdateData(FALSE); //更新表单视图 }
BOOL CSectionForm::OnMove(UINT nIDMoveCommand) {
if (m_bAddMode) { if (!UpdateData()) return FALSE; TRY { m_pSet->Update(); } CATCH(CDBException, e) { AfxMessageBox(e->m_strError); return FALSE; } END_CATCH
m_pSet->Requery(); //重新查询,使新加的记录对用户可见 UpdateData(FALSE); m_ctlSection.SetReadOnly(TRUE); m_bAddMode = FALSE; return TRUE; } else { return CRecordView::OnMove(nIDMoveCommand);
} }
我们先来看看Add命令的处理函数CSectionForm::OnRecordAdd函数。在该函数中,最重要的代码是调用CRecordset::AddNew进入添加模式。其余代码的解释如下:
CSectionForm::OnMove负责处理滚动命令。与缺省的CRecordView::OnMove函数不同的是,该函数对于添加模式下的滚动进行了重新处理:
在Delete命令的处理函数CSectionForm::OnDelete中调用了CRecordset::Delete来删除记录,并对可能发生的异常进行了处理。在调用Delete后,滚动记录到新的位置上以跳过被删除的记录。 Refresh命令的处理函数CSectionForm::OnRefresh用来放弃修改或添加记录的操作。对该函数的解释为:
CSectionForm::OnSelendokCourselist函数中多了一个用来判断当前是否处于添加模式的if语句。如果处于添加模式,那么就不能调用Requery重新查询,因为此时Course组合框的作用仅仅是让用户选择一个字段值,而不是指定过滤器。 编译并运行Enroll,试试新增加的功能。
作者: 不详, 来源: Visual C++王朝 |