學習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對話方塊
| 提示:如果要為dBase或FoxPro資料庫註冊資料來源,則應該選擇一個目錄而不是檔案作為資料來源。這是因為在Access中,一個MDB檔案可以包含多張表,因此可以認為一個MDB檔案就是一個資料庫。而在dBase或FoxPro中,一個DBF檔案只能對應一張表,一個資料庫可能會包含好幾個DBF檔案,所以可以認為某一個包含了若干DBF檔案的目錄是一個資料庫。 |
10.7.2 Enroll的第一個版本
Enroll
第一個版本10.3所示,該程式具有瀏覽記錄集和修改記錄這兩個準系統。在修改了表單中的記錄後,移動到一個新的記錄上就可以儲存對原記錄的修改。注意在表單中Course和Section編輯框都是唯讀,這是因為這兩個欄位的內容較重要,使用者不能隨便修改。Enroll使用了STDREG32.MDB的Section表,該表是一張課程表,其內容如表10.2所示。
現在讓我們來建立Enroll應用程式。首先,用AppWizard來完成Enroll程式架構的建立,請讀者按下面幾步進行:
啟動AppWizard,指定一個名為Enroll的MFC工程。
在MFC AppWizard的第一步選擇Single document。
在MFC AppWizard的第二步選擇Database view without file support,然後點擊Data Source...按鈕,在Database Options對話方塊中的ODBC組合框中選擇Student Registration資料來源,10.7所示。然後按“OK”按鈕,則會開啟一個Select Database Table對話方塊,10.8所示。在該對話方塊中選擇Section表。按OK按鈕退出。
在MFC AppWizard的第六步中,將類CEnrollSet改名為CSectionSet,將類CEnrollView改名為CSectionForm。
按Finish按鈕,建立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資料交換。請讀者按如下步驟操作:
進入ClassWizard,選擇Member Variables頁並且選擇CSectionForm類。
在變數列表中雙擊IDC_CAPACITY項,則會顯示Add Member Variable對話方塊。注意該對話方塊的Member variable name欄顯示的是一個組合框,而不是平常看到的編輯框。10.9所示,在組合框的列表中選擇m_pSet->m_Capacity。按OK按鈕確認。
仿照第2步,為其他控制項串連記錄集的域資料成員。根據控制項的ID,不難確定對應的域資料成員。
圖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類的建構函式的聲明如下:
CSectionSet(CDatabase* pDatabase = NULL);
函數的定義在清單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表
CourseID(Text) |
CourseTitle(Text) |
Hours(int) |
MATH101 |
Algebra |
4 |
MATH201 |
Calculus I |
4 |
MATH202 |
Calculus II |
4 |
現在就讓我們開始在上一小節Enroll的基礎上製作新版本。若當前工程不是Enroll,請讀者開啟上一小節建立的Enroll工程。
首先要把Course編輯框替換成組合框,這包括下面幾步:
開啟IDD_ENROLL_FORM對話方塊模板並刪除Course編輯框。
在原來編輯框的位置加入一個組合框。開啟該控制項的屬性對話方塊,令其ID為IDC_COURSELIST,並在Styles頁中選擇Drop List。注意要適當調整組合框的下拉式清單尺寸(點擊向下的箭頭後擴大其尺寸)。
自上到下從新安排Tab順序。
接下來要用ClassWizard做一些與新加的組合框有關的工作:
進入ClassWizard,選擇Member Variables頁並選擇CSectionForm類。單擊列表中的IDC_COURSE項並按Delete鍵刪除該項。然後雙擊IDC_COURSELIST項,在Add Member Variable對話方塊的組合框中選擇m_pSet->m_CourseID。
再次雙擊IDC_COURSELIST,並為CSectionForm類加入一個名為m_ctlCourseList的CComboBox類成員。
選擇Message Maps頁,為IDC_COURSELIST組合框加入CBN_SELENDOK通知訊息處理函數,函數名為OnSelendokCourselist。該函數負責響應使用者在組合框中選擇的變化。
按OK按鈕退出ClassWizard。
接著,需要為Course表建立一個名為CCourseSet的記錄集類,這個工作可由ClassWizard完成,請讀者按下面幾步操作:
進入ClassWizard,點擊Add Class...按鈕並在彈出的菜單中選擇New...,然後在Create New Class對話方塊中的Name欄中輸入CCourseSet,在Base class欄中選擇CRecordset,按Create按鈕。
在彈出的Database Options對話方塊中,在ODBC組合框裡選擇Student Registration資料來源。然後按OK按鈕。
在彈出的Select Database Tables對話方塊中選擇Course表。按OK確認。
看看建立的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