wxWidgets源碼分析(8) - MVC架構

MVC架構

wxDocManager文檔管理器

wxWidgets使用wxDocManager類來管理MVC中的文檔和視圖的對應關係,使用方法:canvas

  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進行的,咱們接下來跟蹤一下建立新文檔的流程,用戶代碼以下:mvc

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運行用戶的代碼,這個是在建立過程當中的惟一機會。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的類型是在模板對象中指定的,天然它知道怎麼建立。

  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();
}
相關文章
相關標籤/搜索