7

我最近开始创建一个图像编辑工具来满足一个非常特殊的需求。对于将要使用它的人来说,这和我自己的娱乐一样多。但是,我很早就遇到了一些架构障碍。

像任何图像编辑器一样,用户将使用“工具”来绘制和操作图像。我的第一次尝试包括一个简单的界面:

public interface IDrawingTool
{
    void DrawEffect( Graphics g );
    // other stuff
}

这(我认为)会很好而且很干净,并且可以轻松维护和扩展。只需添加接口对象并在运行时调用所选对象的 DrawEffect 方法。

这种方法的问题是不同的绘图工具不能干净地遵循一个界面。例如,钢笔工具只需要知道要绘制的点就可以工作。然而,矩形需要单击的第一个点以及当前位置。多边形工具需要跟踪多次鼠标点击。

我很难想出一个很好的方法来实现这一点。我现在能想到的最好的方法是为每个工具添加一个 switch 语句和一个 case,这意味着绘图逻辑将在 Canvas 类中,而不是由 Tool 类型的对象封装。因为这是实践,我想以正确的方式做到这一点。感谢您提前提供任何帮助。

4

4 回答 4

1

好的,经验法则:如果您switch在代码草图中看到一条语句,则表明您需要改用多态性。因此,在这种情况下,您希望能够进行各种操作,并且您发现自己想要 a switch,因此您应该考虑“我怎样才能使用多态性来制作这个东西?”

现在,看看命令模式,你的对象是动词而不是名词。每个 Command 实现一个doThis()方法;当您构造对象时,您确定命令将做什么。

public interface Command {
   public void doThis(Graphics g);  // I don't promise returning 
                                    // void is the best choice
   // Would it be better to return a Graphics object?
}

public class DrawRectangle implements Command {
   public DrawRectagle( Point topLeft, Point btmRight) { // ...
   }
   public void doThis(Graphics g){ // ...
   }
}

现在,考虑一下如果你想实现undo会怎么做?

更新

好的,让我们再扩展一下。使用这种模式的目的是确保客户不需要知道那么多,除非您在进行原始构造。所以对于这个例子,让我们考虑绘制一个矩形。当您选择矩形工具时,您将在按钮单击事件处理程序上有一些代码(顺便说一句,这都是伪代码)

 cmdlist = [] // empty list
 bool firstClick = true
 Point tl = br = new Point(0,0)
 onClick:
   if firstClick:
     get mouse position into tl
     firstClick = false
   else:
     get mouse position into br
     cmdlist.append(new DrawRectangle(tl, br))
     firstClick = true

因此,现在当您选择矩形时,您将 DrawRectangle 对象添加到命令列表结构中。一段时间后,您浏览了列表

for cmd in cmdlist:
   cmd.doThis(Graphics g)

这些事情就完成了。现在应该很明显,您将通过向 Command 添加“undoThis”方法来实现撤消。创建命令时,您必须构建代码,以便对象知道如何撤消自身。然后 undo 意味着只从列表中取出最后一个 Command 对象并执行它的 undoThis 方法。

于 2009-04-05T04:41:38.527 回答
1

你如何设计你的界面有点复杂?让我们从一些代码开始,然后我将解释它应该如何工作。

public class AbstractDrawingTool {

    private Graphics g;

    void AbstractDrawingTool( Graphics g ) {
        this.g = g;
    }

    void keyDown(KeyEvent e);
    void keyUp(KeyEvent e);
    void mouseMove(MouseEvent e);
    void mouseClick(MouseEvent e);
    void drop();
    // other stuff
}

这个想法是一旦用户开始使用特定的实现,就将用户输入传递给工具。这样,您可以使用相同的界面创建许多不同的绘图工具。例如,一个简单的 PointDrawingTool 只会实现 mouseClick 事件以在画布上放置一个点。PolygonDrawingTool 还将实现 keyUp 事件,以便在按下特定键(即退出键)时停止绘制线条。

一个特例是 drop 方法。它将被称为“删除”当前选定的工具。如果从工具栏或类似工具中选择另一个实现,就会发生这种情况。

您还可以将此定义与命令模式结合使用。在这种情况下,AbstractDrawingTool 的实现将负责创建 Command 接口的实例,并可能在操作完成后将它们放在堆栈上(即在画布上放置一个点)。

于 2009-04-06T22:47:59.000 回答
0

在尝试重新设计我的映射软件以支持 GDI+ 和 Cairo 图形库时,我遇到了类似的问题。我通过将绘图界面减少到一些常见的操作/基元来解决它,请参见下面的代码。

在此之后,您要绘制的“效果”是命令(如查理所说)。他们使用IPainter界面进行绘制。这种方法的好处是效果与 GDI+ 等具体的绘图引擎完全分离。这对我来说很方便,因为我可以通过切换到 Cairo 引擎将我的绘图导出到 SVG。

当然,如果您需要一些额外的图形操作,您将不得不扩展IPainter接口,但基本原理保持不变。在此处查看更多信息:http: //igorbrejc.net/development/c/welcome-to-cairo

public interface IPainter : IDisposable
{
    void BeginPainting ();
    void Clear ();
    void DrawLines (int[] coords);
    void DrawPoint (int x, int y);
    void EndPainting ();
    void PaintCurve (PaintOperation operation, int[] coords);
    void PaintPolygon (PaintOperation operation, int[] coords);
    void PaintRectangle (PaintOperation operation, int x, int y, int width, int height);
    void SetHighQualityLevel (bool highQuality);
    void SetStyle (PaintingStyle style);
}

public class PaintingStyle
{
    public PaintingStyle()
    {
    }

    public PaintingStyle(int penColor)
    {
        this.penColor = penColor;
    }

    public int PenColor
    {
        get { return penColor; }
        set { penColor = value; }
    }

    public float PenWidth
    {
        get { return penWidth; }
        set { penWidth = value; }
    }

    private int penColor;
    private float penWidth;
}

public enum PaintOperation
{
    Outline,
    Fill,
    FillAndOutline,
}
于 2009-04-05T19:29:36.130 回答
0

您的确切问题在 Kent Beck 和 Ralph Johnson 的以下论文“Patterns Generate Architectures”中使用编辑器模式进行了描述和解决:

https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.46.8603&rep=rep1&type=pdf

于 2021-09-05T16:52:15.777 回答