图像编辑工具的高级设计模式

7

我最近开始创建一个图像编辑工具,它将满足非常特定的需求。这不仅是为了那些将要使用它的人,也是为了我的娱乐。然而,我在早期就遇到了一些架构上的问题。

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

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

我曾认为这种方法会更加简单明了,便于维护和扩展。只需添加接口对象并在运行时调用所选对象的DrawEffect方法即可。

但是,这种方法存在问题,不同的绘图工具并没有严格遵循单一接口。例如,钢笔工具只需要知道要绘制的点即可正常工作。然而,矩形需要记录第一个点击的点以及当前位置。多边形工具需要跟踪多个鼠标点击。

我正在思考如何实现这个功能。目前我能想到的最好的方法是使用switch语句,并为每个工具添加一个case语句,这意味着绘图逻辑将在Canvas类中,而不是由Tool类型对象封装。

因为这是一次实践,所以我希望能够用正确的方式来完成。感谢您提前提供的任何帮助。

4个回答

1

好的,经验法则是:如果你在代码草图中看到一个switch语句,那么这是一个需要使用多态性的信号。因此,在这种情况下,您希望能够拥有各种操作,并且发现自己需要一个switch,因此您应该思考“如何利用多态性使其成为可能?”

现在,让我们来看看命令模式,其中您的对象是动词而不是名词。每个命令实现一个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){ // ...
   }
}

现在,考虑一下如果你想要实现撤销功能,你会怎么做?

更新

好的,让我们进一步扩展。使用这种模式的重点是确保客户端并不需要了解太多,除非你正在进行原始构建。因此,对于这个例子,让我们考虑画一个矩形。当你选择一个矩形工具时,你将在按钮点击事件处理程序上有一些代码(这都是伪代码)。

 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”方法来实现撤销操作。当你创建一个命令时,必须构建代码,使得对象知道如何撤消自己。然后,撤销操作就意味着从列表中取出最后一个Command对象并执行其undoThis方法。


是的,当我看到 switch 语句自己编写时,我停了下来 :). 然而,我不认为这解决了我的问题。我想通过同一界面使用这些工具,但如何获取它们执行任务所需的(不同的)信息呢? - Ed S.
例如,DrawRectnagle方法未被Command接口公开,因此客户端仍需要知道他们正在处理的是一个DrawRectangle对象而不仅仅是一个Command对象。 - Ed S.
为什么?只要实现命令的对象知道该做什么,客户端为什么要关心呢?我会在下面添加更多内容,因为这不适合放在评论中。 - Charlie Martin

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事件,以便在按下特定键(即escape键)时停止绘制线条。

特殊情况是drop方法。如果从工具栏或类似工具中选择了另一个实现,则会调用该方法来“放弃”当前选定的工具。

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


0

当我尝试重新设计我的mapping SW以支持GDI+和Cairo图形库时,我遇到了类似的问题。我通过将绘图接口减少到一些常见的操作/基元来解决这个问题,如下所示。

在此之后,您想要绘制的“效果”是命令(就像Charlie所说)。它们使用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,
}

0

您的确切问题可以在 Smalltalk-80 的 HotDraw 应用程序中介绍的 editor 模式中找到解决方案,该模式由 Kent Beck 和 Ralph Johnson 在论文 “Patterns Generate Architectures” 中描述。原始源代码可在 here 找到,更多改进请参见 here

该应用程序后来被移植到 Java 并由 Dirk Riehle 在他的博士论文 "Framework Design A Role Modeling Approach"第8章 中进行了描述。

还有一个使用Objective-J实现的叫做cupDraw 这里


请添加更多细节以扩展您的答案,例如工作代码或文档引用。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接