引言
MFC引入了"文檔/視圖"結(jié)構(gòu)的概念,理解這個(gè)結(jié)構(gòu)是編寫基于MFC編寫復(fù)雜Visual C++程序的關(guān)鍵。"文檔/視圖"中主要涉及到四種類:
(1)文檔模板:
class CDocTemplate; // template for document creation class CSingleDocTemplate; // SDI support class CMultiDocTemplate; // MDI support
?。?)文檔:
class CDocument; // main document abstraction
?。?)視圖:
// views on a document class CView; // a view on a document class CScrollView; // a scrolling view
(4)框架窗口:
// frame windows class CFrameWnd; // standard SDI frame class CMDIFrameWnd; // standard MDI frame class CMDIChildWnd; // standard MDI child class CMiniFrameWnd; // half-height caption frame wnd
理解了這4個(gè)類各自的意義及它們縱橫交錯(cuò)的關(guān)系也就理解了"文檔/視圖"結(jié)構(gòu)的基本概念,在此基礎(chǔ)上,我們還需要進(jìn)一步研究"文檔/視圖"結(jié)構(gòu)的MFC程序消息流動的方向,這樣就完全徹底明白了基于"文檔/視圖"結(jié)構(gòu)MFC程序的"生死因果"。
出于以上考慮,本文這樣組織了各次連載的內(nèi)容:
第1次連載進(jìn)行基本概念的介紹,第2~5次連載分別講述文檔模板、文檔、視圖和框架窗口四個(gè)類的功能和主要函數(shù),連載6則綜合闡述四個(gè)類之間的關(guān)系,接著以連載7講解消息流動的方向,最后的連載8則以實(shí)例剖析連載1~7所講述的所有內(nèi)容。
本文所有的代碼基于WIN32平臺開發(fā),調(diào)試環(huán)境為Visual C++6.0。在本文的連載過程中,您可以通過如下方式聯(lián)系作者(熱忱歡迎讀者朋友對本文的內(nèi)容提出質(zhì)疑或給出修改意見):
作者email:21cnbao@21cn.com(可以來信提問,筆者將力求予以回信解答);
另外,對本文的轉(zhuǎn)載請務(wù)必注明作者和出處。未經(jīng)同意,不得用于任何形式的商業(yè)目的。
架構(gòu)
MFC"文檔/視圖"結(jié)構(gòu)被認(rèn)為是一種架構(gòu),關(guān)于什么是架構(gòu),這是個(gè)"仁者見仁,智者見智"的問題。在筆者看來,成其為架構(gòu)者,必具備如下兩個(gè)特性:
(1)它是一種基礎(chǔ)性平臺,是一個(gè)模型。通過這個(gè)平臺、這個(gè)模型,我們在上面進(jìn)一步修飾,可以得到無窮無盡的新事物。譬如,建筑學(xué)上的鋼筋混凝土結(jié)構(gòu)、ISO(國際標(biāo)準(zhǔn)化組織)的OSI(開放式系統(tǒng)互連)七層模型。架構(gòu)只是一種基礎(chǔ)性平臺,不同于用這個(gè)架構(gòu)造出的實(shí)例。鋼筋混凝土結(jié)構(gòu)是架構(gòu),而用鋼筋混凝土結(jié)構(gòu)造出的房子就不能稱為架構(gòu)。
這個(gè)特性強(qiáng)調(diào)了架構(gòu)的外部特征,即架構(gòu)具有可學(xué)習(xí)、可再生、可實(shí)例化的特點(diǎn),是所有基于該架構(gòu)所構(gòu)造實(shí)例的共性,是貫串在它們體內(nèi)的一根"筋",但各個(gè)基于該架構(gòu)所構(gòu)造的實(shí)例彼此是存在差異的。
?。?)它是一個(gè)由內(nèi)部有聯(lián)系的事物所組成的一個(gè)有機(jī)整體。架構(gòu)中的內(nèi)部成員不是彼此松散的,并非各自"占山為王",它們歃血為盟,緊密合作,彼此都有明確的責(zé)任和分工,因此共同構(gòu)筑了一個(gè)統(tǒng)一的基礎(chǔ)性平臺、一個(gè)統(tǒng)一的模型。譬如,OSI模型從物理層到應(yīng)用層進(jìn)行了良好的合作,雖然內(nèi)部包含了復(fù)雜的多個(gè)層次,但仍然脈絡(luò)清晰。
由此可見,架構(gòu)的第2個(gè)特性是服務(wù)于第1個(gè)特性的。理解架構(gòu),關(guān)鍵是理解以上兩個(gè)特性。而針對特定的"文檔/視圖"結(jié)構(gòu),則需理解如下兩個(gè)問題:
?。?)學(xué)習(xí)這個(gè)架構(gòu),并學(xué)會在這個(gè)架構(gòu)上造房子(編寫基于"文檔/視圖"結(jié)構(gòu)的程序);
?。?)理解這個(gè)架構(gòu)內(nèi)部的工作機(jī)理(文檔模板、文檔、視圖和框架窗口四個(gè)類是如何聯(lián)系為一個(gè)有機(jī)整體的),并在造房子時(shí)加以靈活應(yīng)用(重載相關(guān)的類)。
在這里,我們再引用幾位專家(或企業(yè))關(guān)于架構(gòu)的定義以供讀者進(jìn)一步參考:
The key ideas of a commercial application framework : a generic app on steroids that provides a large amount of general-purpose functionality within a well-planned, welltested, cohesive structure. (Application framework is) an extended collection of classes that cooperate to support a complete application architecture or application model, providing more complete application development support than a simple set of class libraries. ――MacApp(Apple's C++ application framework) An application framework is an integrated object-oriented software system that offers all the application-level classes(documents, views, and commands)needed by a generic application. An application framework is meant to be used in its entirety, and fosters both design reuse and code reuse. An application framework embodies a particular philosophy for structuring an application, and in return for a large mass of prebuilt functionality, the programmer gives up control over many architectural-design decisions. ――Ray Valdes
什么是Application Framework?Framework 這個(gè)字眼有組織、框架、體制的意思,Application Framework 不僅是一般性的泛稱,它其實(shí)還是對象導(dǎo)向領(lǐng)域中的一個(gè)專有名詞。
基本上你可以說,Application Framework 是一個(gè)完整的程序模型,具備標(biāo)準(zhǔn)應(yīng)用軟件所需的一切基本功能,像是檔案存取、打印預(yù)視、數(shù)據(jù)交換...,以及這些功能的使用接口(工具列、狀態(tài)列、選單、對話盒)。如果更以術(shù)語來說,Application Framework 就是由一整組合作無間的"對象"架構(gòu)起來的大模型。喔不不,當(dāng)它還沒有與你的程序產(chǎn)生火花的時(shí)候,它還只是有形無體,應(yīng)該說是一組合作無間的"類別"架構(gòu)起來的大模型。
――侯捷
最后,要強(qiáng)調(diào)的是,筆者之所以用一個(gè)較長的篇幅來連載關(guān)于"文檔/視圖"結(jié)構(gòu)的內(nèi)容,是因?yàn)?文檔/視圖"結(jié)構(gòu)是MFC中結(jié)構(gòu)最為復(fù)雜,體系最為龐大,而又最富有特色的部分,其中涉及到應(yīng)用、文檔模板、文檔、視圖、SDI窗口、MDI框架窗口、MDI子窗口等多種不同的類,如果不了解這些類及其盤根錯(cuò)節(jié)的內(nèi)部聯(lián)系的話,就不可能編寫出高水平的文檔/視圖程序。當(dāng)然,學(xué)習(xí)"文檔/視圖"結(jié)構(gòu)的意義還不只于其本身,通過該架構(gòu)的學(xué)習(xí),一步步領(lǐng)略MFC設(shè)計(jì)者的神功奧妙,也將進(jìn)一步增強(qiáng)我們自身對龐大程序框架的把握能力。一個(gè)優(yōu)秀的程序員是可以寫出一個(gè)個(gè)優(yōu)秀函數(shù)的程序員,而一個(gè)優(yōu)秀的系統(tǒng)設(shè)計(jì)師則需從全局把握軟件的架構(gòu),分析和學(xué)習(xí)"文檔/視圖"結(jié)構(gòu)相信將是我們成為系統(tǒng)設(shè)計(jì)師之旅的一個(gè)有利環(huán)節(jié)。
文檔模板管理者類CDocManager
在"文檔/視圖"架構(gòu)的MFC程序中,提供了文檔模板管理者類CDocManager,由它管理應(yīng)用程序所包含的文檔模板。我們先看看這個(gè)類的聲明:
///////////////////////////////////////////////////////////////////////////// // CDocTemplate manager object class CDocManager : public CObject { DECLARE_DYNAMIC(CDocManager) public:
// Constructor CDocManager();
//Document functions virtual void AddDocTemplate(CDocTemplate* pTemplate); virtual POSITION GetFirstDocTemplatePosition() const; virtual CDocTemplate* GetNextDocTemplate(POSITION& pos) const; virtual void RegisterShellFileTypes(BOOL bCompat); void UnregisterShellFileTypes(); virtual CDocument* OpenDocumentFile(LPCTSTR lpszFileName); // open named file virtual BOOL SaveAllModified(); // save before exit virtual void CloseAllDocuments(BOOL bEndSession); // close documents before exiting virtual int GetOpenDocumentCount();
// helper for standard commdlg dialogs virtual BOOL DoPromptFileName(CString& fileName, UINT nIDSTitle, DWORD lFlags, BOOL bOpenFileDialog, CDocTemplate* pTemplate);
//Commands // Advanced: process async DDE request virtual BOOL OnDDECommand(LPTSTR lpszCommand); virtual void OnFileNew(); virtual void OnFileOpen();
// Implementation protected: CPtrList m_templateList; int GetDocumentCount(); // helper to count number of total documents
public: static CPtrList* pStaticList; // for static CDocTemplate objects static BOOL bStaticInit; // TRUE during static initialization static CDocManager* pStaticDocManager; // for static CDocTemplate objects
public: virtual ~CDocManager(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif }; |
從上述代碼可以看出,CDocManager類維護(hù)一個(gè)CPtrList類型的鏈表m_templateList(即文檔模板鏈表,實(shí)際上,MFC的設(shè)計(jì)者在MFC的實(shí)現(xiàn)中大量使用了鏈表這種數(shù)據(jù)結(jié)構(gòu)),CPtrList類型定義為:
class CPtrList : public CObject { DECLARE_DYNAMIC(CPtrList)
protected: struct CNode { CNode* pNext; CNode* pPrev; void* data; }; public:
// Construction CPtrList(int nBlockSize = 10);
// Attributes (head and tail) // count of elements int GetCount() const; BOOL IsEmpty() const;
// peek at head or tail void*& GetHead(); void* GetHead() const; void*& GetTail(); void* GetTail() const;
// Operations // get head or tail (and remove it) - don't call on empty list! void* RemoveHead(); void* RemoveTail();
// add before head or after tail POSITION AddHead(void* newElement); POSITION AddTail(void* newElement);
// add another list of elements before head or after tail void AddHead(CPtrList* pNewList); void AddTail(CPtrList* pNewList);
// remove all elements void RemoveAll();
// iteration POSITION GetHeadPosition() const; POSITION GetTailPosition() const; void*& GetNext(POSITION& rPosition); // return *Position++ void* GetNext(POSITION& rPosition) const; // return *Position++ void*& GetPrev(POSITION& rPosition); // return *Position-- void* GetPrev(POSITION& rPosition) const; // return *Position--
// getting/modifying an element at a given position void*& GetAt(POSITION position); void* GetAt(POSITION position) const; void SetAt(POSITION pos, void* newElement);
void RemoveAt(POSITION position);
// inserting before or after a given position POSITION InsertBefore(POSITION position, void* newElement); POSITION InsertAfter(POSITION position, void* newElement);
// helper functions (note: O(n) speed) POSITION Find(void* searchValue, POSITION startAfter = NULL) const; // defaults to starting at the HEAD // return NULL if not found POSITION FindIndex(int nIndex) const; // get the 'nIndex'th element (may return NULL)
// Implementation protected: CNode* m_pNodeHead; CNode* m_pNodeTail; int m_nCount; CNode* m_pNodeFree; struct CPlex* m_pBlocks; int m_nBlockSize;
CNode* NewNode(CNode*, CNode*); void FreeNode(CNode*);
public: ~CPtrList(); #ifdef _DEBUG void Dump(CDumpContext&) const; void AssertValid() const; #endif // local typedefs for class templates typedef void* BASE_TYPE; typedef void* BASE_ARG_TYPE; }; 很顯然,CPtrList是對鏈表結(jié)構(gòu)體 struct CNode { CNode* pNext; CNode* pPrev; void* data; }; |
本身及其GetNext、GetPrev、GetAt、SetAt、RemoveAt、InsertBefore、InsertAfter、Find、FindIndex等各種操作的封裝。
作為一個(gè)抽象的鏈表類型,CPtrList并未定義其中節(jié)點(diǎn)的具體類型,而以一個(gè)void指針(struct CNode 中的void* data)巧妙地實(shí)現(xiàn)了鏈表節(jié)點(diǎn)成員具體類型的"模板"化。很顯然,在Visual C++6.0開發(fā)的年代,C++語言所具有的語法特征"模板"仍然沒有得到廣泛的應(yīng)用。 而CDocManager類的成員函數(shù)
virtual void AddDocTemplate(CDocTemplate* pTemplate); virtual POSITION GetFirstDocTemplatePosition() const; virtual CDocTemplate* GetNextDocTemplate(POSITION& pos) const; |
則完成對m_TemplateList鏈表的添加及遍歷操作的封裝,我們來看看這三個(gè)函數(shù)的源代碼:
void CDocManager::AddDocTemplate(CDocTemplate* pTemplate) { if (pTemplate == NULL) { if (pStaticList != NULL) { POSITION pos = pStaticList->GetHeadPosition(); while (pos != NULL) { CDocTemplate* pTemplate = (CDocTemplate*)pStaticList->GetNext(pos); AddDocTemplate(pTemplate); } delete pStaticList; pStaticList = NULL; } bStaticInit = FALSE; } else { ASSERT_VALID(pTemplate); ASSERT(m_templateList.Find(pTemplate, NULL) == NULL);// must not be in list pTemplate->LoadTemplate(); m_templateList.AddTail(pTemplate); } } POSITION CDocManager::GetFirstDocTemplatePosition() const { return m_templateList.GetHeadPosition(); } CDocTemplate* CDocManager::GetNextDocTemplate(POSITION& pos) const { return (CDocTemplate*)m_templateList.GetNext(pos); }
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
| 1、文檔類CDocument
在"文檔/視圖"架構(gòu)的MFC程序中,文檔是一個(gè)CDocument派生對象,它負(fù)責(zé)存儲應(yīng)用程序的數(shù)據(jù),并把這些信息提供給應(yīng)用程序的其余部分。CDocument類對文檔的建立及歸檔提供支持并提供了應(yīng)用程序用于控制其數(shù)據(jù)的接口,類CDocument的聲明如下:
///////////////////////////////////////////////////////////////////////////// // class CDocument is the main document data abstraction class CDocument : public CCmdTarget { DECLARE_DYNAMIC(CDocument) public: // Constructors CDocument();
// Attributes public: const CString& GetTitle() const; virtual void SetTitle(LPCTSTR lpszTitle); const CString& GetPathName() const; virtual void SetPathName(LPCTSTR lpszPathName, BOOL bAddToMRU = TRUE);
CDocTemplate* GetDocTemplate() const; virtual BOOL IsModified(); virtual void SetModifiedFlag(BOOL bModified = TRUE);
// Operations void AddView(CView* pView); void RemoveView(CView* pView); virtual POSITION GetFirstViewPosition() const; virtual CView* GetNextView(POSITION& rPosition) const;
// Update Views (simple update - DAG only) void UpdateAllViews(CView* pSender, LPARAM lHint = 0L, CObject* pHint = NULL);
// Overridables // Special notifications virtual void OnChangedViewList(); // after Add or Remove view virtual void DeleteContents(); // delete doc items etc
// File helpers virtual BOOL OnNewDocument(); virtual BOOL OnOpenDocument(LPCTSTR lpszPathName); virtual BOOL OnSaveDocument(LPCTSTR lpszPathName); virtual void OnCloseDocument(); virtual void ReportSaveLoadException(LPCTSTR lpszPathName, CException* e, BOOL bSaving, UINT nIDPDefault); virtual CFile* GetFile(LPCTSTR lpszFileName, UINT nOpenFlags, CFileException* pError); virtual void ReleaseFile(CFile* pFile, BOOL bAbort);
// advanced overridables, closing down frame/doc, etc. virtual BOOL CanCloseFrame(CFrameWnd* pFrame); virtual BOOL SaveModified(); // return TRUE if ok to continue virtual void PreCloseFrame(CFrameWnd* pFrame);
// Implementation protected: // default implementation CString m_strTitle; CString m_strPathName; CDocTemplate* m_pDocTemplate; CPtrList m_viewList; // list of views BOOL m_bModified; // changed since last saved
public: BOOL m_bAutoDelete; // TRUE => delete document when no more views BOOL m_bEmbedded; // TRUE => document is being created by OLE
#ifdef _DEBUG virtual void Dump(CDumpContext&) const; virtual void AssertValid() const; #endif //_DEBUG virtual ~CDocument();
// implementation helpers virtual BOOL DoSave(LPCTSTR lpszPathName, BOOL bReplace = TRUE); virtual BOOL DoFileSave(); virtual void UpdateFrameCounts(); void DisconnectViews(); void SendInitialUpdate();
// overridables for implementation virtual HMENU GetDefaultMenu(); // get menu depending on state virtual HACCEL GetDefaultAccelerator(); virtual void OnIdle(); virtual void OnFinalRelease();
virtual BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra,AFX_CMDHANDLERINFO* pHandlerInfo); friend class CDocTemplate;
protected: // file menu commands //{{AFX_MSG(CDocument) afx_msg void OnFileClose(); afx_msg void OnFileSave(); afx_msg void OnFileSaveAs(); //}}AFX_MSG // mail enabling afx_msg void OnFileSendMail(); afx_msg void OnUpdateFileSendMail(CCmdUI* pCmdUI); DECLARE_MESSAGE_MAP() }; |
一個(gè)文檔可以有多個(gè)視圖,每一個(gè)文檔都維護(hù)一個(gè)與之相關(guān)視圖的鏈表(CptrList類型的 m_viewList實(shí)例)。CDocument::AddView將一個(gè)視圖連接到文檔上,并將視圖的文檔指針指向該文檔:
void CDocument::AddView(CView* pView) { ASSERT_VALID(pView); ASSERT(pView->m_pDocument == NULL); // must not be already attached ASSERT(m_viewList.Find(pView, NULL) == NULL); // must not be in list
m_viewList.AddTail(pView); ASSERT(pView->m_pDocument == NULL); // must be un-attached pView->m_pDocument = this;
OnChangedViewList(); // must be the last thing done to the document }
|
CDocument::RemoveView則完成與CDocument::AddView相反的工作:
void CDocument::RemoveView(CView* pView) { ASSERT_VALID(pView); ASSERT(pView->m_pDocument == this); // must be attached to us
m_viewList.RemoveAt(m_viewList.Find(pView)); pView->m_pDocument = NULL;
OnChangedViewList(); // must be the last thing done to the document } |
從CDocument::AddView和CDocument::RemoveView函數(shù)可以看出,在與文檔關(guān)聯(lián)的視圖被移走或新加入時(shí)CDocument::OnChangedViewList將被調(diào)用:
void CDocument::OnChangedViewList() { // if no more views on the document, delete ourself // not called if directly closing the document or terminating the app if (m_viewList.IsEmpty() && m_bAutoDelete) { OnCloseDocument(); return; }
// update the frame counts as needed UpdateFrameCounts(); } |
CDocument::DisconnectViews將所有的視圖都與文檔"失連":
void CDocument::DisconnectViews() { while (!m_viewList.IsEmpty()) { CView* pView = (CView*)m_viewList.RemoveHead(); ASSERT_VALID(pView); ASSERT_KINDOF(CView, pView); pView->m_pDocument = NULL; } } |
實(shí)際上,類CDocument對視圖的管理與類CDocManager對文檔模板的管理及CDocTemplate對文檔的管理非常類似,少不了的,類CDocument中可遍歷對應(yīng)的視圖(出現(xiàn)GetFirstXXX和GetNextXXX兩個(gè)函數(shù)):
POSITION CDocument::GetFirstViewPosition() const { return m_viewList.GetHeadPosition(); }
CView* CDocument::GetNextView(POSITION& rPosition) const { ASSERT(rPosition != BEFORE_START_POSITION); // use CDocument::GetFirstViewPosition instead ! if (rPosition == NULL) return NULL; // nothing left CView* pView = (CView*)m_viewList.GetNext(rPosition); ASSERT_KINDOF(CView, pView); return pView; }
|
CDocument::GetFile和CDocument::ReleaseFile函數(shù)完成對參數(shù)lpszFileName指定文檔的打開與關(guān)閉操作:
CFile* CDocument::GetFile(LPCTSTR lpszFileName, UINT nOpenFlags, CFileException* pError) { CMirrorFile* pFile = new CMirrorFile; ASSERT(pFile != NULL); if (!pFile->Open(lpszFileName, nOpenFlags, pError)) { delete pFile; pFile = NULL; } return pFile; }
void CDocument::ReleaseFile(CFile* pFile, BOOL bAbort) { ASSERT_KINDOF(CFile, pFile); if (bAbort) pFile->Abort(); // will not throw an exception else pFile->Close(); delete pFile; } |
CDocument類的OnNewDocument、OnOpenDocument、OnSaveDocument及OnCloseDocument這一組成員函數(shù)用于創(chuàng)建、打開、保存或關(guān)閉一個(gè)文檔。在這一組函數(shù)中,上面的CDocument::GetFile和CDocument::ReleaseFile兩個(gè)函數(shù)得以調(diào)用:
BOOL CDocument::OnOpenDocument(LPCTSTR lpszPathName) { if (IsModified()) TRACE0("Warning: OnOpenDocument replaces an unsaved document.\n");
CFileException fe; CFile* pFile = GetFile(lpszPathName, CFile::modeRead|CFile::shareDenyWrite, &fe); if (pFile == NULL) { ReportSaveLoadException(lpszPathName, &fe,FALSE, AFX_IDP_FAILED_TO_OPEN_DOC); return FALSE; }
DeleteContents(); SetModifiedFlag(); // dirty during de-serialize
CArchive loadArchive(pFile, CArchive::load | CArchive::bNoFlushOnDelete); loadArchive.m_pDocument = this; loadArchive.m_bForceFlat = FALSE; TRY { CWaitCursor wait; if (pFile->GetLength() != 0) Serialize(loadArchive); // load me loadArchive.Close(); ReleaseFile(pFile, FALSE); } CATCH_ALL(e) { ReleaseFile(pFile, TRUE); DeleteContents(); // remove failed contents
TRY { ReportSaveLoadException(lpszPathName, e,FALSE, AFX_IDP_FAILED_TO_OPEN_DOC); } END_TRY DELETE_EXCEPTION(e); return FALSE; } END_CATCH_ALL
SetModifiedFlag(FALSE); // start off with unmodified
return TRUE; }
/////////////////////////////////////////////////////////////////////////////////////
| 視圖類CView
在MFC"文檔/視圖"架構(gòu)中,CView類是所有視圖類的基類,它提供了用戶自定義視圖類的公共接口。在"文檔/視圖"架構(gòu)中,文檔負(fù)責(zé)管理和維護(hù)數(shù)據(jù);而視圖類則負(fù)責(zé)如下工作:
?。?) 從文檔類中將文檔中的數(shù)據(jù)取出后顯示給用戶;
?。?) 接受用戶對文檔中數(shù)據(jù)的編輯和修改;
?。?) 將修改的結(jié)果反饋給文檔類,由文檔類將修改后的內(nèi)容保存到磁盤文件中。
文檔負(fù)責(zé)了數(shù)據(jù)真正在永久介質(zhì)中的存儲和讀取工作,視圖呈現(xiàn)只是將文檔中的數(shù)據(jù)以某種形式向用戶呈現(xiàn),因此一個(gè)文檔可對應(yīng)多個(gè)視圖。
下面我們來看看CView類的聲明:
class CView : public CWnd { DECLARE_DYNAMIC(CView) // Constructors protected: CView();
// Attributes public: CDocument* GetDocument() const;
// Operations public: // for standard printing setup (override OnPreparePrinting) BOOL DoPreparePrinting(CPrintInfo* pInfo);
// Overridables public: virtual BOOL IsSelected(const CObject* pDocItem) const; // support for OLE
// OLE scrolling support (used for drag/drop as well) virtual BOOL OnScroll(UINT nScrollCode, UINT nPos, BOOL bDoScroll = TRUE); virtual BOOL OnScrollBy(CSize sizeScroll, BOOL bDoScroll = TRUE);
// OLE drag/drop support virtual DROPEFFECT OnDragEnter(COleDataObject* pDataObject,DWORD dwKeyState, CPoint point); virtual DROPEFFECT OnDragOver(COleDataObject* pDataObject,DWORD dwKeyState, CPoint point); virtual void OnDragLeave(); virtual BOOL OnDrop(COleDataObject* pDataObject,DROPEFFECT dropEffect, CPoint point); virtual DROPEFFECT OnDropEx(COleDataObject* pDataObject, DROPEFFECT dropDefault, DROPEFFECT dropList, CPoint point); virtual DROPEFFECT OnDragScroll(DWORD dwKeyState, CPoint point);
virtual void OnPrepareDC(CDC* pDC, CPrintInfo* pInfo = NULL);
virtual void OnInitialUpdate(); // called first time after construct
protected: // Activation virtual void OnActivateView(BOOL bActivate, CView* pActivateView,CView* pDeactiveView); virtual void OnActivateFrame(UINT nState, CFrameWnd* pFrameWnd);
// General drawing/updating virtual void OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint); virtual void OnDraw(CDC* pDC) = 0;
// Printing support virtual BOOL OnPreparePrinting(CPrintInfo* pInfo); // must override to enable printing and print preview
virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo); virtual void OnPrint(CDC* pDC, CPrintInfo* pInfo); virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);
// Advanced: end print preview mode, move to point virtual void OnEndPrintPreview(CDC* pDC, CPrintInfo* pInfo, POINT point,CPreviewView* pView);
// Implementation public: virtual ~CView(); #ifdef _DEBUG virtual void Dump(CDumpContext&) const; virtual void AssertValid() const; #endif //_DEBUG
// Advanced: for implementing custom print preview BOOL DoPrintPreview(UINT nIDResource, CView* pPrintView,CRuntimeClass* pPreviewViewClass, CPrintPreviewState* pState);
virtual void CalcWindowRect(LPRECT lpClientRect,UINT nAdjustType = adjustBorder); virtual CScrollBar* GetScrollBarCtrl(int nBar) const; static CSplitterWnd* PASCAL GetParentSplitter(const CWnd* pWnd, BOOL bAnyState);
protected: CDocument* m_pDocument;
public: virtual BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra,AFX_CMDHANDLERINFO* pHandlerInfo); protected: virtual BOOL PreCreateWindow(CREATESTRUCT& cs); virtual void PostNcDestroy();
// friend classes that call protected CView overridables friend class CDocument; friend class CDocTemplate; friend class CPreviewView; friend class CFrameWnd; friend class CMDIFrameWnd; friend class CMDIChildWnd; friend class CSplitterWnd; friend class COleServerDoc; friend class CDocObjectServer;
//{{AFX_MSG(CView) afx_msg int OnCreate(LPCREATESTRUCT lpcs); afx_msg void OnDestroy(); afx_msg void OnPaint(); afx_msg int OnMouseActivate(CWnd* pDesktopWnd, UINT nHitTest, UINT message); // commands afx_msg void OnUpdateSplitCmd(CCmdUI* pCmdUI); afx_msg BOOL OnSplitCmd(UINT nID); afx_msg void OnUpdateNextPaneMenu(CCmdUI* pCmdUI); afx_msg BOOL OnNextPaneCmd(UINT nID);
// not mapped commands - must be mapped in derived class afx_msg void OnFilePrint(); afx_msg void OnFilePrintPreview(); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; |
CView類首先要維護(hù)文檔與視圖之間的關(guān)聯(lián),它通過CDocument* m_pDocument保護(hù)性成員變量記錄關(guān)聯(lián)文檔的指針,并提供CView::GetDocument接口函數(shù)以使得應(yīng)用程序可得到與視圖關(guān)聯(lián)的文檔。而在CView類的析構(gòu)函數(shù)中,需將對應(yīng)文檔類視圖列表中的本視圖刪除:
CView::~CView() { if (m_pDocument != NULL) m_pDocument->RemoveView(this); } |
CView中地位最重要的函數(shù)是virtual void OnDraw(CDC* pDC) = 0;從這個(gè)函數(shù)的聲明可以看出,CView是一個(gè)純虛基類。這個(gè)函數(shù)必須被重載,它通常執(zhí)行如下步驟:
?。?) 以GetDocument()函數(shù)獲得視圖對應(yīng)文檔的指針;
(2) 讀取對應(yīng)文檔中的數(shù)據(jù);
?。?) 顯示這些數(shù)據(jù)。
以MFC向?qū)Ы⒌囊粋€(gè)初始"文檔/視圖"架構(gòu)工程將這樣重載OnDraw()函數(shù),注意注釋中的"add draw code for native data here(添加活動數(shù)據(jù)的繪制代碼)":
///////////////////////////////////////////////////////////////////////////// // CExampleView drawing void CExampleView::OnDraw(CDC* pDC) { CExampleDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: add draw code for native data here } CView::PreCreateWindow負(fù)責(zé)View的初始化: ///////////////////////////////////////////////////////////////////////////// // CView second phase construction - bind to document BOOL CView::PreCreateWindow(CREATESTRUCT & cs) { ASSERT(cs.style & WS_CHILD);
if (cs.lpszClass == NULL) { VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG)); cs.lpszClass = _afxWndFrameOrView; // COLOR_WINDOW background }
if (afxData.bWin4 && (cs.style & WS_BORDER)) { cs.dwExStyle |= WS_EX_CLIENTEDGE; cs.style &= ~WS_BORDER; }
return TRUE; } |
CView::OnUpdate函數(shù)在文檔的數(shù)據(jù)被改變的時(shí)候被調(diào)用(即它被用來通知一個(gè)視圖的關(guān)聯(lián)文檔的內(nèi)容已經(jīng)被修改),它預(yù)示著我們需要重新繪制視圖以顯示變化后的數(shù)據(jù)。其中的Invalidate(TRUE)將整個(gè)窗口設(shè)置為需要重繪的無效區(qū)域,它會產(chǎn)生WM_PAINT消息,這樣OnDraw將被調(diào)用:
void CView::OnUpdate(CView* pSender, LPARAM /*lHint*/, CObject* /*pHint*/) { ASSERT(pSender != this); UNUSED(pSender); // unused in release builds
// invalidate the entire pane, erase background too Invalidate(TRUE); } |
假如文檔中的數(shù)據(jù)發(fā)生了變化,必須通知所有鏈接到該文檔的視圖,這時(shí)候文檔類的UpdateAllViews函數(shù)需要被調(diào)用。
此外,CView類包含一系列函數(shù)用于進(jìn)行文檔的打印及打印預(yù)覽工作:
(1)CView::OnBeginPrinting在打印工作開始時(shí)被調(diào)用,用來分配GDI資源;
?。?)CView::OnPreparePrinting函數(shù)在文檔打印或者打印預(yù)覽前被調(diào)用,可用來初始化打印對話框;
(3)CView::OnPrint用來打印或打印預(yù)覽文檔;
?。?)CView::OnEndPrinting函數(shù)在打印工作結(jié)束時(shí)被調(diào)用,用以釋放GDI資源;
?。?)CView::OnEndPrintPreview在退出打印預(yù)覽模式時(shí)被調(diào)用。
CView派生類
MFC提供了豐富的CView派生類,各種不同的派生類實(shí)現(xiàn)了對不同種類控件的支持,以為用戶提供多元化的顯示界面。這些CView派生類包括:
(1)CScrollView:提供滾動支持;
(2)CCtrlView:支持tree、 list和rich edit控件;
(3)CDaoRecordView:在dialog-box控件中顯示數(shù)據(jù)庫記錄;
(4)CEditView:提供了一個(gè)簡單的多行文本編輯器;
(5)CFormView:包含dialog-box控件,可滾動,基于對話框模板資源;
(6)CListView:支持list控件;
(7)CRecordView:在dialog-box控件中顯示數(shù)據(jù)庫記錄;
(8)CRichEditView:支持rich edit控件;
(9)CTreeView:支持tree控件。
其中,CRichEditView、CTreeView及CListView均繼承自CCtrlView類;CFormView繼承自CScrollView類;CRecordView、CDaoRecordView則進(jìn)一步繼承自CFormView類。
下圖描述了CView類體系的繼承關(guān)系:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
| 從前文可知,在MFC中,文檔是真正的數(shù)據(jù)載體,視圖是文檔的顯示界面,對應(yīng)同一個(gè)文檔,可能存在多個(gè)視圖界面,我們需要另外一種東東來將這些界面管理起來,這個(gè)東東就是框架。
MFC創(chuàng)造框架類的初衷在于:把界面管理工作獨(dú)立出來!框架窗口為應(yīng)用程序的用戶界面提供結(jié)構(gòu)框架,它是應(yīng)用程序的主窗口,負(fù)責(zé)管理其包容的窗口。一個(gè)應(yīng)用程序啟動時(shí)會創(chuàng)建一個(gè)最頂層的框架窗口。
MFC提供二種類型的框架窗口:單文檔窗口SDI和多文檔窗口MDI(你可以認(rèn)為對話框是另一種框架窗口)。單文檔窗口一次只能打開一個(gè)文檔框架窗口,而多文檔窗口應(yīng)用程序中可以打開多個(gè)文檔框架窗口,即子窗口(Child Window)。這些子窗口中的文檔可以為同種類型,也可以為不同類型。
在Visual C++ AppWizard的第一個(gè)對話框中,會讓用戶選擇應(yīng)用程序是基于單文檔、多文檔還是基于對話框的,如圖5.1。
 圖5.1 在AppWizard中選擇框架窗口 |
MFC提供了三個(gè)類CFrameWnd、CMDIFrameWnd、CMDIChildWnd用于支持單文檔窗口和多文檔窗口,這些類的層次結(jié)構(gòu)如圖5.2。
 圖5.2 CFrameWnd、CMDIFrameWnd、CMDIChildWnd類的層次 |
?。?)CFrameWnd類用于SDI應(yīng)用程序的框架窗口,SDI框架窗口既是應(yīng)用程序的主框架窗口,也是當(dāng)前文檔對應(yīng)的視圖的邊框;CFrameWnd類也作為CMDIFrameWnd和CMDIChildWnd類的父類,而在基于SDI的應(yīng)用程序中,AppWizard會自動為我們添加一個(gè)繼承自CFrameWnd類的CMainFrame類。
CFrameWnd類中重要的函數(shù)有Create(用于創(chuàng)建窗口)、LoadFrame(用于從資源文件中創(chuàng)建窗口)、PreCreateWindow(用于注冊窗口類)等。Create函數(shù)第一個(gè)參數(shù)為窗口注冊類名,第二個(gè)參數(shù)為窗口標(biāo)題,其余幾個(gè)參數(shù)指定了窗口的風(fēng)格、大小、父窗口、菜單名等,其源代碼如下:
BOOL CFrameWnd::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT &rect, CWnd *pParentWnd, LPCTSTR lpszMenuName, DWORD dwExStyle, CCreateContext *pContext) { HMENU hMenu = NULL; if (lpszMenuName != NULL) { // load in a menu that will get destroyed when window gets destroyed HINSTANCE hInst = AfxFindResourceHandle(lpszMenuName, RT_MENU); if ((hMenu = ::LoadMenu(hInst, lpszMenuName)) == NULL) { TRACE0("Warning: failed to load menu for CFrameWnd.\n"); PostNcDestroy(); // perhaps delete the C++ object return FALSE; } } m_strTitle = lpszWindowName; // save title for later
if (!CreateEx(dwExStyle, lpszClassName, lpszWindowName, dwStyle, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, pParentWnd ->GetSafeHwnd(), hMenu, (LPVOID)pContext)) { TRACE0("Warning: failed to create CFrameWnd.\n"); if (hMenu != NULL) DestroyMenu(hMenu); return FALSE; } return TRUE; } |
LoadFrame函數(shù)用于從資源文件中創(chuàng)建窗口,我們通常只需要給其指定一個(gè)參數(shù),LoadFrame使用該參數(shù)從資源中獲取主邊框窗口的標(biāo)題、圖標(biāo)、菜單、加速鍵等,其源代碼為:
BOOL CFrameWnd::LoadFrame(UINT nIDResource, DWORD dwDefaultStyle, CWnd *pParentWnd, CCreateContext *pContext) { // only do this once ASSERT_VALID_IDR(nIDResource); ASSERT(m_nIDHelp == 0 || m_nIDHelp == nIDResource);
m_nIDHelp = nIDResource; // ID for help context (+HID_BASE_RESOURCE)
CString strFullString; if (strFullString.LoadString(nIDResource)) AfxExtractSubString(m_strTitle, strFullString, 0); // first sub-string
VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));
// attempt to create the window LPCTSTR lpszClass = GetIconWndClass(dwDefaultStyle, nIDResource); LPCTSTR lpszTitle = m_strTitle; if (!Create(lpszClass, lpszTitle, dwDefaultStyle, rectDefault, pParentWnd, MAKEINTRESOURCE(nIDResource), 0L, pContext)) { return FALSE; // will self destruct on failure normally }
// save the default menu handle ASSERT(m_hWnd != NULL); m_hMenuDefault = ::GetMenu(m_hWnd);
// load accelerator resource LoadAccelTable(MAKEINTRESOURCE(nIDResource));
if (pContext == NULL) // send initial update SendMessageToDescendants(WM_INITIALUPDATE, 0, 0, TRUE, TRUE);
return TRUE; } |
在SDI程序中,如果需要修改窗口的默認(rèn)風(fēng)格,程序員需要修改CMainFrame類的PreCreateWindow函數(shù),因?yàn)锳ppWizard給我們生成的CMainFrame::PreCreateWindow僅對其基類的PreCreateWindow函數(shù)進(jìn)行調(diào)用,CFrameWnd::PreCreateWindow的源代碼如下:
BOOL CFrameWnd::PreCreateWindow(CREATESTRUCT &cs) { if (cs.lpszClass == NULL) { VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG)); cs.lpszClass = _afxWndFrameOrView; // COLOR_WINDOW background }
if ((cs.style &FWS_ADDTOTITLE) && afxData.bWin4)cs.style |= FWS_PREFIXTITLE;
if (afxData.bWin4) cs.dwExStyle |= WS_EX_CLIENTEDGE; return TRUE; }
///////////////////////////////////////////////////////////////////////////////////////
| 1、模板、文檔、視圖、框架的關(guān)系
連載1~5我們各個(gè)擊破地講解了文檔、文檔模板、視圖和框架類,連載1已經(jīng)強(qiáng)調(diào)這些類有著親密的內(nèi)部聯(lián)系,總結(jié)1~5我們可以概括其聯(lián)系為:
?。?)文檔保留該文檔的視圖列表和指向創(chuàng)建該文檔的文檔模板的指針;文檔至少有一個(gè)相關(guān)聯(lián)的視圖,而視圖只能與一個(gè)文檔相關(guān)聯(lián)。
(2)視圖保留指向其文檔的指針,并被包含在其父框架窗口中;
?。?)文檔框架窗口(即包含視圖的MDI子窗口)保留指向其當(dāng)前活動視圖的指針;
?。?)文檔模板保留其已打開文檔的列表,維護(hù)框架窗口、文檔及視圖的映射;
?。?)應(yīng)用程序保留其文檔模板的列表。
我們可以通過一組函數(shù)讓這些類之間相互可訪問,表6-1給出這些函數(shù)。
表6-1 文檔、文檔模板、視圖和框架類的互相訪問
| 從該對象 | 如何訪問其他對象 | | 全局函數(shù) | 調(diào)用全局函數(shù)AfxGetApp可以得到CWinApp應(yīng)用類指針 | | 應(yīng)用 | AfxGetApp()->m_pMainWnd為框架窗口指針;用CWinApp::GetFirstDocTemplatePostion、CWinApp::GetNextDocTemplate來遍歷所有文檔模板 | | 文檔 | 調(diào)用CDocument::GetFirstViewPosition,CDocument::GetNextView來遍歷所有和文檔關(guān)聯(lián)的視圖;調(diào)用CDocument:: GetDocTemplate 獲取文檔模板指針 | | 文檔模板 | 調(diào)用CDocTemplate::GetFirstDocPosition、CDocTemplate::GetNextDoc來遍歷所有對應(yīng)文檔 | | 視圖 | 調(diào)用CView::GetDocument 得到對應(yīng)的文檔指針; 調(diào)用CView::GetParentFrame 獲取框架窗口 | | 文檔框架窗口 | 調(diào)用CFrameWnd::GetActiveView 獲取當(dāng)前得到當(dāng)前活動視圖指針; 調(diào)用CFrameWnd::GetActiveDocument 獲取附加到當(dāng)前視圖的文檔指針 | | MDI 框架窗口 | 調(diào)用CMDIFrameWnd::MDIGetActive 獲取當(dāng)前活動的MDI子窗口(CMDIChildWnd) |
我們列舉一個(gè)例子,綜合應(yīng)用上表中的函數(shù),寫一段代碼,它完成遍歷文檔模板、文檔和視圖的功能:
CMyApp *pMyApp = (CMyApp*)AfxGetApp(); //得到應(yīng)用程序指針 POSITION p = pMyApp->GetFirstDocTemplatePosition();//得到第1個(gè)文檔模板 while (p != NULL) //遍歷文檔模板 { CDocTemplate *pDocTemplate = pMyApp->GetNextDocTemplate(p); POSITION p1 = pDocTemplate->GetFirstDocPosition();//得到文檔模板對應(yīng)的第1個(gè)文檔 while (p1 != NULL) //遍歷文檔模板對應(yīng)的文檔 { CDocument *pDocument = pDocTemplate->GetNextDoc(p1); POSITION p2 = pDocument->GetFirstViewPosition(); //得到文檔對應(yīng)的第1個(gè)視圖 while (p2 != NULL) //遍歷文檔對應(yīng)的視圖 { CView *pView = pDocument->GetNextView(p2); } } } |
由此可見,下面的管理關(guān)系和實(shí)現(xiàn)途徑都是完全類似的:
?。?)應(yīng)用程序之于文檔模板;
?。?)文檔模板之于文檔;
(3)文檔之于視圖。
圖6.1、6.2分別給出了一個(gè)多文檔/視圖框架MFC程序的組成以及其中所包含類的層次關(guān)系。
 圖6.1 多文檔/視圖框架MFC程序的組成 |
 圖6.2 文檔/視圖框架程序類的層次關(guān)系 |
關(guān)于文檔和視圖的關(guān)系,我們可進(jìn)一步細(xì)分為三類:
?。?)文檔對應(yīng)多個(gè)相同的視圖對象,每個(gè)視圖對象在一個(gè)單獨(dú)的 MDI 文檔框架窗口中;
?。?)文檔對應(yīng)多個(gè)相同類的視圖對象,但這些視圖對象在同一文檔框架窗口中(通過"拆分窗口"即將單個(gè)文檔窗口的視圖空間拆分成多個(gè)單獨(dú)的文檔視圖實(shí)現(xiàn));
(3)文檔對應(yīng)多個(gè)不同類的視圖對象,這些視圖對象僅在一個(gè)單獨(dú)的 MDI 文檔框架窗口中。在此模型中,由不同的類構(gòu)造成的多個(gè)視圖共享單個(gè)框架窗口,每個(gè)視圖可提供查看同一文檔的不同方式。例如,一個(gè)視圖以字處理模式顯示文檔,而另一個(gè)視圖則以"文檔結(jié)構(gòu)圖"模式顯示文檔。
圖6.3顯示了對應(yīng)三種文檔與視圖關(guān)系應(yīng)用程序的界面特點(diǎn)。
 圖6.3文檔/視圖的三種關(guān)系
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
| 為了能夠把我們所學(xué)的所有知識都在實(shí)例中得以完整的體現(xiàn),我們來寫一個(gè)盡可能復(fù)雜的"文檔/視圖"架構(gòu)MFC程序,這個(gè)程序復(fù)雜到:
?。?)是一個(gè)多文檔/視圖架構(gòu)MFC程序;
?。?)支持多種文件格式(假設(shè)支持?jǐn)U展名為BMP的位圖和TXT的文本文件);
?。?)一個(gè)文檔(BMP格式)對應(yīng)多個(gè)不同類型的視圖(圖形和二進(jìn)制數(shù)據(jù))。
相信上述程序已經(jīng)是一個(gè)包含"最復(fù)雜"特性的"文檔/視圖"架構(gòu)MFC程序了,搞定了這個(gè)包羅萬象的程序,還有什么簡單的程序搞不定呢?
用Visual C++工程向?qū)?chuàng)建一個(gè)名為"Example"的多文檔/視圖框架MFC程序,最初的應(yīng)用程序界面如圖7.1。
 圖7.1 最初的Example工程界面 |
這個(gè)時(shí)候的程序還不支持任何文檔格式,我們需讓它支持TXT(由于本文的目的是講解框架而非具體的讀寫文檔與顯示,故將程序簡化為只顯示包含一行的TXT文件)和BMP文件。
定義IDR_TEXTTYPE、IDR_BMPTYPE宏,并在資源文件中增加對應(yīng)IDR_TEXTTYPE、IDR_BMPTYPE文檔格式的字符串:
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by EXAMPLE.RC // #define IDD_ABOUTBOX 100 #define IDR_MAINFRAME 128 //#define IDR_EXAMPLTYPE 129 #define IDR_TEXTTYPE 10001 #define IDR_BMPTYPE 10002 … #endif STRINGTABLE PRELOAD DISCARDABLE BEGIN IDR_MAINFRAME "Example" IDR_EXAMPLTYPE "\nExampl\nExampl\n\n\nExample.Document\nExampl Document" IDR_TEXTTYPE "\nTEXT\nTEXT\nExampl 文件 (*.txt)\n.txt\nTEXT\nTEXT Document" IDR_BMPTYPE "\nBMP\nBMP\nExampl 文件 (*.bmp)\n.bmp\nBMP\nBMP Document" END
|
我們讓第一個(gè)文檔模板(由VC向?qū)桑?yīng)TXT格式,修改CExampleApp::InitInstance函數(shù):
BOOL CExampleApp::InitInstance() { … CMultiDocTemplate* pDocTemplate; pDocTemplate = new CMultiDocTemplate( IDR_TEXTTYPE, //對應(yīng)文本文件的字符串 RUNTIME_CLASS(CExampleDoc), RUNTIME_CLASS(CChildFrame), // custom MDI child frame RUNTIME_CLASS(CExampleView)); AddDocTemplate(pDocTemplate); … } |
為了讓程序支持TXT文件的讀取和顯示,我們需要重載CexampleDoc文檔類和CExampleView視圖類。因?yàn)閺奈臋n模板new CMultiDocTemplate中的參數(shù)可以看出,CExampleDoc和CExampleView分別為對應(yīng)TXT文件的文檔類和視圖類:
class CExampleDoc : public CDocument { … CString m_Text; //在文檔類中定義成員變量用于存儲TXT文件中的字符串 … }
//重載文檔類的Serialize,讀取字符串到m_Text中 void CExampleDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO: add storing code here } else { // TODO: add loading code here ar.ReadString(m_Text); } } //重載視圖類的OnDraw函數(shù),顯示文檔中的字符串 ///////////////////////////////////////////////////////////////////////////// // CExampleView drawing void CExampleView::OnDraw(CDC* pDC) { CExampleDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: add draw code for native data here pDC->TextOut(0,0,pDoc->m_Text); } |
這個(gè)時(shí)候的程序已經(jīng)支持TXT文件了,例如我們打開一個(gè)TXT文件,將出現(xiàn)如圖7.2的界面。
 圖7.2 打開TXT文件的界面
|
|