Практикум прикладного программирования на C# в среде VS.NET 2005

Рисование графических примитивов средствами GDI+

Показывать лекцию целиком

Рисование графических примитивов средствами GDI+

Файлы к лабораторной работе Вы можете скачать здесь.

Компьютерная графика делится на два вида:

  • Векторная графика - практическое применение аналитической геометрии, включая рисование линий и заливку замкнутых областей, а также отображение текста с различными атрибутами.
  • Растровая графика - включает битовые карты и изображения предметов реального мира.
  • Ниже мы познакомимся с векторной графикой в том виде, в каком она реализована в Microsoft Windows Forms и GDI+. Все рассматриваемые здесь функции рисования являются методами класса Graphics и начинаются с префикса Draw (рисуют линии) или Fill (выполняют заливку в границах).

    Для рисования нужен объект Graphics, который определяет контекст устройства. Класс System.Drawing.Graphics объявлен как sealed (не поддерживает наследование) и его конструктор не является открытым, поэтому напрямую создать объект Graphics нельзя

  • Graphics gr = new Graphics(); // Нельзя!!!
  • class MyGraphics : Graphics{}; MyGraphics gr = new MyGraphics(); // Тоже нельзя!!!
  • Способы создания объекта Graphics

    Для создания объекта Graphics можно применить один из следующих способов:

  • При переопределении метода OnPaint() или создании обработчика события Paint в любом классе, производном от System.Windows.Forms.Control (например, Form) мы получаем объект Graphics как свойство Graphics класса PaintEventArgs
    class MyGraphics : Form
    {
      public MyGraphics()
      {
        this.Paint +=new PaintEventHandler(MyGraphics_Paint);
      }
      
      private void MyGraphics_Paint(object sender, PaintEventArgs e)
      {
        Graphics graphics = e.Graphics; // Это верно!
      }
    }
  • Для рисования на форме или элементе управления (все они наследуют Control ) не только в замещенном методе OnPaint() или обработчике события Paint, но и в других местах программы, можно вызывать метод

    Часто классы вызывают метод CreateGraphics() в своих конструкторах, чтобы получить нужные данные о GDI+ или выполнить инициализацию. Элементы управления или формы могут также рисовать во время событий мыши, клавиатуры или таймера, временно создавая и освобождая объект Graphics (контекст устройства). Главное, чтобы объект Graphics создавался только на период обработки события, и ни в коем случае не хранился в поле класса. По завершении использования объекта graphics программа должна немедленно вызывать его метод graphics.Dispose(), чтобы отдать контекст устройства другим программам.

  • Можно получить объект типа System.Drawing.Printing.PrintPageEventArgs, который в свойстве содержит подходящий для данного принтера объект Graphics, установив обработчик события PrintPage при печати.
  • У некоторых элементов управления (меню, списков, строк состояния) есть свойство OwnerDraw, позволяющее программе динамически рисовать на этом элементе управления. События MeasureItem и DrawItem передают своим обработчикам в аргументах объекты типа MeasureItemEventArgs и DrawItemEventArgs, у которых в свойствах имеется объект Graphics и который можно использовать в обработчике для рисования.
  • Чтобы рисовать по битовой карте или метафайлу, нужно получить специальный объект

  • Если нужно получить информацию из объекта Graphics, связанного с принтером, не печатая никаких данных, можно использовать метод

  • При взаимодействии с кодом Win32 получить объект Graphics позволяют статические методы уровня класса Graphics.FromHdc() и Graphics.FromHwnd()
  • Создание заготовки приложения для выполнения упражнений C#

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

  • Перейдите в каталог для размещения работы и создайте новое решение с именем Petzold5
  • Оболочка создаст новый подкаталог Petzold5 в выбранном каталоге размещения работы.

  • Щелкните правой кнопкой мыши на узле решения в панели Solution Explorer и добавьте к решению новый пустой проект с именем Test
  • В панели Solution Explorer щелкните правой кнопкой мыши на узле References (ссылки) проекта и выполните команду Add Reference контекстного меню
  • Добавьте ссылки на нужные нам библиотеки DLL как показано на снимке
  • Щелкните по узлу Test панели Solution Explorer и добавьте в проект новый класс с именем Start.cs
  • Мастер оболочки создаст заготовку, которую мы и будем использовать для испытания примеров, рассматриваемых далее.

    using System;
    
    namespace Test
    {
      /// 
      /// Summary description for Start.
      /// 
      public class Start
      {
        public Start()
        {
          //
          // TODO: Add constructor logic here
          //
        }
      }
    }

  • Добавьте в начало файла Start.cs нужные пространства имен, соответствующие добавленным ссылкам на библиотеки DLL. Сделайте наш класс наследником библиотечного класса Form. Удалите сгенерированные оболочкой комментарии и должен получиться такой код
  • using System;
    using System.Windows.Forms;
    using System.Drawing;
      
    namespace Test
    {
      public class Start : Form
      {
        public Start()
        {
          // В этом конструкторе будем размещать
          // настройки формы
        }
      }
    }

    Отметьте, что как только мы добавили к нашему классу базовый класс Form, в панели Solution Explorer пиктограмма класса заменилась на значек формы.

  • Добавьте к проекту новый пустой файл с именем AppClass.cs, в котором мы разместим точку входа приложения
  • Удалите из кода файла AppClass.cs лишние комментарии и конструктор. Окончательно содержащий точку входа класс должен иметь такой код
  • namespace Test
    {
      public class AppClass
      {
        // Точка входа приложения
        static void Main()
        {
          System.Windows.Forms.Application.Run(new Start());
        }
      }
    }

    Здесь мы из точки входа, расположенной в классе AppClass, создаем экземпляр главной формы вызовом конструктора класса Start.

  • Выполните команду меню оболочки Project/Test Properties и в поле Output Type вкладки Common Properties/General установите значение Windows Application, чтобы отключить окно командной строки приложения
  • В этом же окне свойств проекта установите параметр Startup Object в значение Test.AppClass

  • Задайте значение строки заголовка главной формы, разместив в конструкторе класса Start следующий код
  • using System;
    using System.Windows.Forms;
    using System.Drawing;
      
    namespace Test
    {
      public class Start : Form
      {
        public Start()
        {
          this.Text = "Главная форма Lab36B";
        }
      }
    }
  • Постройте проект и убедитесь, что пустая форма создается
  • Теперь мы поместим на главную форму элемент управления ListBox и заполним в нем строки заголовков упражнений, которые нам предстоит выполнить в этой лабораторной работе. Затем мы создадим обработчики событий выбора любого из упражнений, внутри которых будем запускать дочерние формы, соответствующие каждому отдельному упражнению. Упражнения будут демонстрировать возможности классов библиотеки .NET Framework и языка C#.

  • Откройте форму Start.cs в режиме View Designer и поместите на ней из панели Toolbox элемент управления ListBox. Выделите элемент управления ListBox и через панель Properties задайте значения его свойств в соответствии с таблицей
  • Значения свойств элемента управления ListBox
    Свойство Значение
    Name listBox
    Dock Fill
    Items
  • Рисование крестика в клиентской области формы (XMarksTheSpot)
  • Простая программа печати (HelloPrinter)
  • Разделение кода вывода на экран и печать (PrintableForm)
  • Очерчивание границы клиентской области (BoxingTheClient)
  • Рисование домика функцией DrawLines() (DrawHouse)
  • Построение синусоиды функцией DrawLines() (SineCurve)
  • Рисование эллипса функцией DrawLines() (PolyEllipse)
  • Рисование эллипса функцией DrawEllipse() (ClientEllipse)
  • Рисование спирали функцией DrawLines() (Spiral)
  • Прямоугольник по краю клиентской области (OutlineClientRectangle)
  • Рисование эллипса функцией DrawArc() (DashedEllipse)
  • Рисование прямоугольника со скругленными углами (RoundRect)
  • Рисование круговой диаграммы (PieChart)
  • Еще одна версия круговой диаграммы (BetterPieChart)
  • Заливка сложной фигуры в разных режимах (FillModesClassical)
  • Растяните форму Start.cs так, чтобы все строки списка были полностью в ней видны
  • Запустите приложение.
  • Мы не увидели тех изменений, которые внесли в форму и ожидали увидеть. Как и прежде, она оказалось пустой. Исправим это.

  • Щелкните на форме правой кнопкой мыши и выполните команду View Code
  • Мы видим, что оболочка сгенерировала соответствующий код, когда мы создавали пользовательский интерфейс. Весь сгенерированный код разместился в функции InitializeComponent(), но вызова этой функции в коде нет.

  • Поместите в конструктор класса Start код вызова функции InitializeComponent()
  • public Start()
        {
          this.Text = "Главная форма Lab36B";
          InitializeComponent();
        }
  • Постройте приложение - должен получиться такой результат
  • Теперь нам нужно создать обработчик, который бы при выборе любой строки списка запускал соответствующую форму.

  • Перейдите в режим View Designer формы Start и выделите компонент listBox. Установите в панели Properties режим Events и для события SelectedIndexChanged двойным щелчком на его поле значений создайте обработчик, который заполните так
  • private void listBox_SelectedIndexChanged(object sender, System.EventArgs e)
        {
          switch(listBox.SelectedIndex + 1)
          {
            case 1:
              XMarksTheSpot frm1 = new XMarksTheSpot();
              frm1.Show();
              break;
            case 2:
              HelloPrinter frm2 = new HelloPrinter();
              frm2.Show();
              break;
            case 3:
              PrintableForm frm3 = new PrintableForm();
              frm3.Show();
              break;
            case 4:
              BoxingTheClient frm4 = new BoxingTheClient();
              frm4.Show();
              break;
            case 5:
              DrawHouse frm5 = new DrawHouse();
              frm5.Show();
              break;
            case 6:
              SineCurve frm6 = new SineCurve();
              frm6.Show();
              break;
            case 7:
              PolyEllipse frm7 = new PolyEllipse();
              frm7.Show();
              break;
            case 8:
              ClientEllipse frm8 = new ClientEllipse();
              frm8.Show();
              break;
            case 9:
              Spiral frm9 = new Spiral();
              frm9.Show();
              break;
            case 10:
              OutlineClientRectangle frm10 = new OutlineClientRectangle();
              frm10.Show();
              break;
            case 11:
              DashedEllipse frm11 = new DashedEllipse();
              frm11.Show();
              break;
            case 12:
              RoundRect frm12 = new RoundRect();
              frm12.Show();
              break;
            case 13:
              PieChart frm13 = new PieChart();
              frm13.Show();
              break;
            case 14:
              BetterPieChart frm14 = new BetterPieChart();
              frm14.Show();
              break;
            case 15:
              FillModesClassical frm15 = new FillModesClassical();
              frm15.Show();
              break;
        }

    Создание заготовок для дочерних форм

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

  • Добавьте в проект новый класс с именем Template командой Project/Add Class
  • Отредактируйте код файла Template.cs так
  • using System;
    using System.Windows.Forms;
    using System.Drawing;
      
    namespace Test
    {
      public class Template : Form
      {
        public Template()
        {
        }
      }
    }
  • Сохраните изменения файла Template.cs на диске, выполнив команду File/Save All
  • В панели Solution Explorer выделите файл Template.cs, затем через меню Edit оболочки выполните команду Copy, а затем 15 раз подряд команду Paste или Ctrl-V . Таким образом мы получим 15 копий файла Template.cs
  • Выделите файл Template.cs и выполните команду Edit/Delete, чтобы удалить файл физически.
  • В панели Solution Explorer переименуйте полученные копии файлов в соответствии со следующим списком
  • XMarksTheSpot.cs
  • HelloPrinter.cs
  • PrintableForm.cs
  • BoxingTheClient.cs
  • DrawHouse.cs
  • SineCurve.cs
  • PolyEllipse.cs
  • ClientEllipse.cs
  • Spiral.cs
  • OutlineClientRectangle.cs
  • DashedEllipse.cs
  • RoundRect.cs
  • PieChart.cs
  • BetterPieChart.cs
  • FillModesClassical.cs
  • Откройте каждый из только что переименованных файлов в редакторе кода и присвойте классам и их конструкторам имена соответствующих файлов, например
  • using System;
    using System.Windows.Forms;
    using System.Drawing;
      
    namespace Test
    {
      public class XMarksTheSpot : Form
      {
        public XMarksTheSpot()
        {
        }
      }
    }
  • Постройте приложение и проверьте, что при щелчке на каждой строке списка создается соответствующая ей новая форма (пока еще пустая).
  • После таких грандиозных подготовительных операций перейдем к выполнению самих упражнений. В каждом упражнении мы будем ставить задачу и рассматривать ее решение с использованием библиотеки FCL (Framework Class Library) и средств C#.

    Упражнение 1. Рисование прямых линий

    Чтобы нарисовать линию, нужно определить ее толщину, цвет, стиль и другие параметры. Этим ведает класс Pen (перо), поэтому чтобы нарисовать линию, нужно прежде всего содать объект типа Pen. Перья могут быть созданы также и на основе кистей (класс Brush). В свою очередь, кисти можно создать из растровых изображений и графических контуров.

    Создать простое приро можно так

    Pen pen = new Pen(color);

    где color - экземпляр структуры System.Drawing.Color.

    Можно, также, использовать готовые статические неизменяемые свойства класса System.Drawing.Pens, каждое из которых возвращает объект Pen с толщиной линии 1. Это можно делать напрямую или через переменную, например

    Pen pen = Pens.Red;

    У класса System.Drawing.SystemPens 15 общих (статических, уровня класса) неизменяемых (только для чтения) свойств, которые возвращают объекты Pen на основе системных цветов. Но если нужно создать перо, гарантированно видимое на фоне, цвет которого выбрал пользователь, следует поставить цвет пера в зависимость от текущего значения свойства ForeColor элемента управления или формы, например

    Pen pen = new Pen(ForeColor);

    Для создания пера с нужной толщиной линии используется перегруженные конструкторы класса Pen, у которых последний параметр ожидает толщину

    Объекты Pen, которые возвращают свойства классов Pens и SystemPens, имеют тольщину 1. Пока можно считать, что это пикселы. Но реально толщина выражается в глобальных координатах и подвержена влиянию преобразований. Можно создать перья, толщина которых будет всегда равна 1 пикселу, независимо от преобразований. Для этого при создании пера следует указать в конструкторе толщину 0 или установить свойство пера Width равным 0, например

    Pen pen = new Pen(color, 0);
    Pen pen1 = new Pen(color); 
    pen1.Width = 0;

    Для рисования одиночных прямых служит метод DrawLine(), а для последовательности прямых - DrawLines() класса Graphics, применяемых к объекту.

    Метод DrawLine() рисует прямую от начальной точки до конечной, включая в линию сами точки. Например, вызов graphics.DrawLine(pen, 2, 2, 3, 3) рисует два пиксела между точками (2,2) и (3,3), а вызов graphics.DrawLine(pen, 3, 3, 3, 3) ничего не рисует.

    Размер клиентской области формы определяется ее свойством ClientSize, где ширина ClientSize.Width и высота ClientSize.Height измеряются в пикселах, нумерация пикселов начинается с нуля.

  • Ниже приведен пример программы, рисующей крестик в клиентской области
  • using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class XMarksTheSpot : Form
      {
        public XMarksTheSpot()
        {
          this.Text = "Крестик в клиентской области";
          this.Width += 50;
          this.Height /= 2;
          this.BackColor = System.Drawing.SystemColors.Window;
          this.ForeColor = System.Drawing.SystemColors.WindowText;
          this.ResizeRedraw = true;
        }
      
        protected override void OnPaint(PaintEventArgs e)
        {
          Graphics graphics = e.Graphics;
          Pen pen = new Pen(this.ForeColor);
          graphics.DrawLine(pen, // Перо
            0, 0, // Верхний левый угол клиентской области
            this.ClientSize.Width - 1, this.ClientSize.Height - 1);// Правый нижний
          graphics.DrawLine(pen, 
            this.ClientSize.Width - 1, 0, // Правый верхний
            0, this.ClientSize.Height - 1);// Левый нижний
      
          base.OnPaint (e);
        }
      }
    }

    Результат работы кода такой

    Упражнение 2. Использование некоторых возможностей печати документа

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

    Чтобы печатать на принтере по умолчанию, нужно создать объект типа System.Drawing.Printing.PrintDocument

    PrintDocument prndoc = new PrintDocument();

    Это класс содержит много методов, свойств и событий

    На данном этапе нам понадобятся только одно свойство, один метод и одно событие, а именно

  • prndoc. DocumentName (public string DocumentName [ get, set ]) - строковое свойство, в которой устанавливается идентификатор задания в очереди печати. Обычно программа, работающая с документом, заносит в это свойство свое имя (текст заголовка окна программы).
  • PrintPage - событие, в обработчике которого будем использовать вызовы на вывод графической информации.
  • prndoc. Print() - метод начала печати. Этот метод возвращает управление не сразу - некоторое время отображается информационное окно с именем документа и кнопкой, позволяющей отменить задание печати.
  • В результате исполнения метода Print() также вызывается обработчик события PrintPage с передаваемыми ему параметрами. Значением первого параметра Object является ранее созданный объект prndoc класса PrintDocument. Второй параметр имеет тип PrintPageEventArgs, свойства которого предоставляют информацию о принтере, в том числе свойство Graphics. Это свойство является объектом и содержит информацию для страницы принтера, а не для клиентской области формы. Методы объекта Graphics выводят информацию на страницу принтера.

    Если нужно напечатать несколько страниц, следует установить свойство HasMorePages переданного в обработчик объекта класса PrintPageEventArgs как true. Но в нашем примере будет печататься одна страница, поэтому оставим значение свойства HasMorePages по умолчанию (false) и обработчик события PrintPage сразу вернет управление.

    Свойство ClientSize формы предоставляет размеры своей клиентской области в пикселах. Этого недостаточно для страницы принтера. Страница принтера определяется тремя разными областями:

  • Полный размер страницы
  • Область печати, заданная пользователем
  • Предельная область печати, доступная принтеру или иному печатающему устройству
  • Полный размер страницы

    Об этом содержатся сведения в свойстве PageBounds переданного в обработчик объекта класса PrintPageEventArgs. Его значением является структура Rectangle, чьи свойства X и Y равны нулю, а свойства Width и Height предаставляют размеры бумаги по умолчанию, выраженные в сотых долях дюйма. Например, для листа размером 8,5x11 дюймов значения свойств Width и Height объекта PageBounds равно 850x1100 единиц (сотых долей дюйма). Если в принтере задана альбомная, а не книжная ориентация страницы, то размеры будут равны 1100x850 единиц.

    Область печати, заданная пользователем

    Размер этой области близок к полному размеру страницы, за исключением полей, недоступных печатающей головке принтера. Ширина верхнего, правого, нижнего и левого полей может различаться. Значением свойства VisibleClipBounds объекта класса Graphics является структура RectangleF, предоставляющая размер установленной области печати страницы.

    Свойства X и Y этой структуры устанавливаются в 0, а ее свойства Width и Height равны горизонтальному и вертикальному размерам установленной области печати страницы, выраженных в тех же единицах, что будут использованы и для рисования на странице принтера.

    Предельная область печати

    Эта область рассчитывается с учетом 1-дюймового отступа по периметру страницы и представляет собой границы, в пределах которых может печатать пользователь. Эта информация предоставляется свойством MarginBounds объекта PrintPageEventArgs в виде структуры Rectangle.

    Пример кода программы

    Цвета, видимые на экране, большей частью для принтера непригодны. Лучшим цветом для принтера является Color.Black, лучшее перо - Pens.Black, лучшая кисть - Brushes.Black. Это подходит для всех.

  • Вот программа, которая выводит в клиентскую область приглашение "Щелкни на форме для печати!" и начинает печатать на принтере после щелчка на клиентской области формы текст.
  • using System;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Drawing.Printing;
      
    namespace Test
    {
      public class HelloPrinter : Form
      {
        public HelloPrinter()
        {
          this.Text = "Простая программа печати";
          this.Width += 50;
          this.Height /= 2;
          this.BackColor = System.Drawing.SystemColors.Window;
          this.ForeColor = System.Drawing.SystemColors.WindowText;
          this.ResizeRedraw = true;
        }
      
        protected override void OnPaint(PaintEventArgs e)
        {
          Graphics graphics = e.Graphics;
          System.Drawing.StringFormat strfmt = new StringFormat();
          strfmt.Alignment = StringAlignment.Center;
          strfmt.LineAlignment = StringAlignment.Center;
          graphics.DrawString("Щелкни на форме для печати!", 
            Font, 
            new SolidBrush(ForeColor),
            RectangleF.FromLTRB(// Преобразовать из Left, Top, Right, Bottom
              this.ClientRectangle.Left,
              this.ClientRectangle.Top,
              this.ClientRectangle.Right,
              this.ClientRectangle.Bottom),
            strfmt);
      
          base.OnPaint (e);
        }
      
        protected override void OnClick(EventArgs e)
        {
          PrintDocument prndoc = new PrintDocument();
          prndoc.DocumentName = this.Text;
          // Делегат события печати
          // Создать вручную
          prndoc.PrintPage +=new PrintPageEventHandler(prndoc_PrintPage);
          prndoc.Print(); // Печать документа (инициирует событие PrintPage)
      
          base.OnClick (e);
        }
      
        private void prndoc_PrintPage(object sender, PrintPageEventArgs e)
        {
          Graphics printgr = e.Graphics;
          string message = "Привет студентам от Снеткова В.М.!";
          // Печатаем сообщение
          printgr.DrawString(message, 
            this.Font, Brushes.Black, 0, 0);
          // Измеряем текстовый блок с сообщением для начала линии
          SizeF szf = printgr.MeasureString(message, this.Font);
          // Проводим линию из нижнего правого угла текстового блока
          // до правого нижнего угла видимой области печати
          printgr.DrawLine(Pens.Black, szf.ToPointF(),
            printgr.VisibleClipBounds.Size.ToPointF());
        }
      }
    }

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

    Упражнение 3.Разделение кода вывода на экран и печать

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

  • Вот как будет выглядеть усовершенствованный класс
  • using System;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Drawing.Printing;
      
    namespace Test
    {
      public class PrintableForm : Form
      {
        public PrintableForm()
        {
          this.Text = "Форма, готовая наследоваться";
          this.BackColor = System.Drawing.SystemColors.Window;
          this.ForeColor = System.Drawing.SystemColors.WindowText;
          this.ResizeRedraw = true;
        }
      
        protected override void OnPaint(PaintEventArgs e)
        {
          // Вывод на экран
          // Наша функция, см. ниже (зри в корень!)
          DoPage(e.Graphics, ForeColor, ClientSize.Width, ClientSize.Height);
      
          base.OnPaint (e);
        }
      
        protected override void OnClick(EventArgs e)
        {
          PrintDocument prndoc = new PrintDocument();
          prndoc.DocumentName = this.Text;
          // Делегат события печати
          prndoc.PrintPage +=new PrintPageEventHandler(prndoc_PrintPage);
          prndoc.Print(); // Печать документа (инициирует событие PrintPage)
      
          base.OnClick (e);
        }
      
        private void prndoc_PrintPage(object sender, PrintPageEventArgs e)
        {
          // Получаем контекст устройства принтера
          Graphics printgr = e.Graphics;
      
          // Измеряем область печати
          SizeF szf = printgr.VisibleClipBounds.Size;
          // Вывод на принтер
          DoPage(printgr, Color.Black, (int)szf.Width, (int)szf.Height);
        }
      
        // Создадим виртуальный метод с возможностью замещения в наследнике
        protected virtual void DoPage(Graphics graphics, Color color,
          int cx, int cy)
        {
          Pen pen = new Pen(color);
          // В этом варианте замещаемой функции рисуем крестик
          graphics.DrawLine(pen, 0, 0, cx - 1, cy - 1);
          graphics.DrawLine(pen, cx - 1, 0, 0, cy - 1);
        }
      }
    }

    Здесь мы часть кода выделили в отдельную функцию DoPage(), которая выводит графическую информацию и на форму, и на принтер. Ей передают объект Graphics, цвет и размеры области вывода функции OnPaint() - вывод на форму, и prndoc_PrintPage() - вывод на принтер. Каждая вызывающая функция передает свои значения аргументов. Функция OnClick() инициирует срабатывание обработчика печати на принтер. Функцию DoPage() мы сделали защищенной (доступной только наследникам) и виртуальной (с возможностью замещения в наследниках), чтобы ее можно было заместить в наследующих классах для рисования других фигур.

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

  • Постройте форму и проверьте функциональность. Экранная часть должна выглядеть примерно так
  • Упражнение 4. Рисование ломаных линий

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

    graphics.DrawLine(pen, 0, 0, cx - 1, 0);
    graphics.DrawLine(pen, cx - 1, 0, cx - 1, cy - 1);
    graphics.DrawLine(pen, cx - 1, cy - 1, 0, cy - 1);
    graphics.DrawLine(pen, 0, cy - 1, 0, 0);

    Здесь конечная точка текущего вызова должна повторяться как начальная точка следующего.

    Для экономии кода в класс Graphics включен метод DrawLines() рисования нескольких соединенных линий, в совокупности называемых ломаной. Приведем пример программы, очерчивающей границу клиентской области. В ней мы будем наследовать разработанное нами расширение PrintableForm вместо класса Form. Обратите внимание, что в новой форме мы не устанавливаем все ее свойства заново - они унаследованы из класса PrintableForm

    using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class BoxingTheClient : PrintableForm
      {
        public BoxingTheClient()
        {
          this.Text = "Очерчивание границы клиентской области";
          this.Width += 80;
        }
      
        // Перегруженная функция базового класса PrintableForm
        protected override void DoPage(Graphics graphics, Color color, int cx, int cy)
        {
          Point[] apoint = {
                    new Point(0, 0),
                    new Point(cx - 1, 0),
                    new Point(cx - 1, cy - 1),
                    new Point(0, cy - 1),
                    new Point(0, 0)
                   };
          graphics.DrawLines(new Pen(color, 10), apoint);
        }
      }
    }

    Результатом работы кода будет

    Упражнение 5. Рисование домика ломаными линиями

    Для практики приведем еще один пример кода рисования ломаных. На этот раз нарисуем домик. Как и в предыдущем коде, воспользуемся собственным базовым классом PrintableForm.

    using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class DrawHouse : PrintableForm
      {
        public DrawHouse()
        {
          this.Text = "Рисование домика функцией DrawLines()";
        }
      
        // Перегруженная функция базового класса PrintableForm
        protected override void DoPage(Graphics graphics, Color color, int cx, int cy)
        {
          Point[] apoint = {
                    new Point(cx / 4, 3 * cy / 4),
                    new Point(cx / 4, cy / 2),
                    new Point(cx / 2, cy / 4),
                    new Point(3 * cx / 4, cy / 2),
                    new Point(3 * cx / 4, 3 * cy / 4),
                    new Point(cx / 4, cy / 2),
                    new Point(3 * cx / 4, cy / 2),
                    new Point(cx / 4, 3 * cy / 4),
                    new Point(3 * cx / 4, 3 * cy / 4),
                   };
          graphics.DrawLines(new Pen(color), apoint);
        }
      }
    }

    Результатом работы кода будет

    Упражнение 6. Применение функции DrawLines() для рисования графиков (рисование синусоиды)

    Метод DrawLines() позволяет настраивать тип пера так, чтобы для толстых перьев получать нужные формы линий в местах соединения ломаных (закругленные, угловые и т.д.). Эти элементы линий называются концами или соединениями. Чтобы придать концам правильный вид, GDI+ должен знать, какими являются линии, имеющие общие точки: отдельными или соединенными. Об этом можно сообщить GDI+, используя метод DrawLines() вместо DrawLine().

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

    using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class SineCurve : PrintableForm
      {
        public SineCurve()
        {
          this.Text = "Рисование синусоиды функцией DrawLines()";
        }
      
        // Перегруженная функция базового класса PrintableForm
        protected override void DoPage(Graphics graphics, Color color, int cx, int cy)
        {
          PointF[] apointf = new PointF[cx];
          for(int i = 0; i < cx; i++)
          {
            apointf[i].X = i;
            apointf[i].Y = ((float)(cy - 1) / 2) * (float)
              (1 - Math.Sin(i * 2 * Math.PI / (cx - 1)));
          }
          graphics.DrawLines(new Pen(color), apointf);
        }
      }
    }

    Здесь мы используем в качестве базового наш класс PrintableForm, и переопределяем в нем виртуальный метод DoPage(), задающий суть выводимой информации. Для вычисления синуса используется класс System.Math сборки mscorlib.lib. Большинство методов класса Math возвращает значение типа Double, которые нужно явно привести к типу float (структура System.Single сборки mscorlib.lib ). Отметим, что преобразование целого к вещественному является безопасным, поскольку не приведет к потере значений целого, поэтому можно преобразовать неявно и компилятор это воспринимает нормально.

    Аргумент функции Math.Sin() задается в радианах. Полный круг ( 360o ) соответствует 2*Pi радиан. Здесь у нас cx=ClientSize.Width, cy=ClientSize.Height. Таким образом, аргумент может принимать значения от 0 (когда i=0) до 2*Pi (когда i=cx). Значение метода Math.Sin() находится в интервале (-1,+1), поэтому его нужно умножить на полувысоту клиентской области. Полученные значения будут изменяться в интервале (-cy/2,+cy/2). Затем их нужно вычесть из половины высоты клиентской области, чтобы результат варьировался в диапазоне (0,cy).

    Экран с результатом выполнения выглядит так

    Метод DrawPolygon() класса Graphics отличается от DrawLines() тем, что автоматически замыкает первую и последнюю точки рисования.

    Упражнение 7. Параметрические уравнения и рисование кривых (рисование эллипса)

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

    $$x^2+y^2=r^2$$

    Если представить это уравнение в удобном для построения графика виде, то получим

    $$y=\pm \sqrt{r^2-x^2}$$

    которым пользоваться нельзя.

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

    x(t) = r*cos(t);
    y(t) = r*sin(t);

    Для t, изменяющегося от 0 до Pi, получим график окружности. Аналигичным образом можно построить эллипс

    x(t) = Rx*cos(t);
    y(t) = Ry*sin(t);

    Вот программа рисования эллипса, вписанного в экран

    using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class PolyEllipse : PrintableForm
      {
        public PolyEllipse()
        {
          this.Text = "Рисование эллипса функцией DrawLines()";
          this.Width += 100;
        }
      
        // Перегруженная функция базового класса PrintableForm
        protected override void DoPage(Graphics graphics, Color color, int cx, int cy)
        {
          int iNum = 2 * (cx + cy);
          PointF[] apointf = new PointF[iNum + 1];
          for(int i = 0; i <= iNum; i++)
          {
            double rAng = i * 2 * Math.PI / iNum;
            apointf[i].X = (float)((cx - 1) / 2 * (1 + Math.Cos(rAng)));
            apointf[i].Y = (float)((cy - 1) / 2 * (1 + Math.Sin(rAng)));
          }
      
          graphics.DrawLines(new Pen(color), apointf);
        }
      }
    }

    Изображение экрана для этой программы будет таким

    Этот пример мы выполнили для упражнения, поскольку класс Graphics содержит метод рисования эллипса DrawEllipse().

    Упражнение 8. Рисование эллипса специализированным методом

    Приведем код, который в клиентской области формы рисует вписанный эллипс специализированным методом

    using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class ClientEllipse : PrintableForm
      {
        public ClientEllipse()
        {
          this.Text = "Рисование эллипса функцией DrawEllipse()";
        }
      
        // Перегруженная функция базового класса PrintableForm
        protected override void DoPage(Graphics graphics, Color color, int cx, int cy)
        {
          graphics.DrawEllipse(Pens.Red, 0, 0, cx - 1, cy - 1);
        }
      }
    }

    Выход программы следующий

    Упражнение 9. Рисование спирали

    Используя метод DrawLines() рисования линий по их параметрическим уравнениям, приведем еще один код, рисующий спираль.

    using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class Spiral : PrintableForm
      {
        public Spiral()
        {
          this.Text = "Рисование спирали функцией DrawLines()";
          this.Width += 100;
        }
      
        // Перегруженная функция базового класса PrintableForm
        protected override void DoPage(Graphics graphics, Color color, int cx, int cy)
        {
          const int iNumRevs = 10;
          int iNumPoints = iNumRevs * 2 * (cx + cy);
          PointF[] apointf = new PointF[iNumPoints];
          double rAngle, rScale;
      
          for(int i = 0; i < iNumPoints; i++)
          {
            rAngle =  iNumRevs * i * 2 * Math.PI / iNumPoints;
            // Когда хотя-бы один операнд вещественный, то автоматически преобразуется
            rScale = 1 - i / Convert.ToDouble(iNumPoints);
            apointf[i].X = (float)(Convert.ToDouble(cx) / 2 * (1 + rScale * Math.Cos(rAngle)));
            apointf[i].Y = (float)(Convert.ToDouble(cy) / 2 * (1 + rScale * Math.Sin(rAngle)));
          }
      
          graphics.DrawLines(new Pen(color), apointf);
        }
      }
    }

    Экран программы будет таким

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

    Упражнение 10. Рисование прямоугольников

    Прямоугольник можно нарисовать при помощи функций DrawLine() и DrawLines(), что мы и делали ранее, вписывая его в клиентскую область формы. Но проще сделать это методом DrawRectangle() объекта Graphics. Имеются три перегруженных версии метода DrawRectangle() и две DrawRectangles():

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

    При рисовании прямоугольников часто возникает ошибка смещения на 1 пиксел. При использовании свойства пера по умолчанию методу DrawRectangle() передаются значения ширины и высоты, равные 3. Если задать ширине и высоте значение 2, будет нарисован прямоугольник размером 3x3 пиксела, внутренняя область которого состоит лишь из одного пиксела.

    Вот программа, которая рисует прямоугольник по краю клиентской области методом DrawRectangle()

    using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class OutlineClientRectangle : PrintableForm
      {
        public OutlineClientRectangle()
        {
          this.Text = "Рисование прямоугольника DrawRectangle()";
    
        }
      
        // Перегруженная функция базового класса PrintableForm
        protected override void DoPage(Graphics graphics, Color color, int cx, int cy)
        {
          graphics.DrawRectangle(new Pen(Color.Red, 20), 0, 0, cx - 1, cy - 1);
        }
      }
    }

    Результатом работы будет экран

    Упражнение 11. Рисование дуг

    Дуга - это сегмент эллипса. Чтобы определить дугу, нужно задать координаты эллипса, плюс угол для начальной и угловую длину дуги. Для рисования дуг используется четыре перегруженных метода DrawArc() класса Graphics

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

    Приведем код, рисующий эллипс с пунктирным контуром толщиной 30 пикселов. Угловой размер штрихов пунктира равен 10o, а промежутков между штрихами 15o - 10o = 5o

    using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class DashedEllipse : PrintableForm
      {
        public DashedEllipse()
        {
          this.Text = "Рисование эллипса функцией DrawArc()";
        }
      
        // Перегруженная функция базового класса PrintableForm
        protected override void DoPage(Graphics graphics, Color color, int cx, int cy)
        {
          int width = 30;
          Rectangle rect = new Rectangle(width / 2, width / 2, 
               cx - 1 - width, cy - 1 - width);
          Pen pen = new Pen(Color.Red, width);
          for(int iAngle = 0; iAngle < 360; iAngle += 15)
            graphics.DrawArc(pen, rect, iAngle, 10);
        }
      }
    }

    Выходной экран имеет вид

    Упражнение 12. Рисование прямоугольника с закругленными углами

    В Win32 API есть функция RoundRect(), рисующая прямоугольник с закругленными углами. Эта функция принимает 4 аргумента, указывающих координаты левого верхнего и правого нижнего углов прямоугольника, плюс еще два, определяющие ширину и высоту эллипса, используемого для скругления углов прямоугольника. В классе Graphics BCL такой функции нет, но ее можно вполне имитировать, используя другие функции, вот каким кодом

    using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class RoundRect : PrintableForm
      {
        public RoundRect()
        {
          this.Text = "Рисование прямоугольника со скругленными углами (RoundRect)";
        }
      
        // Перегруженная функция базового класса PrintableForm
        protected override void DoPage(Graphics graphics, Color color, int cx, int cy)
        {
          RoundedRectangle(graphics, Pens.Red,
            new Rectangle(0, 0, cx - 1, cy - 1),
            new Size(cx / 5, cy / 5));
        }
      
        private void RoundedRectangle(Graphics gr, Pen pn, Rectangle rect, Size sz)
        {
          gr.DrawLine(pn, rect.Left + sz.Width / 2, rect.Top,
            rect.Right - sz.Width / 2, rect.Top);
          gr.DrawArc(pn, rect.Right - sz.Width, rect.Top,
            sz.Width, sz.Height, 270, 90);
          gr.DrawLine(pn, rect.Right, rect.Top + sz.Height / 2,
            rect.Right, rect.Bottom - sz.Height / 2);
          gr.DrawArc(pn, rect.Right - sz.Width, rect.Bottom - sz.Height,
            sz.Width, sz.Height, 0, 90);
          gr.DrawLine(pn, rect.Right - sz.Width / 2, rect.Bottom,
            rect.Left + sz.Width / 2, rect.Bottom);
          gr.DrawArc(pn, rect.Left, rect.Bottom - sz.Height,
            sz.Width, sz.Height, 90, 90);
          gr.DrawLine(pn, rect.Left, rect.Bottom - sz.Height / 2,
            rect.Left, rect.Top + sz.Height / 2);
          gr.DrawArc(pn, rect.Left, rect.Top,
            sz.Width, sz.Height, 180, 90);
        }
      }
    }

    Вывод на экран будет таким

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

    Упражнение 13. Рисование секторов

    Рисование секторов выполняется функцией DrawPie() класса Graphics. От метода DrawArc() эта функция отличается только тем, что соединяет концы дуги с центром эллипса линиями, создавая замкнутую область, а аргументы сохраняют те же смысловые значения. Этот метод часто применяют для построения круговых диаграмм. Метод имеет 4 перегруженных версии

    Приведем пример, строящий круговую диаграмму на основе массива значений, хранимых как поле класса

    using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class PieChart : PrintableForm
      {
        private int[] aiValues = {50, 100, 25, 150, 100, 75};
      
        public PieChart()
        {
          this.Text = "Рисование круговой диаграммы (PieChart)";
        }
      
        // Перегруженная функция базового класса PrintableForm
        protected override void DoPage(Graphics graphics, Color color, int cx, int cy)
        {
          Rectangle rect = new Rectangle(50, 50, 200, 200);
          Pen pen = new Pen(color);
          int iTotal = 0;
          float fAngle = 0;
          Single fSweep; // Структура Single равноценна типу float
      
          foreach(int iValue in aiValues)
            iTotal += iValue;
      
          foreach(int iValue in aiValues)
          {
            fSweep = 360.0F * iValue / (Single)iTotal;
            DrawPieSlice(graphics, pen, rect, fAngle, fSweep);
            fAngle += fSweep;
          }
        }
      
        // Создание замещаемого метода
        protected virtual void DrawPieSlice(Graphics gr, Pen pn, Rectangle rect,
          Single fAngle, Single fSweep)
        {
          gr.DrawPie(pn, rect, fAngle, fSweep);
        }
      }
    }

    Выход на экран будет таким

    Упражнение 14. Улучшенное рисование секторов

    Метод DoPage() суммирует значения массива и рассчитывает угол каждого сектора, деля значение для этого сектора на сумму значений всех секторов и умножая частное на 360o. Мы специально определили метод DrawPieSlice() виртуальным, чтобы можно было просто написать улучшенный вариант программы, используя класс PieChart как базовый

    using System;
    using System.Drawing;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class BetterPieChart : PieChart
      {
        public BetterPieChart()
        {
          this.Text = "Рисование круговой диаграммы (BetterPieChart)";
          this.Height += 20;
        }
      
        // Создание замещаемого метода
        protected override void DrawPieSlice(Graphics gr, Pen pn, 
          Rectangle rect, Single fAngle, Single fSweep)
        {
          Single fSlice = Convert.ToSingle(2 * Math.PI / 360 * (fAngle + fSweep / 2));
          rect.Offset(Convert.ToInt32((double)rect.Width / 10 * Math.Cos(fSlice)),
            Convert.ToInt32((double)rect.Height / 10 * Math.Sin(fSlice)));
      
          base.DrawPieSlice (gr, pn, rect, fAngle, fSweep);
        }
      }
    }

    Экранный вывод будет таким

    Упражнение 15. Заливка прямоугольников, эллипсов и секторов

    Для некоторых функций рисования контуров класс Graphics содержит аналогичные методы, которые вместо префикса Draw начинаются с Fill. Например

    Есть еще один метод, который похож на метод DrawPolygon(), но имеет один дополнительный параметр FillMode.

    FillMode - это статическое перечисление, определяемое в пространстве имен System.Drawing.Drawing2D. У него всего два возможных константных значения

    Перечисление FillMode
    Член Значение Пояснение
    System.Drawing.Drawing2D.FillMode.Alternate 0 Задано по умолчанию, чередует закрашенные и незакрашенные области фигуры
    System.Drawing.Drawing2D.FillMode.Winding 1 Закрашивает максимально возможное число внутренних областей фигуры

    Режим заливки имеет значение лишь тогда, когда линии, определяющие многоугольник, пересекаются. Он определяет, какие из замкнутых областей будут закрашены, а какие - нет. Если режим заливки в методе FillPolygon() не задан, по умолчанию используется режим Alternate. При этом замкнутая область заполняется, если она отделена от бесконечности нечетным числом границ.

    Классический пример - пятиконечная звезда. Ее внутренний пятиугольник закрашивается в режиме Winding, но остается свободным в режиме Alternate

    using System;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.Windows.Forms;
      
    namespace Test
    {
      public class FillModesClassical : PrintableForm
      {
        public FillModesClassical()
        {
          this.Text = "Заливка сложной фигуры (FillModesClassical)";
          this.ClientSize = new Size(2 * this.ClientSize.Height,
            this.ClientSize.Height);
        }
      
        // Перегруженная функция базового класса PrintableForm
        protected override void DoPage(Graphics graphics, Color color, int cx, int cy)
        {
          SolidBrush brush = new SolidBrush(color);
          Point[] apoint = new Point[5];
          int iMax = apoint.GetUpperBound(0); // Верхнее значение индекса-ключа
      
          // Первая звезда
          for(int i = 0; i <= iMax; i++)
          {
            Double rAngle = (i * 0.8 - 0.5) * Math.PI;
            apoint[i].X = Convert.ToInt32(cx * (0.25 + 0.24 * Math.Cos(rAngle)));
            apoint[i].Y = Convert.ToInt32(cy * (0.5 + 0.48 * Math.Sin(rAngle)));
          }
          graphics.FillPolygon(brush, apoint, FillMode.Alternate);
      
          // Вторая звезда
          for(int i = 0; i <= iMax; i++)
            apoint[i].X += cx / 2;
          graphics.FillPolygon(brush, apoint, FillMode.Winding);
        }
      }
    }

    Экранный результат имеет вид

  • Откомпилируйте проект в режиме Release и проверьте его функциональность во всех построенных нами режимах
  • Вернуться к учебному плану