MVC架构

wxDocManager文档管理器

wxWidgets使用wxDocManager类来管理MVC中的文档和视图的对应关系,使用方法:

  1. 创建一个wxDocManager对象,然后向此对象中增加文档模板wxDocTemplate对象,文档模板对象中说明了文档类型和该文档对应的文档类、视图类;
  2. 将此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和模板的关联:

  1. 调用wxDocManagerAssociateTemplate将自己和manager关联起来,在wxDocManager中,它将所有的文档模板保存到m_templates容器中;
  2. 保存doc和view的ClassInfo。
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进行的,我们接下来跟踪一下创建新文档的流程,用户代码如下:

docManager->CreateNewDocument();

跟踪到wxDocManager::CreateNewDocument,它复用CreateDocument的实现,CreateDocument使用flags参数,有如下数据:

  1. 获取用户注册的文档模板,如果没有则不需要处理了;
  2. 选择一个文档模板,这个是必须的,因为所有的重要操作都是在文档模板中完成的;
  3. 接着进行其他的有效性验证等。
  4. 最后调用用户注册的模板类来创建文档
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运行用户的代码,这个是在创建过程中的唯一机会。

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

文档创建过程中方法的调用顺序:

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的类型是在模板对象中指定的,自然它知道怎么创建。

  1. 调用wxDocTemplate::DoCreateView来实例化一个view对象;
  2. 调用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方法,所以就是这个了,函数中调用了两个函数:

  1. BaseFrame::TryBefore(event)这个必然是wxFrame::TryBefore,可忽略
  2. 第二个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的标准流程处理。

SDI消息传递总结

处理原则:

  1. wxDocParentFrame收到消息后处理:优先传递给wxDocManager处理,然后自己处理;
  2. wxDocManager收到消息后,优先传递给wxView处理,然后自己处理;
  3. 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();
}

wxWidgets源码分析(8) - MVC架构的更多相关文章

  1. jQuery 2.0.3 源码分析core - 整体架构

    拜读一个开源框架,最想学到的就是设计的思想和实现的技巧. 废话不多说,jquery这么多年了分析都写烂了,老早以前就拜读过, 不过这几年都是做移动端,一直御用zepto, 最近抽出点时间把jquery ...

  2. jQuery源码分析-01总体架构

    1. 总体架构 1.1自调用匿名函数 self-invoking anonymous function 打开jQuery源码,首先你会看到这样的代码结构: (function( window, und ...

  3. zeromq源码分析笔记之架构(1)

    1.zmq概述 ZeroMQ是一种基于消息队列的多线程网络库,其对套接字类型.连接处理.帧.甚至路由的底层细节进行抽象,提供跨越多种传输协议的套接字.引用云风的话来说:ZeroMQ 并不是一个对 so ...

  4. js菜鸟进阶-jQuery源码分析(1)-基本架构

    导读: 本人JS菜鸟一枚,为加强代码美观和编程思想.所以来研究下jQuery,有需要进阶JS的同学很适合阅读此文!我是边看代码(jquery2.2.1),边翻“javascript高级程序设计”写的, ...

  5. jQuery源码分析系列 : 整体架构

    query这么多年了分析都写烂了,老早以前就拜读过, 不过这几年都是做移动端,一直御用zepto, 最近抽出点时间把jquery又给扫一遍 我也不会照本宣科的翻译源码,结合自己的实际经验一起拜读吧! ...

  6. wxWidgets源码分析(9) - wxString

    目录 wxString wxString的中文字符支持 Windows Linux Unicode Linux UTF-8 总结 wxString与通用字符串的转换 wxString对象的创建 将wx ...

  7. wxWidgets源码分析(1) - App启动过程

    目录 APP启动过程 wxApp入口定义 wxApp实例化准备 wxApp的实例化 wxApp运行 总结 APP启动过程 本文主要介绍wxWidgets应用程序的启动过程,从app.cpp入手. wx ...

  8. wxWidgets源码分析(7) - 窗口尺寸

    目录 窗口尺寸 概述 窗口Size消息的处理 用户调整Size消息的处理 调整窗口大小 程序调整窗口大小 wxScrolledWindow设置窗口大小 获取TextCtrl控件最合适大小 窗口尺寸 概 ...

  9. wxWidgets源码分析(6) - 窗口关闭过程

    目录 窗口关闭过程 调用流程 关闭文档 删除视图 删除文档对象 关闭Frame App清理 多文档窗口的关闭 多文档父窗口关闭 多文档子窗口关闭 窗口的正式删除 窗口关闭过程总结 如何手工删除view ...

随机推荐

  1. Educational Codeforces Round 41

    Educational Codeforces Round 41  D. Pair Of Lines 考虑先把凸包找出来,如果凸包上的点数大于\(4\)显然不存在解,小于等于\(2\)必然存在解 否则枚 ...

  2. POJ2774 Long Long Message 【SAM】

    POJ2774 Long Long Message 找两个串的最长公共字串 对其中一个串\(s\)建\(SAM\),然后我们如何找到最长公共字串,办法就是枚举\(t\)串所有的前缀,然后找各个前缀的最 ...

  3. [IOI1998] Polygon (区间dp,和石子合并很相似)

    题意: 给你一个多边形(可以看作n个顶点,n-1条边的图),每一条边上有一个符号(+号或者*号),这个多边形有n个顶点,每一个顶点有一个值 最初你可以把一条边删除掉,这个时候这就是一个n个顶点,n-2 ...

  4. Codeforces Round #582 (Div. 3) G. Path Queries (并查集计数)

    题意:给你带边权的树,有\(m\)次询问,每次询问有多少点对\((u,v)\)之间简单路径上的最大边权不超过\(q_i\). 题解:真的想不到用最小生成树来写啊.... 我们对边权排序,然后再对询问的 ...

  5. hdu3506 Monkey Party

    Problem Description Far away from our world, there is a banana forest. And many lovely monkeys live ...

  6. CF1462-D. Add to Neighbour and Remove

    codeforces1462D 题意: 给出一个由n个数组成的数组,现在你可以对这个数组进行如下操作:将数组中的一个元素加到这个元素的两边中的一边,然后将这个元素删掉.若该元素在最左边,那么该元素不能 ...

  7. 关于free和delete的使用

    上一篇篇幅太长,这里再区分free和delete的用法. 两个同时存在是有它的原因的,我们前面说过,free是函数,它只释放内存,但不会调用析构函数,如果用free去释放new申请的空间,会因为无法调 ...

  8. sql-libs(1) -字符型注入

    关于sql-libs的安装就不做过多的说明, 环境:win7虚拟机 192.168.48.130(NAT连接),然后用我的win10物理机去访问. 直接加 ' 报错,后测试 and '1'='1 成功 ...

  9. npm & app-node-env

    npm & app-node-env $ npm i -g app-node-env # OR $ yarn global add app-node-env demo $ ane env=ap ...

  10. scroll calendar & scroll view

    scroll calendar & scroll view https://taro-docs.jd.com/taro/docs/components/viewContainer/scroll ...