【问题标题】:High level design pattern for image editing tools图像编辑工具的高级设计模式
【发布时间】:2009-04-05 04:29:01
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

    标签: design-patterns oop interface-design


    【解决方案1】:

    好的,经验法则:如果您在代码草图中看到 switch 语句,则表明您需要改用多态性。因此,在这种情况下,您希望能够进行各种操作,并且您发现自己想要一个 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 会怎么做?

    更新

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

     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 方法。

    【讨论】:

    • 是的,当我看到 switch 语句自己写的时候我停了下来 :)。但是,我认为这不能解决我的问题。我想通过相同的界面使用这些工具,但如何让他们获得完成工作所需的(不同的)信息?
    • ...例如,DrawRectnagle 方法没有被 Command 接口公开,因此客户端仍然需要知道他们正在处理的是 DrawRectangle 对象,而不仅仅是一个 Command 对象。跨度>
    • 为什么?只要实现 Command 的对象知道该做什么,客户为什么要关心?我会在此添加更多内容,它不适合评论。
    【解决方案2】:

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

    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 接口的实例,并可能在操作完成后将它们放入堆栈(即在画布上放置一个点)。

    【讨论】:

      【解决方案3】:

      在尝试重新设计我的mapping SW 以支持 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,
      }
      

      【讨论】:

        【解决方案4】:

        Kent Beck 和 Ralph Johnson “Patterns Generate Architectures” 的论文中描述了 Smalltalk-80 HotDraw 应用程序中引入的 editor 模式来描述和解决您的确切问题。原始源代码在here,进一步改进在here

        该应用后来作为 JHotDraw 移植到 Java,并由 Dirk Riehle 在他的论文“Framework Design A Role Modeling Approach”chapter 8 中进行了描述。

        Objective-J 中还有一个实现,称为 cupDraw here

        【讨论】:

        • 请添加更多详细信息以扩展您的答案,例如工作代码或文档引用。
        猜你喜欢
        • 2022-06-14
        • 1970-01-01
        • 2016-09-14
        • 2011-07-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多