65.9K
CodeProject 正在变化。 阅读更多。
Home

SBJ MVC 框架 - 设计视图,响应模型变更

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (20投票s)

2009年3月19日

CPOL

13分钟阅读

viewsIcon

86932

downloadIcon

2439

与 MFC 文档/视图架构集成的模型-视图-控制器框架。

SbjDevSrc202

目录

引言

这是我发布的第三篇介绍 MVC 框架的文章。第一篇 MFC 的模型-视图-控制器实现介绍,介绍了将框架与 MFC 文档/视图架构集成的底层类。第二篇 SBJ MVC 框架 - 从抽象到实现的模型,详细介绍了模型以及它如何与 MFC CDocument 类关联以提供对模型数据源的抽象接口。在阅读本文之前,您需要熟悉前两篇文章中介绍的概念。

在这些文章中,以及随附的文章中,都有一个 AppWizard 创建的 MFC 应用程序Shapes.exe,用于说明框架集成。对于其模型Shapes 读取一个 XML 文件,该文件包含定义一系列图形矩形和椭圆对象的元素。正如您在文章开头的图形中看到的,Shapes 应用程序呈现了模型的三种不同视图。

  • 设计视图 - 应用程序的主 CView,显示形状的图形形式
  • 资源管理器视图 - 包含在 CDockingPane 中的 CTreeCtrl,按名称列出形状
  • 属性视图 - 包含在 CDockingPane 中的 CMFCPropertyGridCtrl,列出当前形状的属性

我最初计划在这篇文章中讨论所有三个视图;然而,当文章篇幅超过 50 页,甚至我自己也无法忍受阅读时,我决定将其拆分为至少两篇,甚至三篇独立文章。

在本文中,我计划详细介绍第一个视图,即设计视图,以及它如何由模型更改驱动,以及框架如何将其大部分功能通用化,以便在应用程序级别只需少量额外编码即可利用。

源代码

本文提供的源代码包含在一个 VS2008 解决方案SbjDev中,其中包含三个项目。这三个项目与第二篇文章中介绍的相同,只有一些小的错误修复。

  • SbjCore - SbjCore2090.dll - 包含 MVC 框架的基础 DLL。
  • XmlMvc - XmlMvc2090.dll - 包含具体 XML 模型实现的 DLL。
  • Shapes - Shapes.exe - 示例 EXE。

注意:请参阅文章末尾的附录,其中概述了源文件通用说明,了解有关编码风格的信息。

模型再探

模型架构中还有最后一个部分,我一直等到现在才介绍,因为设计视图和资源管理器视图提供了其用法的示例。在 SbjCore::Mvc::Model 命名空间中,您会发现两个类,Model::ActionModel::ActionHandler

Model::Action 类提供了对模型的通用递归遍历。在遍历过程中,如果ModelItem具有与其Type关联的 Model::ActionHandler 类,则会创建一个处理程序实例并调用它来执行其实现的任何操作。

由于遍历的递归性质,处理程序实现了两个处理ModelItem的方法。第一个 Model::ActionHandler::BeginHandling 在首次遇到ModelItem时调用。第二个 Model::ActionHandler::EndHandling 在处理完ModelItem的子项后调用。通过这种方式,Model::ActionHandler 可以在处理子ModelItem时维护状态。虽然本文讨论的设计视图不需要利用此功能,但在下一篇文章中,当您看到资源管理器视图如何处理插入到其 CTreeCtrl 中时,它的价值就会显现出来。

类 Model::Action
// Project Location: SbjCore/Mvc/Model/ModelAction.h

class AFX_EXT_API Action
{
public:
  typedef enum
  {
    kStop     = (-1),
    kRejected = ( 0),
    kContinue = ( 1)
  } Results;

  typedef std::map<CString, CRuntimeClass*> HandlerMap;

public:
  Action(const Controller* pCtrlr, HandlerMap* pMap = NULL, 
         CRuntimeClass* pDefaultHandler = NULL);
  virtual ~Action();

  void SetDefaultHandler(CRuntimeClass* pDefault);

  int Apply(HANDLE hItem);
  
  const Controller* GetModelController();

private:
  struct ActionImpl* const m_pImpl;
};

typedef enum Results 声明了从 Model::ActionHandler 处理方法返回的值。kStopkContinue 的意图应该不言自明。kReject 可以返回以短路模型分支的遍历。

Model::Action 类是为给定的 Model::Controller 构建的。还可以提供两个可选参数;第一个是 HandleMap,第二个是指向要用于未特别映射的任何ModelItemTypes的默认 CRuntimeClass 指针。

如前所述,Model::Action 类在遍历过程中根据需要创建 Model::ActionHandler 对象。HandlerMapModelItemTypesModel::ActionHandler 派生类的 CRuntimeClass 对象的映射。如果提供了没有默认值的 HandlerMap,则未映射处理程序的ModelItemTypes将分配一个基础 Model::ActionHandler 对象,该对象仅返回 kContinue。如果没有提供 HandleMap,则显然必须提供一个默认值,因为基础 Model::ActionHandler 对象不执行任何操作。SetDefaultHandler 方法可用于在遍历过程中的任何时间切换默认值。

Apply 方法是实际的遍历,可以传递任何ModelItem HANDLE 作为起点。

类 Model::ActionHandler
// Project Location: SbjCore/Mvc/Model/ModelActionHandler.h

class ActionHandler : public CCmdTarget
{
  DECLARE_DYNCREATE(ActionHandler)
public:
  virtual ~ActionHandler();

  void Initialize(Action* pAction, HANDLE hItem);

  Action* GetAction() const;

  HANDLE GetItem() const;

  int BeginHandling();
  int EndHandling();

protected:
  ActionHandler();

private:
  virtual int OnBeginHandling();
  virtual int OnEndHandling();

private:
  struct ActionHandlerImpl* const m_pImpl;
};

Model::ActionHandler 应该相当容易理解。Model::Action::Apply 方法使用它自身作为 Action 参数以及当前的ModelItem HANDLE 来调用处理程序的 Initialize 方法。通过提供对 Action 的访问,ActionHandler 可以在处理ModelItem HANDLE 时检索 Action 维护的任何状态信息。派生类重写两个 virtual 方法 OnBeginHandlingOnEndHandling 来实现所需的操作。

设计视图

首先,框架中没有实际的 DesignView 类;而是一个 DesignView 命名空间。应用程序特定的 CView 或其派生类通过 DesignView::Controller 类和相关组件获得其功能。Shapes 应用程序中设计视图的实现是其 ShapesView 类,它派生自 ControlledView

// Project Location: SbjCore/Mvc/Views/ControlledViews.h
typedef ControlledWndT<CView> ControlledView;

在向您展示 ShapesView 类及其如何利用框架之前,我将解释 DesignView 组件的工作原理。

类 DesignView::Item

通过 DesignView 命名空间中的组件,视图绘制应用程序特定ModelItem的图形表示。框架中ModelItem的图形表示由 DesignView::Item 类定义。应用程序将为要显示的每个ModelItemType定义一个 Item 派生类。应用程序特定的 Item 类唯一需要提供的是其 OnDraw 方法的实现(默认实现为空)。

// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewItem.h

class AFX_EXT_CLASS Item : public CObject
{
  DECLARE_DYNCREATE(Item)
public:
  virtual ~Item();

  void SetController(Controller* pCtrlr);
  Controller* GetController() const;
  
  bool IsTrackable() const;

  RectTracker* GetRectTracker() const;

  bool IsTrackedItem(const RectTracker* p);

  void SetModelItemHandle(const HANDLE hModelItem);
  HANDLE GetModelItemHandle() const;

  void Draw(CDC* pDC);

protected:
  Item();

private:
  virtual void OnDraw(CDC* pDC);
  virtual bool OnIsTrackable() const;
  virtual SbjCore::Mvc::RectTracker* OnGetRectTracker() const;

private:
  struct ItemImpl* const m_pImpl;
};

每个 DesignView::Item 都有一个关联的 RectTracker 对象(派生自 MFC CRectTracker 类,我相信您很熟悉),它定义了在 ControlledView 设备上下文Item 绘图应受限制的矩形。只有当其关联的 Item 被选中时,RectTracker 对象本身才会被显示。这在本文开头Shapes应用程序的图形中得到了说明,其中名为“Second Rectangle”的形状被选中。

CRectTracker 类似,它与用户交互,提供 Item 的选择、移动和调整大小功能。目前,RectTracker 绑定到其 Item 对象关联的ModelItem HANDLE,并直接使用用户的更改更新Model。通过重写 OnIsTrackable 虚拟方法并返回 false,可以在派生 Item 中禁用选择、移动和调整关联 Item 的大小的功能。如果应用程序特定的 Item 需要 RectTracker 类的不同实现,可以通过重写 OnGetRectTracker 虚拟方法来提供。例如,如果应用程序特定的 Item 被允许移动但不调整大小。

注意:我在写这篇文章时想到,将 RectTracker 类与 MVC 框架完全解耦,并通过事件触发来通信用户的更改,可能更好。您可能会在框架的未来版本中看到此更改。

类 DesignView::Action 和 类 DesignView::DrawAction

除了 DesignView::Controller 负责响应Model更改来维护 DesignView::Item 状态之外,其主要功能是在响应 EVID_FILE_OPEN 事件时创建 Item 对象,并在响应发送到其 ControlledViewWM_PAINT 消息时要求它们绘制自身。虽然 Controller 提供了创建 Item 对象的例程,并且 Item 对象负责绘制自身,但有两个由 Controller 创建的 Model::Action 派生类提供了对Model的遍历,以及它们关联的ModelItem特定的 Model:ActionHandler 类,用于将操作应用于每个 Item

第一个类 DesignView::Action 用作插入过程的操作,并且仅向分配的 DesignView::Controller 添加一个访问器到基类 Model::ActionDesignView::Action 还为 DesignView::DrawAction 提供了基础,后者还提供了对 ControlledView 设备上下文的访问器。

// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewAction.h

class AFX_EXT_CLASS Action : public SbjCore::Mvc::Model::Action
{
public:

  Action(const SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    Controller* pCtrlr, 
    SbjCore::Mvc::Model::Action::HandlerMap* pMap = NULL, 
    CRuntimeClass* pDefaultHandler = NULL);

  virtual ~Action();
  
public:
  Controller* GetController() const;
  
private:
  struct ActionImpl* const m_pImpl;
};


// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewDrawAction.h
class DrawAction : public Action
{
public:

  DrawAction(SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    Controller* pCtrlr, CDC* pDC, 
    SbjCore::Mvc::Model::Action::HandlerMap* pMap = NULL, 
    CRuntimeClass* pDefaultHandler = NULL);

  virtual ~DrawAction();
  
public:
  CDC* GetDC() const;
  
private:
  struct DrawActionImpl* const m_pImpl;
};
类 DesignView::InsertActionHandler 和 类 DesignView::DrawActionHandler

所有两个 Model::ActionHandler 派生类所做的就是重写基类 OnBeginHandling 方法,实际的操作应用发生在该方法中。

// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewInsertActionHandler.cpp

int InsertActionHandler::OnBeginHandling()
{
  Action* pAction = dynamic_cast<Action*>(GetAction());
  Controller* pCtrlr = pAction->GetController();
  const SbjCore::Mvc::Model::Controller* pModelCtrlr = pAction->GetModelController();
  HANDLE hItem = GetItem();
  (void)pCtrlr->CreateItem(pModelCtrlr, hItem);
  return Model::Action::kContinue;
}

正如您在 InsertActionHandler::OnBeginHandling 方法中看到的,处理程序从 Action 中检索 DesignView::Controller,以及 Model::Controller 和当前ModelItemHANDLE。利用这些,它调用 DesignView::ControllerCreateItem 方法。

// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewDrawActionHandler.cpp

int DrawActionHandler::OnBeginHandling()
{
  DrawAction* pAction = dynamic_cast<DrawAction*>(GetAction());
  HANDLE hItem = GetItem();
  Controller* pCtrlr = pAction->GetController();
  CDC* pDC = pAction->GetDC();
  Item* pItem = pCtrlr->GetItem(hItem);
  if (pItem != NULL)
  {
    pItem->Draw(pDC);
  }
  return Model::Action::kContinue;
}

对于 DesignView::DrawActionHandler,处理程序从 Action 中检索 DesignView::Controller,以及当前ModelItemHANDLEControlledView设备上下文。利用这些,它调用 DesignView::ControllerGetItem 方法,然后调用 DesignView::Item::Draw

类 DesignView::Controller
// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewController.h

class AFX_EXT_CLASS Controller : public WndController
{
  DECLARE_DYNCREATE(Controller)

public:
  Controller();
  virtual ~Controller();
  
public:
  void MapItemRTCToModelTypeName(LPCTSTR lpszName, CRuntimeClass* pRTC);

  Item* CreateItem(const SbjCore::Mvc::Model::Controller* pModelCtrlr, HANDLE hModelItem);

  void LoadDesign(const SbjCore::Mvc::Model::Controller* pModelCtrlr, HANDLE hItem);

  void InsertDesign(const SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    HANDLE hItem, HANDLE hAfter = NULL);

  virtual void DeleteItem(Item* pItem);

  void DeleteAllItems();

  Item* GetItem(const HANDLE hItem) const;

  Item* GetItemFromPoint(const CPoint& pt) const;

  SbjCore::Mvc::MultiRectTracker* GetTracker() const;

public:
  void Draw(CDC* pDC);

protected:
  virtual SbjCore::Utils::Menu::ItemRange OnPrepareCtxMenu(CMenu& ctxMenu);
  virtual void OnInitialize();

private:
  virtual void OnDraw(CDC* pDC);

private:
  struct ControllerImpl* const m_pImpl;
};

DesignView::Controller 维护一个映射,将 DesignView::ItemCRuntimeClass 指针与应用程序通过调用 MapItemToModelTypeName 映射的ModelItemType名称关联起来。如上一节所述,Controller 在插入操作期间遍历Model时使用此映射来为ModelItems创建适当的 Item 对象。然后,这些 Item 对象存储在ModelItem HANDLEDesignView::Item 指针的映射中。Controller 在绘制操作期间遍历Model时使用此映射来访问关联的 Item 对象。

InsertDesign 是创建负责插入的 Model::Action 的实际方法。LoadDesign 增加了删除所有先前 DesignView::Item 对象后再调用 InsertDesign 的额外步骤。这样,无论是在插入初始Model还是在向现有Model添加新的ModelItems时,都可以使用相同的 InsertDesign 方法。

Draw 方法调用私有的虚拟方法 OnDraw,该方法正如您所料,创建负责绘制ModelModel::Action

除了用于维护 DesignView::Item 对象的预期方法外,还有一个 MultiRectTracker 实例。如果您曾经使用 AppStudio 创建过Dialog,您肯定熟悉多选的工作原理。MultiRectTracker 负责协调选定的 RectTracker 对象,并通过其 EVID_SELCOUNT_CHANGED 事件提供所选更改的通知。通过处理此事件,Controller 可以更新Model中当前选定的ModelItems集合。

结构体 ControllerImpl

与框架中的大多数类一样,DesignView::Controller 将大部分实现委托给其 private 成员 struct ControllerImpl* const m_pImpl

// Project Location: SbjCore/Mvc/Views/DesignView/DesignViewController.cpp

struct ControllerImpl
{
  SbjCore::Mvc::MultiRectTracker theTracker;

  typedef std::map<HANDLE, Item*> HandleToItem;
  typedef HandleToItem::iterator HandleToItemIter;
  HandleToItem theItems;
  
  typedef std::map<CString, CRuntimeClass*> ItemRTCMap;
  typedef ItemRTCMap::iterator ItemRTCMapIter;
  ItemRTCMap theItemRTCMap;

  localNS::FileOpenEventHandler theFileOpenEventHandler;
  localNS::ItemInsertEventHandler theItemInsertEventHandler;
  localNS::SelCountChangedEventHandler theSelCountChangedEventHandler;
  localNS::ItemChangeEventHandler theItemChangedHandler;

  localNS::DocModifiedEventHandler theDocModifiedEventHandler;
  localNS::ItemRemoveEventHandler theItemRemoveEventHandler;

  localNS::OnCreateHandler theOnCreateHandler;
  localNS::OnEraseBkgndHandler theOnEraseBkgndHandler;
  localNS::LButtonDownMsgHandler theLButtonDownHandler;
  localNS::SetCursorMsgHandler theSetCursorHandler;
  localNS::SetFocusHandler theSetFocusHandler;
  localNS::KillFocusHandler theKillFocusHandler;

  ControllerImpl()
  {
  }

  virtual ~ControllerImpl()
  {
    DeleteAllItems();
  }

  void DeleteAllItems()
  {
    for (HandleToItemIter iter = theItems.begin(); iter != theItems.end(); iter++)
    {
      Item* pItem = iter->second;
      if (pItem != NULL)
      {
        if (pItem->IsTrackable())
        {
          theTracker.Remove(pItem->GetRectTracker());
        }
        delete pItem;
      }
    }
    theItems.clear();
    theTracker.RemoveAll();
  }

  void DeleteItem(Item* pItem)
  {
    if (pItem != NULL)
    {
      if (pItem->IsTrackable())
      {
        theTracker.Remove(pItem->GetRectTracker());
        HANDLE hItem = pItem->GetModelItemHandle();
        theItems.erase(hItem);
      }
      delete pItem;
    }
  }

  Item* GetItemFromPoint(const CPoint& pt) const
  {
    pt;
    return NULL;
  }
  
  Item* CreateItem(Controller* pCtrlr, 
    const SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    HANDLE hModelItem )
  {
    Item* pItem = NULL;
    CString sItemType = pModelCtrlr->GetItemTypeName(hModelItem);
    if (!sItemType.IsEmpty())
    {
      CRuntimeClass* pRTC = theItemRTCMap[sItemType];
      if (pRTC != NULL)
      {
        pItem = dynamic_cast<Item*>(pRTC->CreateObject());
        pItem->SetController(pCtrlr);
        pItem->SetModelItemHandle(hModelItem);
        theItems[hModelItem] = pItem;
        if (pItem->IsTrackable())
        {
          theTracker.Add(pItem->GetRectTracker());
        }
      }
    }
    return pItem;
  }
  

  void LoadDesign(Controller* pCtrlr, 
    const SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    HANDLE hItem )
  {
    DeleteAllItems();
    InsertDesign(pCtrlr, pModelCtrlr, hItem);
  }

  void InsertDesign(Controller* pCtrlr, 
    const SbjCore::Mvc::Model::Controller* pModelCtrlr, 
    HANDLE hItem, HANDLE hAfter /*= NULL*/)
  {
    hAfter;
    Action theAction(pModelCtrlr, pCtrlr, NULL, RUNTIME_CLASS(InsertActionHandler));
    theAction.Apply(hItem);
  }


  void OnDraw(Controller* pCtrlr, CDC* pDC)
  {
    SbjCore::Utils::GDI::CMemDC dc(pDC);
    DrawAction theAction(SbjCore::Mvc::Model::GetCurController(), pCtrlr, &dc, NULL, 
      RUNTIME_CLASS(DrawActionHandler));
    if (theItems.size() > 0)
    {
      theAction.Apply(theItems.begin()->first);
      theTracker.Draw(&dc);
    }
  }

  SbjCore::Utils::Menu::ItemRange OnPrepareCtxMenu( CMenu& ctxMenu )
  {
    SbjCore::Utils::Menu::ItemRange menuItems;
    SbjCore::Mvc::MultiRectTracker::t_RectTrackers rts;
    int nCount = theTracker.GetSelectedRectTrackers(rts);
    if (nCount > 0)
    {

      menuItems.nFirst = (int)ctxMenu.GetMenuItemCount()-1;
      menuItems.nLast = menuItems.nFirst; 

      if (SbjCore::Utils::Menu::InsertSeparator(ctxMenu, menuItems.nLast))
      {
        menuItems.nLast++;
      }

      (void)ctxMenu.InsertMenu(menuItems.nLast, MF_BYPOSITION, ID_SBJCORE_CTX_DELETE, 
        _T("Delete Selected"));
    }
    return menuItems;
  }

};

除了包含 MultiRectTracker、映射集合和 Item 维护方法外,ControllerImpl 还包含与Model和用户通信所需的 EventHandlerMsgHandler 类。我将简要解释每个类的作用,而不是详细介绍并显示每个类的源代码,如果您愿意,可以自行查阅源代码。

事件处理程序

  • SbjCore::Mvc::Doc::Events
    • localNS::FileOpenEventHandler - 调用 DesignView::Controller::LoadDesign 来加载 FileOpen 事件传递的根 HANDLE 所指示的设计
    • localNS::DocModifiedEventHandler - 响应 DocModified 事件调用 CView::Invalidate
  • SbjCore::Mvc::Model::Events
    • localNS::ItemInsertEventHandler - 插入 ItemInsert 事件传递的 HANDLE 所指示的ModelItem
    • localNS::ItemChangeEventHandler - 更新 ItemChange 事件传递的 HANDLE 所指示的ModelItem RectTracker
    • localNS::ItemRemoveEventHandler - 删除 ItemRemove 事件传递的 HANDLE 所指示的ModelItem
  • SbjCore::Mvc::MTE (MultiRectTracker 事件)
    • localNS::SelCountChangedEventHandler - 使用 MultiRectTracker 对象当前选定的 RectTracker 对象引用的ModelItem HANDLE 列表调用 SbjCore::Mvc::Model::Controller::SetSelectedItems

消息处理程序

  • localNS::OnCreateHandler - 调用 DesignView::Controller::Initialize,该函数随后初始化事件处理程序
  • localNS::OnEraseBkgndHandler - 禁用 WM_ERASEBKGND 的默认处理,以支持 DesignView 的双缓冲实现
  • localNS::LButtonDownMsgHandler - 调用 DesignView::Controller 对象的 MultiRectTracker::Track 方法
  • localNS::SetCursorMsgHandler - 设置 DesignView::Controller 对象的 MultiRectTracker 的光标
  • localNS::SetFocusHandler - 调用 CView::Invalidate
  • localNS::KillFocusHandler - 调用 DesignView::Controller 对象的 MultiRectTracker::DeselectAll 方法,然后调用 CView::Invalidate

将设计视图应用于应用程序

再次,正如在关于模型实现的上一篇文章中所述,将 DesignView 集成到 ShapesView 类只需很少的修改。

注意:对原始文件的修改以粗体标出。

ShapesView.h
#pragma once

struct ShapesViewImpl;

class ShapesView : public SbjCore::Mvc::ControlledView
{
  typedef SbjCore::Mvc::ControlledView t_Base;
  
protected: // create from serialization only
  ShapesView();
  DECLARE_DYNCREATE(ShapesView)

// Attributes
public:
  ShapesDoc* GetDocument() const;

// Operations
public:

// Overrides
public:
  virtual void OnDraw(CDC* pDC);  // overridden to draw this view
  virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
protected:
  virtual BOOL OnPreparePrinting(CPrintInfo* pInfo);
  virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);
  virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);

// Implementation
public:
  virtual ~ShapesView();
#ifdef _DEBUG
  virtual void AssertValid() const;
  virtual void Dump(CDumpContext& dc) const;
#endif

protected:

// Generated message map functions
protected:
  afx_msg void OnFilePrintPreview();
  afx_msg void OnRButtonUp(UINT nFlags, CPoint point);
  afx_msg void OnContextMenu(CWnd* pWnd, CPoint point);
  DECLARE_MESSAGE_MAP()
private:
  struct ShapesViewImpl* const m_pImpl;
};

仅进行了三处更改;ShapesView 现在派生自 ControlledView;移除了 OnRButtonUpOnContextMenu 方法,并且该类配备了一个Pimpl struct ShapesViewImpl* const m_pImpl 成员。

接下来,我将展示对ShapesView.cpp的更改。由于即使是 AppWizard 生成的文件也相当大,我将仅限于显示已修改的部分。

ShapesView.cpp
namespace localNS
{
  // DesignViewItems //////////////////////////////////////
  
  // invisible root item (not drawn)
  class DrawingItem : public SbjCore::Mvc::DesignView::Item
  {
    DECLARE_DYNCREATE(DrawingItem)
  
    virtual bool OnIsTrackable() const
    {
      return false;
    }
  };
  IMPLEMENT_DYNCREATE(DrawingItem, SbjCore::Mvc::DesignView::Item)
  
  class ShapeItem : public SbjCore::Mvc::DesignView::Item
  {
  public:
    ShapeItem() :
      r(0,0,0,0),
      nBorderWidth(1),
      clrBorder(RGB(0,0,0)),
      clrFill((COLORREF)-1)
    {
    }
    virtual ~ShapeItem()
    {
    }
  protected:
    CRect r;
    int nBorderWidth;
    COLORREF clrBorder;
    COLORREF clrFill;
  private:
    DECLARE_DYNCREATE(ShapeItem)
    
    virtual void OnDraw(CDC* pDC)
    {
      pDC;
      HANDLE hItem = GetModelItemHandle();
      SbjCore::Mvc::Model::Controller* pModelCtrlr = SbjCore::Mvc::Model::GetCurController();
      r = SbjCore::Mvc::Model::Rect::GetItemValue(pModelCtrlr, hItem);
      nBorderWidth = pModelCtrlr->GetItemAttrValue(hItem, _T("borderWidth"));
      clrBorder = pModelCtrlr->GetItemAttrValue(hItem, _T("borderRGB"));
      clrFill = pModelCtrlr->GetItemAttrValue(hItem, _T("fillRGB"));

    }
    
  };
  IMPLEMENT_DYNCREATE(ShapeItem, SbjCore::Mvc::DesignView::Item)

  
  
  class RectangleItem : public ShapeItem
  {
    DECLARE_DYNCREATE(RectangleItem)
    virtual void OnDraw(CDC* pDC)
    {
      ShapeItem::OnDraw(pDC);
      
      CPen pen(PS_SOLID, nBorderWidth, clrBorder);
      SbjCore::Utils::GDI::Object<cpen> objPen(pDC, &pen);
      LOGBRUSH lb = {BS_HOLLOW, 0, 0};
      CBrush brHollow;
      brHollow.CreateBrushIndirect(&lb);
      SbjCore::Utils::GDI::Object<cbrush> objBrush(pDC, &brHollow);

      if (clrFill != (COLORREF)-1)
      {
        CBrush brush;
        brush.CreateSolidBrush(clrFill);
        pDC->FillRect(r, &brush);
      }
      pDC->Rectangle(r);
    }
  };
  IMPLEMENT_DYNCREATE(RectangleItem, ShapeItem)


  class EllipseItem : public ShapeItem
  {
    DECLARE_DYNCREATE(EllipseItem)
    virtual void OnDraw(CDC* pDC)
    {
      ShapeItem::OnDraw(pDC);
      CPen pen(PS_SOLID, nBorderWidth, clrBorder);
      SbjCore::Utils::GDI::Object<cpen> objPen(pDC, &pen);
      CBrush br;
      br.CreateSolidBrush(clrFill);
      SbjCore::Utils::GDI::Object<cbrush> objBrush(pDC, &br);

      pDC->Ellipse(r);
    }
  };
  IMPLEMENT_DYNCREATE(EllipseItem, ShapeItem)
}

struct ShapesViewImpl : public SbjCore::Mvc::DesignView::Controller
{
  ShapesViewImpl()
  {
   // must have a root for the design but it isn't drawn
    MapItemRTCToModelTypeName(_T("Drawing"), RUNTIME_CLASS(localNS::DrawingItem));
    MapItemRTCToModelTypeName(_T("Rectangle"), RUNTIME_CLASS(localNS::RectangleItem));
    MapItemRTCToModelTypeName(_T("Ellipse"), RUNTIME_CLASS(localNS::EllipseItem));
  }
  
  virtual ~ShapesViewImpl()
  {
  }
  
  virtual SbjCore::Utils::Menu::ItemRange OnPrepareCtxMenu( CMenu& ctxMenu )
  {
    SbjCore::Utils::Menu::ItemRange menuItems;

    menuItems.nFirst = (int)ctxMenu.GetMenuItemCount()-1;
    menuItems.nLast = menuItems.nFirst; 

    if (SbjCore::Utils::Menu::InsertSeparator(ctxMenu, menuItems.nLast))
    {
      menuItems.nLast++;
    }

    (void)ctxMenu.InsertMenu(0, MF_BYPOSITION, ID_CMDS_NEWELLIPSE, _T("New &Ellipse"));
    (void)ctxMenu.InsertMenu(0, MF_BYPOSITION, ID_CMDS_NEWRECTANGLE, _T("New &Rectangle"));
    

    menuItems = SbjCore::Mvc::DesignView::Controller::OnPrepareCtxMenu(ctxMenu);

    return menuItems;
  }
  
  
};

// ShapesView ///////////////////////////////////////////////////////

ShapesView::ShapesView() :
  m_pImpl(new ShapesViewImpl)
{
  SetController(m_pImpl);
}

ShapesView::~ShapesView()
{
  try
  {
    delete m_pImpl;
  }
  catch(...)
  {
    ASSERT(FALSE);
  }
}

void ShapesView::OnDraw(CDC* pDC)
{
  ShapesDoc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);
  if (!pDoc)
  {
    return;
  }
  m_pImpl->Draw(pDC);
}

在局部命名空间中,您可以看到四个 DesignView::Item 派生类。首先,一个 DrawingItem 类,它充当设计的必需根,除了重写 OnIsTrackable 以防止其被选中之外,不做任何事情。由于它没有 OnDraw 重写,所以它也不会被显示。其他三个包括一个基础 ShapeItem 类和两个特定形状,RectangleItemEllipseItem。正如在关于 DesignView::Item 的讨论中所述,这些派生类唯一需要做的就是提供 OnDraw 的实现。

接下来是 ShapesViewImplDesignView::Controller 的实现。它有两个功能:首先,它实例化 DesignView::Item 派生类,并将它们的 CRuntimeClass 指针映射到适当的ModelItem名称;其次,它提供了 OnPrepareCtxMenu 重写,以添加创建新形状的命令。

除了创建其Pimpl之外,ShapesView 类本身不做任何事情,只是在其 OnDraw 方法中调用 ShapesViewImpl 控制器的基类方法 Draw

就是这样,其余的一切都由框架处理!

结论

本文详细介绍了 MVC 框架设计视图的实现,以及应用程序如何通过提供仅满足模型特定要求的类来利用其功能。Shapes 应用程序的设计就是简单的;然而,我认为您可以看到设计视图可以通过此处所示的技术支持极其复杂的模型。未来的文章将介绍资源管理器视图和属性视图,您将在其中看到 MVC 框架再次提供同等水平的功能支持,仅限于应用程序提供与其特定模型域相关的详细信息。而且,正如我在前两篇文章中所说,所有代码都包含在本篇文章的附件中,所以请运行 Shapes 应用程序,探索代码,并请提供您想贡献的任何反馈。

待办事项

  • 添加对所有通用控件的支持
  • 添加对第三方控件的支持
  • 添加对 CView 派生类的支持
  • 添加对 GDI+ 和可能 OpenGL 的支持
  • CDockablePane 实现 CDocTemplate 派生类
  • 显然,继续泛化和重构

附录

源文件通用说明

MVC 框架的所有组件都属于 SbjCore::Mvc 命名空间。

我试图遵循以下规则:要么将 virtual 方法设为 private,要么在派生类必须调用基类实现的情况下,将其设为 protected,并且只能通过调用基类中声明的公共方法来访问。

//MyClass.h

class MyClass 
{
public:
  MyClass() {} 
  virtual ~MyClass() {} 

  void Init()
  {
    OnInit();
  }
  
  void DoIt()
  {
    OnDoIt();
  }

protected:  
  virtual void OnInit()
  {
    // init base class
  }
  
private:
  virtual void OnDoIt
  {
    // do it the base class way
  }
}

class MyDerivedClass : public MyClass
{
protected:  
  virtual void OnInit()
  {
    MyClass::OnInit();
    // init this class
  }
private:
  virtual void OnDoIt
  {
    // do it the derived class way
  }
}

class MyFinalClass : public MyDerivedClass
{
protected:  
  virtual void OnInit()
  {
    MyDerivedClass::OnInit();
    // init this class
  }
private:
  virtual void OnDoIt
  {
    // do it the final class way
  }
}

int main()
{
  MyFinalClass myFinalClass;
  myFinalClass.Init();
  
  myFinalClass.DoIt();
}

大多数类使用Pimpl idiom来隐藏实现细节。

//MyClass.h

struct MyClassImpl;

class MyClass 
{
public:
  MyClass(); 
  virtual ~MyClass(); 
  
  void APublicMethod();
  
private:
  struct MyClassImpl* const m_pImpl;
}

//MyClass.cpp

struct MyClassImpl
{
  void APrivateImpl()
  {
    // do something
  }
}

MyClass::MyClass() :
  m_pImpl(new MyClassImpl)
{
}

MyClass::~MyClass()
{
  delete m_pImpl;
}

void MyClass::APublicMethod()
{
    m_pImpl->APrivateImpl();
}

对于 ControlledCmdTargetTControlledWndT 类,Pimpl通常兼作该类的 Controller

大多数 .cpp 文件包含一个名为 localNS 的局部命名空间;然而,localNS 实际上是 Platform.h 中定义的宏,它解析为空字符串。它仅用作文档工具,类似于 MFC 的 afxmsg 宏,用于指示充当消息处理程序的函数。

//MyClass.cpp

namespace localNS
{
  void ALocalFunction()
  {
    // do something
  }
}

void MyClass::APublicMethod()
{
  localNS::ALocalFunction();
}

历史

  • 2009 年 3 月 19 日 - 提交原始文章。
© . All rights reserved.