SBJ MVC 框架 - 设计视图,响应模型变更
与 MFC 文档/视图架构集成的模型-视图-控制器框架。
目录
引言
这是我发布的第三篇介绍 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::Action
和 Model::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
处理方法返回的值。kStop
和 kContinue
的意图应该不言自明。kReject
可以返回以短路模型分支的遍历。
Model::Action
类是为给定的 Model::Controller
构建的。还可以提供两个可选参数;第一个是 HandleMap
,第二个是指向要用于未特别映射的任何ModelItemTypes的默认 CRuntimeClass
指针。
如前所述,Model::Action
类在遍历过程中根据需要创建 Model::ActionHandler
对象。HandlerMap
是ModelItemTypes到 Model::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
方法 OnBeginHandling
和 OnEndHandling
来实现所需的操作。
设计视图
首先,框架中没有实际的 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
对象,并在响应发送到其 ControlledView
的 WM_PAINT
消息时要求它们绘制自身。虽然 Controller
提供了创建 Item
对象的例程,并且 Item
对象负责绘制自身,但有两个由 Controller
创建的 Model::Action
派生类提供了对Model的遍历,以及它们关联的ModelItem特定的 Model:ActionHandler
类,用于将操作应用于每个 Item
。
第一个类 DesignView::Action
用作插入过程的操作,并且仅向分配的 DesignView::Controller
添加一个访问器到基类 Model::Action
。DesignView::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
和当前ModelItem的 HANDLE
。利用这些,它调用 DesignView::Controller
的 CreateItem
方法。
// 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
,以及当前ModelItem的 HANDLE
和 ControlledView
的设备上下文。利用这些,它调用 DesignView::Controller
的 GetItem
方法,然后调用 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::Item
类 CRuntimeClass
指针与应用程序通过调用 MapItemToModelTypeName
映射的ModelItemType名称关联起来。如上一节所述,Controller
在插入操作期间遍历Model时使用此映射来为ModelItems创建适当的 Item
对象。然后,这些 Item
对象存储在ModelItem HANDLE
到 DesignView::Item
指针的映射中。Controller
在绘制操作期间遍历Model时使用此映射来访问关联的 Item
对象。
InsertDesign
是创建负责插入的 Model::Action
的实际方法。LoadDesign
增加了删除所有先前 DesignView::Item
对象后再调用 InsertDesign
的额外步骤。这样,无论是在插入初始Model还是在向现有Model添加新的ModelItems时,都可以使用相同的 InsertDesign
方法。
Draw
方法调用私有的虚拟方法 OnDraw
,该方法正如您所料,创建负责绘制Model的 Model::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和用户通信所需的 EventHandler
和 MsgHandler
类。我将简要解释每个类的作用,而不是详细介绍并显示每个类的源代码,如果您愿意,可以自行查阅源代码。
事件处理程序
SbjCore::Mvc::Doc::Events
localNS::FileOpenEventHandler
- 调用DesignView::Controller::LoadDesign
来加载FileOpen
事件传递的根HANDLE
所指示的设计localNS::DocModifiedEventHandler
- 响应DocModified
事件调用CView::Invalidate
SbjCore::Mvc::Model::Events
localNS::ItemInsertEventHandler
- 插入ItemInsert
事件传递的HANDLE
所指示的ModelItemlocalNS::ItemChangeEventHandler
- 更新ItemChange
事件传递的HANDLE
所指示的ModelItemRectTracker
localNS::ItemRemoveEventHandler
- 删除ItemRemove
事件传递的HANDLE
所指示的ModelItemSbjCore::Mvc::MTE
(MultiRectTracker
事件)localNS::SelCountChangedEventHandler
- 使用MultiRectTracker
对象当前选定的RectTracker
对象引用的ModelItemHANDLE
列表调用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
;移除了 OnRButtonUp
和 OnContextMenu
方法,并且该类配备了一个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
类和两个特定形状,RectangleItem
和 EllipseItem
。正如在关于 DesignView::Item
的讨论中所述,这些派生类唯一需要做的就是提供 OnDraw
的实现。
接下来是 ShapesViewImpl
对 DesignView::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();
}
对于 ControlledCmdTargetT
和 ControlledWndT
类,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 日 - 提交原始文章。