# 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(參見下圖)的程序來演示上面的步驟.正如它的名字那樣,它支持在一個窗口上任意亂畫,并且支持將這些涂鴉保存在文件里或者從文件里讀取.也支持簡單的重做和撤消操作.

第一步:選擇用戶界面類型
傳統上,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目錄.
- 第一章 介紹
- 1.1 為什么要使用wxWidgets?
- 1.2 wxWidgets的歷史
- 1.3 wxWidgets社區
- 1.4 wxWidgets和面向對象編程
- 1.5 wxWidgets的體系結構
- 1.6 許可協議
- 第一章小結
- 第二章 開始使用
- 2.1 一個小例子
- 2.2 應用程序類
- 2.3 Frame窗口類
- 2.4 事件處理函數
- 2.5 Frame窗口的構造函數
- 2.6 完整的例子
- 2.7 wxWidgets程序一般執行過程
- 2.8 編譯和運行程序
- 第二章小結
- 第三章 事件處理
- 3.1 事件驅動編程
- 3.2 事件表和事件處理過程
- 3.3 過濾某個事件
- 3.4 掛載事件表
- 3.5 動態事件處理方法
- 3.6 窗口標識符
- 3.7 自定義事件
- 第三章小結
- 第四章 窗口的基礎知識
- 4.1 窗口解析
- 4.2 窗口類概覽
- 4.3 基礎窗口類
- 4.4 頂層窗口
- 4.5 容器窗口
- 4.6 非靜態控件
- 4.7 靜態控件
- 4.8 菜單
- 4.9 控制條
- 第四章小結
- 第五章繪畫和打印
- 5.1 理解設備上下文
- 5.2 繪畫工具
- 5.3 設備上下文中的繪畫函數
- 5.4 使用打印框架
- 5.5 使用wxGLCanvas繪制三維圖形
- 第五章小節
- 第六章處理用戶輸入
- 6.1 鼠標輸入
- 6.2 處理鍵盤事件
- 6.3 處理游戲手柄事件
- 第六章小結
- 第七章使用布局控件進行窗口布局
- 7.1 窗口布局基礎
- 7.2 窗口布局控件
- 7.3 使用布局控件進行編程
- 7.4 更多關于布局的話題
- 第七章小結
- 第八章使用標準對話框
- 8.1信息對話框
- 8.2 文件和目錄對話框
- 8.3 選擇和選項對話框
- 8.4 輸入對話框
- 8.5 打印對話框
- 第八章小結
- 第九章創建定制的對話框
- 9.1 創建定制對話框的步驟
- 9.2 一個例子:PersonalRecordDialog
- 9.3 在小型設備上調整你的對話框
- 9.4 一些更深入的話題
- 9.5 使用wxWidgets資源文件
- 第九章小結
- 第十章使用圖像編程
- 10.1 wxWidgets中圖片相關的類
- 10.2 使用wxBitmap編程
- 10.3 使用wxIcon編程
- 10.4 使用wxCursor編程
- 10.5 使用wxImage編程
- 10.6 圖片列表和圖標集
- 10.7 自定義wxWidgets提供的小圖片
- 第十章小結
- 第十一章剪貼板和拖放操作
- 11.1 數據對象
- 11.2 使用剪貼板
- 11.3 實現拖放操作
- 第十一章小結
- 第十二章高級窗口控件
- 12.1 wxTreeCtrl
- 12.2 wxListCtrl
- 12.3 wxWizard
- 12.4 wxHtmlWindow
- 12.5 wxGrid
- 12.6 wxTaskBarIcon
- 12.7 編寫自定義的控件
- 第十二章小結
- 第十三章數據結構類
- 13.1 為什么沒有使用STL?
- 13.2 字符串類型
- 13.3 wxArray
- 13.4 wxList和wxNode
- 13.5 wxHashMap
- 13.6 存儲和使用日期和時間
- 13.7 其它常用的數據類型
- 第十三章小結
- 第十四章文件和流操作
- 14.1 文件類和函數
- 14.2 流操作相關類
- 第十四章小結
- 第十五章內存管理,調試和錯誤處理
- 15.1 內存管理基礎
- 15.2 檢測內存泄漏和其它錯誤
- 15.3 構建自防御的程序
- 15.4 錯誤報告
- 15.5 提供運行期類型信息
- 15.6 使用wxModule
- 15.7 加載動態鏈接庫
- 15.8 異常處理
- 15.9 調試提示
- 第十五章小結
- 第十六章編寫國際化程序
- 16.1 國際化介紹
- 16.2 從翻譯說起
- 16.3 字符編碼和Unicode
- 16.4 數字和日期
- 16.5 其它媒介
- 16.6 一個小例子
- 第十六章小結
- 第十七章編寫多線程程序
- 17.1 什么時候使用多線程,什么時候不要使用
- 17.2 使用wxThread
- 17.3 用于線程同步的對象
- 17.4 多線程的替代方案
- 第十七章小結
- 第十八章使用wxSocket編程
- 18.1 Socket類和功能概覽
- 18.2 Socket及其基本處理介紹
- 18.3 Socket標記
- 18.4 使用Socket流
- 18.5 替代wxSocket
- 第十八章小結
- 第十九章使用文檔/視圖框架
- 19.1 文檔/視圖基礎
- 19.2 文檔/視圖框架的其它能力
- 19.3 實現Undo/Redo的策略
- 第十九章小結
- 第二十章完善你的應用程序
- 20.1 單個實例和多個實例
- 20.2 更改事件處理機制
- 20.3 降低閃爍
- 20.4 實現聯機幫助
- 20.5 解析命令行參數
- 20.6 存儲應用程序資源
- 20.7 調用別的應用程序
- 20.8 管理應用程序設置
- 20.9 應用程序安裝
- 20.10 遵循用戶界面設計規范
- 20.11 全書小結