13.8. Графический дизайнер

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

Допустим, что мы хотим написать редактор сада, в котором можно визуально сажать различные розы. В нашем примере мы ограничимся только одним типом и одним рисунком розы. Класс для описания каждой розы или горшка с розой можно увидеть в листинге 13.3.

Листинг 13.3. Класс розы

class Rose
{
  const int DEFAULT_WIDTH = 50;
  const int DEFAULT_HEIGHT = 46;
  Image roseImage = Designer.Properties.Resources.Roze;

  public Rose(string name, int x, int y)
  {
    this.Name = name;
    this.X = x;
    this.Y = y;
    this.Width = DEFAULT_WIDTH;
    this.Height = DEFAULT_HEIGHT;
  }

  #region Свойства
  public string Name { get; set; }
  public int X { get; set; }
  public int Y { get; set; }
  public int Width { get; set; }
  public int Height { get; set; }

  public Size Size
  {
    get { return new Size(Width, Height); }
  }

  public Point Location
  {
    get { return new Point(X, Y); }
  }

  #endregion

  #region Открытые Методы

  public void Draw(Graphics g)
  {
    g.DrawImage(roseImage, X, Y, Width, Height);
    g.DrawString(Name, SystemFonts.DefaultFont,
        SystemBrushes.WindowText, new Point(X, Y + Height));
  }

  #endregion
}

Для того чтобы роза рисовалась, ей нужно изображение (картинка), и обратите внимание, как мы ее используем:

   Image roseImage = Designer.Properties.Resources.Roze;

Здесь заводится переменная roseImage, которой присваивается значение Designer.Properties.Resources.Roze, где:

  • Designer — пространство имен;
  • Properties — это реальная папка, которую вы можете увидеть в Solution Explorer или даже на диске в своем проекте;
  • Resources — файл ресурсов в этой папке, в котором хранятся глобальные ресурсы проекта;
  • Roze — имя ресурса.

Вот так очень легко и удобно обращаться к картинкам, которые сохранены в ресурсах. У вас еще нет такого ресурса? Настало время его создать. Создайте графический файл с именем Roze. Раскройте папку Properties в панели Solution Explorer и щелкните двойным щелчком по файлу Resources. Перед вами откроется редактор ресурсов. Щелкните по кнопке со стрелкой вниз справа от кнопки Add Resource, чтобы вызвать выпадающее меню. В выпадающем меню выберите Add Existing File. Выберите созданный файл Roze и добавьте его.

В главной форме нам понадобятся следующие поля для реализации задачи:

   List roses = new List(); // массив роз
   Rose SelectedRose = null;            // выделенная роза
   Boolean dragging;             // режим перетаскивания
   Point startDragPoint;         // точка начала перетаскивания

Две переменные: dragging и startDragPoint будут использоваться при перетаскивании точно так же, как и при перетаскивании компонентов.

А как добавлять розы на форму, если у нас не будет компонентов? Все динамически, и рисовать мы их тоже будем динамически. Для начала создайте на форме меню и в нем какой-нибудь пункт для добавления розы, а также напишите следующий код, который будет выполняться по его нажатию:

   Rose rose = new Rose("Роза " + roses.Count.ToString(), 0, 0);
   roses.Add(rose);
   designerPanel.Invalidate();

Мы создаем объект розы и добавляем ее в список. Теперь просим панель designerPanel перерисоваться. Да, мы снова будем использовать в качестве контейнера панель, поэтому поместите ее на форму, растяните по форме и измените ее имя на designerPanel. Теперь создайте обработчик события Paint для панели и в нем напишите следующий код:

   foreach (Rose rose in roses)
     rose.Draw(e.Graphics);

В цикле перебираются все розы из списка и вызывается их метод Draw(), который есть в листинге 13.3. Этому методу нужен объект класса Graphics, на котором должна рисоваться роза. То, что мы реализовали код рисования в классе розы, — не просто так. Достаточно изменить объект, и будет уже другой рисунок.

Полностью весь код с возможностями перемещения по форме мы здесь рассматривать не станем — вы можете его увидеть в примере, размещенном в электронном архиве, сопровождающем книгу. Здесь же мы рассмотрим лишь самое интересное. Для начала взглянем на обработчик события MouseDown:

   private void designerPanel_MouseDown(object sender, MouseEventArgs e)
   {
     Rose rose = GetItemAt(e.X, e.Y);
     if (rose != null)
     {
       SelectedRose = rose;
       dragging = true;
       startDragPoint = e.Location;
       DrawDraggingShape();
     }
   }

Мы должны определить, по какой розе щелкнули. У нас есть список роз, и у каждой из них мы знаем позицию и размеры, а значит, можем определить, где щелкнули мышью. Так как возможность определения объекта по координатам в реальном приложении может понадобиться в разных местах, я оформил это в виде отдельного метода GetItemAt(), который и вызывается здесь в первой строке. В качестве параметров метод получает координаты и возвращает розу. Координаты мыши, где щелкнул пользователь, определить несложно, они передаются в свойствах X и Y второго параметра обработчика события. Если роза не найдена, то результат равен нулю. Код метода можно увидеть в листинге 13.4. Он очень простой, поэтому оставим без комментариев.

Листинг 13.4. Определение розы в указанной позиции

Rose GetItemAt(int x, int y)
{
  foreach (Rose currRose in roses)
  {
    if (
      currRose.X < x &&
      currRose.X + currRose.Width > x &&
      currRose.Y < y &&
      currRose.Y + currRose.Height > y
    )
    return currRose;
  }
  return null;
}

Вернемся к методу обработки события MouseDown. Если мышью щелкнули на компоненте розы, и соответствующий объект найден, то устанавливаем переменную dragging в true, что будет означать режим перетаскивания объекта, сохраняем для удобства в переменной SelectedRose найденный объект розы и вызываем метод DrawDraggingShape(), который выглядит следующим образом:

   void DrawDraggingShape()
   {
     Point point = designerPanel.PointToScreen(SelectedRose.Location);
     ControlPaint.DrawReversibleFrame(new Rectangle(point,
     SelectedRose.Size), SystemColors.ButtonFace, FrameStyle.Dashed);
   }

Задача метода — нарисовать рамку перетаскивания. Когда вы перетаскиваете панели по окну или выделяете что-то в графическом редакторе, то рамка рисуется пунктирной линией. Вот что-то подобное и мы нарисуем. Для этого используется статичный метод DrawReversibleFrame() класса ControlPaint. Рамка, которую ри- сует этот метод, особенная. Когда мы рисуем ее в первый раз, то она появляется на экране. Если нарисовать рамку второй раз в тех же координатах, то рамка исчезает.

Обратите внимание — я сказал, что рамка появляется на экране, а не на форме или компоненте. Да, именно на экране, и размеры рамки, которые нужно передавать методу, именно в экранных координатах. Обычно мы рисуем внутри компонентов и ограничены холстом поверхности компонента. В данном случае рамка может быть нарисована на всей поверхности рабочего экрана. Наша роза позиционируется относительно компонента родителя (панели), а чтобы получить экранные координаты, мы используем метод PointToScreen() панели, т. е. компонента родителя.

Второй параметр метода DrawReversibleFrame() — это цвет рамки, а третий параметр определяет форму линий рисуемого прямоугольника.

Последнее, что мы увидим, — это код метода обработки события передвижения курсора мыши:

    private void designerPanel_MouseMove(
        object sender, MouseEventArgs e)
    {
       if (dragging)
       {
          DrawDraggingShape();
          SelectedRose.X = SelectedRose.Location.X +
              (e.Location.X — startDragPoint.X);
          SelectedRose.Y = SelectedRose.Location.Y +
              (e.Location.Y — startDragPoint.Y);
          DrawDraggingShape();
          startDragPoint = e.Location;
      }
   }

Если идет перетаскивание, то сначала мы вызываем метод DrawDraggingShape(), чтобы нарисовать рамку в текущих координатах, т. е. удалить ту рамку, что уже была нарисована ранее. После этого рассчитываем новые координаты объекта и снова рисуем рамку с помощью метода DrawDraggingShape(). На этот раз рамка будет нарисована в новой позиции.

Пример получился очень познавательным — мы познакомились с несколькими интересными трюками программирования. Такой метод дизайнера объектов намного эффективнее и удобнее, потому что позволяет создать очень гибкие решения. Я в своих решениях использую именно такие дизайнеры во всех случаях, где пользователь должен иметь возможность строить какие бы то ни было диаграммы. Например, программа CyD Network Utilities (russia.cydsoft.com) использует дизайнер именно такого стиля для рисования сети. Только вместо объекта розы в программе сетевой объект, имеющий свойство наподобие ObjectKind, которое определяет тип объекта, и в зависимости от этого типа рисует на форме компьютер, сервер, принтер или любое другое устройство вашей сети.

Предыдущая глава

13.7. Работа с картинками в C#

О блоге

Программист, автор нескольких книг серии глазами хакера и просто блогер. Интересуюсь безопасностью, хотя хакером себя не считаю

Обратная связь

Без проблем вступаю в неразборчивые разговоры по e-mail. Стараюсь отвечать на письма всех читателей вне зависимости от страны проживания, вероисповедания, на русском или английском языке.

Пишите мне