6.1 Уроки OpenGL. Создание растрового редактора на OpenGL + C#.

Целью данной главы будет создание простого растрового редактора в OpenGL с использованием C# .NET - небольшого приложения, по своей функциональности напоминающего Windows Paint.
Важные темы, рассматриваемые в данной части главы:
  1. Введение, постановка задач.
  2. Изучение модели работы программы. Описание создаваемых классов и методов их взаимодействий.
  3. Создание базовой оболочки программы.
  4. Создание дополнительных классов и их базовых методов.

1. Введение, постановка задач.

Так как нам придется реализовать довольно большой объем функциональности, мы разобьем создание приложение на части (функциональность программы будет расширяться от главы к главе):
  • Часть 6.1 (Вы ее сейчас читаете).
    В этой части главы мы опишем объектную модель и принципы того, как будет работать наша программа. Визуализацию конечной картинки мы всецело переложим на OpenGL, хотя для удобства и будем помогать себе стандартными классами C# .NET.
    После рассмотрения общего принципа функционирования программы, мы создадим основу оконного приложения, необходимые меню, панели инструментов и другие элементы.
    Создав основу оболочки программы, мы сначала добьемся минимального уровня работы программы – рисование лишь одной тестовой кистью, слой будет работать только один, цвет рисования нельзя будет изменять.
  • Часть 6.2.
    Здесь мы доведем до ума оболочку программы, а именно добавим несколько инструментов рисования, функцию выбора цвета.
  • Часть 6.3.
    Здесь мы завершим реализацию системы слоев и ускорим систему визуализации, использовав более быстрые методы визуализации в OpenGL.
  • Часть 6.4.
    Завершение оболочки программы - функционирование меню, взаимодействие элементов и т.д.
  • Часть 6.5.
    Оптимизация функции визуализации - добавление дисплейных списков, отрисовка массивов вершин.
На этом создание редактора будет завершено. Стоит помнить - что это не профессиональный редактор, а демонстрация основ работы с растровой графикой, а так же методов ее реализации с тесным использованием OpenGL.

2. Изучение модели работы программы. Описание создаваемых классов и методов их взаимодействий.

Итак, начнем мы с определения того, что понимается под растровым рисунком.

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

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

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

Сейчас мы реализуем не все функции, а лишь самые главные, которые должны обеспечить работоспособность программы – основа оболочки, класс движка, класс слоев, класс кистей. Работать на самом деле будет только один слой, одна кисть и алгоритмы инициализации OpenGL (в оболочке), а также визуализации.
Большую часть мы усложним и доработаем в последствии. Схема работы программы так же представлена на рисунке 1.
Уроки OpenGL + C#: Схема работы разрабатываемого растрового редактора Рисунок 1. Схема работы разрабатываемого растрового редактора.
Теперь когда наши цели определены и оглашена схема работы программы, мы перейдем к написанию её основы.

3. Создание базовой оболочки программы.

Создайте новый проект. В качестве шаблона установите приложение Windows Form.
Теперь создайте окно, на основе примера, представленного на рисунке 2.
Уроки OpenGL + C#: Создаваемое окно Рисунок 2. Создаваемое окно.
На окне будут расположены следующие элементы (описание в соответствии с цифрами на рисунке 2):
  1. Меню программы. На данный момент мы создадим 3 раздела меню: Файл, Рисование и Слои (их подменю можно увидеть на рисунке 3).
    Уроки OpenGL + C#: Примеры создаваемых меню Рисунок 3. Примеры создаваемых меню.
  2. Панель инструментов. Ширина панели 44 пикселя. Создайте на будущее заготовки 3-х кнопок. Эту панель в последующих частях главы мы заполним кнопками для установки режимов рисования кистей, рисования геометрических объектов и т.д.
  3. Элемент SimpleOpenGLControl. Расположите его так, как показано на рисунке, затем свойство name установите равным AnT. Здесь будет проходить основной render.
  4. Здесь находится элемент CheckedListBox. В следующей части главы мы реализуем систему слоев, которые будут активно участвовать в визуализации. В этом элементе будет отображаться список наших слоев. Оболочка также получит функции для редактирования параметров слоев.
  5. Здесь будет находиться панель инструментов. Пока что мы расположим на ней 2 кнопки. В дальнейшем кнопки этой панели будут отвечать за операции над слоями.
Если у вас возникли какие-либо вопросы в процессе создания элементов управления или настройки начальной инициализации элемента SimpleOpneGLControl, то напомним вам, что в главе 2.2 описывается процесс создания меню и панелей инструментов, а в главе 4.4 – процесс установки элементов для визуализации OpenGL и их первоначальной инициализации.

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

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

using Tao.OpenGl;
using Tao.FreeGlut;
using Tao.Platform.Windows; 


Конструктор формы выглядит следующим образом:

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

public Form1()
{ 
  InitializeComponent();
  // инициализация элемента SimpleOpenGLControl (AnT) 
  AnT.InitializeContexts();
}


Далее добавим код объявления экземпляра класса движка растрового редактора:

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

private anEngine ProgrammDrawingEngine; 


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

Теперь добавьте один таймер, назовите его RenderTimer. Затем добавьте обработку события загрузки формы.
Функция, вызываемая событием загрузки формы (чтобы ее добавить, щелкните двойным щелчком левой клавиши мыши по заголовку окна или перейдите через свойства формы приложения к списку доступных событий (event) и добавьте двойным щелчком в пустой области обработку события Load).

Эта функция будет содержать код инициализации OpenGL. Ее код представлен ниже:

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

private void Form1_Load(object sender, EventArgs e)
{ 

  // инициализация библиотеки GLUT 
  Glut.glutInit();
  // инициализация режима окна 
  Glut.glutInitDisplayMode(Glut.GLUT_RGB | Glut.GLUT_DOUBLE | Glut.GLUT_DEPTH);
  // устанавливаем цвет очистки окна 
  Gl. glClearColor(255, 255, 255, 1);
  // устанавливаем порт вывода, основываясь на размерах элемента управления AnT 
  Gl. glViewport(0, 0, AnT.Width, AnT.Height);
  // устанавливаем проекционную матрицу 
  Gl. glMatrixMode(Gl. GL_PROJECTION);
  // очищаем ее 
  Gl. glLoadIdentity();

  Glu.gluOrtho2D(0.0, AnT.Width, 0.0, AnT.Height);

  // переходим к объектно-видовой матрице 
  Gl. glMatrixMode(Gl. GL_MODELVIEW);

  ProgrammDrawingEngine = new anEngine(AnT.Width, AnT.Height, AnT.Width, AnT.Height);

  RenderTimer.Start();

}


Как видно из кода, мы создаем 2D ортогональную проекцию, причем в отличие от кода, который мы использовали в предыдущих главах, здесь мы устанавливаем проекцию таким образом, что размер видимой области в проекции будет равен размерам элемента AnT. Другими словами, координата X на элементе AnT будет равна координате X видимой части в координатной системе OpenGL. Но координатная ось Y направлена в противоположенную сторону, поэтому координата Y в координатной системе OpenGL будет равна AnT.HeightY’ (Y’ – координата Y на элементе AnT).

После настройки проекции мы инициализируем экземпляр движка растрового редактора и активируем таймер.
В настройках таймера добавьте обработчик события Tick, либо выделив элемент таймер (далее перейти к его свойствам – меню Event, затем двойной щелчок на пустой строке справа от события event), либо просто двойным щелчком на элементе RenderTimer.
В свойствах таймера установите интервал равным 30 миллисекундам.

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

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

// событие Tick таймера 
private void RenderTimer_Tick(object sender, EventArgs e)
{ 
  // вызываем функция рисования 
  Drawing();
}

// функция рисования 
private void Drawing()
{
  // очистка буфера цвета и буфера глубины 
  Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);
  // очищение текущей матрицы 
  Gl.glLoadIdentity();
  // установка черного цвета 
  Gl.glColor3f(0, 0, 0);

  // визуализация изображения из движка 
  ProgrammDrawingEngine.SwapImage();

  // дожидаемся завершения визуализации кадра 
  Gl.glFlush();
  // сигнал для обновление элемента, реализующего визуализацию. 
  AnT.Invalidate();
}


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

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

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

// функция обработчик события движения мыши (событие MouseMove для элемента AnT) 
private void AnT_MouseMove(object sender, MouseEventArgs e)
{ 

  // если нажата левая клавиша мыши 
  if(e.Button == MouseButtons.Left)
    ProgrammDrawingEngine.Drawing(e.X, AnT.Height - e.Y);

}


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

Здесь немного коснемся того, что же происходит. С инициализацией OpenGL в функции Form1_Load, созданием счетчиков и назначением обработчиков событий – все понятно (иначе вы не выполняли предыдущие главы).

Функция обработки движения мыши проверяет, нажата ли левая клавиша мыши. Если да, то будет вызываться метод рисования кистью (Drawing), который мы далее реализуем в классе anEngine. В этот метод в качестве параметров передаются значения координаты на элементе AnT - X и AnT.HeightY (так как оси Y у нас направлены в противоположенные стороны, мы это уже обсудили в настройках инициализации OpenGL).

Функция Drawing выполняет стандартные операции очистки буфера глубины и цвета, очищение объектно-видовой матрицы, после чего вызывается функция, отвечающая за визуализацию рисунка.
Вот и всё.

Теперь прейдем к созданию «ядра» или «движка» нашей программы.

4. Создание дополнительных классов и их базовых методов.

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

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

using System;
using System.Collections;
using System.Drawing;
using System.Linq;
using System.Text;
using System.IO;

namespace My_Paint
{ 

  class anEngine
  { 

  }

}


Приступим к его обновлению: первым делом добавим все пространства имен, что и в файле Form1.cs, чтобы в будущем не столкнуться с тем, что какое-либо пространство имен не подключено.

Подключаемые пространства имен:

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

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using Tao.OpenGl;
using Tao.FreeGlut;
using Tao.Platform.Windows; 


Теперь переключимся на добавление в исходный код еще двух классов: anLayer и anBrush.

Начнем с anBrush. Разместите код этого класса перед кодом заготовки класса anEngine. Пока он будет не очень разнообразным: вся его функциональность будет сводиться к хранения маски кисти. Причем на данном этапе (создания заготовки всех классов и настройки их работоспособности) мы занесем в конструктор стандартную кисть в виде крестика.

Код класса anBrush:

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

public class anBrush
{ 

  // объект Bitmap, в котором мы будем хранить битовую карту нашей кисти. 
  // этот объект выбран из-за удобства, а также из-за того, что в будущем мы добавим возможность загрузки кистей из графических файлов с расширением bmp. 
  public Bitmap myBrush;

  // конструктор класса 
  public anBrush()
  { 
    // создаем плоскость 5х5 пикселей 
    myBrush = new Bitmap(5, 5);

    // заполняем все пиксели красным цветом (все пиксели красного цвета мы будем считать не значимыми, 
    // а черного – значимыми при рисования кистью 
    // для установки пискеля, как видно из кода, используется функция SetPixel. 
    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);

  }

}


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

Кисть, которую мы задаем по умолчанию кодом:

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

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


Легче представить и понять смысл этого кода, изучив рисунок полученного массива чисел (рис. 4).
Уроки OpenGL + C#: Принцип, по которому задается маска кисти Рисунок 4. Принцип, по которому задается маска кисти.
Теперь рассмотрим класс anLayer. Его задачей будет хранение данных закрашенных пикселей одного слоя изображения. Помимо хранения графических данных, слой также содержит алгоритм рисования на основе выбранной кисти: когда пользователь пытается что-либо нарисовать на окне, зажав левую клавишу мыши и перемещая курсор по элементу AnT, оболочка получает событие мыши, которое обрабатывается (корректируется координата Y) и передается в экземпляр класса anEngine с помощью функции Drawing. Этот код мы уже рассмотрели.

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

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

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

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

public class anLayer
{ 

  // размеры экранной области 
  public int Width, Heigth;

  // массив, представляющий область рисунка (координаты пикселя и его цвет), 
  private int[,,] DrawPlace;

  // который будет хранить растровые данные для данного слоя
  public int[, ,] GetDrawingPlace()
  {
    return DrawPlace;
  }

  // флаг видимости слоя: true - видимый, false - невидимый 
  private bool isVisible;

  // текущий установленный цвет 
  private Color ActiveColor;

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

  public anLayer(int s_W, int s_H)
  { 
  
    // запоминаем значения размеров рисунка 
    Width = s_W;
    Heigth = s_H;

    // создаем в памяти массив, соответствующий размерам рисунка 
    // каждая точка на плоскости массива будет иметь 3 составляющие цвета 
    // + 4 ячейка - флаг, о том что данный пиксель пуст (или полностью прозрачен) 
    DrawPlace = new int[Width, Heigth, 4];

    // проходим по всей плоскости и устанавливаем всем точкам флаг,
    // сигнализирующий, что они прозрачны 
    for (int ax = 0; ax < Width; ax++)
    {
      for (int bx = 0; bx < Heigth; bx++)
      { 
        // флаг прозрачности точки в координатах ax,bx. 
        DrawPlace[ax, bx, 3] = 1;
      }
    }

    // устанавливаем флаг видимости слоя (по умолчанию создаваемый слой всегда видимый) 
    isVisible = true;

    // текущим активным цветом устанавливаем черный 
    // в следующей главе мы реализуем функции установки цветов из оболочки. 
    ActiveColor = Color.Black;
  }

  // функция установки режима видимости слоя 
  public void SetVisibility(bool visiblityState)
  { 
    isVisible = visiblityState;
  }

  // функция получения текущего состояния видимости слоя 
  public bool GetVisibility()
  { 
    return isVisible;
  }

  // функция рисования 
  // получает в качестве параметров кисть для рисования и координаты, 
  // где сейчас необходимо перерисовать пиксели заданной кистью 
  public void Draw(anBrush BR, int x, int y)
  { 
    // определяем позиция старта рисования 
    int real_pos_draw_start_x = x - BR.myBrush.Width / 2;
    int real_pos_draw_start_y = y - BR.myBrush.Height / 2;

    // корректируем ее для не выхода за границы массива 
    // проверка на отрицательные значения (граница "справа") 
    if(real_pos_draw_start_x < 0)
      real_pos_draw_start_x = 0;

    if(real_pos_draw_start_y < 0)
      real_pos_draw_start_y = 0;

    // проверки на выход за границу "справа" 
    int boundary_x = real_pos_draw_start_x + BR.myBrush.Width;
    int boundary_y = real_pos_draw_start_y + BR.myBrush.Height;


    if(boundary_x > Width)
    boundary_x = Width;

    if(boundary_y > Heigth)
    boundary_y = Heigth;

    // счетчик пройденных строк и столбцов массива, представляющий собой маску кисти 
    int count_x = 0, count_y = 0;

    // цикл по области с учетом смещения кисти и коррекции для невыхода за границы массива 
    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++)
      { 
      
      // получаем текущий цвет пикселя маски 
      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;
        }
      }

    }

  }

  // функция визуализации слоя 
  public void RenderImage()
  { 
    // данную функцию мы улучшим в следующих частях, для того чтобы получить более быструю визуализацию, 
    // но пока она будет выглядеть следующим образом 
    // активируем режим рисования точек 
    Gl.glBegin(Gl.GL_POINTS);

    // проходим по всем точкам рисунка 
    for (int ax = 0; ax < Width; ax++)
    { 
      for (int bx = 0; bx < Heigth; bx++)
      {
        // если точка в координатах ax,bx не помечена флагом "прозрачная",
        if (DrawPlace[ax, bx, 3] != 1)
        {
          // устанавливаем заданный в ней цвет 
          Gl.glColor3f(DrawPlace[ax, bx, 0], DrawPlace[ax, bx, 1], DrawPlace[ax, bx, 2]);
          // и выводим ее на экран 
          Gl.glVertex2i(ax, bx);
        }

      }
    }
    // завершаем режим рисования 
    Gl.glEnd();
  }

}


Код очень подробно прокомментирован и довольно прост. Поясним лишь функцию рисования: в нее передаются в качестве параметров кисть и координаты, в которых должно пройти рисование.
Так как кисть в ширину и высоту может быть больше одного пикселя, нам необходимо определить середину кисти (для этого мы получаем ее ширину и высоту, через свойства Width и Heigth элемента myBrush (который, кстати, является public элементом).

Далее мы вычитаем из координаты, где должно произойти рисование, половину ширины кисти для оси X и половину высоты для оси Y. При этом мы можем получить отрицательные значения, если, например, кисть будет шириной 10 пикселей, нам, следовательно, надо будет сместиться на 5 пикселей влево, чтобы начать алгоритм рисования кистью. Но что если при этом координата X точки, где должно пройти рисование будет равна, к примеру, 2? Тогда мы получим отрицательно индекс элемента массива, откуда должно начаться рисование и получим ошибку.

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

Теперь остается проанализировать код класса anEngine. В нем мы заранее объявим некоторые методы, которые пока не будут задействованы, но они нам еще точно пригодятся.

Код класса anEngine:

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

// класс, реализующий "ядро" нашего растрового редактора. 
public class anEngine
{

  // размеры изображения 
  private int picture_size_x , picture_size_y;

  // положение полос прокрутки будет использовано в будущем 
  private int scroll_x, scroll_y;

  // размер оконной части (объекта AnT) 
  private int screen_width, screen_height;

  // номер активного слоя 
  private int ActiveLayerNom;

  // массив слоев 
  private ArrayList Layers = new ArrayList();

  // стандартная кисть 
  private anBrush standartBrush;

  // конструктор класса 
  public anEngine(int size_x, int size_y, int screen_w, int screen_h)
  { 
  
    // при инициализации экземпляра класса сохраним настройки 
    // размеров элементов и изображения в локальных переменных 

    picture_size_x = size_x;
    picture_size_y = size_y;

    screen_width = screen_w;
    screen_height = screen_h;

    // полосы прокрутки у нас пока отсутствуют, поэтому просто обнулим значение переменных 
    scroll_x = 0;
    scroll_y = 0;

    // добавим новый слой для работы, пока он будет единственным 
    Layers.Add( new anLayer(picture_size_x, picture_size_y) );

    // номер активного слоя - 0 
    ActiveLayerNom = 0;

    // и создадим стандартную кисть 
    standartBrush = new anBrush();

  }

  // функция для установки номера активного слоя 
  public void SetActiveLayerNom(int nom)
  { 
    ActiveLayerNom = nom;
  }

  // установка видимости / невидимости слоя 
  public void SetWisibilityLayerNom(int nom, bool visible)
  { 
  // вернемся к этой функции в следующей части главы
  }

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

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

}


Как видите, некоторые функции объявлены, но не несут никакой функциональной нагрузки.

Визуализация реализована так же, как просто вызов визуализации одного единственного слоя.

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

Но даже такой, на первый взгляд, недописанный код уже создает минимальный объем объектной модели программы, который заставит ее работать. На рисунке 5 вы можете видеть пример работы программы.
Уроки OpenGL + C#: Проверка работы основы 2D растрового редактора Рисунок 5. Проверка работы основы 2D растрового редактора.
Откомпелировав приложение, вы также можете удостовериться в работе функции рисования.

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

Примечания

Если вы используете Visual Studio 2010, то вам необходимо добавить строку:

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


using System.Collections;


для возможности использования ArrayList в вашем приложении.

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

^