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(); }