wxWidgets使用wxDocManager
類來管理MVC中的文檔和視圖的對應關係,使用方法:canvas
wxDocManager
對象,而後向此對象中增長文檔模板wxDocTemplate
對象,文檔模板對象中說明了文檔類型和該文檔對應的文檔類、視圖類;wxDocManager
對象傳遞給wxDocParentFrame
類(SDI),這樣框架類就和文檔類關聯起來了。//// SDI wxDocManager *docManager = new wxDocManager; //// Create a template relating drawing documents to their views new wxDocTemplate(docManager, "#docDescription", "*.#docExtention", "", "#docExtention", "Doc", "View", CLASSINFO(CClassPrefixDoc), CLASSINFO(CClassPrefixView)); wxFrame *frame = new wxDocParentFrame(docManager, ...);
看下wxDocTemplate
構造函數,這裏實現了Manager和模板的關聯:架構
wxDocManager
的AssociateTemplate
將本身和manager關聯起來,在wxDocManager
中,它將全部的文檔模板保存到m_templates
容器中;wxDocTemplate::wxDocTemplate(wxDocManager *manager, ...) { m_documentManager = manager; m_documentManager->AssociateTemplate(this); m_docClassInfo = docClassInfo; m_viewClassInfo = viewClassInfo; } void wxDocManager::AssociateTemplate(wxDocTemplate *temp) { if (!m_templates.Member(temp)) m_templates.Append(temp); }
後續的全部文檔操做都是經過wxDocManager
進行的,咱們接下來跟蹤一下建立新文檔的流程,用戶代碼以下:mvc
docManager->CreateNewDocument();
跟蹤到wxDocManager::CreateNewDocument
,它複用CreateDocument
的實現,CreateDocument
使用flags參數,有以下數據:框架
wxDocument *CreateNewDocument() { return CreateDocument(wxString(), wxDOC_NEW); } wxDocument *wxDocManager::CreateDocument(const wxString& pathOrig, long flags) { wxDocTemplateVector templates(GetVisibleTemplates(m_templates)); const size_t numTemplates = templates.size(); if ( !numTemplates ) return NULL; // 選擇文檔模板,若是用戶傳遞進來的pathOrig有效,則根據這個pahOrig指定的 // 擴展名進行選擇 // wxDOC_SILENT: 若是無此標記,則當用戶要建立新文檔,而且又有多種文檔類型時, // 會彈出對話框讓用戶選擇要建立的文檔類型。 wxString path = pathOrig; // may be modified below wxDocTemplate *temp; if ( flags & wxDOC_SILENT ) { temp = FindTemplateForPath(path); } else // not silent, ask the user { if ( (flags & wxDOC_NEW) || !path.empty() ) temp = SelectDocumentType(&templates[0], numTemplates); else temp = SelectDocumentPath(&templates[0], numTemplates, path, flags); } if ( !temp ) return NULL; // 檢查文檔數量是否是已經超出範圍 if ( (int)GetDocuments().GetCount() >= m_maxDocsOpen ) { if ( !CloseDocument((wxDocument *)GetDocuments().GetFirst()->GetData()) ) { // can't open the new document if closing the old one failed return NULL; } } // 調用文檔模板類來生成文檔對象 wxDocument * const docNew = temp->CreateDocument(path, flags); if ( !docNew ) return NULL; docNew->SetDocumentName(temp->GetDocumentName()); // 若是是新建立文檔則要調用doc的`OnNewDocument`和`OnOpenDocument`方法; if ( !(flags & wxDOC_NEW ? docNew->OnNewDocument() : docNew->OnOpenDocument(path)) ) { docNew->DeleteAllViews(); return NULL; } // 歷史文件。。。 if ( !(flags & wxDOC_NEW) && temp->FileMatchesTemplate(path) ) AddFileToHistory(path); docNew->Activate(); return docNew; }
文檔模板類wxDocTemplate
建立文檔的過程比較簡單,經過用戶註冊的docClassInfo來建立一個文檔對象,建立完成後再調用InitDocument
執行初始化:函數
wxDocument *wxDocTemplate::CreateDocument(const wxString& path, long flags) { wxDocument * const doc = DoCreateDocument(); return doc && InitDocument(doc, path, flags) ? doc : NULL; } wxDocument *wxDocTemplate::DoCreateDocument() { if (!m_docClassInfo) return NULL; return static_cast<wxDocument *>(m_docClassInfo->CreateObject()); }
文檔初始化過程,將文檔對象和文檔模板、文檔管理器都關聯起來,而後調用doc->OnCreate
運行用戶的代碼,這個是在建立過程當中的惟一機會。this
bool wxDocTemplate::InitDocument(wxDocument* doc, const wxString& path, long flags) { doc->SetFilename(path); doc->SetDocumentTemplate(this); GetDocumentManager()->AddDocument(doc); doc->SetCommandProcessor(doc->OnCreateCommandProcessor()); if ( doc->OnCreate(path, flags) ) return true; if ( GetDocumentManager()->GetDocuments().Member(doc) ) doc->DeleteAllViews(); return false; }
文檔建立過程當中方法的調用順序:code
1. Constructor() 2. OnCreate() 3. OnNewDocument() or OnOpenDocument()
能夠看到,上面的流程,建立文檔完成後就沒事了,返回成功,那視圖是在哪建立的呢?對象
在定義文檔類時,可能會實現OnCreate
方法,若是用戶想讓doc類直接建立關聯的視圖,那麼此時就必須調用父類的wxDocument::OnCreate
方法。繼承
bool CClassPrefixDoc::OnCreate(const wxString& path, long flags) { if (!wxDocument::OnCreate(path, flags)) return false; return true; }
咱們看下wxDocument::OnCreate
方法的實現,wxDocTemplate
在初始化doc對象的時候,已經將本身傳遞進去了,那麼此時doc就能夠再經過模板對象來建立View類,由於View的類型是在模板對象中指定的,天然它知道怎麼建立。
wxDocTemplate::DoCreateView
來實例化一個view對象;OnCreate
方法,這個方法也是給用戶使用的。bool wxDocument::OnCreate(const wxString& WXUNUSED(path), long flags) { return GetDocumentTemplate()->CreateView(this, flags) != NULL; } wxView *wxDocTemplate::CreateView(wxDocument *doc, long flags) { wxScopedPtr<wxView> view(DoCreateView()); if ( !view ) return NULL; view->SetDocument(doc); if ( !view->OnCreate(doc, flags) ) return NULL; return view.release(); } wxView *wxDocTemplate::DoCreateView() { if (!m_viewClassInfo) return NULL; return static_cast<wxView *>(m_viewClassInfo->CreateObject()); }
從上面能夠知道,建立順序以下:
wxDocTemplate -> Document -> View
wxDocParentFrame
菜單入口當用戶調用菜單保存文檔時,會產生菜單消息命令,因爲菜單屬於Frame的子項,因此此時會調用Frame的消息處理入口,調用流程以下:
// wxDocParentFrame wxFrame::HandleCommand() -> wxFrame::HandleCommand() -> wxFrameBase::ProcessCommand() // 參考前文分析,接着會調用 menu 和 menuBar 的處理函數,隨後主動權 // 再次回到 wxDocParentFrame 中,此時的處理函數位於 wxEvtHandler 中。 wxEvtHandler::ProcessEventLocally -> ... -> wxDocParentFrame::TryBefore
因爲TryBefore
是虛方法,此時咱們要看下 wxDocParentFrame
的繼承關係才能搞清楚到底調用哪一個函數:
wxDocParentFrame -> wxDocParentFrameBase (wxDocParentFrameAny<wxFrame>) -> wxFrame & wxDocParentFrameAnyBase
wxDocParentFrame
繼承關係中的wxDocParentFrameAny
模板類實現了TryBefore
方法,因此就是這個了,函數中調用了兩個函數:
wxFrame::TryBefore
,可忽略TryProcessEvent
方法,則是繼承自wxDocParentFrameAnyBase
,因此調用的是wxDocChildFrameAnyBase::TryProcessEvent
。template <class BaseFrame> class WXDLLIMPEXP_CORE wxDocParentFrameAny : public BaseFrame, public wxDocParentFrameAnyBase { virtual bool TryBefore(wxEvent& event) { return BaseFrame::TryBefore(event) || TryProcessEvent(event); }
咱們繼續看wxDocChildFrameAnyBase::TryProcessEvent
,這個函數改寫了原有frame類的處理過程,它查找當前Frame關聯的wxDocManager
,而後把消息傳遞給這個對象去處理。在給wxDocManager
處理以前,咱們能夠看到它其實是先調用childFrame->HasAlreadyProcessed
函數處理的,若是這個函數沒有處理則交給wxDocManager
。
因爲咱們此次使用的是預約義的,而且咱們自身沒有實現任何消息映射,因此此時必定會走到m_docManager->ProcessEventLocally
中。
bool wxDocParentFrameAnyBase::TryProcessEvent(wxEvent& event) { if ( !m_docManager ) return false; if ( wxView* const view = m_docManager->GetAnyUsableView() ) { wxDocChildFrameAnyBase* const childFrame = view->GetDocChildFrame(); if ( childFrame && childFrame->HasAlreadyProcessed(event) ) return false; } return m_docManager->ProcessEventLocally(event); }
wxDocManager
類的處理此時咱們能夠先看下wxDocManager
的消息映射表,若是已經有註冊,那麼此時就會走到
wxDocManager
的消息註冊表中。
參考源碼能夠看到wxDocManager
已經實現了不少個消息的預處理,對於Save來講已經有了wxDocManager::OnFileSave
:
BEGIN_EVENT_TABLE(wxDocManager, wxEvtHandler) EVT_MENU(wxID_OPEN, wxDocManager::OnFileOpen) EVT_MENU(wxID_CLOSE, wxDocManager::OnFileClose) EVT_MENU(wxID_CLOSE_ALL, wxDocManager::OnFileCloseAll) EVT_MENU(wxID_REVERT, wxDocManager::OnFileRevert) EVT_MENU(wxID_NEW, wxDocManager::OnFileNew) EVT_MENU(wxID_SAVE, wxDocManager::OnFileSave)
繼續跟蹤wxDocManager::OnFileSave
,發現轉向到了doc的save函數,doc中處理save時首先檢查是不是新建的文件而且有改變,若是是新建的則走SaveAs流程,不然繼續處理保存。
void wxDocManager::OnFileSave(wxCommandEvent& WXUNUSED(event)) { wxDocument *doc = GetCurrentDocument(); if (!doc) return; doc->Save(); } bool wxDocument::Save() { if ( AlreadySaved() ) return true; if ( m_documentFile.empty() || !m_savedYet ) return SaveAs(); return OnSaveDocument(m_documentFile); }
走Save
也好,走SaveAs
也好,最終都會調用wxDocument::OnSaveDocument
來實現文檔的保存,這裏最後調用的DoSaveDocument
方法來實現保存,這個是虛方法,須要用戶來實現。
bool wxDocument::OnSaveDocument(const wxString& file) { if ( !file ) return false; if ( !DoSaveDocument(file) ) return false; if ( m_commandProcessor ) m_commandProcessor->MarkAsSaved(); Modify(false); SetFilename(file); SetDocumentSaved(true); #if defined( __WXOSX_MAC__ ) && wxOSX_USE_CARBON wxFileName fn(file) ; fn.MacSetDefaultTypeAndCreator() ; #endif return true; }
對於文檔的另存、打開等也有一樣的處理。wxWidgets的文檔視圖框架已經提供了比較好的支持,咱們能夠省掉不少重複代碼了。
固然咱們也能夠經過重載OnSaveDocument (const wxString &filename)
來實現文檔的保存,這樣的話,用戶須要本身去保存文檔的狀態等等,實在是沒有必要。
wxView
類的處理前文有描述,當收到命令菜單時,最終會調用到m_docManager->ProcessEventLocally(event)
,咱們再回過頭看下ProcessEventLocally
的調用關係,先調用TryBefore
而後再調用TryHereOnly
:
bool wxEvtHandler::ProcessEventLocally(wxEvent& event) { return TryBeforeAndHere(event) || DoTryChain(event); } bool wxEvtHandler::TryBeforeAndHere(wxEvent& event) { return TryBefore(event) || TryHereOnly(event); }
TryBefore
是虛方法,咱們看下wxDocManager
的實現,查找當前的一個view,而後再調用view->ProcessEventLocally
,這裏須要關注GetAnyUsableView
,具體的實現就是獲取到當前最新的View,若是獲取不到在獲取到當前最近使用的doc,並獲取到這個doc中的第一個view:
bool wxDocManager::TryBefore(wxEvent& event) { wxView * const view = GetAnyUsableView(); return view && view->ProcessEventLocally(event); } wxView *wxDocManager::GetAnyUsableView() const { wxView *view = GetCurrentView(); if ( !view && !m_docs.empty() ) { wxList::compatibility_iterator node = m_docs.GetFirst(); if ( !node->GetNext() ) { wxDocument *doc = static_cast<wxDocument *>(node->GetData()); view = doc->GetFirstView(); } } return view; }
接着繼續看wxView的wxView::TryBefore
方法,view中會先找Doc類來處理這個命令,若是doc不處理那麼View纔會處理。
bool wxView::TryBefore(wxEvent& event) { wxDocument * const doc = GetDocument(); return doc && doc->ProcessEventLocally(event); }
對於MVC程序來講,View類並無真實的窗口,在建立wxView
對象的時候,由view建立一個新的wxWindow
窗口,此窗口繼承自Frame,而後將此窗口綁定到wxFrame
對象上。
而右鍵菜單則應用在wxWindow
窗口上,彈出菜單代碼爲:
void XXXWindow::OnMouseEvent(wxMouseEvent& event) { if (event.RightDown()) { wxMenu *popMenu = new wxMenu; popMenu->Append(ID_MenuTest, "Canvas Test Menu"); PopupMenu(popMenu); } }
此時消息處理流程略有變動,因爲菜單綁定在當前窗口上,因此最早處理此消息的是當前的wxWindow
對象,若是此對象不進行處理,則交給父類wxFrame
處理,父類則會安裝MVC的標準流程處理。
處理原則:
wxDocParentFrame
收到消息後處理:優先傳遞給wxDocManager
處理,而後本身處理;wxDocManager
收到消息後,優先傳遞給wxView
處理,而後本身處理;wxView
優先將消息傳遞給wxDocument
處理,而後本身處理;這樣最後將致使處理優順序爲:
wxDocument > wxView > wxDocParentFrame
若是是右鍵菜單命令,則優先觸發此菜單的對象處理,剩下的流程同上。
wxDocument提供了UpdateAllViews方法,能夠在doc發生變動時,通知全部的view更新視圖,其實是調用view的OnUpdate
方法實現更新:
void wxDocument::UpdateAllViews(wxView *sender, wxObject *hint) { wxList::compatibility_iterator node = m_documentViews.GetFirst(); while (node) { wxView *view = (wxView *)node->GetData(); if (view != sender) view->OnUpdate(sender, hint); node = node->GetNext(); } }
wxView並無實現OnUpdate
方法,這個須要用戶自行實現,對於繪圖類的,最簡單的辦法就是直接調用更新圖板:
void CProjectView::OnUpdate(wxView *sender, wxObject *hint) { if (canvas) canvas->Refresh(); }