ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 19.1 文档/视图基础 文档/视图框架在很多编程框架中都专门提供了支持,因为它可以很大程度的简化编写类似的程序需要的代码量. 文档/视图框架主要是让你用文档和视图两个概念来建模.所谓文档,指的是那些用来存储数据和提供用户界面无关的操作的类,而视图,指的是用来显示数据的那些类.这和所谓的MVC模型(模型-视图-控制器)很相似,只不过这里把视图和控制器合在一起,作为一个概念. 基于这个框架,wxWidgets可以提供大量的用户界面控件和默认行为.你需要先定义自己的派生类以及它们之间的关系,框架本身则负责显示文件选择,打开和关闭文件,询问用户保存数据,将菜单项和对应的代码关联,甚至一些基本的打印和预览功能,还有就是重做/撤消功能的支持等.这个框架已经被高度的模块化了,允许你的应用程序通过重载和替换函数和对象的方式来更改这些默认的行为. 如果你觉得框架适合你即将制作的程序,你可以采用下面的步骤来使用这个框架.这些步骤的顺序并不是非常的严格的,你大可以先创建你的文档类,然后再考虑你的文档在应用程序中的表现形式. 1. 决定你要使用的用户界面: 微软的MDI (多文档界面,所有的子文档窗口被包含在一个父窗口内), SDI (单文档界面,每个文档一个单独的frame窗口), 或者是单一界面(同时只能打开一个文档,就象windows的写字板程序那样). 2. 基于前面的选择来使用对应的父窗口和子窗口类,比如wxDocParentFrame和wxDocChildFrame类. 在OnInit函数中创建一个父窗口的实例,对应于每个文档视图创建一个子窗口的实例(如果不是单文档界面的话).使用标准的菜单标识符创建菜单(比如 wxID_OPEN和wxID_PRINT). 3. 定义你自己的文档和视图类,重载尽可能少的成员函数用于输入和输出,绘画以及初始化.如果你需要重做/撤消的支持,你应该尽早实现它而不要等到程序快完成的时候再回来返工. 4. 定义任意的子窗口(比如一个滚动窗口)用来显示视图.你可能需要将它的一些事件传递给视图或者文档类处理,比如通常它的重绘事件都需要传递给wxView::OnDraw函数. 5. 在你的wxApp::OnInit函数的开始部分创建一个wxDocManager实例以及足够多的wxDocTemplate实例,以便定义文档和视图之间的关系.对于简单的应用程序来说,一个wxDocTemplate的实例就可以了. 我们将用一个简单的叫做Doodle(参见下图)的程序来演示上面的步骤.正如它的名字那样,它支持在一个窗口上任意乱画,并且支持将这些涂鸦保存在文件里或者从文件里读取.也支持简单的重做和撤消操作. ![](img/mhtCD41%281%29.tmp) 第一步:选择用户界面类型 传统上,windows平台的多文档程序都使用的是多文档界面,我们已经在第4章,"窗口基础"中的"wxMDIParentFrame"小节对此有过描述.多文档界面使用一个父frame窗口管理和包含多个文档子frame窗口,而其菜单条则用来反应当前活动窗口或者父窗口(如果没有当前活动窗口的话) 相关联的菜单命令. 或者你也可以选择使用一个主窗口,多个顶层的用于显示文档的frame窗口的方式,这种方式下文档窗口可以不受主窗口的限制,在桌面上任意移动.这通常是Mac OS采用的风格,不过在Mac OS上,每次只能显示一个菜单条(当前活动窗口的菜单条).Mac OS上另外一个和别的平台不同的地方在于,Mac 用户并不期望应用程序的所有的窗口被关闭以后退出应用程序.Mac系统有一个应用程序菜单条,上面显示了的应用程序所有的窗口都隐藏时候可以的少数的几个命令,在wxWidgets上,要实现这种行为,你需要创建一个不可见的frame窗口,它的菜单条将在所有其它可见窗口被释放以后自动显示在那个位置. 这种技术的另外一种用法是显示一个主窗口之外的非文档视图的frame窗口.不过,这种用法非常罕见,一般都不会这样使用.另外一种方法是仅显示文档窗口,不显示主窗口,仅在最后一个文档窗口被关闭的时候显示主窗口以用来创建或者打开新的文档窗口,这种模型被近期的Microsoft Word采用,这其实是一个和Mac OS很接近的作法,只不过在Mac OS上,在这种情况下,没有任何可见的窗口,只有一个菜单条. 也许最简单的模型是只有一个主窗口,没有独立的子窗口,每次也只能打开一个文档:微软的写字板就是这样的一个例子.这也是我们的Doodle例子所选择的形式. 最后,你当然也可以创建自己的模型,或者你可以采用上面这些模型的组合方式.比如DialogBlocks就是这样的一个例子,它组合了几种方式以便用户自己作出选择.DialogBlocks中最常用的是方式是每次只显示一个视图,,当你在工程树中选择了一个文档的时候,当前视图隐藏, 新的视图打开.你也可以打开多页面支持,以便快速的在你最感兴趣的几个文档之间切换.另外,你还可以通过拖拽标题栏的方式,将某个文档以单独的窗口拖动到桌面上,以便你可以同时看到几个文档的视图.在DialogBlocks程序内部,它自己管理视图和文档以及窗口之间的关系,采用的就是和标准的 wxWidgets不同的方式.很明显,创建这样的定制文档视图管理系统需要很多时间,因此,你可能更愿意选择wxWidgets提供的标准方式. 第二步: 创建和使用frame窗口类. 对于MDI界面应用程序来说,你应该使用wxDocMDIParentFrame和wxDocMDIChildFrame窗口类,而对于主窗口和文档窗口分离的模型来说,你可以选择使用wxDocParentFrame和wxDocChildFrame类.如果你使用的是单个主窗口每次打开一个文档这种模型,你可以只使用wxDocParentFrame类. 如果你的应用程序没有主窗口,只有多个文档窗口,你既可以使用wxDocParentFrame,也可以使用 wxDocChildFrame.不过,如果你使用的是wxDocParentFrame,你需要拦截EVT_CLOSE事件,以便只删除和这个窗口绑定的文档视图,因为这个窗口类默认的EVT_CLOSE事件处理函数将删除所有文档管理器知道的视图(这将导致关闭所有的文档). 下面列出了doodle例子的窗口类定义.其中保存了一个指向doodle画布的指针和一个指向编辑菜单的指针,以便文档视图系统可以视情况更新重做和撤消菜单. ``` // 定义一个新的frame窗口类. class DoodleFrame: public wxDocParentFrame { DECLARE_CLASS(DoodleFrame) DECLARE_EVENT_TABLE() public: DoodleFrame(wxDocManager *manager, wxFrame *frame, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long type); /// 显示关于对话框 void OnAbout(wxCommandEvent& event); /// 获得编辑菜单指针 wxMenu* GetEditMenu() const { return m_editMenu; } /// 获得画布指针 DoodleCanvas* GetCanvas() const { return m_canvas; } private: wxMenu * m_editMenu; DoodleCanvas* m_canvas; }; ``` 下面的代码演示了DoodleFrame的实现.其中构造函数创建了一个菜单条和一个DoodleCanvas对象,后者拥有一个铅笔状的鼠标指针.文件菜单被传递给文档视图模型的管理对象,以便其可以增加最近使用文件的显示. ``` IMPLEMENT_CLASS(DoodleFrame, wxDocParentFrame) BEGIN_EVENT_TABLE(DoodleFrame, wxDocParentFrame) EVT_MENU(DOCVIEW_ABOUT, DoodleFrame::OnAbout) END_EVENT_TABLE() DoodleFrame::DoodleFrame(wxDocManager *manager, wxFrame *parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long type): wxDocParentFrame(manager, parent, id, title, pos, size, type) { m_editMenu = NULL; m_canvas = new DoodleCanvas(this, wxDefaultPosition, wxDefaultSize, 0); m_canvas->SetCursor(wxCursor(wxCURSOR_PENCIL)); // 增加滚动条 m_canvas->SetScrollbars(20, 20, 50, 50); m_canvas->SetBackgroundColour(*wxWHITE); m_canvas->ClearBackground(); // 增加图标 SetIcon(wxIcon(doodle_xpm)); // 创建菜单 wxMenu *fileMenu = new wxMenu; wxMenu *editMenu = (wxMenu *) NULL; fileMenu->Append(wxID_NEW, wxT("&New...")); fileMenu->Append(wxID_OPEN, wxT("&Open...")); fileMenu->Append(wxID_CLOSE, wxT("&Close")); fileMenu->Append(wxID_SAVE, wxT("&Save")); fileMenu->Append(wxID_SAVEAS, wxT("Save &As...")); fileMenu->AppendSeparator(); fileMenu->Append(wxID_PRINT, wxT("&Print...")); fileMenu->Append(wxID_PRINT_SETUP, wxT("Print &Setup...")); fileMenu->Append(wxID_PREVIEW, wxT("Print Pre&view")); editMenu = new wxMenu; editMenu->Append(wxID_UNDO, wxT("&Undo")); editMenu->Append(wxID_REDO, wxT("&Redo")); editMenu->AppendSeparator(); editMenu->Append(DOCVIEW_CUT, wxT("&Cut last segment")); m_editMenu = editMenu; fileMenu->AppendSeparator(); fileMenu->Append(wxID_EXIT, wxT("E&xit")); wxMenu *helpMenu = new wxMenu; helpMenu->Append(DOCVIEW_ABOUT, wxT("&About")); wxMenuBar *menuBar = new wxMenuBar; menuBar->Append(fileMenu, wxT("&File")); menuBar->Append(editMenu, wxT("&Edit")); menuBar->Append(helpMenu, wxT("&Help")); // 指定菜单条 SetMenuBar(menuBar); // 历史文件访问记录的显示将使用这个菜单. manager->FileHistoryUseMenu(fileMenu); } void DoodleFrame::OnAbout(wxCommandEvent& WXUNUSED(event) ) { (void)wxMessageBox(wxT("Doodle Sample\n(c) 2004, Julian Smart"), wxT("About Doodle")); } ``` 第三步: 定义你的文档和视图类 你的文档类应该有一个默认的构造函数,而且应该使用DECLARE_DYNAMIC_CLASS和IMPLEMENT_DYNAMIC_CLASS宏来使其提供RTTI并且支持动态创建(否则你就需要重载wxDocTemplate::CreateDocument函数,以实现的文档实例创建函数). 你还需要告诉文档视图框架怎样保存和读取你的文档对象,如果你想直接使用wxWidgets流操作,你可以重载SaveObject和 LoadObject函数,就象我们例子中的作法一样.或者你可以直接重载DoSaveDocument函数和DoOpenDocument函数,这两个函数的参数为文件名而不是流对象.wxWidget流操作相关内容我们已经在第14章,"文件和流操作"中介绍过. 注意:框架本身在保存数据的时候不使用临时文件系统.这也是为什么我们有时候需要重载DoSaveDocument函数的一个理由,我们可以通过流操作将文档保存在wxTempFile中,正如我们在第14章中介绍的那样. 下面是我们的DoodleDocument类的声明部分: ``` /* * 代表一个Doodle文档 */ class DoodleDocument: public wxDocument { DECLARE_DYNAMIC_CLASS(DoodleDocument) public: DoodleDocument() {}; ~DoodleDocument(); /// 保存文档 wxOutputStream& SaveObject(wxOutputStream& stream); /// 读取文档 wxInputStream& LoadObject(wxInputStream& stream); inline wxList& GetDoodleSegments() { return m_doodleSegments; }; private: wxList m_doodleSegments; }; ``` 你的文档类也许要包含文档内容对应的数据.在我们的例子中,我们的数据就是一个doodle片断的列表,每一个数据片断代表从鼠标按下到鼠标释放过程中鼠标划过的所有的线段.这些片断所属的类知道怎样将自己保存在流中,这使得我们实现文档保存和读取的流操作变的相对容易.下面是用来代表这些线段片断的类的声明: ``` /* * 定义了一个两点之间的线段 */ class DoodleLine: public wxObject { public: DoodleLine(wxInt32 x1 = 0, wxInt32 y1 = 0, wxInt32 x2 = 0, wxInt32 y2 = 0) { m_x1 = x1; m_y1 = y1; m_x2 = x2; m_y2 = y2; } wxInt32 m_x1; wxInt32 m_y1; wxInt32 m_x2; wxInt32 m_y2; }; /* * 包含一个线段的列表,用来代表一次鼠标绘画操作 */ class DoodleSegment: public wxObject { public: DoodleSegment(){}; DoodleSegment(DoodleSegment& seg); ~DoodleSegment(); void Draw(wxDC *dc); /// 保存一个片断 wxOutputStream& SaveObject(wxOutputStream& stream); /// 读取一个片断 wxInputStream& LoadObject(wxInputStream& stream); /// 获取片断中的线段列表 wxList& GetLines() { return m_lines; } private: wxList m_lines; }; ``` DoodleSegment类知道怎么在某个设备上下文上绘制自己,这有助于我们实现我们的doodle绘制代码. 下面的代码是这些类的实现部分: ``` /* * DoodleDocument */ IMPLEMENT_DYNAMIC_CLASS(DoodleDocument, wxDocument) DoodleDocument::~DoodleDocument() { WX_CLEAR_LIST(wxList, m_doodleSegments); } wxOutputStream& DoodleDocument::SaveObject(wxOutputStream& stream) { wxDocument::SaveObject(stream); wxTextOutputStream textStream( stream ); wxInt32 n = m_doodleSegments.GetCount(); textStream << n << wxT('\n'); wxList::compatibility_iterator node = m_doodleSegments.GetFirst(); while (node) { DoodleSegment *segment = (DoodleSegment *)node->GetData(); segment->SaveObject(stream); textStream << wxT('\n'); node = node->GetNext(); } return stream; } wxInputStream& DoodleDocument::LoadObject(wxInputStream& stream) { wxDocument::LoadObject(stream); wxTextInputStream textStream( stream ); wxInt32 n = 0; textStream >> n; for (int i = 0; i < n; i++) { DoodleSegment *segment = new DoodleSegment; segment->LoadObject(stream); m_doodleSegments.Append(segment); } return stream; } /* * DoodleSegment */ DoodleSegment::DoodleSegment(DoodleSegment& seg) { wxList::compatibility_iterator node = seg.GetLines().GetFirst(); while (node) { DoodleLine *line = (DoodleLine *)node->GetData(); DoodleLine *newLine = new DoodleLine(line->m_x1, line->m_y1, line->m_x2, line->m_y2); GetLines().Append(newLine); node = node->GetNext(); } } DoodleSegment::~DoodleSegment() { WX_CLEAR_LIST(wxList, m_lines); } wxOutputStream &DoodleSegment::SaveObject(wxOutputStream& stream) { wxTextOutputStream textStream( stream ); wxInt32 n = GetLines().GetCount(); textStream << n << wxT('\n'); wxList::compatibility_iterator node = GetLines().GetFirst(); while (node) { DoodleLine *line = (DoodleLine *)node->GetData(); textStream << line->m_x1 << wxT(" ") << line->m_y1 << wxT(" ") << line->m_x2 << wxT(" ") << line->m_y2 << wxT("\n"); node = node->GetNext(); } return stream; } wxInputStream &DoodleSegment::LoadObject(wxInputStream& stream) { wxTextInputStream textStream( stream ); wxInt32 n = 0; textStream >> n; for (int i = 0; i < n; i++) { DoodleLine *line = new DoodleLine; textStream >> line->m_x1 >> line->m_y1 >> line->m_x2 >> line->m_y2; GetLines().Append(line); } return stream; } void DoodleSegment::Draw(wxDC *dc) { wxList::compatibility_iterator node = GetLines().GetFirst(); while (node) { DoodleLine *line = (DoodleLine *)node->GetData(); dc->DrawLine(line->m_x1, line->m_y1, line->m_x2, line->m_y2); node = node->GetNext(); } } ``` 到目前为止,我们还没有介绍怎样将doodle片断增加到我们的文档中,除了从文件读取以外.我们需要将那些用来响应鼠标和键盘操作,以更改文档内容的命令代码模型化,这是实现重做/撤消操作的关键.DoodleCommand是一个继承自wxCommand的类,它实现了虚函数Do和Undo,这些函数将被框架在合适的时候调用.因此,我们将不会直接更改文档内容,取而代之的是在相应的事件处理函数中,创建一个一个的DoodleCommand对象,并将这些对象提交给文档命令处理器(一个wxCommandProcessor类的实例)处理.文档命令处理器在执行这些命令前会自动将这些命令保存在一个重做/撤消堆栈中.文档命令处理器对象是在文档被初始化的时候被框架自动创建的,因此在这个例子中你看不到显式创建这个对象的代码. 下面是DoodleCommand类的声明: ``` /* * 一个doodle命令 */ class DoodleCommand: public wxCommand { public: DoodleCommand(const wxString& name, int cmd, DoodleDocument *doc, DoodleSegment *seg); ~DoodleCommand(); /// Overrides virtual bool Do(); virtual bool Undo(); /// 重做和撤消的命令是对称的,因此将它们组合在一起. bool DoOrUndo(int cmd); protected: DoodleSegment* m_segment; DoodleDocument* m_doc; int m_cmd; }; /* * Doodle命令标识符 */ #define DOODLE_CUT 1 #define DOODLE_ADD 2 ``` 我们定义了两种类型的命令: DOODLE_ADD和DOODLE_CUT.用户可以删除最后一次的绘画操作或者增加新的绘画操作.这里我们的两个命令都使用同一个类,不过这不是必须的.每一个命令对象都会保存一个文档指针,一个DoodleSegment(代表一次绘画操作)指针和一个命令标识符.下面是DoodleCommand 类的实现部分: ``` /* * DoodleCommand */ DoodleCommand::DoodleCommand(const wxString& name, int command, DoodleDocument *doc, DoodleSegment *seg): wxCommand(true, name) { m_doc = doc; m_segment = seg; m_cmd = command; } DoodleCommand::~DoodleCommand() { if (m_segment) delete m_segment; } bool DoodleCommand::Do() { return DoOrUndo(m_cmd); } bool DoodleCommand::Undo() { switch (m_cmd) { case DOODLE_ADD: { return DoOrUndo(DOODLE_CUT); } case DOODLE_CUT: { return DoOrUndo(DOODLE_ADD); } } return true; } bool DoodleCommand::DoOrUndo(int cmd) { switch (cmd) { case DOODLE_ADD: { wxASSERT( m_segment != NULL ); if (m_segment) m_doc->GetDoodleSegments().Append(m_segment); m_segment = NULL; m_doc->Modify(true); m_doc->UpdateAllViews(); break; } case DOODLE_CUT: { wxASSERT( m_segment == NULL ); // Cut the last segment if (m_doc->GetDoodleSegments().GetCount() > 0) { wxList::compatibility_iterator node = m_doc->GetDoodleSegments().GetLast(); m_segment = (DoodleSegment *)node->GetData(); m_doc->GetDoodleSegments().Erase(node); m_doc->Modify(true); m_doc->UpdateAllViews(); } break; } } return true; } ``` 因为在我们的例子中Do和Undo操作使用共用的代码,我们直接使用一个DoOrUndo函数来实现所有的操作.如果我们被要求执行 DOODLE_ADD的撤消操作,我们可以直接执行DOODLE_CUT,而要执行DOODLE_CUT的撤消操作,我们则直接执行 DOODLE_ADD. 当增加一个绘画片断(或者对某个Cut命令执行撤消操作)时,DoOrUndo函数所做的事情就是把这个绘画片断增加到文档的绘画片断列表,并且将自己内部的绘画片断的指针清除,以便在释放这个命令对象的时候不需要释放这个绘画片断对象.相应的,当执行Cut操作(或者Add的Undo 操作)的时候,将文档的片断列表中的最后一个片断从列表中移除,并且保存其指针,以便用于相应的恢复操作.DoOrUndo函数做的另外一件事情是将文档标记为已修改状态(以便在应用程序退出时提醒用户保存文档)以及告诉文档需要更新和自己相关的所有的视图. 要定义自己的视图类,你需要实现wxView的派生类,同样的,需要使用动态创建的宏,并至少重载OnCreate,OnDraw,OnUpdate和OnClose函数. OnCreate函数在视图和文档对象刚被创建的时候调用,你应该执行的动作包括:创建frame窗口,使用SetFrame函数将其和当前视图绑定. OnDraw函数的参数为一个wxDC指针,用来实现窗口绘制操作. 实际上,这一步不是必须的,但是一旦你不使用重载OnDraw函数的方法来实现窗口绘制,默认的打印/预览机制将不能正常工作. OnUpdate函数的参数是一个指向导致这次更新操作的视图的指针以及一个指向一个用来帮助优化视图更新操作的对象的指针.这个函数在视图需要被更新的时候调用,这通常意味着由于执行某个文档命令导致相关视图需要更新,或者应用程序显式调用了wxDocument:: UpdateAllViews函数. OnClose函数在视图需要被关闭的时候调用,默认的实现是调用wxDocument::OnClose函数关闭视图绑定的文档. 下面是DoodleView的类声明.我们重载了前面介绍的四个函数,并且增加了一个DOODLE_CUT命令的处理函数.为什么这里没有DOODLE_ADD命令的处理函数,是因为绘画片断是随着鼠标的操作而增加的,因此DOODLE_ADD命令对应的视图动作已经在 DoodleCanvas对象的鼠标处理函数中实现了.我们很快就会看到. ``` /* * DoodleView是文档和窗口之间的桥梁. */ class DoodleView: public wxView { DECLARE_DYNAMIC_CLASS(DoodleView) DECLARE_EVENT_TABLE() public: DoodleView() { m_frame = NULL; } ~DoodleView() {}; /// 当文档被创建的时候调用 virtual bool OnCreate(wxDocument *doc, long flags); /// 当需要绘制文档的时候被调用 virtual void OnDraw(wxDC *dc); /// 当文档需要更新的时候被调用 virtual void OnUpdate(wxView *sender, wxObject *hint = NULL); /// 当视图被关闭的时候调用 virtual bool OnClose(bool deleteWindow = true); /// 用于处理Cut命令 void OnCut(wxCommandEvent& event); private: DoodleFrame* m_frame; }; ``` 下面的代码是其实现部分: ``` IMPLEMENT_DYNAMIC_CLASS(DoodleView, wxView) BEGIN_EVENT_TABLE(DoodleView, wxView) EVT_MENU(DOODLE_CUT, DoodleView::OnCut) END_EVENT_TABLE() // 当视图被创建的时候需要做的动作 bool DoodleView::OnCreate(wxDocument *doc, long WXUNUSED(flags)) { // 将当前主窗口和视图绑定 m_frame = GetMainFrame(); SetFrame(m_frame); m_frame->GetCanvas()->SetView(this); // 让视图管理器感知当前视图 Activate(true); // 初始化编辑菜单中的重做/撤消项目 doc->GetCommandProcessor()->SetEditMenu(m_frame->GetEditMenu()); doc->GetCommandProcessor()->Initialize(); return true; } // 这个函数被默认的打印/打印预览以及窗口绘制函数共用 void DoodleView::OnDraw(wxDC *dc) { dc->SetFont(*wxNORMAL_FONT); dc->SetPen(*wxBLACK_PEN); wxList::compatibility_iterator node = ((DoodleDocument *)GetDocument ())->GetDoodleSegments().GetFirst(); while (node) { DoodleSegment *seg = (DoodleSegment *)node->GetData(); seg->Draw(dc); node = node->GetNext(); } } void DoodleView::OnUpdate(wxView *WXUNUSED(sender), wxObject *WXUNUSED(hint)) { if (m_frame && m_frame->GetCanvas()) m_frame->GetCanvas()->Refresh(); } // 清除用于显式这个视图的窗口 bool DoodleView::OnClose(bool WXUNUSED(deleteWindow)) { if (!GetDocument()->Close()) return false; // 清除画布 m_frame->GetCanvas()->ClearBackground(); m_frame->GetCanvas()->SetView(NULL); if (m_frame) m_frame->SetTitle(wxTheApp->GetAppName()); SetFrame(NULL); // 告诉文档管理器不要再给我发送任何事件了. Activate(false); return true; } void DoodleView::OnCut(wxCommandEvent& WXUNUSED(event)) { DoodleDocument *doc = (DoodleDocument *)GetDocument(); doc->GetCommandProcessor()->Submit( new DoodleCommand(wxT("Cut Last Segment"), DOODLE_CUT, doc, NULL)); } ``` 第4步: 定义你的窗口类 通常你需要创建特定的编辑窗口来维护你视图中的数据.在我们的例子中,DoodleCanvas用来显示对应的数据,和相关的事件交互等,wxWidgets的事件处理机制也要求我们最好创建一个新的派生类.DoodleCanvas类的声明如下: ``` /* * DoodleCanvas是用来显示文档的窗口类 */ class DoodleView; class DoodleCanvas: public wxScrolledWindow { DECLARE_EVENT_TABLE() public: DoodleCanvas(wxWindow *parent, const wxPoint& pos, const wxSize& size, const long style); /// 绘制文档内容 virtual void OnDraw(wxDC& dc); /// 处理鼠标事件 void OnMouseEvent(wxMouseEvent& event); /// 设置和获取视图对象 void SetView(DoodleView* view) { m_view = view; } DoodleView* GetView() const { return m_view; } protected: DoodleView *m_view; }; ``` DoodleCanvas包含一个指向对应视图对象的指针(通过DoodleView::OnCreate函数初始化),以便在绘画和鼠标事件处理函数中使用.下面是这个类的实现部分: ``` /* * Doodle画布的实现 */ BEGIN_EVENT_TABLE(DoodleCanvas, wxScrolledWindow) EVT_MOUSE_EVENTS(DoodleCanvas::OnMouseEvent) END_EVENT_TABLE() // 构造函数部分 DoodleCanvas::DoodleCanvas(wxWindow *parent, const wxPoint& pos, const wxSize& size, const long style): wxScrolledWindow(parent, wxID_ANY, pos, size, style) { m_view = NULL; } // 定制重绘行为 void DoodleCanvas::OnDraw(wxDC& dc) { if (m_view) m_view->OnDraw(& dc); } // 这个函数实现了主要的涂鸦操作,主要用了鼠标左键事件. void DoodleCanvas::OnMouseEvent(wxMouseEvent& event) { // 上一次的位置 static int xpos = -1; static int ypos = -1; static DoodleSegment *currentSegment = NULL; if (!m_view) return; wxClientDC dc(this); DoPrepareDC(dc); dc.SetPen(*wxBLACK_PEN); // 将滚动位置计算在内 wxPoint pt(event.GetLogicalPosition(dc)); if (currentSegment && event.LeftUp()) { if (currentSegment->GetLines().GetCount() == 0) { delete currentSegment; currentSegment = NULL; } else { // 当鼠标左键释放的时候我们获得一个绘画片断,因此需要增加这个片断 DoodleDocument *doc = (DoodleDocument *) GetView()->GetDocument(); doc->GetCommandProcessor()->Submit( new DoodleCommand(wxT("Add Segment"), DOODLE_ADD, doc, currentSegment)); GetView()->GetDocument()->Modify(true); currentSegment = NULL; } } if (xpos > -1 && ypos > -1 && event.Dragging()) { if (!currentSegment) currentSegment = new DoodleSegment; DoodleLine *newLine = new DoodleLine; newLine->m_x1 = xpos; newLine->m_y1 = ypos; newLine->m_x2 = pt.x; newLine->m_y2 = pt.y; currentSegment->GetLines().Append(newLine); dc.DrawLine(xpos, ypos, pt.x, pt.y); } xpos = pt.x; ypos = pt.y; } ``` 正如你看到的那样,当鼠标处理函数检测到一个新的绘画片断被创建的时候,它提交给对应的文档对象一个DOODLE_ADD命令,这个命令将被保存以便支持撤消(以及将来的重做)动作.在我们的例子中,它被保存在文档的绘画片断列表中. 第5步,使用wxDocManager和wxDocTemplate 你需要在应用程序的整个生命周期内维持一个wxDocManager实例,这个实例负责整个文档视图框架的协调工作. 你也需要至少一个wxDocTemplate对象.这个对象用来实现文档视图模型中文档和视图相关联的那部分工作.每一个文档/视图对, 对应一个wxDocTemplate对象,wxDocManager对象将管理一个wxDocTemplate对象的列表以便用来创建文档和视图. wxDocTemplate对象知道怎样的文件扩展名对应目前的文档对象以及怎样创建相应的文档或者视图对象等. 举例来说,如果我们的Doodle文档支持两种视图:图形视图和绘图片断列表视图,那么我们就需要创建两种视图对象 (DoodleGraphicView和DoodleListView),相应的我们也需要创建两种文档模板对象,一个用于图形视图,一个用于列表视图. 你可以给这两个wxDocTemplate使用同样的文档类和同样的文件扩展名,但是传递不同的视图类型.当用户点击应用程序的打开菜单时,文件选择对话框将额外显示一组可用的文件过滤器,每一个过滤器对应一个wxDocTemplate类型,当某个文件被选中打开的时候,wxDocManager将使用对应的wxDocTemplate创建相应的文档类和视图类.同样的逻辑也被应用于创建新对象的时候.当然,在我们的例子中,只有一种 wxDocManager对象,因此打开和新建文档的对话框就显得简单一些了. 你可以在你的应用程序种存储一个wxDocManager指针,但是对于wxDocTemplate通常没有这个必要,因为后者是被wxDocManager管理和维护的.下面是我们的DoodleApp类的定义部分: ``` /* *声明一个应用程序类 */ class DoodleApp: public wxApp { public: DoodleApp(); virtual bool OnInit(); virtual int OnExit(); private: wxDocManager* m_docManager; }; DECLARE_APP(DoodleApp) ``` 在DoodleApp的实现部分,我们在OnInit函数种创建wxDocManager对象和一个和我们的 DoodleDocument和DoodleView绑定的wxDocTemplate对象.我们给wxDocTemplate传递的参数包括: wxDocManager对象,描述字符串,文件过滤器(在文件对话框种使用),默认打开目录(在文件对话框种使用),默认的文件扩展名(.drw,用来区分我们的文件类型)以及我们的文档和视图类型以及对应的类型信息.DoodleApp的实现部分如下所示: ``` IMPLEMENT_APP(DoodleApp) DoodleApp::DoodleApp() { m_docManager = NULL; } bool DoodleApp::OnInit() { // 创建一个wxDocManager m_docManager = new wxDocManager; // 创建我们需要的wxDocTemplate (void) new wxDocTemplate(m_docManager, wxT("Doodle"), wxT("*.drw"), wxT(""), wxT ("drw"), wxT("Doodle Doc"), wxT("Doodle View"), CLASSINFO(DoodleDocument), CLASSINFO(DoodleView)); // 在Mac系统上登记文档类型 #ifdef __WXMAC__ wxFileName::MacRegisterDefaultTypeAndCreator( wxT("drw") , 'WXMB' , 'WXMA' ) ; #endif // 对于我们的单文档界面,我们只支持最多同时打开一个文档 m_docManager->SetMaxDocsOpen(1); // 创建主窗口 DoodleFrame* frame = new DoodleFrame(m_docManager, NULL, wxID_ANY, wxT("Doodle Sample"), wxPoint(0, 0), wxSize(500, 400), wxDEFAULT_FRAME_STYLE); frame->Centre(wxBOTH); frame->Show(true); SetTopWindow(frame); return true; } int DoodleApp::OnExit() { delete m_docManager; return 0; } ``` 因为我们只支持同时显示一个文档,我们需要通过函数SetMaxDocsOpen告诉文档管理器这一点.为了在Mac OS上提供一些额外的系统特性,我们也通过MacRegisterDefaultTypeAndCreator函数在Mac系统中注册了我们的文件类型. 这个函数的参数为文件扩展名,文档类型标识符以及创建标识符(按照惯例通常采用四个字节的字符串来标识,你也可以在苹果公司的网站上注册这种类型以避免可能存在的冲突). Doodle例子完整的代码请参考附带光盘的examples/chap19/doodle目录.