wxWidgets源码分析(8) - MVC架构
MVC架构
wxDocManager文档管理器
wxWidgets使用wxDocManager
类来管理MVC中的文档和视图的对应关系,使用方法:
- 创建一个
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
容器中; - 保存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参数,有如下数据:
- 获取用户注册的文档模板,如果没有则不需要处理了;
- 选择一个文档模板,这个是必须的,因为所有的重要操作都是在文档模板中完成的;
- 接着进行其他的有效性验证等。
- 最后调用用户注册的模板类来创建文档
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的类型是在模板对象中指定的,自然它知道怎么创建。
- 调用
wxDocTemplate::DoCreateView
来实例化一个view对象; - 调用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
方法,所以就是这个了,函数中调用了两个函数:
- BaseFrame::TryBefore(event)这个必然是
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的标准流程处理。
SDI消息传递总结
处理原则:
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();
}
wxWidgets源码分析(8) - MVC架构的更多相关文章
- jQuery 2.0.3 源码分析core - 整体架构
拜读一个开源框架,最想学到的就是设计的思想和实现的技巧. 废话不多说,jquery这么多年了分析都写烂了,老早以前就拜读过, 不过这几年都是做移动端,一直御用zepto, 最近抽出点时间把jquery ...
- jQuery源码分析-01总体架构
1. 总体架构 1.1自调用匿名函数 self-invoking anonymous function 打开jQuery源码,首先你会看到这样的代码结构: (function( window, und ...
- zeromq源码分析笔记之架构(1)
1.zmq概述 ZeroMQ是一种基于消息队列的多线程网络库,其对套接字类型.连接处理.帧.甚至路由的底层细节进行抽象,提供跨越多种传输协议的套接字.引用云风的话来说:ZeroMQ 并不是一个对 so ...
- js菜鸟进阶-jQuery源码分析(1)-基本架构
导读: 本人JS菜鸟一枚,为加强代码美观和编程思想.所以来研究下jQuery,有需要进阶JS的同学很适合阅读此文!我是边看代码(jquery2.2.1),边翻“javascript高级程序设计”写的, ...
- jQuery源码分析系列 : 整体架构
query这么多年了分析都写烂了,老早以前就拜读过, 不过这几年都是做移动端,一直御用zepto, 最近抽出点时间把jquery又给扫一遍 我也不会照本宣科的翻译源码,结合自己的实际经验一起拜读吧! ...
- wxWidgets源码分析(9) - wxString
目录 wxString wxString的中文字符支持 Windows Linux Unicode Linux UTF-8 总结 wxString与通用字符串的转换 wxString对象的创建 将wx ...
- wxWidgets源码分析(1) - App启动过程
目录 APP启动过程 wxApp入口定义 wxApp实例化准备 wxApp的实例化 wxApp运行 总结 APP启动过程 本文主要介绍wxWidgets应用程序的启动过程,从app.cpp入手. wx ...
- wxWidgets源码分析(7) - 窗口尺寸
目录 窗口尺寸 概述 窗口Size消息的处理 用户调整Size消息的处理 调整窗口大小 程序调整窗口大小 wxScrolledWindow设置窗口大小 获取TextCtrl控件最合适大小 窗口尺寸 概 ...
- wxWidgets源码分析(6) - 窗口关闭过程
目录 窗口关闭过程 调用流程 关闭文档 删除视图 删除文档对象 关闭Frame App清理 多文档窗口的关闭 多文档父窗口关闭 多文档子窗口关闭 窗口的正式删除 窗口关闭过程总结 如何手工删除view ...
随机推荐
- httprunner(6)配置信息config
前言 每个测试用例都应该有config部分,可以配置用例级别.比如name.base_url.variables.verify.export等等 案例演示 from httprunner import ...
- Codeforces Round #690 (Div. 3)
第一次 ak cf 的正式比赛,不正式的是寒假里 div4 的 Testing Round,好啦好啦不要问我为什么没有 ak div4 了,差一题差一题 =.= 不知不觉已经咕了一个月了2333. 比 ...
- Codeforces Round #625 (Div. 2, based on Technocup 2020 Final Round) A. Contest for Robots(数学)
题意: n 道题,2 个答题者,已知二者的做题情况,你是受贿裁判,可以给每题指定分值(≥1),求甲乙分数(甲>乙)相差最小时最大分值的最小值. 思路: 统计只有甲或乙做出的题目数. 加一取下整判 ...
- AtCoder Beginner Contest 163
比赛链接:https://atcoder.jp/contests/abc163/tasks A - Circle Pond 题意 由半径输出圆周长. 代码 #include <bits/stdc ...
- CF1415-C. Bouncing Ball
CF1415-C. Bouncing Ball 题意: 在\(x\)轴上有\(n\)个点(从\(1\)到\(n\)),每个点都有一个值\(0\)或\(1\),\(0\)代表这个点不能走,\(1\)代表 ...
- Cobbler自定义标题及菜单密码
sha1pass mypassword || openssl passwd -1 -salt sXiKzkus mypassword $1$sXiKzkus$x12Z3ZaiC34GhceLH5LXw ...
- Redis 穿透 & 击穿 & 雪崩
原文:https://www.cnblogs.com/binghe001/p/13661381.html 缓存穿透 如果在请求数据时,在缓存层和数据库层都没有找到符合条件的数据,也就是说,在缓存层和数 ...
- VS中使用TreeView的Checked属性问题
VS中使用TreeView,当需要用到Checked属性,并需要同步子节点和父节点的Checked属性时,若使用AfterCheck事件会导致死循环,这里我使用的是NodeMouseClick事件.代 ...
- 操作系统 part1
实验好多,人好累... 一.进程和线程 references: 进程三种基本状态 进程和线程的概念.区别和联系 进程和线程的主要区别(总结) 进程间通信IPC 1.进程 进程,是资源分配和调度的基本单 ...
- bzoj4695 最假女选手(势能线段树/吉司机线段树)题解
题意: 已知\(n\)个数字,进行以下操作: \(1.\)给一个区间\([L,R]\) 加上一个数\(x\) \(2.\)把一个区间\([L,R]\) 里小于\(x\) 的数变成\(x\) \(3.\ ...