Visual Component Framework中的MVC,第二部分






3.67/5 (14投票s)
第 2 部分(共 4 部分),探讨 Visual Component Framework 中的模型-视图-控制器模式。
- 从 SourceForge 下载 VCF 源代码 (tar.gz) - 10.5 MB
- 从 SourceForge 下载 VCF Windows 安装程序 - 37.2 MB
- 下载 Scribble1 - 743.52 KB
- 下载 Scribble2 - 753.86 KB
- 下载 Scribble3 - 769.05 KB
引言
上一篇文章介绍了模型-视图-控制器(MVC)模式的思想以及它如何在Visual Component Framework中实现。本文将在此基础上,用一个新应用程序来构建,我们将为其添加完整的MVC支持和各种其他很酷的功能。它在性质上将与Microsoft分发的MFC Scribble示例类似。
Scribble 1
首先,我们将为我们的涂鸦应用程序创建一个新项目,命名为Scribble1,然后开始处理我们的模型类。我们的模型将包含一个形状列表,每个形状包含一个点数组、填充和描边颜色、一个2D矩阵、描边宽度、一个指示形状是否填充的布尔值,以及一个指示我们正在处理的形状类型的类型。更“正确”的OO设计可能会用派生类替换特定绘图形状的类型,但为了简洁起见,我们将忽略这一点。这是形状类。
class ScribbleShape : public VCF::Object { public: ScribbleShape():type(stLine),strokeWidth(1.0),filled(true) {} enum Type { stLine, stRect, stEllipse, stPolygon, stFreehand }; Matrix2D mat; std::vector<Point> points; Type type; double strokeWidth; bool filled; Color fill; Color stroke; };为了便于实现,我们的模型类将从SimpleListModel派生。这是ListModel类的一个基本实现,并将数据存储为VariantData对象的数组。此外,我们的模型还将添加一个默认形状,形状将从中获取其初始设置,以及一个背景颜色。
class ScribbleModel : public VCF::SimpleListModel { public: ScribbleModel(){ //set this to true to //delete all our objects deleteVariantObjects_ = true; backColor = *Color::getColor("white"); } ScribbleShape* getShape( const unsigned int& index ) { return (ScribbleShape*) (VCF::Object*) get(index); } void addLine( const Point& p1, const Point& p2 ) { ScribbleShape* s = new ScribbleShape(); *s = defaultShape; s->points.push_back( p1 ); s->points.push_back( p2 ); s->type = ScribbleShape::stLine; add( s ); } void addRect( const Point& p1, const Point& p2 ) { ScribbleShape* s = new ScribbleShape(); *s = defaultShape; s->points.push_back( p1 ); s->points.push_back( p2 ); s->type = ScribbleShape::stRect; add( s ); } void addEllipse( const Point& p1, const Point& p2 ) { ScribbleShape* s = new ScribbleShape(); *s = defaultShape; s->points.push_back( p1 ); s->points.push_back( p2 ); s->type = ScribbleShape::stEllipse; add( s ); } //rest omitted void setDefaultWidth( const double& val ) { defaultShape.strokeWidth = val; } void setDefaultFilled( const bool& val ) { defaultShape.filled = val; } //rest omitted void setBackColor( Color* val ) { backColor = *val; VCF::ModelEvent e( this, VCF::Model::MODEL_CHANGED ); changed( &e ); } Color* getBackColor() { return &backColor; } Rect getMaxBounds(); protected: ScribbleShape defaultShape; Color backColor; };
这为我们提供了一个基本的模型来存储形状。由于每个ScribbleShape都派生自Object,因此我们可以将其存储在模型为我们维护的VariantData项数组中。当我们向模型添加形状时,会触发ModelChanged委托,并发生任何关联视图的正常更新机制。考虑到这一点,让我们继续进行UI。
Scribble UI:初版
我们涂鸦应用程序的第一个UI将非常简单。我们将使用主窗口本身作为视图,并在那里处理绘制。我们的绘制逻辑只是遍历模型中的所有形状对象,然后绘制每个形状。
virtual void paint( GraphicsContext* ctx ) { Window::paint( ctx ); ScribbleModel* scribble = (ScribbleModel*) getViewModel(); Rect r = getClientBounds(); ctx->rectangle( r ); ctx->setColor( scribble->getBackColor() ); ctx->fillPath(); size_t count = scribble->getCount(); for (size_t i=0;i<count;i++ ) { const ScribbleShape* shape = scribble->getShape(i); int gcs = ctx->saveState(); ctx->setCurrentTransform( shape->mat ); switch ( shape->type ) { case ScribbleShape::stLine : { ctx->setColor( &shape->stroke ); ctx->moveTo( shape->points[0] ); ctx->lineTo( shape->points[1] ); ctx->strokePath(); } break; case ScribbleShape::stRect : { r.setRect( shape->points[0].x_, shape->points[0].y_, shape->points[1].x_, shape->points[1].y_ ); ctx->rectangle( r ); } break; case ScribbleShape::stEllipse : { r.setRect( shape->points[0].x_, shape->points[0].y_, shape->points[1].x_, shape->points[1].y_ ); ctx->ellipse( r.getTopLeft(), r.getBottomRight() ); } break; case ScribbleShape::stPolygon : { ctx->polyline( shape->points ); ctx->closePath( shape->points.back() ); } break; case ScribbleShape::stFreehand : { ctx->polyline( shape->points ); } break; } if ( shape->type != ScribbleShape::stLine ) { if ( shape->filled ) { ctx->setColor( &shape->fill ); ctx->fillPath(); } ctx->setStrokeWidth( shape->strokeWidth ); ctx->setColor( &shape->stroke ); ctx->strokePath(); } ctx->restoreState( gcs ); } }
代码相当简单,请注意,Window派生自Control,因此也是View,它通过getViewModel()方法获取其模型。这使得代码很容易迁移到稍后拥有模型的任何其他内容。
与上一篇文章一样,我们将在此处使用VFF格式来定义基本的窗口UI,但它将非常简单。
object Scribble1Window : VCF::Window top = 200 left = 200 height = 300 width = 320 caption = 'Scribble1 Window' end足以定位窗口并设置其标题。其余工作将在窗口的构造函数中完成。
Scribble1Window() { ScribbleModel* scribble = new ScribbleModel(); addComponent( scribble ); scribble->addView( this ); scribble->addLine( Point(10, 100), Point(100, 300) ); scribble->addRect( Point(100, 20), Point(200, 110) ); ScribbleShape* shape = new ScribbleShape(); shape->type = ScribbleShape::stPolygon; shape->points.push_back( Point(40, 30) ); shape->points.push_back( Point(34, 120) ); shape->points.push_back( Point(200, 180) ); shape->points.push_back( Point(300, 100) ); shape->points.push_back( Point(260, 80) ); shape->points.push_back( Point(100, 40) ); shape->strokeWidth = 5; shape->filled = true; shape->fill = *Color::getColor("red"); shape->stroke = *Color::getColor("purple"); shape->mat = Matrix2D::translation( -40, -30 ) * Matrix2D::rotation(-25) * Matrix2D::translation( 70, 200 ); scribble->add( shape ); }
代码创建一个新的ScribbleModel实例并将窗口(作为视图)添加到模型中。我们创建三个形状,前两个调用使用特定的ScribbleModel方法来创建形状,第三个调用从头开始创建一个ScribbleShape并使用ListModel::add()方法添加它。由于ListModel::add()方法接受VariantData对象,因此我们可以传入我们的ScribbleShape实例指针。一旦我们构建了所有这些并运行它,我们就会得到类似这样的结果。
此时,我们现在拥有一个功能齐全(尽管简单)的模型,可以维护形状,以及一个可以绘制它们的视图。接下来,我们将使模型更加复杂,以便我们可以在VFF资源中对其进行操作。
Scribble 2:VFF支持
我们的下一步将允许我们在程序的VFF资源中拥有类似这样的东西。
object scribble : ScribbleModel backColor.red = 1.0 backColor.green = 0.20 shapes[0].fill.red = 0.12 shapes[0].fill.blue = 0.52 shapes[0].fill.green = 0.92 shapes[0].data = 'line M 10 10 L 50 300' end如您所见,我们可以创建涂鸦模型,并根据需要添加形状,并修改其属性。这使得测试各种形状、颜色等变得容易。
为此,我们需要做的第一件事是为我们的两个类ScribbleModel和ScribbleShape添加VCF的RTTI支持。我们使用_class_rtti_宏来实现这一点,如下所示。
_class_rtti_(ScribbleShape, "VCF::Object", "ScribbleShape") _class_rtti_end_ _class_rtti_(ScribbleModel, "VCF::SimpleListModel", "ScribbleModel") _class_rtti_end_这使我们能够动态创建类实例。接下来,我们需要为ScribbleShape类添加属性支持,以便我们可以访问类的各种属性。为此,我们使用VCF的_property_宏,如下所示。
_class_rtti_(ScribbleShape, "VCF::Object", "ScribbleShape") _property_object_( Color, "fill", getFill, setFill, "" ); _property_object_( Color, "stroke", getStroke, setStroke, "" ); _property_( double, "strokeWidth", getStrokeWidth, setStrokeWidth, "" ); _property_( bool, "filled", getFilled, setFilled, "" ); _property_( String, "data", getData, setData, "" ); _property_( double, "rotation", getRotation, setRotation, "" ); _property_( double, "transX", getTranslateX, setTranslateX, "" ); _property_( double, "transY", getTranslateY, setTranslateY, "" ); _property_( double, "scaleX", getScaleX, setScaleX, "" ); _property_( double, "scaleY", getScaleY, setScaleY, "" ); _property_( double, "shearX", getShearX, setShearX, "" ); _property_( double, "shearY", getShearY, setShearY, "" ); _class_rtti_end_该宏接受属性类型、属性名称、getter和setter方法以及描述字符串(我们将将其留空)。_property_object_类似于_property_宏,只是它专门用于派生自VCF的Object类的属性。
我们在这里添加的主要内容是能够在一个字符串中定义形状的点,类似于SVG中定义点的方式,例如。
line M 10 10 L 50 300这定义了一个具有2个点的线形,第一个是移动到点,第二个是画线到点。您可以定义一个多边形。
poly M 50 100 L 150 250 L 170 220 L 175 210 L 180 165显然不是非常复杂,但它为我们提供了一种定义形状的简单(尽管粗糙)的方法。
我们可以为ScribbleModel做同样的事情。
_class_rtti_(ScribbleModel, "VCF::SimpleListModel", "ScribbleModel") _property_object_( Color, "backColor", getBackColor, setBackColor, "" ); _property_object_( Color, "defaultFill", getDefaultFillColor, setDefaultFillColor, "" ); _property_object_( Color, "defaultStroke", getDefaultStrokeColor, setDefaultStrokeColor, "" ); _property_( double, "defaultStrokeWidth", getDefaultWidth, setDefaultWidth, "" ); _property_( bool, "defaultFilled", getDefaultFilled, setDefaultFilled, "" ); _class_rtti_end_现在唯一缺少的是添加对访问单个ScribbleShape元素的特定支持。这就是我们使用_property_array_宏的原因。该宏接受元素类型以及用于获取、设置、插入、删除和获取集合/数组中元素数量的方法。我们已经拥有ListModel类提供的所有get、set、insert、remove和count方法。所以我们可以这样添加它。
_class_rtti_(ScribbleModel, "VCF::SimpleListModel", "ScribbleModel") //rest omitted _property_array_( VariantData, "shapes", get,set,insert,remove,getCount, "" ) _class_rtti_end_这现在允许我们通过索引访问列表模型中的元素,使用“[]”表示法。因此,我们可以在VFF中编写代码,如下所示。
shapes[1].fill.red = 0.12这将获取索引为1的形状,并将其填充属性(它是Color对象)设置为红色分量,值为0.12。唯一的问题是,我们需要确保在调用get()方法时我们的形状存在。所以让我们修改我们的ScribbleModel get方法。
virtual VariantData get( const uint32& index ) { size_t missing = 0; if ( (index+1) > data_.size() ) { missing = (index+1) - data_.size(); } //add empty shapes if we need to if ( missing > 0 ) { size_t i=data_.size(); data_.resize( missing + data_.size() ); while ( i < data_.size() ) { data_[i] = new ScribbleShape(); i++; } } return data_[index]; }这会检查以确保我们始终有足够的形状来满足对某个索引处元素的请求。此类逻辑已存在于SimpleListModel的set()方法中,但由于我们没有“设置”元素,因此不会调用它。相反,因为我们正在访问元素的属性,所以我们首先“获取”元素然后对其进行修改。因此,我们需要进行上述修改。
此时,我们可以修改VFF资源中的模型。这简化了我们的窗口构造函数,我们可以清除其中以前的代码。取而代之的是,我们现在在VFF资源中定义模型并将其与视图关联,如下所示。
object Scribble2Window : VCF::Window //rest omitted caption = 'Scribble2 Window' model = @scribble object scribble : ScribbleModel backColor.red = 1.0 backColor.green = 0.20 shapes[0].fill.red = 0.12 shapes[0].fill.blue = 0.52 shapes[0].fill.green = 0.92 shapes[0].data = 'line M 10 10 L 50 300' //rest omitted end end请注意“model = @scribble”。即使这出现在scribble实例定义之前,我们仍然可以引用它,因为VFF加载机制实际上稍后会将两者链接起来。与上一篇文章一样,通过在此处指定模型,视图会自动添加到模型中。scribble模型已定义,我们可以向其中添加形状并设置其属性。我们最终得到的结果是这样的。
好的,好的,不是最有艺术感的图画,但请耐心点,我们稍后会做一些更有趣的事情……
Scribble 3:视图和控制器
在完成本系列这部分之前,我们将添加另外两个功能。我们将为ScribbleModel的渲染创建一个特定的Controller和View类。我们将看一下添加一些交互性以及响应模型更改。
首先,我们将创建一个View类。如前所述,View通常非常简单,至少在其接口方面是如此。我们的类将重新实现一个方法paintView()。我们之前放在窗口paint()方法中的代码将被移到这里。
class ScribbleView : public AbstractView { public: ScribbleView(){} virtual void paintView( GraphicsContext* ctx ) { ScribbleModel* scribble = (ScribbleModel*) getViewModel(); Control* control = getViewControl(); Rect r = control->getClientBounds(); ctx->rectangle( r ); ctx->setColor( scribble->getBackColor() ); ctx->fillPath(); Scrollable* scrollable = control->getScrollable(); if ( scrollable ) { Rect viewBounds = ctx->getViewableBounds(); Point origin = ctx->getOrigin(); control->adjustViewableBoundsAndOriginForScrollable( ctx, viewBounds, origin ); ctx->setOrigin( origin ); ctx->setViewableBounds( viewBounds ); } //rest omitted } };正如我们在上一篇文章中提到的,每个视图都有一个与之关联的控件。我们使用此控件来确定我们的初始边界矩形。接下来,我们添加对滚动条的支持。Scrollable表示可以垂直和/或水平滚动的内容。如果控件被分配了Scrollable实例(默认情况下没有),则它支持滚动,因此当调用Control::getScrollable()方法时,它会返回一个非null值。如果控件具有滚动支持,我们将调整图形上下文的原点和可视边界以适应可滚动对象。这考虑了滚动条可能在的位置以及视图的“虚拟”高度/宽度。在此之后,其余的绘制代码与之前的代码基本相同。我们添加的另一个功能是“活动”形状的概念。如果模型有一个活动形状,那么我们将以略微不同的方式绘制它,使其更加突出。
有了我们的新视图类,我们的窗口代码大大简化了!
class Scribble3Window : public Window { public: Scribble3Window() { } virtual ~Scribble3Window(){}; };
接下来:让我们正式添加一个真正的Controller类。我们将称之为ScribbleController,它会有一些职责。首先,它将监听其目标控件(控制器与之关联)的鼠标移动事件。如果控制器确定鼠标位于形状上,它将设置模型的活动形状。接下来,它将在模型更改时收到通知。当发生这种情况时,控制器将从模型获取最大边界,然后设置控件的可滚动实例的控件的虚拟高度和宽度。这会调整控件的滚动条的存在(或不存在)。
这方面的内容看起来是这样的。
class ScribbleController : public Component { public: ScribbleController(): model(NULL), modelControl(NULL),activeShape(NULL) { addCallback( new ClassProcedure1<Event*,ScribbleController>(this, &ScribbleController::onModelChanged), "ScribbleController::onModelChanged" ); addCallback( new ClassProcedure1<MouseEvent*,ScribbleController>(this, &ScribbleController::onMouseMove), "ScribbleController::onMouseMove" ); addCallback( new ClassProcedure1<MouseEvent*,ScribbleController>(this, &ScribbleController::onMouseDown), "ScribbleController::onMouseDown" ); addCallback( new ClassProcedure1<MouseEvent*,ScribbleController>(this, &ScribbleController::onMouseUp), "ScribbleController::onMouseUp" ); } void setControl( Control* val ); void setModel( ScribbleModel* scribbleModel ); void onModelChanged( Event* e ); void onMouseMove( MouseEvent* e ); void onMouseDown( MouseEvent* e ); void onMouseUp( MouseEvent* e ); const ScribbleShape* hitTest( const Point& point ); Control* modelControl; ScribbleModel* model; const ScribbleShape* activeShape; };我们的控件将分两步进行初始化。首先,它将设置其控件,setControl()方法将用于将控制器的各种回调连接到控件的委托。然后,它将设置其模型,并向模型添加一个回调,以便在模型更改时通知控制器。
onMouseMove()回调将用于跟踪用户的鼠标位置。它将调用hitTest()来确定鼠标是否在形状上。如果是在,它将高亮显示它。这里需要注意的一点是:窗口系统的paint周期之外没有进行任何绘制。相反,会设置状态变量,然后如果某些状态已更改(例如模型),则控件将被重绘。这与某些Win32/MFC/WTL示例过去的工作方式略有不同,它们会在需要时在HDC上绘制。
正如我们所提到的,我们将在这一步支持滚动。如果控件具有可滚动实例,则框架将为您完成大部分繁重的工作。您需要担心的是设置虚拟宽度和高度。这使框架能够理解如何正确设置滚动条。您只需要设置一次虚拟尺寸,或者至少在它们需要更改时(例如,当模型更改时)。如果控件的边界发生更改(例如,它们被调整大小),框架将相应地调整滚动条。
虚拟宽度和高度
实现此目的的代码非常简单。
void onModelChanged( Event* e ) { Scrollable* scrollable = modelControl->getScrollable(); if ( NULL != scrollable ) { Rect bounds = model->getMaxBounds(); bounds.inflate( 0,0,20,20); scrollable->setVirtualViewHeight( bounds.getHeight() ); scrollable->setVirtualViewWidth( bounds.getWidth() ); } }为了实现这一点,我们需要对窗口的VFF资源进行一些小的更改。我们需要创建一个滚动条管理器组件,并将其“目标”(它监控的控件)设置为窗口。如下所示。
object Scribble3Window : VCF::Window top = 200 left = 200 height = 300 width = 320 caption = 'Scribble3 Window' //rest omitted object scrollbarMgr : VCF::ScrollbarManager target = @Scribble3Window hasVerticalScrollbar = true hasHorizontalScrollbar = true end end我们将目标指定为主窗口——名为Scribble3Window的组件。就是这样,框架将处理其余的事情。
最后,最后一步是将所有这些连接起来。VFF资源基本保持不变,我们将在其中定义模型实例并定义其形状。应用程序初始化是我们创建新的Controller和View类并将其连接到应用程序其余部分的地方。
class Scribble1Application : public Application { public: Scribble1Application( int argc, char** argv ); virtual bool initRunningApplication(){ bool result = Application::initRunningApplication(); Window* mainWindow = Frame::createWindow( classid(Scribble3Window) ); setMainWindow(mainWindow); ScribbleModel* m = (ScribbleModel*)findComponent( "scribble", true ); ScribbleView* view = new ScribbleView(); mainWindow->setView( view ); m->addView( view ); ScribbleController* controller = new ScribbleController(); addComponent(controller); controller->setControl( mainWindow ); controller->setModel( m ); mainWindow->show(); return result; } };首先,我们创建主窗口,然后创建视图。请注意,我们将主窗口的视图设置为我们的新实例。这现在将绘图委托给我们的自定义视图实例。然后我们将我们的视图添加到模型。最后,我们创建我们的Controller,并设置其控件和模型。瞧!
如果我们移动鼠标,就会出现高亮显示。
如果我们将鼠标悬停在形状上,就会出现工具提示。
调整窗口大小,滚动条会相应地做出反应。
关于构建示例的说明
您需要安装最新版本的VCF(至少0-9-8或更高版本)。当前示例有一个使用Visual C++ 6构建的可执行文件。如果您想使用不同版本的Visual C++进行构建,则需要手动构建它们,并确保构建静态框架库,而不是动态库。
如果您不确定如何构建框架本身,请参阅:Building the VCF,它解释了使用各种IDE和工具链构建框架的基础知识。
结论
我们已经完成了创建自定义模型、视图和控制器基础知识的介绍,以及添加在VFF资源中操作模型的功能。此外,我们还增加了有限的交互性和滚动条功能来查看模型。请继续关注第三部分……
欢迎提出关于框架的问题,您可以直接在此处或在论坛中提问。如果您对如何改进这些内容有任何建议,我将非常乐意听到!