Esate.ru
Esate.ru Уроки Программирование 3D Уроки OpenGL + C#Создание растрового редактора - часть 3. Система слоев.

Уроки OpenGL + C#

Выполняя главы последовательно, вы ознакомитесь с основами синтаксиса C#, увидите, как просто создавать оконные приложения с помощью .net, познакомитесь с библиотекой Tao, которая обеспечивает поддержку OpenGl в среде .NET, изучите основы 2D визуализации, работу как с примитивами, так и принцип загрузки и построения сложных 3D моделей , экспортированных из 3D редакторов.

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);
} 





Источник: Esate.ru
08 Января 2010


Комментарии (из ветки форума)


Мне нравится0
В функции &amp;quot;добавить слой&amp;quot; должно инкрементироваться и значение AllLayrsCount, и именно с помощью него нужно создавать имя нового слоя. Иначе, если удалить слой, а потом добавить, то повторяются имена.
Как-то так:
Код
private void добавитьСлойToolStripMenuItem_Click(object sender, EventArgs e)
        {
            LayersCount++; 
            AllLayrsCount++; //!
            ProgramDrawingEngine.AddLayer();
            
            int AddingLayerNum = LayersControl.Items.Add("Слой" + AllLayrsCount.ToString(), false); //!

            LayersControl.SelectedIndex = AddingLayerNum;
 
            ActiveLayer = AddingLayerNum;
        }
Хотя глянул приложенный к 6.5 проект - там так и есть, поправьте в уроке только(:


Функция удаления слоя заработала, только когда переставил местами вот эти две строки:
Код
LayersControl.SelectedIndex = 0;
ActiveLayer = 0;
Если делать, как в уроке, то при установке SelectedIndex равным нулю, выбрасывает OutOfRange в событии SelectedValueChanged: сам слой (скажем, шестой) уже удалён, но событие обращается к нему, ибо значение ActiveLayer ещё осталось необновлённым. Вроде бы надо так:
Код
ActiveLayer = 0;
LayersControl.SelectedIndex = 0;
А ещё чёт не нашёл, где в уроках разбирается функция ClearList().
Мне нравится1
А, нашёл ClearList в 6.5. Но на данном этапе, в этом уроке, наверное, её не должно было быть.

И да, клёвые уроки, респект автору!
Мне нравится0
Цитата
Александр написал:
А, нашёл ClearList в 6.5. Но на данном этапе, в этом уроке, наверное, её не должно было быть.
Привет.
Да-да, в исходные коды, прикрепленные к последнему уроку надо поглядывать, т.к. в первых частях могут быть ошибки.
Я пометил себе, что нужно перепроверить урок - 6й самый проблемный из всех, надо уже взять себя в руки и переписать его :D
Мне нравится0

Оставить сообщение:

Авторизируйтесь или Зарегистрируйтесь
чтобы оставлять комментарии.

OpenGL

OpenGL

OpenGL (Open Graphics Library — открытая графическая библиотека, графический API) — спецификация, определяющая независимый от языка программирования платформонезависимый программный интерфейс для написания приложений, использующих двумерную и трёхмерную компьютерную графику.

Регистрация

Регистрируясь, вы принимаете правила сайта. Если вы не получили код подтв. регистрации - не забудьте проверить папку спам.
Логин*
Email*
Пароль*
Подтверждение пароля*
 
Логин*
Код*
 

Восстановление пароля

Пожалуйста, заполните поля, после чего вы получите код подтверждения на ваш E-mail. Если код не пришел в течении нескольких минут - проверьте папку спам.
Логин

или Email
 
Логин*
Код подтверждения*
Новый пароль*
Подтверждение пароля*
 

Авторизация

Пожалуйста, авторизуйтесь, для входа на сайт с помощью соц. сети:
  • Используйте вашу учетную запись на Facebook.com для входа на сайт.
  • Используйте вашу учетную запись VKontakte для входа на сайт.
  • Используйте вашу учетную запись Google для входа на сайт.

или с помощью аккаунта на сайте:

Логин
Пароль