Шаблон проектирования высокого уровня для инструментов редактирования изображений - PullRequest
6 голосов
/ 05 апреля 2009

Я недавно начал создавать инструмент для редактирования изображений, который будет удовлетворять очень специфические потребности. Это как для людей, которые собираются использовать его, так и для моих собственных развлечений. однако, я столкнулся с небольшим архитектурным препятствием на раннем этапе.

Как и любой графический редактор, пользователь будет использовать «инструменты» для рисования и манипулирования изображением. Моя первая попытка сделать это состояла из простого интерфейса:

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

Это (я думал) будет красиво и чисто и позволит легко обслуживать и расширять. Просто добавьте объекты интерфейса и вызовите метод DrawEffect для выбранного объекта во время выполнения.

Проблема с этим подходом состоит в том, что различные инструменты рисования не придерживаются одного интерфейса. Например, инструмент пера должен знать только точку для рисования, чтобы работать. Прямоугольник, однако, нуждается в нажатии на первую точку, а также в текущую позицию. Инструмент многоугольника должен отслеживать несколько щелчков мыши.

У меня проблемы с поиском хорошего способа реализации этого. Лучший метод, который я могу придумать сейчас, будет включать оператор switch и регистр для каждого инструмента, что будет означать, что логика рисования будет в классе Canvas, а не в объектах типа Tool. потому что это практика, я хотел бы сделать это правильно. Спасибо за любую помощь заранее.

Ответы [ 3 ]

1 голос
/ 07 апреля 2009

Как насчет того, чтобы сделать свой интерфейс немного сложнее? Давайте начнем с некоторого кода, а потом я объясню, как он должен работать.

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).

Особый случай - метод отбрасывания. Это будет вызвано, чтобы "удалить" выбранный в данный момент инструмент. Это произойдет, если на панели инструментов или подобной будет выбрана другая реализация.

Вы также можете объединить это определение с шаблоном команды. В этом случае реализация AbstractDrawingTool будет отвечать за создание экземпляров интерфейса Command и, возможно, поместит их в стек после завершения операции (т. Е. Поместит точку на холст).

1 голос
/ 05 апреля 2009

Хорошо, эмпирическое правило: если вы видите оператор 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){ // ...
   }
}

Теперь подумайте, что бы вы сделали, если бы хотели реализовать отменить ?

Обновление

Хорошо, давайте расширим это немного подробнее. Смысл использования этого шаблона - убедиться, что клиенту не нужно знать все, кроме случаев, когда вы делаете первоначальную конструкцию. Итак, для этого примера давайте подумаем о рисовании прямоугольника. Когда вы выберете инструмент 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. Когда вы создаете команду, вы должны построить код, чтобы объект знал, как отменить себя. Тогда отменить означает просто удалить последний объект Command из списка и выполнить его метод undoThis.

0 голосов
/ 05 апреля 2009

Я столкнулся с аналогичной проблемой при попытке изменить мой mapping SW для поддержки графических библиотек GDI + и Cairo. Я решил это, сократив интерфейс рисования до некоторых распространенных операций / примитивов, см. Код ниже.

После этого «эффектами», которые вы хотите нарисовать, являются команды (как говорит Чарли). Они используют IPainter интерфейс для рисования. Хорошая вещь в этом подходе состоит в том, что эффекты полностью отделены от конкретного механизма рисования, такого как GDI +. Это очень удобно для меня, поскольку я могу экспортировать свой рисунок в SVG, переключившись на движок Cairo.

Конечно, если вам нужны дополнительные графические операции, вам придется расширить с ним интерфейс 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,
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...