在绘图应用程序中分离模型和视图/控制器

时间:2015-05-10 19:33:03

标签: java user-interface oop model-view-controller vector-graphics

我正在使用矢量绘图应用程序(在java中),我正在努力解决模型类和视图/控制器类之间的分离。

某些背景:

您可以绘制不同的形状:
矩形,线条和饼段

有四种工具可以操纵画布上的形状:
缩放工具,移动工具,旋转工具和变形工具

对于这个问题,变形工具是最有趣的一个: 它允许您通过拖动其中一个点并调整其他属性来更改形状,如下图所示:

The different options of manipulating a shape

这些转换规则对于每个形状都不同,我认为它们是模型业务逻辑的一部分,但在某种程度上它们需要暴露给视图/控制器(工具类),以便它们可以应用正确的形状。 / p>

此外,形状在内部通过不同的值表示:   - 矩形存储为中心,宽度,高度,旋转   - 该行存储为起点和终点   - 饼图段存储为center,radius,angle1,angle2

我计划在未来添加更多形状,如星星,气泡或箭头,每个都有自己的控制点。

我还计划在未来添加更多工具,例如旋转或缩放形状组。

每个工具的控制点都不同。例如,当使用缩放工具时,您无法抓住中心点,但每个缩放控制点需要与一个枢轴点(或多个以供用户选择)相关联。

对于像矩形,直线和饼图这样的简单形状,控制点对于每个类的实例都是相同的,但是期货形状如bezier路径或星形(具有可配置的尖峰计数)将具有不同的控制点数量实例

所以问题是建模和实施这些控制点的好方法是什么?

由于它们对于每个工具略有不同并且携带一些工具/控制器特定数据,因此它们以某种方式属于工具/控制器。但由于它们也适用于每种类型的形状并且具有非常重要的域逻辑,因此它们也属于该模型。

我想避免在添加一个工具或形状时为每个工具/形状组合添加特殊类型的控制点的组合爆炸。

更新:再举一个例子:将来可能会出现我想要支持的新形状:弧形。它类似于饼图段,但看起来有点不同,拖动控制点时表现完全不同。

为了实现这一点,我希望能够创建一个实现我的Shape界面的ArcShape类并完成。

The behaviour of the arc shape

4 个答案:

答案 0 :(得分:8)

基本注意事项

首先,让我们为简单起见做一些定义。

Entity是一个域模型对象,它定义了所有结构和行为,即逻辑。 EntityUI是表示用户界面中Entity的图形控件。

所以基本上,对于Shape类,我认为ShapeUI必须非常了解Shape的结构。结构主要由我猜测的控制点组成。换句话说,拥有关于控制点的所有信息(可能是将来的矢量),ShapeUI将能够在UI上绘制自己。

初步建议

我建议Shape类的是Shape类定义所有行为。 ShapeUI类将知道Shape类,并保持对它所代表的类的引用,通过它可以访问控制点,并且能够操作它们,例如设置他们的位置。 Observer模式只是要求在此上下文中使用。特别是,Shape类可以实现ObservableShapeUI将实现Observer并订阅相应的Shape对象。

基本上在这种情况下会发生什么,ShapeUI对象将处理所有UI操作,并负责更新Shape参数,例如控制点位置。之后,只要发生位置更新,Shape对象就会在状态发生变化时执行其逻辑,然后盲目地(不知道ShapeUI)通知ShapeUI更新状态。相应地,ShapeUI将绘制新状态。在这里,您将获得低耦合模型和视图。

关于Tools,我自己的观点是每个Tool必须知道如何操纵每种类型的Shape,即每个形状操作逻辑必须在{{{}内实现1}}类。对于视图和模型的解耦,它与Tool几乎相同。 Shape类处理单击光标的位置,单击ToolUI的内容,单击的控制点等等。通过获取此信息,ShapeUI将其传递给适当的ToolUI对象,然后根据接收的参数应用逻辑。

处理不同的形状类型

现在,Tool以自己的方式处理不同的Tool,我认为Shape模式会介入。每个工具都会实现Abstract Factory我们将为每种Abstract Factory提供操作实现。

<强>摘要

根据我的建议,这里是域模型草案:

Domain Model

为了从我的建议中得到完整的想法,我还发布了特定用例的序列图:

使用Shape用户点击ToolUI的{​​{1}}

enter image description here

答案 1 :(得分:4)

If I correctly understand, here what we have :

  • different figures which all have control points
  • the UI allows to draw figures and drag the control points

My advice here is to say that what characterizes a figure goes in Model layer, and that the UI part go in the View/Controller one.

One step further for the model :

  • figures should implement an interface :

    $(this).closest('.outer').find("img")[0].id
    
  • public interface Figure { List<Segment> segments(); List<ControlPoint> controlPoints(); void drag(ControlPoint point, Pos newPos); void rotate(ControlPoint point, Pos newPos, Pos center); // or rotate(Pos center, double angle); } is an abstraction that can represent a line segment, an arc or a Bezier curve

  • a Segment has a sense for a ControlPoint implementation and has a current Figure

    Pos
  • the public interface ControlPoint{ Figure parent(); void drag(Pos newPos); // unsure if it must exist in both interfaces Pos position(); ToolHint toolHint(); } should be a indication for which tool can use the control point and for which usage - per your requirement, the rotate tool should considere the center as special.

  • a ToolHint represents x,y coordinates

That way the UI does not have to know anything about what the figures actually are.

To Pos a draw, the UI gets the list of Figure and simply draw independely each Segment, and add a mark at each control points. When a control point is dragged, the UI gives new position to the Segment and redraws it. It should be able to erase a Figure before redrawing it in its new position, or alternatively (simpler but slower) it could redraw all at each operation

With the Figure method, we only can drag a simple control point on a single shape. It is easily extensible, but extensions will be have to be added for each tool. For example, I have allready added the drag method that allows to rotate a shape by moving one control point with a define center. You could also add a scale method.

Multiple shapes

If you want to apply a transformation to a set of shapes, you could use a subclass of the rectangle. You build a rectangle with sides parallel to coordinate axes that contain all shapes. I recommend to add a method in rotate that returns an (reasonnably small) enclosing rectangle with sides parralel to coordinate axes to ease the creation of the multi shapes rectangle. Then, when you apply a transformation to the englobing rectangle, it simply reports the transformation to all of its elements. But we come here to transformations that cannot be done by dragging control points, because the point that is dragged does not belong to the internal shape.

Internal transformations

Until now, I have only dealt with the interface between the UI and the model. But with the multi shapes, we saw that we need to apply arbitrary affine transformations (translation of a point of an englobing rectangle or scaling of the englobing rectangle) or rotation. If we choose to implement rotation as Figure the rotation of an included shape is already done. So we simply have to implement the affine transformation

rotate(center, angle)

That way, to apply an affine transformation to a class AffineTransform { private double a, b, c, d; /* creators, getters, setters omitted, but we probably need to implement one creator by use case */ Pos transform(Pos pos) { Pos newpos; newpos.x = a * pos.x + b; newpos.y = c * pos.y + d; return newpos; } } , we just have to implement Figure in a way that simply apply the all the points defining the structure.

Figure is now :

transform(AffineTransform txform)

Summary :

It is just the general ideas, but it should be the basics to allows tools to act on arbitrary shapes, with a low coupling

答案 2 :(得分:3)

我不希望在没有编写代码并遇到实际问题的情况下出现好的设计。但是如果你不知道从哪里开始是我的建议。

inteface Shape {
   List<Point> getPoints(ToolsEnum strategy); // you could use factory here
}

interface Point {
    Shape rotate(int degrees); // or double radians if you like
    Shape translate(int x, int y);
    void setStrategy(TranslationStrategy strategy);
}

interface Origin extends Point {}

interface SidePoint extends Point {}

interface CornerPoint extends Point {}

然后在每个具体形状中实现Point接口扩展作为内部类。

我假设下一个用户流程:

  1. 选择的工具 - 控制器内的currentTool从枚举设置为适当的值。
  2. 用户选择/增加一个形状 - getPoints被调用,取决于工具,某些类型的点可能会被过滤掉。例如。仅为变形操作返回角点。为暴露点注入适当的策略。
  3. 当用户拖动点 - translate被调用时,您使用给定工具转换了新形状。

答案 3 :(得分:1)

原则上,最好使模型与绘图界面匹配。因此,例如,在Java Swing中,可以使用drawRect方法绘制矩形,该方法将左上角的xy,宽度和高度作为参数。因此,通常您希望将矩形建模为{ x-UL, y-UL, width, height }

对于任意路径,包括弧,Swing为GeneralPath对象提供了处理由线或Quadratic / Bezier曲线连接的点序列的方法。要为GeneralPath建模,您可以提供点列表,绕组规则以及二次曲线或贝塞尔曲线的必要参数。