6.3 Создание растрового редактора - часть 3. Система слоев.

Необходимые знания:
Вам могут понадобится следующие статьи:
Целью данной главы будет завершить реализацию системы слоев и обновить систему их визуализации, повысив быстродействие, учитывая алгоритм их взаимодействия при визуализации.

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

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

Первым делом обратимся к форме нашего окна. Переименуем элемент checkedListBox1 в LayersControl.

Теперь перейдем к функции Form1_Load. Перед ней объявим три переменные:

/*http://esate.ru, Anvi*/

// текущий активный слой 
private int ActiveLayer = 0;

// счетчик слоев 
private int LayersCount = 1; // счетчик всех создаваемых слоев для генерации имен 
private int AllLayrsCount = 1; 



В коде самой функции (в конце) появится строка:

/*http://esate.ru, Anvi*/

// добавление элемента, отвечающего за управления главным слоем в объект LayersControl 
LayersControl.Items.Add("Главный слой", true ); 


Теперь мы рассмотрим добавление еще трех функций в оболочку программы, после чего перейдем к их реализации в коде движка программы.

Первым делом добавим обработчики функций «добавить слой» и «удалить слой».

Функция «добавить слой» – здесь мы создаем элемент с именем слоя, содержащим его порядковый номер, после этого мы выделяем слой, помечаем его как активный, после чего вызываем функцию движка AddLayer(), где будет выполняться код, дублирующий создания слоя, но уже не в оболочке программы для управления им, а в движке – для рисования. Функцию AddLayer в движок программы мы добавим позже.

/*http://esate.ru, Anvi*/

// функция добавления слоя 
private void добавитьСлойToolStripMenuItem_Click( object sender, EventArgs e)
{
  // счетчик созданных слоев 
  LayersCount ++; // вызываем функцию добавления слоя в движке графического редактора 
  ProgrammDrawingEngine.AddLayer();

  // добавляем слой, генерирую имя "Слой №" в объекте LayersControl. 
  // обязательно после функции ProgrammDrawingEngine.AddLayer();, 
  // иначе произойдет попытка установки активного цвета для еще не существующего цвета 
  int AddingLayerNom = LayersControl.Items.Add("Слой" + LayersCount.ToString(), false );

  // выделяем его 
  LayersControl.SelectedIndex = AddingLayerNom;

  // устанавливаем его как активный 
  ActiveLayer = AddingLayerNom;

}


Функция «удалить слой» – в ней мы сначала запрашиваем подтверждение на удаление слоя, с помощью MessageBox’а. Затем в случае, если удаляемый слой не нулевой, выполняем удаление выделенного слоя и вызываем функцию RemoveLayer движка графического редактора (код ее мы добавим в программу чуть позже).

/*http://esate.ru, Anvi*/

// функция удаления слоя 
private void удалитьСлойToolStripMenuItem_Click( object sender, EventArgs e)
{

  // запрашиваем подтверждение действия с помощью messageBox 
  DialogResult res = MessageBox.Show("Будет удален текущий активный слой, действительно продолжить?", "Внимание!", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); // если пользователь нажал кнопку "ДА" в окне подтверждения 
  if( res == DialogResult.Yes)
  { 
    // если удаляемый слой - начальный 
    if (ActiveLayer == 0)
    { 
      // сообщаем о невозможности удаления 
      MessageBox.Show("Вы не можете удалить нулевой слой.", "Внимание!", MessageBoxButtons.OK, MessageBoxIcon.Stop);
    }
    else // иначе 
    { 
      // уменьшаем значение счетчика слоев 
      LayersCount--;
      // сохраняем номер удаляемого слоя, т.к. SelectedIndex измениться после операций в LayersControl 
      int LayerNomForDel = LayersControl.SelectedIndex;
      // удаляем запись в элементе LayerControl (с индексом LayersControl.SelectedIndex - текущим выделенным слоем) 
      LayersControl.Items.RemoveAt(LayerNomForDel);
      // устанавливаем выделенным слоем нулевой (главный слой) 
      LayersControl.SelectedIndex = 0;
      // помечаем активный слой - нулевой 
      ActiveLayer = 0;
      // помечаем галочкой нулевой слой 
      LayersControl.SetItemCheckState(0, CheckState.Checked);
      // вызываем функцию удаления слоя в движке программы 
      ProgrammDrawingEngine.RemoveLayer(LayerNomForDel);
    }

  }

}


Теперь выделите элемент LayerControl, после чего добавьте ему обработчик события SelectedValueChanged.

Код этой функции:

/*http://esate.ru, Anvi*/

// данная функция будет обрабатывать изменения значения элементов LayersControl 
private void LayersControl_SelectedValueChanged( object sender, EventArgs e)
{
  // если отметили новый слой, необходимо снять галочку выделения со старого 
  if (LayersControl.SelectedIndex != ActiveLayer)
  { 
    // если выделенный индекс является корректным (больше либо равен нулю и входит в диапазон элементов) 
    if (LayersControl.SelectedIndex != -1 && ActiveLayer < LayersControl.Items.Count)
    { 
      // снимаем галочку с предыдущего активного слоя 
      LayersControl.SetItemCheckState(ActiveLayer, CheckState.Unchecked);
      // сохраняем новый индекс выделенного элемента 
      ActiveLayer = LayersControl.SelectedIndex;
      // помечаем галочкой новый активный слой 
      LayersControl.SetItemCheckState(LayersControl.SelectedIndex, CheckState.Checked);
      // посылаем сигнал движку программы об изменении активного слоя 
      ProgrammDrawingEngine.SetActiveLayerNom(ActiveLayer);
    }
  }
}


Теперь внесем изменения в код движка нашего графического редактора (класс anEngine).

Первым делом добавим функции, отвечающие за добавление и удаление слоев:

/*http://esate.ru, Anvi*/

// функция добавления слоя 
public void AddLayer()
{ 
  // добавляем слой в массив слоев ArrayList 
  int AddingLayer = Layers.Add( new anLayer(picture_size_x, picture_size_y));
  // устанавливаем его активным 
  SetActiveLayerNom(AddingLayer);
}
  
// функция удаления слоев 
public void RemoveLayer( int nom)
{
  // если номер корректен (в диапазоне добавленных в ArrayList 
  if (nom < Layers.Count && nom >= 0)
  { 
    // делаем активным слой 0
    SetActiveLayerNom(0);

    // очищаем дисплейный список данного слоя
    ((anLayer)Layers[nom]).ClearList();

    // удаляем запись о слое
    Layers.RemoveAt(nom);
  }
}


Как видите, здесь нет ничего сложного.

Теперь внесем изменения в функциях Drawing, SetColor и SwapImage.

Здесь вместо жестко вшитого нами ранее «нулевого» слоя теперь будет применяться активный слой, поэтому в коде этих функций будет использоваться переменная ActiveLayerNom вместо «0»:

/*http://esate.ru, Anvi*/

// рисование текущей кистью 
public void Drawing( int x, int y)
{ 
  // транслируем координаты, в которых проходит рисование стандартной кистью 
  ((anLayer)Layers[ActiveLayerNom]).Draw(standartBrush, x, y);
}

// функция установки активного цвета 
public void SetColor(Color NewColor)
{
  ((anLayer)Layers[ActiveLayerNom]).SetColor(NewColor);
  LastColorInUse = NewColor;
}


// визуализация 
public void SwapImage()
{ 
  // вызываем функцию визуализации в нашем слое 
  ((anLayer)Layers[ActiveLayerNom]).RenderImage();
}



Теперь необходимо внести небольшие изменения в функцию SetActiveLayerNom, т.к. при изменении активного слоя необходимо менять его текущий активный цвет.

Поэтому код этой функции будет выглядеть следующим образом:

/*http://esate.ru, Anvi*/

// функция для установки номера активного слоя 
public void SetActiveLayerNom( int nom)
{
  // новый активный слой получает установленный активный цвет для предыдущего активного слоя 
  ((anLayer)Layers[nom]).SetColor( ((anLayer)Layers[ActiveLayerNom]).GetColor() ); // установка номера активного слоя 
  ActiveLayerNom = nom;
}


Также необходимо добавить реализацию функции GetColor для класса anLayer:

/*http://esate.ru, Anvi*/

// получение текущего активного цвета 
  public Color GetColor()
  { 
  // возвращаем цвет 
  return ActiveColor;
  }
  

Система слоев готова. Теперь, если откомпилировать приложение, то в том случае, если вы правильно внесли все изменения кода программы, она сможет создавать слои, причем рисование будет происходить в конкретных устанавливаемых слоях.

Теперь мы изменим алгоритм визуализации: ранее мы визуализировали активный слой. Теперь визуализация будет строиться на основе всех существующих слоев.

/*http://esate.ru, Anvi*/

// визуализация 
  public void SwapImage()
  { 
  // вызываем функцию визуализации в нашем слое для всех существующих слоев 
  for( int ax = 0; ax < Layers.Count; ax++)
  ((anLayer)Layers[ax]).RenderImage();
  }
  

Вот и все. Теперь визуализируются все слои 2D-редактора. Имея основу системы слоев, в любой момент можно добавить ей различную функциональность: отключение какого-либо слоя, реализацию прозрачности и т.д.

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

Для этого достаточно сделать двойной щелчок на данных кнопках, а обработчики события создадутся автоматически. Остается только добавить вызовы функций, которые мы использовали для добавления и удаления слоев.

/*http://esate.ru, Anvi*/

// дублирование создания слоя 
  private void toolStripButton4_Click( object sender, EventArgs e)
  { 
  добавитьСлойToolStripMenuItem_Click(sender, e);
  } 
  
  // дублирование удаления слоя 
  private void toolStripButton5_Click( object sender, EventArgs e)
  {
  удалитьСлойToolStripMenuItem_Click(sender, e);
  }
  

При установке изображений на кнопки не забывайте указывать параметр ImageScaling равным none.

Система слоев пригодится нам, когда наше приложение по своей функциональности доберется до применения фильтров, описанных в главе 7.

Добавление элемента «стерка»

Для того чтобы добавить элемент «стерка», сначала добавим соответствующую кнопку на левую панель инструментов, а также вызов соответствующей функции из класса anEngine. Заодно мы продублируем элементы меню «Рисование», добавленные нами ранее.

Обработчик нажатия на данную кнопку будет выглядеть следующим образом:

/*http://esate.ru, Anvi*/

// обработка кнопки "стерка" на левой панели инструментов 
private void toolStripButton6_Click( object sender, EventArgs e)
{
  // установка кисти-стерки 
  ProgrammDrawingEngine.SetSpecialBrush(1);
}


Теперь перейдем к изменениям в исходном коде движка.
В первую очередь претерпела изменения система создания (установки) кистей. В классе anBrush появился закрытый флаг, сигнализирующий о том, что данная кисть является стеркой. Также появился метод для определения значения этого флага:

/*http://esate.ru, Anvi*/

// флаг, сигнализирующий о том, что установленная кисть является стеркой 
private bool IsErase = false ; // функция, которая будет использоваться для получения информации 
// о том, является ли данная кисть стеркой. 

public bool IsBrushErase()
{
  return IsErase;
}


При создании кистей теперь автоматически помечается, что они не являются стеркой (кроме специальной кисти номер 1, которая как раз и есть стерка).

Код конструктора с изменениями:

/*http://esate.ru, Anvi*/

public anBrush( int Value, bool Special)
{
  if (!Special)
  { 
    myBrush = new Bitmap(Value, Value);
    for ( int ax = 0; ax < Value; ax++)
    for ( int bx = 0; bx < Value; bx++)
      myBrush.SetPixel(ax, bx, Color.Black);

    // не является стеркой 
    IsErase = false ;
  }
  else
  { 
    // здесь мы будем размещать предустановленные кисти 
    // созданная нами ранее кисть в виде перекрестия двух линий будет кистью по умолчанию 
    // на тот случай, если задан не описанный номер кисти 
    switch (Value)
    { 
      // специальная кисть по умолчанию 
      default :
      { 
      
        myBrush = new Bitmap(5, 5);

        for ( int ax = 0; ax < 5; ax++)
        for ( int bx = 0; bx < 5; bx++)
          myBrush.SetPixel(ax, bx, Color.Red);

        myBrush.SetPixel(0, 2, Color.Black);
        myBrush.SetPixel(1, 2, Color.Black);

        myBrush.SetPixel(2, 0, Color.Black);
        myBrush.SetPixel(2, 1, Color.Black);
        myBrush.SetPixel(2, 2, Color.Black);
        myBrush.SetPixel(2, 3, Color.Black);
        myBrush.SetPixel(2, 4, Color.Black);

        myBrush.SetPixel(3, 2, Color.Black);
        myBrush.SetPixel(4, 2, Color.Black);

        // не является стеркой 
        IsErase = false ;

        break ;

      }
      case 1: // стерка 
      { 
        // создается так же, как и обычная кисть, 
        // но имеет флаг IsErase равный true 
        myBrush = new Bitmap(5, 5);

        for ( int ax = 0; ax < Value; ax++)
        for ( int bx = 0; bx < Value; bx++)
          myBrush.SetPixel(0, 0, Color.Black);

        // является стеркой 
        IsErase = true ;
        break ;

      }

    }

  }

}


Как видите, изменения буквально в 3 строках.

Теперь перейдем к следующему этапу – рисованию в слое.

Перейдите к функции Draw класса anLayer. Здесь, в цикле, где осуществлялся перебор пикселей из маски кисти, произошли следующие изменения: теперь, перед тем как нарисовать конкретный пиксель, производится проверка, не является ли данная кисть стеркой. Если да, то в том случае, если пиксель помечен в маске не прозрачным (для этого в маске мы договорились использовать красный цвет), то на рисунке он будет помечен как отсутствующий (невизуализируемый):

/*http://esate.ru, Anvi*/

...
// цикл по области с учетом смещения кисти и коррекции для невыхода за границы массива 
for ( int ax = real_pos_draw_start_x; ax < boundary_x; ax++, count_x++)
{
  count_y = 0;
  for ( int bx = real_pos_draw_start_y; bx < boundary_y; bx++, count_y++)
  { 
    // проверяем, не является ли данная кисть стеркой 
    if (BR.IsBrushErase())
    {
      // данная кисть - стерка. 
      // помечаем данный пиксель как не закрашенный // получаем текущий цвет пикселя маски 
      Color ret = BR.myBrush.GetPixel(count_x, count_y);

      // цвет не красный 
      if (!(ret.R == 255 && ret.G == 0 && ret.B == 0))
      {
        // заполняем данный пиксель соответствующим из маски, используя активный цвет 
        DrawPlace[ax, bx, 3] = 1;
      }

    }
    else
    { 

      // получаем текущий цвет пикселя маски 
      Color ret = BR.myBrush.GetPixel(count_x, count_y);

      // цвет не красный 
      if (!(ret.R == 255 && ret.G == 0 && ret.B == 0))
      { 
        // заполняем данный пиксель соответствующим из маски, используя активный цвет 
        DrawPlace[ax, bx, 0] = ActiveColor.R;
        DrawPlace[ax, bx, 1] = ActiveColor.G;
        DrawPlace[ax, bx, 2] = ActiveColor.B;
        DrawPlace[ax, bx, 3] = 0;
      }

    }

  }

}
…


Вот и все. Только помните, что стирание происходит исключительно тех пикселей, которые были нарисованы в данном слое.

В заключении, создадим обработчики нажатия элементов меню «Рисование». Добавьте для каждого пункта меню обработчик двойным щелчком мыши. Затем добавьте вызовы уже существующих функций (обработчиков нажатия на кнопки панели инструментов):

/*http://esate.ru, Anvi*/


// дублирование установки кисти "карандаш" из меню "рисование" 
private void карандашToolStripMenuItem_Click( object sender, EventArgs e)
{
  // вызываем уже существующую функцию 
  toolStripButton1_Click(sender, e);
} 
// дублирование установки кисти "кисть" из меню "рисование" 
private void кистьToolStripMenuItem_Click( object sender, EventArgs e)
{ 
  // вызываем уже существующую функцию 
  toolStripButton3_Click(sender, e);
}
// дублирование установки кисти "стерка" из меню "рисование" 
private void стеркаToolStripMenuItem_Click( object sender, EventArgs e)
{
  // вызываем уже существующую функцию 
  toolStripButton6_Click(sender, e);
} 


Нет доступа к просмотру комментариев.

^