第七課 文檔/視結構
這一講介紹文檔/視結構的基本概念,並結合一個簡單的文本編輯器的例子說明文檔視結構的內部運行機制和使用。
7.1文檔/視圖概念
7.1.1概念
在文檔視結構裏,文檔是一個應用程序數據基本元素的集合,它構成應用程序所使用的數據單元;另外它還提供了管理和維護數據的手段。
文檔是一種數據源,數據源有很多種,最常見的是磁盤文件,但它不必是一個磁盤文件,文檔的數據源也可以來自串行口、網絡或攝像機輸入信號等。在第十二章“多線程和串行通信編程”中,我們展示瞭如何使用串行口作爲數據輸入的文檔/視結構程序。文檔對象負責來自所有數據源的數據的管理。
視圖是數據的用戶窗口,爲用戶提供了文檔的可視的數據顯示,它把文檔的部分或全部內容在窗口中顯示出來。視圖還給用戶提供了一個與文檔中的數據交互的界面,它把用戶的輸入轉化爲對文檔中數據的操作。每個文檔都會有一個或多個視圖顯示,一個文檔可以有多個不同的視圖。比如,在Excel電子表格中,我們可以將數據以表格方式顯示,也可以將數據以圖表方式顯示。一個視圖既可以輸出到窗口中,也可以輸出到打印機上。
圖7-1說明了文檔及其視圖之間的關係。
圖 7-1 文檔和視圖
MFC的文檔/視結構機制把數據同它的顯示以及用戶對數據的操作分離開來。所有對數據的修改由文檔對象來完成。視圖調用這個對象的方法來訪問和更新數據。
7.1.2兩類文檔視結構程序
有兩種類型的文檔視結構程序:單文檔界面(SDI)應用程序和多文檔界面(MDI)應用程序。
在單文檔界面程序中,用戶在同一時刻只能操作一個文檔。象Windows95下的NotePad記事本程序(如圖7-2所示)就是這樣的例子。在這些應用程序中,打開文檔時會自動關閉當前打開的活動文檔,若文檔修改後尚未保存,會提示是否保存所做的修改。因爲一次只開一個窗口,因此不象WORD那樣需要一個窗口菜單。單文檔應用程序一般都提供一個File菜單,在該菜單下有一組命令,用於新建文檔(New)、打開已有文檔(Open)、保存或換名存盤文檔等。這類程序相對比較簡單,常見的應用程序有終端仿真程序和一些工具程序。
圖7-2 單文檔程序(記事本)
一個多文檔界面應用程序也能操作文檔,但它允許同時操作多個文檔。如圖7-2,Microsoft Word就是這樣的例子。你可以打開多個文件(同時也就爲每個文件打開一個窗口),可以通過切換活動窗口激活相應的文檔進行編輯。多文檔應用程序也提供一個File菜單,用於新建、打開、保存文檔。與單文檔應用程序不同的是,它往往還提供提供一個Close(關閉)菜單項,用於關閉當前打開的文檔。多文檔應用程序還提供一個窗口菜單,管理所有打開的子窗口,包括對子窗口的新建、關閉、層疊、平鋪等。關閉一個窗口時,窗口內的文檔也被自動關閉。在這一章裏,我們只討論單文檔界面應用程序的編制,有關多文檔技術在下一章裏再做討論。
圖7-3 多文檔程序(Microsoft Word)
7.1.3使用文檔/視結構的意義
文檔視結構的提出對於廣大程序員來說是一個福音,它大大簡化了多數應用程序的設計開發過程。文檔視結構帶來的好處主要有:
a. 首先是將數據操作和數據顯示、用戶界面分離開。這是一種“分而治之”的思想,這種思想使得模塊劃分更加合理、模塊獨立性更強,同時也簡化了數據操作和數據顯示、用戶界面工作。文檔只負責數據管理,不涉及用戶界面;視圖只負責數據輸出與用戶界面的交互,可以不考慮應用程序的數據是如何組織的,甚至當文檔中的數據結構發生變化時也不必改動視圖的代碼。
b.MFC在文檔/視結構上提供了許多標準操作界面,包括新建文件、打開文件、保存文件、打印等,減輕了用戶的工作量。用戶不必再書寫這些重複的代碼,從而可以把更多的精力放到完成應用程序特定功能的代碼上:主要是從數據源中讀取數據和顯示。
c. 支持打印預覽和電子郵件發送功能。用戶無需編寫代碼或只需要編寫很少的代碼,就可以爲應用程序提供打印預覽功能。同樣的功能如果需要自己寫的話,需要數千行的代碼。另外,MFC支持在文檔視結構中以電子郵件形式直接發送當前文檔的功能,當然本地要有支持MAPI(微軟電子郵件接口)的應用程序,如Microsoft Exchange。可以這樣理解:MFC已經把微軟開發人員的智慧和技術溶入到了你自己的應用程序中。
由於文檔視結構功能如此強大,因此一般我們都首先使用AppWizard生成基於文檔/視結構的單文檔或多文檔框架程序,然後在其中添加自己的特殊代碼,完成應用程序的特定功能。但是,並非所有基於窗口的應用程序都要使用文檔/視結構。象Visual C++隨帶的例子Hello、MDI都沒有使用文檔/視結構。有兩種情況不宜採用文檔、視結構:
(1)不是面向數據的應用或數據量很少的應用程序,不宜採用文檔/視結構。如一些工具程序包括磁盤掃描程序、時鐘程序,還有一些過程控制程序等。
(2)不使用標準的窗口用戶界面的程序,象一些遊戲等。
7.2文檔視結構程序實例
下面,我們以一個簡單的文本編輯器爲例,說明文檔/視結構的原理及應用。由於我們重在討論文檔/視結構而不是編輯器的實現,因此這個編輯器設計的非常簡單:用戶只能逐行輸入字符,以回車結束一行並換行,不支持字符的刪除和插入,也沒有光標指示當前編輯位置。另外,用戶可以選擇編輯器顯示文本時所使用的字體。
圖7-4
首先,使用AppWizard生成編輯器程序的框架:在New對話框的Project Name編輯框中輸入項目名爲Editor。在AppWizard的第一步選擇Single document ,這將創建一個SDI應用程序。AppWizard第二和第三步選項使用缺省值。在AppWizard Step 4 of 6對話框中,如圖7-4所示,細心的讀者或許會注意到在這一頁裏,有一個Advanced按鈕,以前沒有提到過。現在撳擊該按鈕,彈出Advanced Option對話框,如圖7-5所示。Advanced Option對話框是用來設置文檔視結構和主框架窗口的一些屬性的。
圖7-5
該對話框提供兩個標籤頁,一頁是Document Template String(文檔模板字符串,有關文檔模板字符串,我們還將在後面作詳細介紹),用於設置文檔視結構的一些屬性。它包括以下幾個編輯框:
File Extension:指定應用程序創建的文檔所用的文件名後綴。輸入後綴名txt(不需要·號。),表明Editor使用文本文件的後綴名TXT。
File ID:用於在Windows95的註冊數據庫中標識應用程序的文檔類型。
MainFrame Caption:主框架窗口使用得標題,缺省情況下與項目名相一致,你當然可以將它改爲任何你喜歡的名字,如Editor for Windows等。
Doc Type name:文檔類型名,指定與一個從CDocument派生的文檔類相關的文檔類型名。
Filter Name:用作“打開文件”、“保存文件”對話框中的過濾器。當你在File Extension中輸入後綴名是,Visual Studio會自動給你生成一個過濾器:Editor Files(*.txt)。這樣,當你在Open File對話框中選擇Editor Files(*.txt)時,只有以txt爲後綴名的文件名顯示在文件名列表中。
File new name(short name):用於指定在new對話框中使用的文檔名。當應用程序支持多種文檔類型時,選擇File-New菜單項會彈出一個對話框,列出應用程序所支持的所有文檔類型,供用戶選擇。選擇一種文檔類型後,自動創建相應類型的文檔。這裏我們只支持編輯器這一種文檔類型,故使用缺省值。
File Type name(long name):用於指定當應用程序作爲OLE Automation服務器時使用的文檔類型名。使用缺省值。
另一頁是Window Styles,用於設置主框架窗口的一些屬性,包括框架窗口是否使用最大化按鈕、最小化按鈕,窗口啓動時是否最大化或最小化等。這裏我們使用缺省值,不需要作任何修改。
按OK按鈕,關閉Advanced Option對話框。
AppWizard後面的幾頁對話框都使用缺省值。創建完Editor框架程序後,Visual Studio自動打開Editor工程。現在要修改Editor框架程序,往程序中添加代碼,實現編輯器功能。
7.2.1 文檔/視結構中的主要類
在Editor框架程序中,與文檔視結構相關的類有CEditorApp、CMainFrame、CEditorView和CEditorDoc,它們分別是應用程序類CWinApp、框架窗口類CFrameWnd、視圖類CView和文檔類CDocument的派生類。
應用程序對象
其中,應用程序類負責一個且唯一的一個應用程序對象的創建、初始化、運行和退出清理過程。如果在AppWizard生成框架時指定使用單文檔或多文檔,AppWizard會自動將File菜單下的New、Open和Printer Setup(打印機設置)自動映射到CWinApp的OnFileNew、OnFileOpen、OnFilePrintSetup成員函數,讓CWinApp來處理以上這些消息。如清單7.1,瀏覽CEditorApp類的定義文件有關消息映射的代碼。
清單7.1 CEditorApp的消息映射
BEGIN_MESSAGE_MAP(CEditorApp, CWinApp)
//{{AFX_MSG_MAP(CEditorApp)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
// NOTE - the ClassWizard will add and remove mapping macros here.
// DO NOT EDIT what you see in these blocks of generated code!
//}}AFX_MSG_MAP
// Standard file based document commands
ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
// Standard print setup command
ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
END_MESSAGE_MAP()
這表明,框架已經給我們生成了有關新建文檔、打開文檔以及打印設置的標準代碼,我們不必再去做這些重複的工作了。那麼,當我們新建或打開一個文檔時,應用程序怎麼知道要創建什麼樣的文檔以及創建什麼樣的視圖、框架窗口來顯示該文檔的呢?在文檔/視結構中,應用程序通過爲應用程序所支持的每一種文檔創建一個文檔模板,來創建和管理所有的文檔類型併爲它們生成相應的視圖和框架窗口。
文檔模板
文檔模板負責創建文檔、視圖和框架窗口。一個應用程序對象可以管理一個或多個文檔模板,每個文檔模板用於創建和管理一個或多個同種類型的文檔(這取決於應用程序是單文檔SDI程序還是多文檔MDI程序)。那些支持多種文檔類型(如電子表格和文本)的應用程序,有多種文檔模板對象。應用程序中的每一種文檔,都必需有一種文檔模板和它相對應。比如,如果應用程序既支持繪圖又支持文本編輯,就需要一種一種繪圖文檔模板和文本編輯模板。在下一章裏,我們舉了一個這樣的例子,來說明多種文檔模板的實現技術。
MFC提供了一個文檔模板類CDocTemplate支持文檔模板。文檔模板類是一個抽象的基類,它定義了文檔模板的基本處理函數接口。由於它是一個抽象基類,因此不能直接用它來定義對象而必需用它的派生類。對一個單文檔界面程序,使用CSingleDocTemplate(單文檔模板類),而對於一個多文檔界面程序,使用CMultipleDocTemplate。
文檔模板定義了文檔、視圖和框架窗口這三個類的關係。通過文檔模板,我們可以知道在創建或打開一個文檔時,需要用什麼樣的視圖、框架窗口來顯示它。這是因爲文檔模板保存了文檔和對應的視圖和框架窗口的CRuntimeClass對象的指針。此外,文檔模板還保存了所支持的全部文檔類的信息,包括這些文檔的文件擴展名信息、文檔在框架窗口中的名字、代表文檔的圖標等信息。
提示:每個從CObject派生的類都與一個CRuntimeClass結構相關聯。通過這個結構,你可以在程序運行時刻獲得關於一個對象和它的基類的信息。在函數參數需要作附加類型檢查時,這種運行時刻判別對象類型的能力是非常重要的。C++本身並不支持運行時刻類信息。CRuntimeClass結構包含一個以/0結尾的字符串類名、整型的該類對象大小、基類的運行時刻信息等。 一般在應用程序的InitInstance成員函數實現中創建一個或多個文檔模板,如清單7.2。
清單7.2 CEditorApp的InitInstance成員函數定義
BOOL CEditorApp::InitInstance()
{
//標準的初始化代碼
//......
// Register the application's document templates. Document templates
// serve as the connection between documents, frame windows and views.
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CEditorDoc),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CEditorView));
AddDocTemplate(pDocTemplate);
//其他的初始化代碼和主框架窗口顯示過程
//......
// Enable DDE Execute open
EnableShellOpen();
RegisterShellFileTypes(TRUE);
// Parse command line for standard shell commands, DDE, file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// Dispatch commands specified on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;
// The one and only window has been initialized, so show and update it.
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
// Enable drag/drop open
m_pMainWnd->DragAcceptFiles();
}
在InitInstance中,首先聲明一個CSingleDocTemplate*類型的單文檔模板對象指針(因爲這裏的文本編輯器使用單文檔界面)。然後創建該類型的模板對象。如果要使用多文檔界面,只需要將這裏的CSingleDocTemplate改爲CMultiDocTemplate,當然CMainFrame也要改爲從CFrameWnd改爲CMDIChildWnd或其派生類。
在CSingleDocTemplate構造函數中,還包含一個IDR_MAINFRAME參數。它指向一個字符串資源,這個字符串給出了文檔所使用及顯示時所要求的幾個選項,包括文檔名字、文檔的文件擴展名、在框架窗口上顯示的名字等等,我們稱之爲文檔模板字符串。有關文檔模板字符串還將在下一章使用多個文檔模板這一節作詳細闡述,因此這裏就不展開講了。
然後InitInstance調用AddDocTemplate將創建好的文檔模板加入到應用程序可用的文檔模板鏈表中去。這樣,如果用戶選擇了File-New或File-Open菜單要求創建或打開一個文檔時,應用程序類的OnNewDocument成員函數和OnOpenDocument()成員函數就可以從文檔模板鏈表中檢索出文檔模板提示用戶選擇適當的文檔類型並創建文檔及相關的視圖、框架窗口。
文檔
Editor的文檔類CEditorDoc從CDocument派生下來,它規定了應用程序所用的數據。如果需要在應用程序中提供OLE功能,則需要從COleDocument或其派生類派生出自己的文檔類。
視圖
Editor的視圖類從CView派生,它是數據的用戶窗口。視圖規定了用戶查看文檔數據以及同數據交互的方式。有時一個文檔可能需要多個視圖。
如果文檔需要卷滾,需要從CScrollView派生出視圖類。如果希望視圖按一個對話框模板資源來佈置用戶界面,可以從CFormView派生。由於CFormView經常同數據庫打交道,因此我們把它放在第十章“數據庫技術”中結合數據庫技術講解。感興趣的讀者可以先看看Visual C++ MFC例子CHKBOOK(在SAMPLES/MFC/GENERAL/CHKBOOK目錄下)。
框架窗口
視圖在文檔框架窗口中顯示,它是框架窗口的子窗口。框架窗口作用有二:一是爲視圖提供可視的邊框,還包括標題條、一些標準的窗口組件(最大、最小化按鈕、關閉按鈕),象一個容器一樣把視圖裝起來。二是響應標準的窗口消息,包括最大化、最小化、調整尺寸等。當框架窗口關閉時,在其中的視圖也被自動刪除。視圖和框架窗口關係如圖7-6所示:
圖7-6 視圖和框架窗口的關係
對於SDI程序,文檔框架窗口也就是應用程序的主框架窗口。在MDI應用程序中,文檔框架窗口是顯示在主框架窗口中的子窗口(通常是CMDIChildWnd或其派生類)。
可以從主框架窗口類派生出新類來包含你的視圖,並指定框架的風格和其他特徵。如果是SDI程序,則從CFrameWnd派生出文檔框架窗口:
class CMainFrame:public CFrameWnd
{
...
};
如果是MDI窗口,則需要從CMDIFrameWnd派生出主框架窗口,同時在從CMDIChildWnd或其派生類派生出一個新類,來定製特定文檔窗口的屬性和功能。
在應用程序運行過程中,以上幾種類型的對象相互協作,來處理命令和消息。一個且唯一的一個應用程序對象管理一個或多個文檔模板,每個文檔模板創建和管理一個(SDI)或多個文檔(MDI)。用戶通過包含在框架窗口中的視圖來瀏覽和操作文檔中的數據。在SDI應用程序中,以上對象關係如圖7-7所示。
圖7-7 在SDI程序中各對象的關係
7.2.2 設計文本編輯器的文檔類
弄清這些對象的關係以後,就可以着手往框架裏填寫代碼,實現我們的文本編輯器程序了。從以上分析可以看出,文檔視結構程序的主要工作在於文檔和視圖的設計。
首先設計文檔。程序=數據+算法,在MFC文檔/視結構中,最關鍵的就是文檔的設計。怎樣保存用戶輸入的文本行?方法之一是保存一組指針,每個指針指向一個文本行。如果使用C語言來寫這個程序的話,需要分配內存來存放這些指針,還要自己編寫文本行的動態分配、增加、刪除等例程。但是MFC簡化這些工作,它提供了集合類(collection classes)。
集合類是用來容納和處理一組對象或標準數據類型變量的C++類。每個集合類對象可以看作一個單獨的對象。類成員函數可作用於集合的所有元素。MFC提供兩種類型的集合類:
基於模板的集合類
非基於模板的集合類
這兩種集合類 對用戶來說非常相似。基於模板的集合所包含的元素是用戶自定義的數據結構或者說是抽象的數據結構,它以數組、鏈表和映射表三種方式組織用戶自定義的數據結構。使用基於模板的集合類需要用戶作一些類型轉換工作。非基於 模板的集合類提供的是一組現成的、用於某種預定義的數據類型(如CObject、WORD、BYTE、DWORD、字符串等)的集合。在設計程序時,如果所用的數據類型是預定義的,如下面的編輯要用到的字符串,則使用非基於模板的集合類;如果所用得數據類型是用戶自定義的數據結構類型,那就要用到基於模板的集合類。
根據對象在集合中的組織合存儲方式,集合類又可分爲三種類型:鏈表、數組、映射(或字典)。應當根據特定的編程問題,選擇適當的類型。
鏈表:鏈表類用雙向鏈表實現有序的、非索引的元素鏈表。鏈表有一個頭或尾。很容易從頭或尾增加或刪除元素、遍歷所有元素,在中間插入或刪除元素。鏈表在需要增加、刪除元素的場合效率很高。非基於模板的鏈表有三種:CObList、CPtrList、CStringList,分別用於管理對象指針、無類型指針和字符串。可以使用鏈表創建堆棧和隊列。
要訪問鏈表的成員,可以使用GetNext和GetHeadPosition()。
要刪除鏈表的成員,可以用GetHeadPosition()和GetNext()來遍歷鏈表,然後用delete刪除其中的對象,最後調用RemoveAll刪除鏈表所包含的指針。
數組類提供一個可動態調整數組大小的、有序的、按整數索引的對象數組。數組在內存中連續的存放固定長度的數組元素。數組的最大優點是可以隨時存取任一元素。數組類包括基於模板的CArray,它可以存放任何類型的數據;MFC還爲字節、字、雙字、CString對象、CObject指針和無類型指針提供了預定義的類。數組的元素可以通過一個以零爲基礎的整數下標直接進行訪問。下標操作符([])可用於設置或檢取數組元素。如果要設置一個超過數組當前範圍的元素,可以指定該數組是否自動增大。但是如果要調整數組大小時,則數組佔用的內存塊需要重新移動,效率很低。如果不要求調整數組大小,則對數組集合的訪問和對標準C數組的訪問一樣快。在使用數組之前,應使用SetSize建立其大小,並分配內存。若不用SetSize,象數組添加元素時會導致頻繁的再分配內存和拷貝數據。數組類適用於那些需要快速檢索、很少需要增加或刪除元素的集合。
數組通過GetAt(索引值)來訪問數組中的成員。
要刪除數組中的成員,可以用GetSize()取得大小,然後遍歷數組中成員,用delete刪除,然後調用RemoveAll()清除其中的指針數據。
下面是使用數組模板類的例子:
CArray<CMyClass,CMyClass&> myArray;
CMyClass myClass;
myArray->Add(myClass);
映射類以一種字典的方式組織數據。每個元素由一個關鍵字和一個數值項組成,關鍵字用作數值項的標識符,在集合中不允許重複,必須是唯一的。如果給出一個關鍵字,映射類會很快找到對應的數值項。映射查找是以哈希表的方式進行的,因此在映射中查找數值項的速度很快。除了映射類模板外,預定義的映射類能支持CString對象、字、CObject指針和無類型指針。比如,CMapWordToOb類創建一個映射表對象後,就可以用WORD類型的變量作爲關鍵字來尋找對應的CObject指針。映射類最適用於需要根據關鍵字進行快速檢索的場合。
要訪問映射中的數據,可以用GetStartPosition()定位到開始處,再用GetNextAssoc訪問映射表中的成員。
要刪除映射中的數據,可以用GetStartPosition和GetNextAssoc遍歷並用delete刪除對象,然後調用RemoveAll。
下面是使用CMap模板類的例子:
CMap<CString,LPCSTR,CPerson,CPerson&> myMap;
CPerson person;
LPCSTR lpstrName=“Tom”;
myMap->SetAt(lpstrName,person);
有關集合類的使用可以參見MFC的例子COLLECT。
對於文本編輯器,由於需要動態增加和刪除每一行字符串,因此使用CStringList來保存文本編輯器的數據,CStringList中的每一個元素是CString類型的,它代表一行字符。可以把CString看作一個字符數組,但它提供了豐富的成員函數,比字符數組功能強大的多。
另外,還需要增加一個數據成員nLineNum,用於指示當前編輯行行號。如清單7.3,在文檔類的頭文件EditorDoc.h中,加入以下代碼:
清單7.3 CEditorDoc.h
class CEditorDoc : public CDocument
{
protected: // create from serialization only
CEditorDoc();
DECLARE_DYNCREATE(CEditorDoc)
// Attributes
public:
CStringList lines;
int nLineNum;
...
};
在定義了文檔數據成員後,還要對文檔數據成員進行初始化。
初始化文檔類的數據成員
當用戶啓動應用程序,或從應用程序的File菜單種選擇New選項時,都需要對文檔類的數據成員進行初始化。一般的,類的數據成員的初始化都是在構造函數中完成的,在構造函數調用結束時對象才真正存在。但對於文檔來說卻不同,文檔類的數據成員初始化工作是在OnNewDocument成員函數中完成的,此時文檔對象已經存在。爲什麼呢?這是因爲:在單文檔界面(SDI)應用程序中,在應用程序啓動時,文檔對象就已經被創建。文檔對象直到主框架窗口被關閉時才被銷燬。在用戶選擇File-New菜單時,應用程序對象並不是銷燬原來的文檔對象然後重建新的文檔對象,而只是重新初始化(Re-Initialization)文檔對象的數據成員,這個初始化工作就是應用程序對象的OnFileNew()消息處理成員函數通過調用OnNewDocument()函數來完成的。試想,如果把初始化數據成員的工作放在構造函數中的話,由於對象已經存在,構造函數就無法被調用,也就無法完成初始化數據成員的工作。爲了避免代碼的重複,在應用程序啓動時,應用程序對象也是通過調用OnNewDocument成員函數來初始化文檔對象的數據成員的。如果是多文檔界面(MDI)程序,則數據成員的初始化也可以放到構造函數中完成。因爲在MDI中,選擇File->New菜單時,應用程序對象就讓文檔模板創建一個新文檔並創建對應的框架窗口和視圖。但是,爲了保證應用程序在單文檔和多文檔界面之間的可移植性,我們還是建議將文檔數據成員的初始化工作放在OnNewDocument()中完成,因爲在MDI的應用程序對象的OnFileNew成員函數中,同樣會調用文檔對象的OnNewDocument成員函數。
在OnNewDocument成員函數中手工加入代碼,如清單7.4。
清單7.4 OnNewDocument成員函數
BOOL CEditorDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
// TODO: add reinitialization code here
// (SDI documents will reuse this document)
nLineNum=0;
POSITION pos;
pos=lines.GetHeadPosition();
while(pos!=NULL)
{
((CString)lines.GetNext(pos)).Empty();
}
lines.RemoveAll();
return TRUE;
}
其中pos類型爲POSITION,相當於鏈表的指針,指向鏈表當前元素。CStringList的成員函數GetHeadPosition()返回鏈表頭指針。鏈表的GetNext()函數以當前指針爲參數,返回下一個元素指針,同時修改pos,使它指向下一個元素。使用強制類型轉換將GetNext()函數返回的元素指針轉化爲CString類型,然後調用CString::Empty()方法清除該行中的所有字符。通過一個while循環,清除所有文本行的數據。最後調用CStringList的RemoveAll()成員函數,清除鏈表中的所有指針(注意:此時這些指針指向的元素已經被清除)。
提示:應用程序對象的成員函數CWinApp::OnFileNew()在選擇File菜單的New命令時被調用,缺省時在InitInstance()中也會被調用。原理是在InitInstance()中有一個命令行參數的執行過程,當命令行上沒有參數時,函數ParseCommandLine(cmdInfo)會調用CCommandLineInfo :: 把m_nShellCommand成員置爲CCommandLineInfo::FileNew,這導致ProcessShellCommand成員函數調用OnFileNew。用戶可在InitInstance()中顯式的調用OnFileNew()。
應用程序對象的OnFileNew消息處理流程如下:首先判斷應用程序是否有多個文檔模板,若是,則顯示一個對話框讓用戶選擇創建哪種類型的文檔(模板)。對話框中顯示的字符串是與文檔模板對象的構造函數的第一個參數相對應的字符串(若資源中無相應字符串則不顯示)。然後該函數調用CDocManager::OpenDocumentFile(NULL)成員函數,打開一個新文件。CDocManager::OpenDocumentFile函數調用了CSingleDocTemplate的OpenDocumentFile,後者完成實際的創建文檔、框架、視圖工作。文檔模板的OpenDocumentFile首先判斷文檔是否已經被創建,若未創建,則創建一個新文檔。然後根據文件名參數是否爲空,分別調用CDocument的OnNewDocument( )和CDocument的OnOpenDocument()函數。CDocument的OnNewDocument首先調用DeleteContents(),並將文檔修改標誌該爲FALSE(關閉窗口時將根據文檔修改標誌決定是否提示用戶保存文檔)。清理文檔類的數據成員
在關閉應用程序刪除文檔對象時,或用File->Open菜單打開一個文檔時,需要清理文檔中的數據。同文檔的初始化一樣,文檔的清理也不是在文檔的析構函數中完成,而是在文檔的CDocument::DeleteContents()成員函數中完成的(想想爲什麼?)。析構函數只用於清除那些在對象生存期都將存在的數據項。DeleteContents()成員函數的調用有兩個作用:
1.刪除文檔的數據;
2確信一個文檔在使用前爲空。
前面已經說到,OnNewDocument函數會調用DeleteContents()函數。在用戶選擇File->Open菜單時,應用程序對象調用應用程序類的OnFileOpen成員函數,CWinApp::OnFileOpen調用內部的文檔管理類CDocManager::OnFileOpen()成員函數,提示用戶輸入文件名。然後調用CWinApp::OpenDocumentFile打開一個文件。OpenDocumentFile在打開文件後首先調用DeleteContents成員函數清理文檔中的數據,確保消除以前打開的文檔的數據被清理掉。
缺省的DeleteContents函數什麼也不做。你需要重載DeleteContents函數,並編寫自己的文檔清理代碼。要重載DeleteContents成員函數:
從View菜單下選擇ClassWizard,啓動ClassWizard,選擇Message Maps頁。在ClassName下拉列表框中選擇CEditorDoc,從ObjectIDs列表框選擇CEditorDoc,在Message列表框雙擊DeleteContents。此時DeleteContents出現在Member functions列表框中,並被選中。點Edit Code按鈕,開始編輯DeleteContents函數定義。在DeleteContents函數體中加入代碼後,如清單7.5所示:
清單7.5 CEditorDoc的DeleteContents成員函數
void CEditorDoc::DeleteContents()
{
// TODO: Add your specialized code here and/or call the base class
nLineNum=0;
/*刪除集合類的數據:
用GetHeadPosition和GetNext遍歷並用delete刪除其中的數據,然後調 用RemoveAll()刪除鏈表所包含的指針
*/
POSITION pos;
pos=lines.GetHeadPosition();
while(pos!=NULL)
{
((CString)lines.GetNext(pos)).Empty();
//調用CString的Empty()方法清除文本行的數據,對於其它類型的對 //象,應當調用delete 刪除該對象
}
lines.RemoveAll();
CDocument::DeleteContents();
}
編輯器的DeleteContents()實現與OnNewDocument()基本相同,別的程序則可能會有所不同。
CDocument::OnOpenDocument成員函數在調用DeleteContents()函數後,將文檔修改標記設置爲FALSE(未修改),然後調用Serialize進行文檔的串行化工作。
讀寫文檔——串行化
文檔對象的串行化是指對象的持續性,即對象可以將其當前狀態,由其成員變量的值表示,寫入到永久性存儲體(通常是指磁盤)中。下次則可以從永久性存儲體中讀取對象的狀態,從而重建對象。這種對象的保存和恢復的過程稱爲串行化。對象的可持續性允許你將一個複雜的對象網絡保存到永久性存儲體中,從而在對象從內存中刪去後仍保持它們的狀態。以後,可以從永久性存儲器中載入對象並在內存中重載。保存和載入可持續化、串行化的數據通過CArchive對象作爲中介來完成。
文檔的串行化在Serialize成員函數中進行。當用戶選擇File Save、Save As或Open命令時,都會自動執行這一成員函數。AppWizard只給出了一個Serialze()函數的框架,讀者要做的時定製這個Serialize函數。Serialize()函數由一個簡單的if-else語句組成:
void CEditorDoc::Serialze(CArchive& ar)
{
if(ar.IsStoring())
{
//TODO: add storing code here.
}
else
{
//TODO: add loading code here.
}
}
在框架中,Serialize函數的參數ar是一個CArchive類型對象,它包含一個CFile類型的文件指針(類似於C語言的文件指針),執行一個文件。CArchive對象爲讀寫CFile(文件類)對象中的可串行化數據提供了一種類型安全的緩衝機制。通常CFile代表一個磁盤文件;但它也可以是一個內存文件(CMemFile對象)或剪貼板。一個給定的CArchive對象只能讀數據或寫數據,而不能同時讀寫數據。當保存數據到archive對象中時,archive把它放在一個緩衝區中。直到緩衝區滿,才把數據寫入它所包含的文件指針指向的CFile對象中。同樣的,當從archive對象讀數據時,archive對象從文件中讀取內容到緩衝區,然後再從緩衝區讀入到可串行化的對象中。這種緩衝機制減少了訪問物理磁盤的次數,從而提高了應用程序的性能。
在創建和使用一個CArchive對象之前,必須先創建一個CFile文件類對象。而且還必須確保archive的載入和保存狀態同文件打開模式相兼容。幸運的是,應用程序框架已經爲我們做好了這些工作。
當應用程序響應File->Open、File-Save和File-Save As命令時,應用程序框架都會通過調用CDocument成員函數(對於File->Open調用OnOpenDocument,對於File->Save和File->Save As調用OnSaveDocument)創建CFile對象,並以適當的方式打開文件,對於File->Open是打開文件並讀,對於Save和SaveAs是打開文件並寫。然後框架會自動把文件對象連接到一個CArchive對象上,並設置CArchive的讀寫方式。
在Editor的Serialize()函數體內,我們看到CArchive對象有一個IsStoring()成員函數。該成員函數告訴串行化函數是需要寫入還是讀取串行數據。如果數據要寫入(Save或Save As),IsStoring()返回布爾值TRUE;如果數據是被讀取,則返回FALSE。
現在添加串行化操作代碼,實現編輯器文檔的讀寫功能。修改後的Serialize()函數形式如清單7.6。
清單7.6 CEditorDoc的串行化方法
/////////////////////////////////////////////////////////////////////////////
// CEditorDoc serialization
void CEditorDoc::Serialize(CArchive& ar)
{
CString s("");
int nCount=0;
CString item("");
if (ar.IsStoring())
{
POSITION pos;
pos=lines.GetHeadPosition();
if(pos==NULL)
{
return;
}
while(pos!=NULL)
{
item=lines.GetNext(pos);
ar<<item;
item.Empty();//clear the line buffer
}
}
else
{
// TODO: add loading code here
while(1)
{
try{
ar>>item;
lines.AddTail(item);
nCount++;
}
catch(CArchiveException *e)
{
if(e->m_cause!=CArchiveException::endOfFile)
{
TRACE0("Unknown exception loading file!/n");
throw;
}else
{
TRACE0("End of file reached.../n");
e->Delete();
}
break;
}
}
nLineNum=nCount;
}
}
在If子句中,從字符串鏈表中逐行讀取字符串,然後通過調用CArchive對象的<<操作符,將文本行寫入ar對象中。在else子句中,從CArchive對象逐一讀入字符串對象,然後加入到鏈表中。由於在Serialize()函數的載入文檔調用之前,框架已經調用CDocument的DeleteContents()成員函數作好了清理工作,這裏不必再重複清理字符串鏈表。在載入字符串對象的同時,統計了字符串的個數即文本行數。由於這裏使用CString的串行化,因此獲得的文件不同於普通的文本文件。
文檔串行化與一般文件處理方式最大的不同在於:在串行化中,對象本身對讀和寫負責。在上面的例子中,CArchive並不知道也不需要知道CString類的文本行內部數據結構,它只是調用CString類的串行化方法實現對象到文件的讀寫操作,也就是說,實際完成讀寫操作的是CString類,CArchive只是對象到CFile類的對象的一箇中介。而文檔的串行化正是通過調用文檔中需要保存的各個對象的串行化方法來完成的。這幾個對象的關係如圖7-8所示。這裏的對象必須是MFC對象,如果想讓自己設計的對象也具有串行化能力,就必須定製該對象的串行化方法。有關定製串行化對象的技術在後面再作詳細介紹。
圖7-8 文檔對象和文件對象
CArchive對象使用重載的插入(<<)和提取(>>)操作符執行讀和寫操作。有人會說,這種方式很象C++的輸入輸出流。其實,一個archive對象就是可以理解成一種二進制流。象輸入/輸出流一樣,一個archive對象與一個文件相關聯,並提供緩衝讀寫機制。但是,一個輸入/輸出流處理的是ASCII字符,而一個archive對象處理的是二進制對象。
如果不是使用框架創建和希望自己創建CArchive的話,可以這麼做:
CFile file;//聲明一個CFile類對象
file.Open(“c://readme.txt”,CFile::modeCreate|CFile::modeWrite);//打開文件
CArchive ar(&file,CArchive::store);//用指向file的指針創建CArchive類對
//象,指定模式爲store即存儲,如果需要從CArchive //中載入,可設爲load
...//一些串行化工作
ar.Close();//首先關閉CArchive,然後關閉file
file.Close();
在文檔中引用視圖類
有時要在文檔對象中訪問視圖對象,而一個文檔可能會對應多個視圖,此時可以採用如下方法:
POSITION pos=GetFirstViewPosition();//獲取視圖鏈表的頭指針
CEditorView *MyView=(CMyView*)GetNextView(pos);
7.2.3 文本編輯器的視圖類
視圖類數據成員設計
現在設計文本編輯器的視圖類。由於編輯器需要提供顯示字體選擇功能,因此在編輯器內增加一個數據成員代表當前所用的字體。另外,還需要兩個變量lHeight和cWidth分別代表所用字體的高度和寬度,以便控制輸出,因爲Windows以圖形方式輸出,輸出文本也需要程序員自己計算座標。修改後的視圖類如下面的片段所示:
class CEditorView : public CView
{
protected: // create from serialization only
CEditorView();
DECLARE_DYNCREATE(CEditorView)
CFont* pFont;
int lHeight;
int cWidth;
...
}
也許有人會問:既然文檔類包含應用程序的數據,而視圖只負責輸出,爲什麼不把數據全部放在文檔類之中呢?從應用程序角度來看,視圖是不包含數據的,顯示文檔的所有數據都是從文檔對象中讀取的。但這並不意味着視圖不能包含數據成員。視圖是從CView派生出來的類,作爲類,它當然可以包含數據成員。而且,爲了顯示輸出的需要,它經常包含一些與顯示相關的數據成員。設計文檔視結構的關鍵就是確切的定義用戶文檔應當包含哪些信息。那麼,如何合理分配文檔和視圖的數據成員呢?一條簡單的原則是:如何使用更方便,就如何分配數據成員。另外,還要看該數據成員是否需要保存到文檔中,如果要保存到文檔中,就必須放在文檔中。因爲文檔可以對應多個視圖,如果放在視圖中,由於不同的視圖的數據成員可以有不同的數值,這樣文檔保存時就不知道該使用哪一個數值了。一般的,與顯示相關的數據成員都可以放在視圖類中。在上面的文本編輯器中,我們並不需要保存編輯器使用何種字體這一信息,而這一信息又與文檔顯示密切相關,因此把它放在視圖類中是很恰當的。這樣的話,還可以用多個使用不同字體的視圖觀察同一文檔。但是,如果編輯器是一個類似於Microsoft WORD之類的字處理器,在顯示中支持多種字體的同一屏幕輸出,這時需要保存字體信息,就要把字體信息放在文檔類中了。
視圖數據成員的初始化
在文檔類中,通過成員函數OnNewDocument()來完成文檔類數據成員的初始化工作。視圖類也提供了一個CView::OnInitialUpdate()成員函數來初始化視圖類的數據成員。
在以下情況下,應用程序將自動執行視圖類的OnInitialUpdate()來初始化視圖類數據成員:
用戶啓動應用程序
從File菜單選擇New菜單項,CWinApp::OnFileNew在調用CDocument::OnNewDocument後即調用OnInitialUpdate準備繪圖輸出;
用File->Open命令打開一個文件,此時希望清除視圖原有的顯示內容
在編輯器中要做的主要工作是對編輯器使用的字體的初始化,見清單7.7。
清單7.7 視圖的OnInitialUpdate方法
void CEditorView::OnInitialUpdate()
{
// TODO: Add your specialized code here and/or call the base class
CDC *pDC=GetDC();
pFont=new CFont();
if(!(pFont->CreateFont(0,0,0,0,FW_NORMAL,FALSE,FALSE,FALSE,
ANSI_CHARSET,OUT_TT_PRECIS,CLIP_TT_ALWAYS,
DEFAULT_QUALITY,DEFAULT_PITCH,"Courier New")))
{
pFont->CreateStockObject(SYSTEM_FONT);
}
CFont* oldFont=pDC->SelectObject(pFont);
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
lHeight=tm.tmHeight+tm.tmExternalLeading;
cWidth=tm.tmAveCharWidth;
pDC->SelectObject(oldFont);
CView::OnInitialUpdate();
}
OnInitialUpdate()首先調用GetDC()取得當前窗口的設備上下文指針並存放在pDC中。設備上下文(簡稱DC,英文全稱是device context)Windows數據結構,它描述了在一個窗口中繪圖輸出時所需的信息,包括使用的畫筆、畫刷、當前選用的字體及顏色(前景色和背景色)、繪圖模式,以及其它所需要的繪圖信息。MFC提供一個CDC類封裝設備上下文,以簡化存取DC的操作。
然後OnInitialUpdate()創建視圖顯示時所用的字體。同前面提到的其他MFC對象如框架窗口一樣,字體對象的創建也分爲兩步:第一步,創建一個C++對象,初始化CFont的實例;第二步,調用CreateFont()創建字體。除了CreateFont之外,還有兩個創建字體的函數:CreateFontIndirect和FromHandle(),前者要求一個指向所需字體的LOGFONT(邏輯字體)的指針作參數,後者需要一個字體句柄作參數。如果CreateFont()因爲某種原因失敗,那麼就調用CreateStockObject()從預定義的GDI對象中創建字體。
注意:在Windows的GDI中,包含一些預定義的GDI對象,無需用戶去創建,馬上就可以拿來使用。這些對象稱作庫存(Stock)對象。庫存對象包括BLACK_BRUSH(黑色畫刷)、DKGRAY_BRUSH(灰色畫刷)、HOLLOW_BRUSH(空心畫刷)、WHITE_BRUSH(白色畫刷)、空畫刷、黑色畫筆、白色畫筆以及一些字體和調色板等。CGdiObject:: CreateStockObject()並不真正創建對象,而只是取得庫存對象的句柄,並將該句柄連到調用該函數的GDI對象上。
然後調用CDC的SelectObject()方法,將字體選入到設備上下文中。SelectObject()函數原型如下:CPen* SelectObject( CPen* pPen );
CBrush* SelectObject( CBrush* pBrush );
virtual CFont* SelectObject( CFont* pFont );
CBitmap* SelectObject( CBitmap* pBitmap );
int SelectObject( CRgn* pRgn );
SelectObject的參數可以是一個畫筆、畫刷、字體、位圖或區域,它們統稱爲GDI(圖形設備接口)對象。SelectObject將一個GDI對象選入到一個設備上下文中,新選中的對象將替換原有的同類型對象,然後返回指向被替換的對象的指針。SelectObject()知道它所選中的對象的類型,且總是返回同類的舊對象的指針。還要存儲返回的CFont指針,在退出OnInitialUpdate之前調用pDC->SelectObject(oldFont),將CDC重新設置成原來的初始狀態。
讀者以後編程也應當養成這樣一個習慣:在用SelectObject選擇新的GDI對象時,應當保存指向原先使用的GDI對象的指針,在繪圖結束後,再用SelectObject選擇原來的對象,設置CDC爲其初始狀態。否則的話,會有非法句柄留在設備上下文對象中,積累下去將導致無法預見的錯誤。但是,如果該設備上下文是自己創建而不是用參數傳遞過來的,則不必恢復畫筆或刷子。象上面的例子,其實用戶不必在退出時恢復原來的字體。而在下面要講的OnDraw函數中,由於pDC是框架傳給OnDraw的,因此在退出時必須恢復設備上下文中原來的字體設置。總之,如果用戶能肯定畫筆或刷子等GDI對象廢棄以前設備對象會被銷燬,則不必恢復設備上下文中GDI對象的設置。不過,爲概念上的明確,還是建議調用恢復過程。
TEXTMETRIC是一個數據結構,它包含字體的寬度、高度、字的前後空白等字段。調用CDC::GetTextMetrics()獲取字體的TEXTMETRIC,從而取得字體的寬度和高度等信息。最後調用CView類的OnInitialUpdate()函數來畫視圖。
由於在堆棧上創建了視圖所用的字體對象pFont,在關閉視圖時就需要刪除該字體對象。這部分工作在視圖的析構函數中完成。修改視圖的析構函數:
CEditorView::~CEditorView()
{
if(pFont!=NULL)
delete pFont;
}
視圖的繪製
現在要讓視圖顯示編輯器中的文本。AppWizard爲視圖類CEditorView生成了一個OnDraw()方法,當需要重畫視圖時,該函數就會被調用。清單7.8是編輯器的OnDraw函數定義:
清單7.8 視圖的OnDraw方法
/////////////////////////////////////////////////////////////////////////////
// CEditorView drawing
void CEditorView::OnDraw(CDC* pDC)
{
CEditorDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
CFont *oldFont;
//選擇新字體
oldFont=pDC->SelectObject(pFont);
//縱向yval座標爲0
int yval=0;
POSITION pos;
CString line;
//取得文本行鏈表的頭指針
if(!(pos=pDoc->lines.GetHeadPosition()))
{
return;
}
//循環輸出各文本行
while(pos!=NULL)
{
line=pDoc->lines.GetNext(pos);
pDC->TextOut(0,
yval,
line,
line.GetLength());
//更新y座標值,讓它加上文本行所用字體的高度
yval+=lHeight;
}
//恢復原來DC所用的字體
pDC->SelectObject(pFont);
}
框架調用視圖的CView::OnDraw(CDC* pDC)方法完成屏幕顯示、打印、打印預覽功能,對於不同的輸出功能它會傳遞不同的DC指針給OnDraw()函數。
在OnDraw()函數中,首先調用GetDocument()函數,取得指向當前視圖所對應的文檔的指針。通過這個指針,來訪問文檔中的數據。以後在視圖中修改文檔中的數據,也是通過GetDocument()來取得文檔指針,再通過該文檔指針修改文檔中的數據。
在繪圖時,可以通過傳給OnDraw函數的一個設備上下文DC的指針pDC進行GDI調用。開始繪圖之前,往往需要選擇GDI資源(或GDI對象,包括畫筆、刷子、字體等),將它選入到設備上下文中。在文本編輯器中,我們選擇一種字體pFont到設備上下文中,以後在窗口客戶區的文本輸出就都會使用該字體繪製。在繪製過程中,繪圖代碼是設備無關的,也就是說它並不需要知道目前使用的是什麼設備(屏幕、打印機或其他繪圖設備)。
讀者以前如果用Borland C++或SDK編寫過Windows程序的話,都會知道:當窗口或窗口的一部分變成無效的話(比如其他窗口從本窗口上拖過、窗口調整大小等),操作系統就會向窗口發送一條WM_PAINT消息。窗口接收到該消息之後,調用Borland C++的EvPaint()或Visual C++的OnPaint()完成窗口繪製工作。這裏OnDraw()函數也同樣完成窗口繪圖輸出,這兩者有什麼關係呢?
我們先看一下OnPaint()函數:
void CMyWindow::OnPaint()
{
CPaintDC dc(this); //用於窗口繪製的設備上下文
CString str(“Hello,world!”);
...
//繪圖輸出代碼
dc.TextOut(10,10,str,str.GetLength());
}
在OnPaint()函數中,首先創建一個CPaintDC類的對象dc。CPaintDC必需也只能用在WM_PAINT消息處理中。在CPaintDC類對象dc的構造函數中,調用了在SDK下需要顯式調用的BeginPaint函數,取得處理WM_PAINT消息時所需的設備上下文。然後OnPaint()函數使用該設備上下文完成各種輸出。在OnPaint()函數退出時,dc對象被刪除。在dc對象的析構函數中,包含了對EndPaint函數的調用。EndPaint一方面釋放設備上下文,另一方面還從應用消息隊列中刪除WM_PAINT消息。如果在處理WM_PAINT時不使用CPaintDC,則WM_PAINT不被消除,會產生不斷重畫的現象。
視圖是一個子窗口,它自然也從窗口類繼承了OnPaint()成員函數,用以響應WM_PAINT消息。類似於上面的例子,視圖OnPaint處理函數首先創建一個與顯示器相匹配的CPaintDC類的設備上下文對象dc,但是OnPaint不再直接完成窗口輸出,而是將設備上下文傳給OnDraw()成員函數,由OnDraw()函數去完成窗口輸出。當打印輸出時,框架會調用視圖的DoPreparePrinting創建一個與打印機相匹配的設備上下文並將該DC傳遞給OnDraw()函數,由OnDraw函數完成打印輸出。這樣,OnDraw()函數就把用於屏幕顯示和打印機輸出的工作統一起來,真正體現了設備無關的思想。如果想知道當前OnDraw函數是在用於屏幕顯示還是打印輸出,可以調用CView::IsPrinting()函數。當處於打印狀態時,IsPrinting()返回TRUE;在用於屏幕顯示時,返回FALSE。
文檔修改時通知視圖的更新
當文檔以某種方式變化時,必須通知視圖作相應的更新即重繪,以反應文檔的變化。這種情況通常發生在用戶通過視圖修改文檔時。此時,視圖將調用文檔的UpdateAllViews成員函數通知同一文檔的所有視圖對自己進行更新。UpdateAllViews將調用每個視圖的OnUpdate成員函數,使視圖的客戶區無效。
5 視圖的消息處理
視圖作爲一個子窗口,當然可以處理消息。但是應用程序運行時,除了視圖外,還有應用程序對象、主框架窗口、文檔等,它們都是可以處理消息的。那麼消息傳遞過程是什麼樣的呢?
MFC的命令消息按以下方式傳遞:
圖7-9 文檔視結構中的消息傳遞
鍵盤消息處理
前面的視圖繪製就是完成窗口消息WM_PAINT的處理。編輯器要接收用戶的鍵盤輸入,就必須處理鍵盤消息;另外,在用戶輸入字符時,還必須馬上就把用戶輸入的內容在屏幕上顯示出來。
用ClassWizard生成處理WM_CHAR消息的函數OnChar(),然後打開該函數進行編輯。修改後的OnChar函數如清單7.9:
清單7.9 CEditorView的OnChar()成員函數
void CEditorView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CEditorDoc* pDoc=GetDocument();
CClientDC dc(this);
CFont *oldFont;
//選擇新字體
oldFont=dc.SelectObject(pFont);
CString line("");//存放編輯器當前行字符串
POSITION pos=NULL;//字符串鏈表位置指示
if(nChar=='/r')
{
pDoc->nLineNum++;
}
else
{
//按行號返回字符串鏈表中位置值
pos=pDoc->lines.FindIndex(pDoc->nLineNum);
if(!pos)
{
//沒有找到該行號對應的行,因此它是一個空行,
//我們把它加到字符串鏈表中。
line+=(char)nChar;
pDoc->lines.AddTail(CString(line));
}
else{
//當前文本行還沒有換行結束,因此將文本加入到行末
line=pDoc->lines.GetAt(pos);
line+=(char)nChar;
pDoc->lines.SetAt(pos,line);
}
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
dc.TextOut(0,
(int)pDoc->nLineNum*tm.tmHeight,
line,
line.GetLength());
}
pDoc->SetModifiedFlag();
dc.SelectObject(oldFont);
CView::OnChar(nChar,nRepCnt,nFlags);
}
因爲編輯器要將用戶輸入內容加入到文本行緩衝區中,因此首先調用GetDocument()獲取指向文檔的指針,以便對文檔中的數據進行修改。
爲了在收到鍵盤輸入消息後在窗口中輸入字符,需要定義一個CClientDC類的對象dc。CClientDC是用於管理窗口客戶區的設備上下文對象,它在構造函數中調用GetDC()取得窗口客戶區設備上下文,在析構函數中調用ReleaseDC()釋放該設備上下文。CClientDC同樣用於在窗口客戶區的輸出,它與CPaintDC不同之處在於:
CPaintDC專門用於在窗口OnPaint()中的輸出,而不能用於其它非窗口重畫消息的處理。如果不是在OnDraw或OnPaint()中繪圖,則需要創建一個CClientDC對象,然後調用CClientDC的方法來完成繪圖輸出。
OnChar()接下去處理用戶輸入。如果輸入是一個回車,則將總行數nLineNum加一,否則將輸入字符加到當前行行末。最後調用TextOut函數輸出當前編輯中的文本行。
最後調用文檔的SetModifiedFlag()方法設置文檔的修改標誌。SetModifiedFlag()函數原型如下:
void SetModifiedFlag( BOOL bModified = TRUE );
從函數原型可以看出,函數缺省參數爲TRUE。當調用SetModifiedFlag時,將文檔內的修改標誌置爲真。如果用戶執行了Save或Save As操作,則將文檔的修改標誌置爲假。這樣,當用戶關閉文檔的最後一個視圖時,框架根據該修改標記決定是否提示用戶保存文檔中的數據到文件。如果用戶上次作了修改還沒有存盤,則彈出一個消息框,提示是否保存文件。這些都是框架程序來完成的。
用戶如果在視圖的其它任何地方修改了文檔,也必須調用SetModifiedFlag來設置文檔修改標記,以便關閉窗口時讓框架提示保存文檔。
菜單消息處理
現在還要增加一個菜單,用戶選擇菜單時會彈出一個字體選擇對話框,讓用戶選擇視圖輸出文檔時所用的字體。用菜單編輯器在View菜單下增加一個菜單項“Select Font”,菜單項相關參數如下:
菜單名:Select &Font
菜單ID:ID_SELECT_FONT
提示文字:Select a font for current view
然後用ClassWizard爲該菜單項生成消息處理函數SelectFont。在選擇消息響應的類時,用戶可以選擇文檔、視圖、框架或應用程序類,這根據具體情況而定。如果操作是針對某一視圖(比如象本例中改變字體操作),則消息處理放在視圖中比較合適。如果操作是針對文檔的(比如要顯示文檔中對象的屬性等),則放在文檔中處理比較合適。如果選項對應用程序中的所有文檔和視圖都有效(即是全局的選項),那麼可以把它放在框架窗口中。
修改OnSelectFont()函數,使它能顯示字體選擇對話框,修改後的OnSelect函數見清單7.10:
清單7.10 OnSelectFont()函數
void CEditorView::OnSelectFont()
{
CFontDialog dlg;
if(dlg.DoModal()==IDOK)
{
LOGFONT LF;
//獲取所選字體的信息
dlg.GetCurrentFont(&LF);
//建立新的字體
pFont->DeleteObject();
pFont->CreateFontIndirect(&LF);
Invalidate();
UpdateWindow();
}
}
在OnSelectFont()消息處理函數中,首先定義一個選擇字體公用對話框,然後顯示該對話框,返回所選的字體。有關選擇字體公用對話框的知識參見第五章對話框技術。字體對話框通過GetCurrentFont()返回邏輯字體信息。所謂邏輯字體是一種結構,它包含了字體的各種屬性的描述,包括字體的名字、寬度、高度和是否斜體、加粗等信息。字體對象首先通過DeleteObject刪除原來的字體對象,然後通過CreateFontIndirect、利用邏輯字體的屬性來創建字體。由於我們選擇了一種新的字體,所以要用新的字體來重繪視圖。爲此,調用Invalidate()函數向視圖發送WM_PAINT消息。由於WM_PAINT消息級別比較低,不會立即被處理。因此,再調用UpdateWindow()強制窗口更新。這也是一種常用的技巧。
現在已經完成了編輯器文檔類和視圖類的設計,對主框架窗口類不需要修改。編譯、鏈接並運行程序,彈出文本編輯器窗口。試着輸入幾行文本,存盤。然後再載入剛纔保存的文件,如圖7-10。在File-Exit菜單項上面,有一個文件名列表,列出最近打開過的文件,這個表稱作MRU表(MRU是英文Most Recently Used的縮寫)。可以從MRU中選擇一個文件名,打開該文件。
圖7-10 一個簡單的文本編輯器
7.3 讓文檔視結構程序支持卷滾
但是,編輯器現在還不支持卷滾。當文本行超過窗口大小時,窗口並不自動向上滾動以顯示輸入的字符。當打開一個文件時,如果文件大小超過窗口大小,也無法通過卷滾視圖來看文檔的全部內容。現在我們要讓編輯器增加捲滾功能。
7.3.1邏輯座標和設備座標
在引入文檔卷滾功能之前,首先要介紹以下邏輯座標和設備座標這兩個重要概念。
在Windows中,文檔座標系稱作邏輯座標系,視圖座標系稱爲設備座標系。它們之間的關係如下圖所示:
圖7-11文檔座標和視圖座標
邏輯座標按照座標設置方式(又成爲映射模式)可分爲8種,它們在座標上的特性如下表所示:
表7-1 各種映射模式下的座標轉換方式
映射模式 邏輯單位 x遞增方向
y遞增方向
MM_TEXT 像素 向右
向下
MM_LOMETRIC 0.1mm 向右
向上
MM_HIMETRIC 0.01mm 向右
向上
MM_LOENGLISH 0.01inch 向右
向上
MM_HIENGLISH 0.001inch 向右
向上
MM_TWIPS 1/1440inch 向右
向上
MM_ISOTROPIC 可調整 (x=y) 可選擇
可選擇
MM_ANISOTROPIC 可調整(x!=y) 可選擇
可選擇
我們一般使用的映射模式是MM_TEXT,它也是缺省設置。在該模式下,座標原點在工作區左上角,而x座標值是向右遞增,y座標值是向下遞增,單位值1代表一個像素。
要設置映射模式,可以調用CDC::SetMapMode()函數。
CClientDC dc;
nPreMapMode=dc.SetMapMode(nMapMode);
它將映射模式設置爲nMapMode,並返回前一次的映射模式nPreMapMode,GetMapMode可取得當前的映射模式:
CClientDC dc;
nMapMode=dc.GetMapMode();
MFC繪圖函數都使用邏輯座標作爲位置參數。比如
CString str(“Hello,world!”);
dc.TextOut(10,10,str,str.GetLength());
這裏的(10,10)是邏輯座標而不是像素點數(只是在缺省映射模式MM_TEXT下,正好與像素點相對應),在輸出時GDI函數會將邏輯座標(10,10)依據當前映射模式轉化爲“設備座標”,然後將文字輸出在屏幕上。
設備座標以像素點爲單位,且x軸座標值向右遞增,y軸座標值向下遞增,但原點(0,0)位置卻不限定在工作區的左上角。依據設備座標的原點和用途,可以將Windows下使用的設備座標系統分爲三種:工作區座標系統,窗口座標系統和屏幕座標系統。
(1)工作區座標系統:
工作區座標系統是最常見的座標系統,它以窗口客戶區左上角爲原點(0,0),主要用於窗口客戶區繪圖輸出以及處理窗口的一些消息。鼠標消息WM_LBUTTONDOWN、WM_MOUSEMOVE傳給框架的消息參數以及CDC一些用於繪圖的成員都是使用工作區座標。
(2)屏幕座標系統:
屏幕座標系統是另一類常用的座標系統,以屏幕左上角爲原點(0,0)。以CreateDC(“DISPLAY” , ...)或GetDC(NULL)取得設備上下文時,該上下文使用的座標系就是屏幕座標系。
一些與窗口的工作區不相關的函數都是以屏幕座標爲單位,例如設置和取得光標位置的函數SetCursorPos()和GetCursorPos();由於光標可以在任何一個窗口之間移動,它不屬於任何一個單一的窗口,因此使用屏幕座標。彈出式菜單使用的也是屏幕座標。另外,CreateWindow、MoveWindow、SetWindowPlacement()等函數用於設置窗口相對於屏幕的位置,使用的也是屏幕座標系統。
(3)窗口座標系統:
窗口座標系統以窗口左上角爲座標原點,它包含了窗口控制菜單、標題欄等內容。一般情況下很少在窗口標題欄上繪圖,因此這種座標系統很少使用。
三類設備座標系統關係如下圖所示:
圖7-12. 三類設備座標
MFC提供ClientToScreen()、ScreenToClient()兩個函數用於完成工作區座標和屏幕座標之間的轉換工作。
void ScreenToClient( LPPOINT lpPoint ) const;
void ScreenToClient( LPRECT lpRect ) const;
void ClientToScreen( LPPOINT lpPoint ) const;
void ClientToScreen( LPRECT lpRect ) const;
其實,我們在前面介紹彈出式菜單時已經使用了ClientToScreen函數。在那裏,由於彈出式菜單使用的是屏幕座標,因此當處理彈出式菜單快捷鍵shift+F10時,如果要在窗口左上角(5,5)處顯示快捷菜單,就必須先調用ClientToScreen函數將客戶區座標(5,5)轉化爲屏幕座標。
CRect rect;
GetClientRect(rect);
ClientToScreen(rect);
point = rect.TopLeft();
point.Offset(5, 5);
在視圖滾動後,如果用戶在視圖中單擊鼠標,那麼會得到鼠標位置的設備(視圖)座標。在使用這個數據處理文檔(比如畫點或畫線)時,需要把它轉化爲文檔座標。這是因爲利用MFC繪圖時,所有傳遞給MFC作圖的座標都是邏輯座標。當調用MFC繪圖函數繪圖時,Windows自動將邏輯座標轉換成設備座標,然後再繪圖。設備上下文類CDC提供了兩個成員函數LPToDP和DPToLP完成邏輯座標和設備座標之間的轉換工作。如其名字所示那樣,LPToDP將邏輯座標轉換爲設備座標,DPToLP將設備座標轉換爲邏輯座標。
void LPtoDP( LPPOINT lpPoints, int nCount = 1 ) const;
void LPtoDP( LPRECT lpRect ) const;
void LPtoDP( LPSIZE lpSize ) const;
void DPtoLP( LPPOINT lpPoints, int nCount = 1 ) const;
void DPtoLP( LPRECT lpRect ) const;
void DPtoLP( LPSIZE lpSize ) const;
7.3.2 滾動文檔
由於MFC繪圖函數使用的是邏輯座標,因此用戶可以在一個假想的通常是比視圖要大的“文檔窗口”中繪圖;Windows自動在幕後完成座標轉換工作,並將落在視圖範圍內的那一部分“文檔窗口”顯示出來,其餘的部分被裁剪。
但是光這樣還不能卷滾文檔。要卷滾顯示文檔,還必須知道文檔卷滾到了什麼位置;一旦用戶拖動滾動條時要告訴視圖改變在文檔中的相應位置。所有這些,由MFC的CScrollView來完成。
MFC提供了CScrollView類,簡化了滾動需要處理的大量工作。除了管理文檔中的滾動操作外,MFC還通過調用Windows API函數畫出滾動條、箭頭和滾動光標。它還負責處理:
用戶初始化滾動條範圍(通過滾動視圖的SetScrollRange()方法)
處理滾動條消息,並滾動文檔到相應位置
管理窗口和視圖的尺寸大小
調整滾動條上滑塊(或稱拇指框)的位置,使之與文檔當前位置相匹配
程序員要做的工作是:
從CScrollView類中派生出自己的視圖類,以支持卷滾
提供文檔大小,確定滾動範圍和設置初始位置
協調文檔位置和屏幕座標
要讓應用程序支持卷滾,可以在用AppWizard生成框架程序時就指定視圖的基類爲CSrollView。可以在AppWizard的MFC AppWizard-Step 6 of 6對話框中,在對話框上方應用程序所包含的類中選擇CEditorView,然後在Base Class下拉列表框中選擇應用程序視圖類的基類爲CScrollView,如圖7-11所示:
圖7-13 爲應用程序的視圖類指定基類
現在我們要手工修改CEditorView,使它的基類爲CScrollView。
1. 修改視圖類所對應的頭文件,將所有用到CView的地方改爲CScrollView。通常,首先修改視圖類賴以派生的父類,形式如下:
class CEditorView:public CScrollView
2. 修改視圖類實現的頭文件,把所有用到CView的地方改爲CScrollView。首先修改IMPLEMENT_DYNACREATE一行:
IMPLEMENT_DYNACREATE(CEditorView,CScrollView)
然後修改BEGIN_MESSAGE_MAP宏
BEGIN_MESSAGE_MAP(CEditorView,CScrollView)
然後將其他所有用到CView的地方改爲CScrollView。
一個更簡單的方法是:使用Edit-Replace功能,進行全局替換。
到現在爲止,已經將編輯器視圖類CEditorView的基類由CView轉化爲CScrollView。
現在,要設置文檔大小,以便讓CScrollView知道該如何處理文檔。視圖必需知道文檔的卷滾範圍,這樣才能確定何時卷滾到文檔的頭部和尾部,以及當拖動卷滾條的滑塊時按適當比例調整文檔當前顯示位置。
爲此,我們首先在文檔類CEditorDoc的頭文件editordoc.h中增加一個CSize類型的數據成員m_sizeDoc用以表示文檔的大小。CSize對象包含cx和cy兩個數據成員,分別用於存放文檔的x方向座標範圍和y方向座標範圍。另外,還要提供一個成員函數GetDocSize()來訪問該文檔大小範圍數據成員。修改後的editordoc.h如清單7.11。
清單7.11 CEditorDoc頭文件
class CEditorDoc : public CDocument
{
protected: // create from serialization only
CEditorDoc();
DECLARE_DYNCREATE(CEditorDoc)
//保存文檔大小
CSize m_sizeDoc;
// Attributes
public:
CSize GetDocSize(){return m_sizeDoc;}
// Operations
public:
CStringList lines;
int nLineNum;
......
};
既然增加了m_sizeDoc這一數據成員,就需要在CEditorDoc構造函數中進行初始化,給m_sizeDoc設置一合理的數值,比如說x=700,y=800。構造函數如清單7.12。
清單7.12 CEditorDoc的構造函數
CEditorDoc::CEditorDoc()
{
// TODO: add one-time construction code here
nLineNum=0;
m_sizeDoc=CSize(700,800);
}
一個設計優秀的應用程序應當能夠動態調整文檔的卷滾範圍。比如,在WORD中新建一個文件時,在“頁面模式”下將可卷滾範圍設爲一頁大小。隨着用戶輸入,逐漸增加文檔的卷滾範圍。但是這裏爲簡明起見,將文檔卷滾範圍設爲固定大小700X800點像素大小。設置文檔大小通過由視圖類的CEditorView::OnInitialUpdate()調用SetScrollSizes()成員函數來完成。
SetScrollSizes()用於設置文檔卷滾範圍。一般在重載OnInitialUpdate()成員函數或OnUpdate()時調用該函數,用以調整文檔卷滾特性。比如,在文檔初始顯示或文檔大小作了調整之後。
清單7.13 在OnInitialUpdate()中設置卷滾範圍
void CEditorView::OnInitialUpdate()
{
// TODO: Add your specialized code here and/or call the base class
CDC *pDC=GetDC();
pFont=new CFont();
if(!(pFont->CreateFont(0,0,0,0,FW_NORMAL,FALSE,FALSE,FALSE,
ANSI_CHARSET,OUT_TT_PRECIS,CLIP_TT_ALWAYS,
DEFAULT_QUALITY,DEFAULT_PITCH,"Courier New")))
{
pFont->CreateStockObject(SYSTEM_FONT);
}
CFont* oldFont=pDC->SelectObject(pFont);
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
lHeight=tm.tmHeight+tm.tmExternalLeading;
cWidth=tm.tmAveCharWidth;
SetScrollSizes(MM_TEXT,GetDocument()->GetDocSize());
CScrollView::OnInitialUpdate();
}
SetScrollSizes()第一個參數爲映射模式。SetScrollSizes()可以使用除MM_ISOTROPIC和MM_ANISOTROPIC之外的其他任何映射模式。SetScrollSizes()第二個參數爲文檔大小,用一個CSize類型的數值表示。
另外,我們還要檢查兩個包含繪圖輸出功能的函數:CEditorView::OnChar()和CEditorView::OnDraw()函數。
void CEditorView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CEditorDoc* pDoc=GetDocument();
CClientDC dc(this);
CString line("");//存放編輯器當前行字符串
POSITION pos=NULL;//字符串鏈表位置指示
if(nChar=='/r')
{
pDoc->nLineNum++;
}
else
{
//按行號返回字符串鏈表中位置值
pos=pDoc->lines.FindIndex(pDoc->nLineNum);
if(!pos)
{
//沒有找到該行號對應的行,因此它是一個空行,
//我們把它加到字符串鏈表中。
line+=(char)nChar;
pDoc->lines.AddTail(CString(line));
}
else{
//there is a line,so add the incoming char to the end of
//the line
line=pDoc->lines.GetAt(pos);
line+=(char)nChar;
pDoc->lines.SetAt(pos,line);
}
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
dc.TextOut(0,
(int)pDoc->nLineNum*tm.tmHeight,
line,
line.GetLength());
}
pDoc->SetModifiedFlag();
SetScrollSizes(MM_TEXT,GetDocument()->GetDocSize());
CScrollView::OnChar(nChar,nRepCnt,nFlags);
}
在程序運行開始的時侯,視圖座標原點和文檔座標原點是重合的。但是,當用戶拖動滾動條時,視圖原點就與文檔原點不一致了,如圖7-14。由於GDI是按照文檔座標(邏輯座標)來輸出圖形的,這樣自然就無法正確顯示文檔內容。
圖7-14 文檔滾動前後文檔座標原點和視圖座標原點的變化
這時,要想獲得正確輸出,就必須調整視圖座標,讓視圖座標原點和文檔座標原點重合,如圖7-15所示。
圖7-15 調整視圖設備上下文原點後
CScrollView視圖類提供了一個CScrollView::OnPrepareDC()成員函數,完成視圖設備上下文座標原點的調整工作。
現在修改OnChar(),加入OnPrepareDC()函數,見清單7.15。
清單7.15 修改後的OnChar成員函數
void CEditorView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CEditorDoc* pDoc=GetDocument();
CClientDC dc(this);
OnPrepareDC(&dc);
CFont* pOldFont=dc.SelectObject(pFont);
CString line("");//存放編輯器當前行字符串
POSITION pos=NULL;//字符串鏈表位置指示
if(nChar=='/r')
{
pDoc->nLineNum++;
}
else
{
//按行號返回字符串鏈表中位置值
pos=pDoc->lines.FindIndex(pDoc->nLineNum);
if(!pos)
{
//沒有找到該行號對應的行,因此它是一個空行,
//我們把它加到字符串鏈表中。
line+=(char)nChar;
pDoc->lines.AddTail(CString(line));
}
else{
//there is a line,so add the incoming char to the end of
//the line
line=pDoc->lines.GetAt(pos);
line+=(char)nChar;
pDoc->lines.SetAt(pos,line);
}
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
dc.TextOut(0,
(int)pDoc->nLineNum*tm.tmHeight,
line,
line.GetLength());
}
pDoc->SetModifiedFlag();
dc.SelectObject(pOldFont);
SetScrollSizes(MM_TEXT,GetDocument()->GetDocSize());
CScrollView::OnChar(nChar,nRepCnt,nFlags);
}
但是,對於視圖OnDraw()函數,則不需要作這樣的調整。這是因爲,框架在調用OnDraw()之前,已經自動調用了OnPrepareDC()成員函數完成設備上下文座標調整工作了。
提示:對於框架傳過來的設備上下文,不需要調用OnPrepareDC(),因爲框架知道它是用於繪圖的,因此事先調用了OnPrepareDC()作好了座標調整工作。如果是自己構造或用GetDC()取得得設備上下文,則需要調用OnPrepareDC()完成設備上下文座標調整工作。
現在編輯器已經能夠支持文檔滾動了,如圖7-16。
圖7-16支持滾動的文本編輯器
7.4 定製串行化
前面編輯器的例子使用CString類的字符串來保存文本行,由於它是MFC類,因此可以串行化自己,將自己寫入磁盤或從磁盤文件中讀取二進制數據來建立對象。那麼,如果不是標準的MFC類,比如用戶自己定義的類,如何讓它支持串行化呢?下面,我們結合前面第五章提到的就業調查表的例子來演示如何讓用戶定義的類支持串行化功能。
要讓用戶定義的類支持串行化,一般分爲五步:
1.從CObject或其派生類派生出用戶的類
2.重載Serialize()成員函數,加入必要的代碼,用以保存對象的數據成員到CArchive對象以及從CArchive對象載入對象的數據成員狀態。
3.在類聲明文件中,加入DECLARE_SERIAL宏。編譯時,編譯器將擴充該宏,這是串行化對象所必需的。
4.定義一個不帶參數的構造函數。
5.在實現文件中加入IMPLEMENT_SERIAL宏。
class CRegister:public CObject
{
public:
DECLARE_SERIAL( CRegister)
//必需提供一個不帶任何參數的空的構造函數
CRegister(){};
public:
CString strIncome;
CString strKind;
BOOL bMarried;
CString strName;
int nSex;
CString strUnit;
int nWork;
UINT nAge;
void Serialize(CArchive&);
};
MFC在從磁盤文件載入對象狀態並重建對象時,需要有一個缺省的不帶任何參數的構造函數。串行化對象將用該構造函數生成一個對象,然後調用Serialize()函數,用重建對象所需的值來填充對象的所有數據成員變量。
構造函數可以聲明爲public、protected或private。如果使它成爲protect或private,則可以確保它只被串行化過程所使用。
在類定義文件中給出Serialize()的定義。它包括對象的保存和載入兩部分。前面已經提到,CArchive類提供一個IsStoring()成員函數指示是保存數據到磁盤文件還是從磁盤文件載入對象。
void CRegister::Serialize(CArchive& ar)
{
//首先調用基類的Serialize()方法。
CObject::Serialize( ar);
if(ar.IsStoring())
{
ar<<strIncome;
ar<<strKind;
ar<<(int)bMarried;
ar<<strName;
ar<<nSez;
ar<<strUnit;
ar<<nWork;
ar<<(WORD)nAge;
}
else
{
ar>>strIncome;
ar>>strKind;
ar>>(int)bMarried;
ar>>strName;
ar>>nSex;
ar>>strUnit;
ar>>nWork;
ar>>(WORD)nAge;
}
}
我們看到,對象的串行化實際上是通過調用對象中的數據成員的串行化來完成的。
注意:CArchive類的>>和<<操作符並不支持所有的標準數據類型。它支持的數據類型有:CObject、BYTE、WORD、int、LONG、DWORD、float和double。其他的類型的數據要進行串行化輸入輸出時,需要將該類型的數據轉化爲上述幾種類型之一方可。
另外,在類的實現(類定義)文件開始處,還要加入IMPLEMENT_SERIAL宏。IMPLEMENT_SERIAL( CRegister, CObject, 1 )
IMPLEMENT_SERIAL宏用於定義一個從CObject派生的可串行化類的各種函數。宏的第一和第二個參數分別代表可串行化的類名和該類的直接基類。
第三個參數是對象的版本號,它是一個大於或等於零的整數。MFC串行化代碼在將對象讀入內存時檢查版本號。如果磁盤文件上的對象的版本號和內存中的對象的版本號不一致,MFC將拋出一個CArchiveException異常,阻止程序讀入一個不匹配版本的對象。
現在,我們就可以象使用標準MFC類一樣使用CRegister的串行化功能了。
CArchive ar;
CRegister reg1,reg2;
ar<<reg1<<reg2;
讀者請試着在第五章職工調查表程序基礎上,增加保存調查信息到文件以及從文件中讀入調查表信息功能。對於多個調查表,可考慮採用CObjList鏈表保存多個對象的指針。
串行化簡化了對象的保存和載入,爲對象提供了持續性。但是,串行化本身還是具有一定的侷限性的。串行化一次從文件中載入所有對象,這不適合於大文件編輯器和數據庫。對於數據庫和大文件編輯器,它們每次只是從文件中讀入一部分。此時,就要避開文檔的串行化機制來直接讀取和保存文件了。另外,使用外部文件格式(預先定義的文件格式而不是本應用程序定義的文件格式)的程序一般也不使用文檔的串行化。下面我們就給出這樣一個例子,說明在不使用串行化情況下如何讀取和保存文件。
7.5不使用串行化的文檔視結構程序
在MFC例子中有一個DIBLOOK(見SAMPLES/MFC/GENERAL/DIBLOOK目錄),它是一個位圖顯示程序,演示了在不使用串行化的情況下實現文檔的輸入輸出功能。有關位圖、調色板的使用在第十一章有詳細介紹,這裏只討論與文檔視結構相關的內容。我們先看DIBLOOK的文檔聲明和定義。
清單7-16 CDibDoc的類聲明文件
// dibdoc.h : interface of the CDibDoc class
#include "dibapi.h"
class CDibDoc : public CDocument
{
protected: // create from serialization only
CDibDoc();
DECLARE_DYNCREATE(CDibDoc)
// Attributes
public:
HDIB GetHDIB() const
{ return m_hDIB; }
CPalette* GetDocPalette() const
{ return m_palDIB; }
CSize GetDocSize() const
{ return m_sizeDoc; }
// Operations
public:
void ReplaceHDIB(HDIB hDIB);
void InitDIBData();
// Implementation
protected:
virtual ~CDibDoc();
virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);
virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
protected:
HDIB m_hDIB;
CPalette* m_palDIB;
CSize m_sizeDoc;
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
protected:
virtual BOOL OnNewDocument();
// Generated message map functions
protected:
//{{AFX_MSG(CDibDoc)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
/////////////////////////////////////////////////////////////////////////////
清單7-17 CDibDoc類的實現文件
// dibdoc.cpp : implementation of the CDibDoc class
#include "stdafx.h"
#include "diblook.h"
#include <limits.h>
#include "dibdoc.h"
#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CDibDoc
IMPLEMENT_DYNCREATE(CDibDoc, CDocument)
BEGIN_MESSAGE_MAP(CDibDoc, CDocument)
//{{AFX_MSG_MAP(CDibDoc)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CDibDoc construction/destruction
CDibDoc::CDibDoc()
{
//初始化文檔的DIB句柄和調色板
m_hDIB = NULL;
m_palDIB = NULL;
m_sizeDoc = CSize(1,1); // dummy value to make CScrollView happy
}
CDibDoc::~CDibDoc()
{
if (m_hDIB != NULL)
{
::GlobalFree((HGLOBAL) m_hDIB);
}
if (m_palDIB != NULL)
{
delete m_palDIB;
}
}
BOOL CDibDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
return TRUE;
}
void CDibDoc::InitDIBData()
{
if (m_palDIB != NULL)
{
delete m_palDIB;
m_palDIB = NULL;
}
if (m_hDIB == NULL)
{
return;
}
// Set up document size
LPSTR lpDIB = (LPSTR) ::GlobalLock((HGLOBAL) m_hDIB);
if (::DIBWidth(lpDIB) > INT_MAX ||::DIBHeight(lpDIB) > INT_MAX)
{
::GlobalUnlock((HGLOBAL) m_hDIB);
::GlobalFree((HGLOBAL) m_hDIB);
m_hDIB = NULL;
CString strMsg;
strMsg.LoadString(IDS_DIB_TOO_BIG);
MessageBox(NULL, strMsg, NULL, MB_ICONINFORMATION | MB_OK);
return;
}
m_sizeDoc = CSize((int) ::DIBWidth(lpDIB), (int) ::DIBHeight(lpDIB));
::GlobalUnlock((HGLOBAL) m_hDIB);
// Create copy of palette
m_palDIB = new CPalette;
if (m_palDIB == NULL)
{
// we must be really low on memory
::GlobalFree((HGLOBAL) m_hDIB);
m_hDIB = NULL;
return;
}
if (::CreateDIBPalette(m_hDIB, m_palDIB) == NULL)
{
// DIB may not have a palette
delete m_palDIB;
m_palDIB = NULL;
return;
}
}
BOOL CDibDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
CFile file;
CFileException fe;
if (!file.Open(lpszPathName, CFile::modeRead | CFile::shareDenyWrite, &fe))
{
ReportSaveLoadException(lpszPathName, &fe,
FALSE, AFX_IDP_FAILED_TO_OPEN_DOC);
return FALSE;
}
DeleteContents();
BeginWaitCursor();
// replace calls to Serialize with ReadDIBFile function
TRY
{
m_hDIB = ::ReadDIBFile(file);
}
CATCH (CFileException, eLoad)
{
file.Abort(); // will not throw an exception
EndWaitCursor();
ReportSaveLoadException(lpszPathName, eLoad,
FALSE, AFX_IDP_FAILED_TO_OPEN_DOC);
m_hDIB = NULL;
return FALSE;
}
END_CATCH
InitDIBData();
EndWaitCursor();
if (m_hDIB == NULL)
{
// may not be DIB format
CString strMsg;
strMsg.LoadString(IDS_CANNOT_LOAD_DIB);
MessageBox(NULL, strMsg, NULL, MB_ICONINFORMATION | MB_OK);
return FALSE;
}
SetPathName(lpszPathName);
SetModifiedFlag(FALSE); // start off with unmodified
return TRUE;
}
BOOL CDibDoc::OnSaveDocument(LPCTSTR lpszPathName)
{
CFile file;
CFileException fe;
if (!file.Open(lpszPathName, CFile::modeCreate |
CFile::modeReadWrite | CFile::shareExclusive, &fe))
{
ReportSaveLoadException(lpszPathName, &fe,
TRUE, AFX_IDP_INVALID_FILENAME);
return FALSE;
}
// replace calls to Serialize with SaveDIB function
BOOL bSuccess = FALSE;
TRY
{
BeginWaitCursor();
bSuccess = ::SaveDIB(m_hDIB, file);
file.Close();
}
CATCH (CException, eSave)
{
file.Abort(); // will not throw an exception
EndWaitCursor();
ReportSaveLoadException(lpszPathName, eSave,
TRUE, AFX_IDP_FAILED_TO_SAVE_DOC);
return FALSE;
}
END_CATCH
EndWaitCursor();
SetModifiedFlag(FALSE); // back to unmodified
if (!bSuccess)
{
// may be other-style DIB (load supported but not save)
// or other problem in SaveDIB
CString strMsg;
strMsg.LoadString(IDS_CANNOT_SAVE_DIB);
MessageBox(NULL, strMsg, NULL, MB_ICONINFORMATION | MB_OK);
}
return bSuccess;
}
void CDibDoc::ReplaceHDIB(HDIB hDIB)
{
if (m_hDIB != NULL)
{
::GlobalFree((HGLOBAL) m_hDIB);
}
m_hDIB = hDIB;
}
/////////////////////////////////////////////////////////////////////////////
// CDibDoc diagnostics
#ifdef _DEBUG
void CDibDoc::AssertValid() const
{
CDocument::AssertValid();
}
void CDibDoc::Dump(CDumpContext& dc) const
{
CDocument::Dump(dc);
}
#endif //_DEBUG
/////////////////////////////////////////////////////////////////////////////
// CDibDoc commands
DIBLOOK讀入和保存標準的Windows設備無關位圖。在內存中,位圖以一個HDIB句柄表示。DIBLOOK沒有重 載CDocument::Serialize()函數,而是重載了CDocument::OnOpenDocument和CDocument::OnSaveDocument函數。這兩個函數使用框架傳過來得文件路徑名pszPathName,打開一個文件對象,讀入或保存DIB數據。這就是說,DIBLOOK把本來在Serialize()中完成的對象保存和載入兩個任務分別交與OnSaveDocument()函數和OnOpenDocument()函數去完成。如果讀者希望繞過文檔的串行化提供文檔數據的保存和載入,也只需要重載這兩個成員函數:OnOpenDocument()和OnSaveDocument(),通過文件路徑參數打開文件,從中讀取應用程序數據或向文件裏寫入應用程序數據。
在OnOpenDocument()中,還必需自己調用DeleteContents()清除原來文檔的數據,並調用SetModifiedFlag(FALSE)。在OnSaveDocument()中也要調用SetModifiedFlag(FALSE)將文檔修改標誌改爲FALSE。
在OnOpenDocument()函數開始處(見清單7.18),有一些地方需要解釋一下。
清單7.18 OnOpenDocument()函數
BOOL CDibDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
CFile file;
CFileException fe;
if (!file.Open(lpszPathName, CFile::modeRead | CFile::shareDenyWrite, &fe))
{
ReportSaveLoadException(lpszPathName, &fe,
FALSE, AFX_IDP_FAILED_TO_OPEN_DOC);
return FALSE;
}
......
}
7.5.1 文件操作
文件讀寫
OnOpenDocument首先聲明一個CFile類的對象。CFile是MFC提供的一個類,它提供了訪問二進制文件的接口。可以使用帶參數的CFile構造函數創建對象,在構造函數中指定了文件名和打開文件的模式,這樣在對象創建的同時也就打開了這個文件;也可以象本例那樣使用不帶參數的CFile構造函數構造一個CFile對象,然後調用CFile::Open()打開一個文件。
BOOL CFile::Open( LPCTSTR lpszFileName, UINT nOpenFlags, CFileException* pError = NULL );
CFile::Open成員函數帶三個參數,第一個參數指定了要打開的文件的完整路徑名,如“c:/hello/hello.cpp”;第二個參數指定打開文件的模式。
常見的文件打開模式有以下幾種:
CFile::modeCreate:創建一個新文件,如果該文件已經存在,則把該文件長度置爲零
CFile::modeNoTruncate:與modeCreate一起使用。告訴CFile,如果要創建的文件已經存在,則不再將文件長度設置爲零。這對於系統設置文件、日誌文件等特別有用,因爲第一次啓動系統時,這些文件通常不存在,需要創建,而下次則只需要修改文件。
CFile::modeRead:打開文件用於讀
CFile::modeWrite:打開文件用於寫
CFile::modeReadWrite:打開文件且對文件可讀可寫
可以使用比特位或“|”對上述操作進行組合。比如,要打開文件寫,可以用以下方式打開:
CFile file;
file.Open(“c://readme.txt”,CFile::modeCreate|CFile::modeWrite);
讀文件
既然已經打開了文件,就可以對文件進行讀寫操作了。要讀取文件內容到內存,可以調用CFile::Read()。CFile::Read()函數原型如下:
UINT Read( void* lpBuf, UINT nCount );
Read函數包含兩個參數,第一個參數是一個緩衝區指針,該緩衝區用於存放從文件讀進來的內容。第二個參數是要讀取的字節數。Read函數返回實際讀入的字節數。例如:CFile file;
char buf[100];
int nBytesRead;
nBytesRead=file.Read(buf,100);
寫文件
寫文件與讀文件操作方式類似,通過調用CFile::Write函數來完成。
void Write( const void* lpBuf, UINT nCount );
Write函數第一個參數是指向要寫入到文件中的緩衝區的指針,第二個參數是要寫入到文件中的字節數。例如:
CFile file;
CString str(“This is a string.”);
file.Write(str,str.GetLength());
關閉文件在完成文件讀寫操作後,要調用CFile::Close成員函數及時將文件關閉。
CFile file;
//一些讀寫操作.....
file.Close();
7.5.2異常處理
在打開和保存文件時,我們並未作傳統的錯誤檢查,而是採用一種異常機制來處理錯誤。
異常處理爲異常事件提供了結構化、規範化的服務。它一般是指處理錯誤狀態。
我們先回顧一下傳統的錯誤處理方式。傳統的錯誤處理方式通常有兩種:
1.返回錯誤值
2.使用goto,setjmp/longjmp協助報告錯誤
對於第一種技術,要求程序員記住各種錯誤代碼,並且加入大量的檢查情況。由於大多數錯誤是很少會發生,這樣處理的結果是代碼冗餘性很大,效率不高。
第二種技術不但使程序可讀性降低,更嚴重的是,使得函數裏的對象不能釋放、刪除。比如:
void SomeOperation()
{
CMyClass obj1;
if(error)goto errHandler;
...
}
...
errHandler:
//handler error
在上面的程序片斷中,由於goto跳轉,無法調用obj的析構函數在退出SomeOperation()函數時釋放其所佔的內存,造成內存泄漏。
而且,以上兩種錯誤處理方法都無法考慮到不可預見的錯誤。C++引入異常處理這一重要概念很好的解決了上述問題。異常處理在處理異常事件時會自動調用已經超出範圍的局部對象的析構函數,這樣就可以防止內存泄漏。
下面是OnSaveDocument()函數中的異常處理代碼:
CFile file;
CFileException fe;
if (!file.Open(lpszPathName, CFile::modeCreate |
CFile::modeReadWrite | CFile::shareExclusive, &fe))
{
ReportSaveLoadException(lpszPathName, &fe,
TRUE, AFX_IDP_INVALID_FILENAME);
return FALSE;
}
// replace calls to Serialize with SaveDIB function
BOOL bSuccess = FALSE;
TRY
{
BeginWaitCursor();
bSuccess = ::SaveDIB(m_hDIB, file);
file.Close();
}
CATCH (CException, eSave)
{
file.Abort(); // will not throw an exception
EndWaitCursor();
ReportSaveLoadException(lpszPathName, eSave,
TRUE, AFX_IDP_FAILED_TO_SAVE_DOC);
return FALSE;
}
END_CATCH
異常處理由一個TRY-CATCH-END_CATCH結構組成。TRY{ }語句塊中包含可能發生錯誤的代碼,可以理解爲“試運行”這一語句塊。CATCH{} END_CATCH子塊包含了錯誤處理代碼。如果發生錯誤,就轉入CATCH{} END_CATCH子塊執行。該子塊可以根據CATCH中的參數分析產生錯誤的原因,報告錯誤或做出相應處理。
CATCH()包含兩個參數,第一個參數是異常類。MFC的異常有下列幾種:
MFC異常類
處理的異常
CMemoryException 內存異常 CNotSupportedException 設備不支持 CArchiveException 檔案(archive)異常 CFileException 文件異常 OsErrorException 把DOS錯誤轉換爲異常 ErrnoToException 把錯誤號轉換爲異常 CResourceException 資源異常 COleException OLE異常
用戶還可以從CException類派生出自己的異常類,用以處理特定類型的錯誤。CATCH的第二個參數是產生的異常的名字。
引起異常的原因存放在異常的數據成員m_cause中。OnSaveDocument()只是簡單的處理文件保存錯誤,並沒有指出引起錯誤的原因。我們可以對它進行一些修改,使它能夠報告引起錯誤的原因。
...
TRY
{
...
}
CATCH(CFileException,e)
{
switch(e->m_cause)
{
case CFileException::accessDenied:
AfxMessageBox(“Access denied!”);
break;
case CFileException::badPath:
AfxMessageBox(“Invalid path name”);
break;
case CFileException::diskFull:
AfxMessageBox(“Disk is full”);
break;
case CFileException::hardIO:
AfxMessageBox(“Hardware error”);
break;
}
}
END_CATCH
...
}
用戶也可以不必直接處理異常,而通過調用THROW_LAST(),把異常交給上一級TRY-CATCH結構來處理。其實,在DIBLOOK中,就是這麼做的,請看OnSaveDocument()函數調用的SaveDIB函數的片段:
BOOL WINAPI SaveDIB(HDIB hDib, CFile& file)
{
//...
TRY
{
//...
}
CATCH (CFileException, e)
{
//...
::GlobalUnlock((HGLOBAL) hDib);
THROW_LAST();
}
END_CATCH
//...
}
在SaveDIB中,並沒有直接處理異常,而是通過調用THROW_LAST(),把異常交由調用它的上一級函數OnSaveDocument()去處理。
異常並不僅僅用於錯誤處理。比如,在文本編輯器的CEditorDoc::Serialize()成員函數中,我們就利用讀取文件引起的異常判斷是否已經到了文件尾部。讀者請回顧一下該函數。
異常處理給程序的錯誤處理帶來許多便利。但是,必需意識到異常處理並不是萬能的。在加入異常處理後,程序員仍然有許多工作要做。更不可以濫用異常,因爲異常會帶來一些開銷。應用程序應當儘可能排除可能出現的錯誤。