Основы работа с графикой на С# в Visual Studio 2005/2008

Раздел 7. Основы работа с графикой на С# в Visual Studio 2005/2008

Аннотация: Весь изложенный материал, без особого труда, можно найти в MSDN и сети, но для большинства статей характерен уход в вопросы, которые затрудняют создание четкого и ясного представления о том, как работать с графикой. При решении корпоративной задачи вывода графиков в Web приложении, автору сначала потребовалось обобщить материал, и, лишь после того, приступить собственно к выполнению задания. Материалы этого обобщения и приводятся в данной статье.

PS: Материал первоначально касался только отображения графики на Web страницах, но поступающие от читателей вопросы по использованию графики в Windows стали причиной дополнения данного материала еще одной главой, которую, в силу методических соображений, автор поместил первой.


В начало

Глава 1. Использование графики в Windows приложениях


В начало

Параграф 1. Где и как возможно отображать графическую информацию в Windows приложениях

В Visual Studio Net нет стандартных средств для создания графиков, диаграмм... Поэтому, большинство статей на рассматриваемую тему рекомендуют использовать Office Web Components (OWC) - компонент Windows для построения диаграмм в Web. Как достоинство этого подхода обычно отмечается простота построения графиков и диаграмм и интуитивно близкие задания параметров графической информации с их заданием при использовании графики в MS Office. Но простота не всегда достоинство, когда мы строим график "под себя". Да и достаточно для программиста сомнительна, когда он однажды создав код графического класса, он всегда легко и быстро может его перенастроить его для конкретной задачи, не влезая в дебри некого универсального кода, где порой гораздо больше времени будет затрачено на уяснение параметров и задание их значений и границ. Поэтому, речь далее пойдет о создании собственных графиков с "чистого листа". Основные типы графиков, которые будут рассмотрены, по мнению автора, могут стать достаточными для большинства практических задач и базой для дальнейших собственных разработок.

В Microsoft Windows существует несколько средств для вывода графической информации, включая DirectDraw, OpenGL, GDI и т.д. Мы будем использовать Graphics Device Interface (GDI, более поздние версии GDI+) - подсистему Windows, ответственную за вывод графики и текста на дисплей и принтер. Именно CGI+ обеспечивает вывод на экран всего того, что видит пользователь Windows в окне монитора. GDI+ является базовым способом вывода графики в Windows.

CGI+ - это библиотека функций и программных средств на базе API, позволяющая приложениям абстрагироваться от особенностей конкретного графического оборудования. .Net Framework использует расширенный графический интерфейс GDI+.

С графикой Windows (при использованием GDI+) связано понятия контекста устройства (device context, DC). Это структура данных, содержащая информацию о параметрах и атрибутах вывода графики на устройство (дисплей, принтер...). .Net Framework освобождает программиста от необходимости обращения к DC. Основной объект для графического отображения Graphics - класс имитации поверхности рисования в GDI+. Класс не имеет конструкторов по причине того, что объекты этого класса зависят от контекста конкретных устройств вывода. Создаются объекты специальными методами разных классов, например, он может быть создан из объекта Bitmap, или к нему можно получить доступ, как к некоторому объекту, инкапсулированному в некоторые контролы, в том числе, и в объект формы приложения. Метод CreateGraphics класса Control - наследника класса Form - возвращает объект, ассоциированный с выводом графики на форму.

Рассмотрим простейшие примеры. Создадим решение Windows приложения с одной кнопкой и следующим обработчиком ее нажатия:

private void button1_Click(object sender, EventArgs e)
{
 //Создаем битовую матрицу
 Bitmap bitmap = new Bitmap(ClientSize.Width, ClientSize.Height);
 //Создаем объект класса Graphics на основе битовой матрицы
 Graphics graph = Graphics.FromImage(bitmap);
 //Рисуем линию на поверхности, Graphics использует в качестве нее 
 //битовую матрицу
 graph.DrawLine(new Pen(Color.Red,2), 0, 0, ClientSize.Width, ClientSize.Height);
 //Присваиваем нарисованноу свойству формы
 BackgroundImage = bitmap;
 graph.Dispose();
}

Результат вывода при нажатии кнопки 1, показан на Рис.1:

graph_01.gif

Рис.1. Создание и использование объект класса Graphics на основе битовой матрицы

Одинакового эффекта (Рис.1.), можно добиться, если использовать обработчики некоторых событий, которым передается объект класса Graphics как аргумент (например, обработчик события Paint формы приложения):

private void Form1_Paint(object sender, PaintEventArgs e)
{
 e.Graphics.DrawLine(new Pen(Color.Red, 2), 0, 0, ClientSize.Width, ClientSize.Height);
}

Одинакового эффекта (Рис.1.), можно добиться и при непосредственном создании объекта Graphics:

private void button1_Click(object sender, EventArgs e)
{
 Graphics graph = CreateGraphics();
 graph.DrawLine(new Pen(Color.Red, 2), 0, 0, ClientSize.Width, ClientSize.Height);
 graph.Dispose();
}

А так можно рисовать (писать) на кнопке и на других контролах, для которых может быть создан обработчик события Paint (Рис.2.):

private void button1_Paint(object sender, PaintEventArgs e)
{
 e.Graphics.DrawLine(new Pen(Color.Red, 2), 0, 0, button1.Width, button1.Height);
}

Класс Graphics находятся в пространстве имен Drawing (хотя, забегая вперед, нам понадобится и пространство имен Drawing.Drawing2D, по сему, целесообразно сразу добавить их в решение).

using System.Drawing;
using System.Drawing.Drawing2D;

Для того чтобы рисовать в окне формы Windows приложения, необходимо иметь не только связанный с этим окном объект Graphics, как холст для рисования, но и инструменты рисования. Используют два основных инструмента: карандаш и кисть. Карандаш мы использовали в примерах, приведенных выше. В конструкторе класса Pen задается цвет пера и можно задать толщину (по умолчанию 1). Класс Brush определяет кисти для рисования. Это абстрактный класс. Кисти этого класса создать нельзя, хотя можно создавать кисти классов-потомков Brush:

  • SolidBrush - сплошная закраска цветом кисти;

  • TextureBrush - наложение картинки (image) на область закраски;

  • HatchBrush - закраска области предопределенным узором;

  • LinearGradientBrush - сплошная закраска c переходом цвета кисти (градиентная закраска);

  • PathGradientBrush -сплошная закраска c переходом цвета кисти по специальному алгоритму.

Пример использования конструктора HatchBrush показан на Рис.2.:

private void button1_Click(object sender, EventArgs e)
{
 Graphics graph = CreateGraphics();
 graph.DrawLine(new Pen(Color.Blue, 2), 0, 0, ClientSize.Width, ClientSize.Height);
 graph.FillEllipse(new HatchBrush(HatchStyle.Percent05, Color.Silver), 0, 0, 
                                            ClientSize.Width, ClientSize.Height);
 graph.Dispose();
}

graph_02.gif

Рис.2. Пример использования конструктора HatchBrush

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

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

Отметим еще два важных момента, которые нам понадобятся далее:

  1. Круговые фигуры рисуются при указании в качестве стартовой точки левого верхнего угла (см. последний пример);

  2. Использование событий Paint для отображения графики не всегда оправдано из-за необходимости постоянной перерисовки изображения при перерисовке формы.

Рисование непосредственно на форме не всегда является и не есть хороший тон, кода Visual Studio предлагает специальный контрол, который, как нельзя лучше, подходит для вывода графической информации и обладает всеми преимуществами контролов: программное позиционирование и масштабирование без перерисовки формы, возможность выполнять Stretch Image и множество других полезных свойств и событий. Кроме того простой доступ к Graphics, аналогично через событии Paint и возможность использования битовых карт Bitmap для создания объекта Graphics, с последующим переносом их в контрол (аналогично, как мы делали это в первом примере):

pictureBox1.Image = bitmap; 

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


В начало

Параграф 2. Создание линейных графиков

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

Однажды мне пришлось делать задачку, на базе продолжения работы над которой появилась программа "LitFrequencyMeter" - программа определения частоты повторения слов и знаков в литературных произведениях, графики из которой приведены ниже (Рис.3-5.). И когда я приступил к заключительному этапу работы над программой, то понял, что материал главы практически написан в кодах - осталось только его озвучить.

Что и какие аспекты были заложены в программу:

  1. Настройка всего и вся: цветов, шрифтов, фонов, надписей, положения диаграмм на холсте, использование пояснений, легенд, смена числа отсчетов и т.д., и т.п.

  2. Хранение всех настроек (включая шрифты) в реестре.

  3. Создание статистики из .txt, .html, .doc файлов.

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

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

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

graph_04.gif

Рис.3. Линейеая диаграмма

graph_04.gif

Рис.4. Гистограмма

graph_05.gif

Рис.5. Круговая диаграмма

2.2. Постановка задачи

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

Конечная цель - помещение созданного графического изображения в элемент управления PictureBox.

2.3. Исходные данные

Исходные данные перед их отображением могут находиться где угодно (файл, таблица базы данных...). Однако рассматривать чтения из базы данных или из файла значений графиков - только засорять отображение материала. Мы всегда можем прочитать данные с любого источника в массив значений. Автор предпочитает работать со строковым массивом, как позволяющим хранить цифровые и текстовые значения. В примерах, приводимых ниже, используется массив строк string[,] rgsValues. В программе, о которой шла выше речь, этот массив использован для настройки параметров, отображаемых на графике. Заполнять массив будем с помощью датчиков случайных чисел:

private int viNumInRg=20;//20 - начальное значение
private string[,] rgsValues=null;

private int iCreateRg()
{
 Random rnd = new Random(DateTime.Now.Millisecond);
 Random rnd1 = new Random(DateTime.Now.Millisecond+5);
 rgsValues = new string[viNumInRg, 2];
 for (int i = 0; i < viNumInRg; i++)
 {
  rgsValues[i, 0] = Convert.ToString(((float)(rnd.Next(0, 10) * 100) + 
                                  (float)rnd1.Next(0, 99)) / (float)100);
  rgsValues[i, 1] = "I-" + Convert.ToString(i+1);
 }

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

2.4. Проект решения

Создадим простой проект WindowsApplication решения с любым именем (у меня graph1). Поместим на форму три кнопки, в свойствах "Текст" которых напишем соответственно: "Линейная диаграмма", "Гистонрамма" и "Круговая диаграмма". Ниже кнопок поместим контрол PictureBox. Подберем удобное для себя расположение кнопок и PictureBox (в реальных программах для размещения удобнее использовать контролы TableLayoutPanel, но сейчас нас интересует графика, а не размещение).

В окне Solutation Explorer кликаем правой кнопкой мышки на узле решения (у меня graph1) и в контекстном меню выбираем Add\New Item. В окне Templates выбираем Class, даем ему имя, например PaintCl.cs и нажимаем кнопку Add. Будет создан пустой класс.

using System;
using System.Collections.Generic;
using System.Text;
namespace graph1
{
 class PaintCl
 {
 }
}

Нашей задачей будет постепенное наполнение этого класса при минимуме добавления кода в основной файл кода приложения - Form1.cs.

Для начала создадим обработчик события нажатия кнопки "Линейный график" (клик мышкой на кнопке), а также обработчики для событий Load и FormClozed (первый можно кликом мышки на форме, второй через окно Properties формы - закладка Events - клик в окошечке против события FormClosed). Слегка преобразуем код, как показано ниже:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace graph1
{
 public partial class Form1 : Form
 {
  private int viNumButton = 0;
  private int viNumInRg=20;//20 - начальное значение
  private string[,] rgsValues=null;

  public Form1()
  {
      InitializeComponent();
  }


  private void Form1_Load(object sender, EventArgs e)
  {
      //Здесь при создании реальной программы необходимо 
      //будет предусмотреть восстановление сохраненных 
      //параметров для приложения и графиков
  }
  private void Form1_FormClosed(object sender, FormClosedEventArgs e)
  {
      //Здесь при создании реальной программы необходимо 
      //будет предусмотреть сохранение параметров
      //для приложения и графиков
  }


  #region Создание массива значений
  private void vCreateRg()
  {
   Random rnd = new Random(DateTime.Now.Millisecond);
   Random rnd1 = new Random(DateTime.Now.Millisecond+5);
   rgsValues = new string[viNumInRg, 2];
   for (int i = 0; i < viNumInRg; i++)
   {
    rgsValues[i, 0] = Convert.ToString(((float)(rnd.Next(0, 10) * 100) + 
                                  (float)rnd1.Next(0, 99)) / (float)100);
    rgsValues[i, 1] = "I-" + Convert.ToString(i+1);
   }
  }
  #endregion

  #region создание линейного графика
  private void button1_Click(object sender, EventArgs e)
  {
     viNumButton = 1;
     vCreateLinGr();            
  }
  private void vCreateLinGr()
  {
   //Создаем массив значений для вывода на графике
   vCreateRg();

  }
  #endregion
 }
}

Назначение переменной viNumButton, будет ясно далее. Массив значений у нас создан. Осталось нарисовать по значениям массива график, используя класс.

2.5. Конструкторы класса

Начало класса - конструктор и закрытые переменные. В классе лучше иметь несколько конструкторов. Например, таких как приведено в коде ниже. И, естественно, необходимо сразу определить основные объекты для графики: битовую матрицу, объект Graphiks, шрифт, кисть и перо (о чем мы говорили выше), а также переменные для сохранения размеров холста:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;
namespace graph1
{
 class PaintCl
 {
   //Основные объекты для рисования
   private Bitmap bmp = null;
   private Graphics graph = null;
   private Font objFont = new Font("Arial", 8, FontStyle.Bold);
   private Brush objBrush = Brushes.Black;        
   private Pen objPenLine = new Pen(Color.Black, 1);
   //Размеры холста
   private int viX = 200;
   private int viY = 100;

   #region Конструкторы
   //Первый
   public PaintCl()
   {
      
   }
   //Второй
   public PaintCl(int a, int b)
   {
     bmp = new Bitmap(a, b);
     //Создаем объект Graphics на основе битовой матрицы 
     graph = Graphics.FromImage(bmp);
     //Запоминаем размеры холста
     viX = a;
     viY = b;            
   }
   #endregion
 }
}

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

2.6. Создаем объект для рисования

Используем второй конструктор и создадим и инициализируем в классе сразу все объекты, которые нам необходимы:

private void vCreateLinGr()
{
   //Создаем массив значений для вывода на графике
   vCreateRg();
   //Создаем класс и передаем ему размер холсты
   PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
   //Передадим фон холста в класс
   clPaint.vSetBackground(Color.Red);      
   //Принимаем нарисованное в pictureBox
   pictureBox1.Image = clPaint.Bmp;
}

Таким образом, нам понадобятся в классе еще две функции: установки фона холста и приема изображения из класса. Добавим их в класс:

#region Установка цвет фона диаграммы 
public void vSetBackground(Color bcl)
{
 graph.Clear(bcl);
}
#endregion

#region Доступ к переменным класса
public Bitmap Bmp
{
 get {return bmp;}
}
#endregion

Выполним решение на данном этапе. Результат показан на Рис.6.:

graph_06.gif

Рис.6. Взаимодействие кода формы с кодом класса

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

2.7. Рисуем оси

Добавим в классе переменные для хранения отступов от краев холста (они нам еще понадобятся не один раз).

//Отступы от краев холста
private int viDeltaaxisL = 50;
private int viDeltaaxisR = 50;
private int viDeltaaxisH = 20;

Добавим функцию рисования осей и функции запоминания цвета и толщины осей. Функция выполняет простую задачу - рисует две линии и при необходимости стрелочки на конце осей:

#region Рисование Осей
//Параметры вызоыва: отступы слева - deltaaxisL, справа - deltaaxisR, 
//сверху(снизу) - deltaaxisH, Цвет осей - colorpenaxis, толщина пера - widthpen, 
//нужны ли стрелки - fArrow (true - да)
public void vDravAxis(int deltaaxisL, int deltaaxisR, 
             int deltaaxisH, Color colorpenaxis, int widthpen, bool fArrow)
{
 //Запоминаем отступы                        
 viDeltaaxisL = deltaaxisL;
 viDeltaaxisR = deltaaxisR;
 viDeltaaxisH = deltaaxisH;
 //Запоминаем цвет осей и толщину
 vSetPenColorLine(colorpenaxis);
 if (widthpen > 0) vSetPenWidthLine(widthpen);
 //Точка начала рисования по х и y           
 int x = deltaaxisL;
 int y = viY - deltaaxisH;
 int x1 = viX - deltaaxisR;
 int y1 = deltaaxisH;
 //Переменная определения длины стрелок
 int d = 0;
 if(fArrow) d = widthpen * 10;
 //Оси на d пикселей длинней для стрелок
 graph.DrawLine(objPenLine, x, y, x1 + d, y);
 graph.DrawLine(objPenLine, x, y, x, y1 - d);
 //Надо рисовать стрелки
 if (fArrow)
 {
  int a = 10 * (int)objPenLine.Width;
  int b = 2 * (int)objPenLine.Width;
  int x2 = x1 - a;
  int y2 = y + b;
  //Стрелки
  graph.DrawLine(objPenLine, x1 + 20, y, x2 + d, y2);
  y2 = y - b;
  graph.DrawLine(objPenLine, x1 + 20, y, x2 + d, y2);
  x2 = x - b;
  y2 = y1 + a;
  graph.DrawLine(objPenLine, x, y1 - d, x2, y2 - d);
  x2 = x + b;
  graph.DrawLine(objPenLine, x, y1 - d, x2, y2 - d);
 }
}
#endregion

#region Карандаш, шрифт, кисть
//Цвет карандаша
public void vSetPenColorLine(Color pcl)
{
 if (objPenLine == null)
 {
  objPenLine = new Pen(Color.Black, 1);
 }
 objPenLine.Color = pcl;
}
//Установка толщина карандаша        
public void vSetPenWidthLine(int penwidth)
{
 if (objPenLine == null)
 {
   objPenLine = new Pen(Color.Black, 1);
 }
 objPenLine.Width = penwidth;
}
#endregion

Осталось добавить вызов функции рисования осей:

private void vCreateLinGr()
{
  //Создаем массив значений для вывода на графике
  vCreateRg();
  //Создаем класс и передаем ему размер холсты
  PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
  //Фон холста
  clPaint.vSetBackground(Color.White);
  //Параметры вызоыва: отступы слева, справа, сверху(снизу),
  //Цвет осей, толщина пера, необходимость стрелок
  clPaint.vDravAxis(50, 50, 20, Color.Red, 2,true);
  //Принимаем нарисованное в pictureBox
  pictureBox1.Image = clPaint.Bmp ;
}

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

graph_07.gif

Рис.7 Рисование осей линейного графика

2.8. Рисуем сетку

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

//Параметры линии для сетки
clPaint.vSetPenWidthLine(1);
clPaint.vSetPenColorLine(Color.Silver);

Для хранения размера массива в классе определим переменную и определим доступ к ней через свойство, а также определим функцию, рисующую сетку viMaxRg*viMaxRg клеток. Рисование сетки сводится к рисованию параллельных осям линий:

//Максимальный размер массива
private int viMaxRg = 20;

public int MaxRg
{
 set { viMaxRg=value; }
}

#region Рисование сетки
public void vDravGrid()
{
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH;
 float x1 = viX - viDeltaaxisR;
 float y1 = viDeltaaxisH;
 //Сдвиг линий сетки на один отсчет по Y
 float f = (y - y1) / (float)viMaxRg;
 //Рисуем горизонтальные линии
 for (int i = 1; i < viMaxRg + 1; i++)
 {
  graph.DrawLine(objPenLine, x, y - f * i, x1, y - f * i);
 }
 //Сдвиг линий сетки на один отсчет по X
 f = (x - x1) / (float)(viMaxRg - 1);
 //Рисуем вертикальные линии
 for (int i = 1; i < viMaxRg; i++)
 {
  graph.DrawLine(objPenLine, x - f * i, y, x - f * i, y1);
 }
}
#endregion

В функции vCreateLinGr() добавим код и выполним решение:

clPaint.MaxRg = 20;
clPaint.vDravGrid();

Результат показан на Рис.8.:

graph_08.gif

Рис.8. Рисование сетки

2.8. Рисуем линию графика

Как мы уже делали - зададим цвет и толщину пера. Далее нам понадобятся данные из нашего массива значений непосредственно в классе. Для этого в классе определим массив и доступ к нему:

private string[,] rgsValues = null;

public string[,] RgValue
{
 set { rgsValues = value; }
}

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

#region Рисование линий графика для линейного графика
public void vDrawGraphLines()
{
 string s = string.Empty;
 string s1 = string.Empty;
 string s2 = string.Empty;
 float f = 0;
 float f1 = 0;
 float x1 = 0;
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH;
 float x2 = 0;
 float fMax = float.MinValue;
 //Ищем максимальное значение по оси Y
 for (int i = 0; i < viMaxRg; i++)
 {
  s = rgsValues[i, 0];
  if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 //Пикселей для рисования по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на одну единицу массива значения по X
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 //Пикселей для рисования по оси y
 float fdeltay = viY - 2 * viDeltaaxisH;
 //Пикселей на одну единицу массива значений по Y
 fdeltay = fdeltay / fMax;
 for (int i = 0; i < viMaxRg; i++)
 {
  //Первый раз запоминаем точку старта
  if (i == 0)
  {
    s = rgsValues[i, 0];
    s2 = rgsValues[i, 1];
    f = y - (float.Parse(s) * fdeltay);
    x1 = x;
  }
  else
  {   
    //Здесь рисуем линии
    s1 = rgsValues[i, 0];
    f1 = y - (float.Parse(s1) * fdeltay);
    x2 = x + (int)(fdeltax * i);
    graph.DrawLine(objPenLine, x1, f, x2, f1);
    //Запоминаем координаты конечной точки, точки 
    //начала следующего отрезка линии
    s = rgsValues[i, 0];
    s2 = rgsValues[i, 1];
    f = f1;
    x1 = x + (int)(i * fdeltax);
  }
 }
}
#endregion

Код vCreateLinGr() на данный момент:

private void vCreateLinGr()
{
 //Создаем массив значений для вывода на графике
 vCreateRg();
 //Создаем класс и передаем ему размер холста
 PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
 //Фон холста
 clPaint.vSetBackground(Color.White);
 //Параметры вызоыва: отступы слева, справа, 
 //сверху(снизу),Цвет осей, толщина пера
 clPaint.vDravAxis(50, 50, 30, Color.Red, 2,true);
 //Цвет и толщина пера
 clPaint.vSetPenWidthLine(1);
 clPaint.vSetPenColorLine(Color.Silver);
 clPaint.MaxRg = 20;
 //Рисуем сетку
 clPaint.vDravGrid();
 //Цвет и толщина пера
 clPaint.vSetPenWidthLine(2);
 clPaint.vSetPenColorLine(Color.Green);
 //Передаем массив значений в класс
 clPaint.RgValue = rgsValues;
 //Рисуем линии графика
 clPaint.vDrawGraphLines();
 //Принимаем нарисованное в pictureBox
 pictureBox1.Image = clPaint.Bmp;
}

Результат выполнения решения на данном этапе показан на Рис.9.:

graph_09.gif

Рис.9. Рисование линий графика

2.10. Надписи на графике

Надписи можно наносить по оси Х, по оси Y и над точками линий графика. Причем иногда бывает целесообразно выполнять соседние надписи со сдвигом по оси Y. Кроме того - надписи выполняются не пером, а кистями и требуют задания шрифта. Таким образом, перед выполнением надписей надо установить в классе соответственно шрифт и кисть (Brush).

Для передачи шрифта и кисти создадим в классе свойства:

public Brush  brush
{
 set { objBrush  = value; }
}
public Font font
{
 set { objFont = value; }
}

В функции рисования графика запишем код:

Font objFont = new Font("Arial", 12, FontStyle.Bold | FontStyle.Italic);
clPaint.font = objFont;
clPaint.brush = Brushes.Blue;

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

#region Текст по оси X - Цифры отсчетов
//Параметр: false - соседние значения без сдвига по оси Y
//          true  - соседние значения со здвигом по оси Y
public void vDrawTextAxXNumber(bool f)
{
 //Пикселей для надписей по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH + objPenLine.Width;
 for (int i = 1; i < viMaxRg + 1; i++)
 {
  if (!f || i % 2 == 0)
  {
   graph.DrawString(Convert.ToString(i), objFont, objBrush, x + (i - 1) * fdeltax, y);
  }
  else
  {
   graph.DrawString(Convert.ToString(i), 
    objFont, objBrush, x + (i - 1) * fdeltax, y + objFont.Size);
  }
 }
}
#endregion

#region Текст по оси X -  Параметр массива
//Параметр: false - соседние значения без сдвига по оси Y
//          true  - соседние значения со здвигом по оси Y
public void vDrawTextAxXValues(bool f)
{
 string s = string.Empty;
 //Пикселей для надписей по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH;// +objPenLine.Width;
 for (int i = 0; i < viMaxRg; i++)
 {
  if (!f || i % 2 == 0)
  {
   graph.DrawString(rgsValues[i, 1], objFont, objBrush, x + i * fdeltax, y);
  }
  else
  {
   graph.DrawString(rgsValues[i, 1], objFont, objBrush, x + i * fdeltax, y + objFont.Size);
  }
 }
}
#endregion

#region Текст по оси Y - Значения по отсчетам сетки оси Y
public void vDrawTextAxYValues()
{
 string s = string.Empty;
 float f = 0;
 float fMax = float.MinValue;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 f = fMax / (float)(viMaxRg - 1);
 //Пикселей для надписей по оси х
 float fdeltay = viY - 2 * viDeltaaxisH;
 //Пикселей на один отсчет
 fdeltay = fdeltay / (float)(viMaxRg - 1);
 float y = viY - viDeltaaxisH - objFont.Size;
 for (int i = 0; i < viMaxRg; i++)
 {
  graph.DrawString(((float)(i * f)).ToString("0.00"), 
     objFont, objBrush, viDeltaaxisL - (objFont.Size) * 5 - 5, y - i * fdeltay);
 }
}
#endregion

#region Надписи - Значения  над точкой 
//1 параметр = false - без отображения процентов, true - с отображением
//2 параметр = false - без сдвига, true - со здвигом по оси Y
public void vDrawTextAxYValuesPoint(bool a, bool b)
{
 string s = string.Empty;
 float fMax = float.MinValue;
 float fSum = 0;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     fSum += float.Parse(s);
     if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 //Пикселей для надписей по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет по х
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 float x = viDeltaaxisL;
 float fdeltay = viY - 2 * viDeltaaxisH;
 float y = viY - viDeltaaxisH - objFont.Size;
 //Пикселей на одну единицу
 fdeltay = fdeltay / fMax;
 float fdelta = 0;
 for (int i = 0; i < viMaxRg; i++)
 {
   if (a)
   {
    if (i % 2 == 0) fdelta = objFont.Size;
      else fdelta = 2 * objFont.Size;
   }
   else
   {
     fdelta = objFont.Size;
   }
   if (b)
   {
    graph.DrawString(rgsValues[i, 0], objFont, objBrush, x + i * fdeltax,
           y - (float.Parse(rgsValues[i, 0]) * fdeltay) - fdelta);
   }
   else
   {
    float fp = float.Parse(rgsValues[i, 0]);
    fp = (fp * 100) / fSum;
    graph.DrawString(rgsValues[i, 0] + "-" + fp.ToString("0.0") + "%", 
                    objFont, objBrush, x + i * fdeltax,
                              y - (float.Parse(rgsValues[i, 0]) * fdeltay) - fdelta);
   }
 }
}
#endregion

Мы создали полностью код для отображения линейного графика. Все функции для управления построением и изменения внешнего вида представлены в void vCreateLinGr():

private void vCreateLinGr()
{
 //Создаем массив значений для вывода на графике
 vCreateRg();
 //Создаем класс и передаем ему размер холсты
 PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
 //Фон холста
 clPaint.vSetBackground(Color.White);
 //Параметры вызоыва: отступы слева, справа, сверху(снизу),Цвет осей, толщина пера
 clPaint.vDravAxis(50, 50, 30, Color.Red, 2,true);
 clPaint.vSetPenWidthLine(1);
 clPaint.vSetPenColorLine(Color.Silver);
 clPaint.MaxRg = 20;
 clPaint.vDravGrid();
 clPaint.vSetPenWidthLine(2);
 clPaint.vSetPenColorLine(Color.Green);
 clPaint.RgValue = rgsValues;
 clPaint.vDrawGraphLines();
 Font objFont = new Font("Arial", 7, FontStyle.Bold | FontStyle.Italic);
 clPaint.font = objFont;
 clPaint.brush = Brushes.Blue;
 //Здесь необходимо поэксперементировать с
 //использованием различных надписей и изменением параметров
 clPaint.vDrawTextAxXNumber(false);
 //clPaint.vDrawTextAxXValues(true);
 clPaint.vDrawTextAxYValues();
 clPaint.vDrawTextAxYValuesPoint(true,false);
 //Принимаем нарисованное в pictureBox
 pictureBox1.Image = clPaint.Bmp;        
}

Результат выполнения кода показан на Рис.10.:

graph_10.gif

Рис.10. Линейный график


В начало

Параграф 3. Создание гистограмм

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

private Brush[] br ={
Brushes.LightGreen,Brushes.Chartreuse,Brushes.LimeGreen,Brushes.Green,Brushes.DarkGreen,
Brushes.DarkOliveGreen,Brushes.LightPink,Brushes.LightSeaGreen,Brushes.LightCoral,Brushes.DarkCyan ,
Brushes.Crimson,Brushes.CornflowerBlue ,Brushes.Chocolate,Brushes.CadetBlue,Brushes.BlueViolet, 
Brushes.Maroon, Brushes.Blue,Brushes.Brown,Brushes.DarkBlue, Brushes.Red,
Brushes.Coral,Brushes.DarkRed, Brushes.DarkMagenta, Brushes.DarkOrange,Brushes.DarkOrchid};

И так, нам понадобится всего лишь одна новая функция. Основное отличие - использование функции FillRectangle.

#region Рисование Гистограммы
//Параметры: a=0 без сдвига цвета a=1 со сдвигом
//b = 0 - без разрыва столбиков > 1 - с разрывом и величина разрыва в %
public void vDrawGraphRectangular(int a, int c)
{
 string s = string.Empty;
 string s1 = string.Empty;
 string s2 = string.Empty;
 float f = 0;
 float x1 = 0;
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH;
 float fMax = float.MinValue;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 //Пикселей для рисования по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 //Пикселей для рисования по оси y
 float fdeltay = viY - 2 * viDeltaaxisH;
 //Пикселей на одну единицу массива значений
 fdeltay = fdeltay / fMax;
 float fdx = 0;
 if (c != 0) fdx = (fdeltax * c / 100) / 2;
 Random rand = new Random(DateTime.Now.Millisecond);
 int arn = rand.Next((int)br.Length);
 objBrush = br[arn];
 for (int i = 0; i < viMaxRg - 1; i++)
 {
  s = rgsValues[i, 0];
  f = float.Parse(s);
  x1 = x + ((float)i * fdeltax);
  if (a == 0)
  {
   graph.FillRectangle(objBrush, x1 + fdx, y - fdeltay * f, 
                                   fdeltax - 2 * fdx, fdeltay * f);
  }
  else
  {
   int b = i % br.Length;
   graph.FillRectangle(br[b], x1 + fdx, y - fdeltay * f, 
                                   fdeltax - 2 * fdx, fdeltay * f);
  }
  if (i == viMaxRg - 2)
  {
   int b = (i + 1) % br.Length;
   s = rgsValues[i + 1, 0];
   f = float.Parse(s);
   x1 = x + ((float)(i + 1) * fdeltax);
   if (a == 0)
   {
    graph.FillRectangle(objBrush, x1 - 2, y - fdeltay * f, 
                                         4/*fdeltax*/, fdeltay * f);
   }
   else
   {
    graph.FillRectangle(br[b], x1 - 2, y - fdeltay * f, 
                                         4/*fdeltax*/, fdeltay * f);
   }
  }
 }
}
#endregion

Запишем код обработки нажатия кнопки 2 и выполним решение:

private void button2_Click(object sender, EventArgs e)
{
 viNumButton = 2;
 vCreateRectangleDiagramm();
}
private void vCreateRectangleDiagramm()
{
 //Создаем массив значений для вывода на графике
 vCreateRg();
 //Создаем класс и передаем ему размер холсты
 PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
 //Фон холста
 clPaint.vSetBackground(Color.White);
 //Параметры вызоыва: отступы слева, справа, сверху(снизу),Цвет осей, толщина пера
 clPaint.vDravAxis(50, 50, 30, Color.Red, 2,true);
 clPaint.vSetPenWidthLine(1);
 clPaint.vSetPenColorLine(Color.Silver);
 clPaint.MaxRg = 20;
 clPaint.vDravGrid();
 clPaint.vSetPenWidthLine(2);
 clPaint.vSetPenColorLine(Color.Green);
 clPaint.RgValue = rgsValues;
 //a=0 без сдвига цвета a=1 со сдвигом,b = 0 - без разрыва, > 1 - с разрывом и величина разрыва в %
 clPaint.vDrawGraphRectangular(1, 5);
 Font objFont = new Font("Arial", 7, FontStyle.Bold | FontStyle.Italic);
 clPaint.font = objFont;
 clPaint.brush = Brushes.Blue;
 clPaint.vDrawTextAxXNumber(false);
 //clPaint.vDrawTextAxXValues(true);
 clPaint.vDrawTextAxYValues();
 clPaint.vDrawTextAxYValuesPoint(true, false);
 //Принимаем нарисованное в pictureBox
 pictureBox1.Image = clPaint.Bmp;    
}

Цветом показано единственное отличие от кода создания линейной диаграммы. Результат работы кода приведен на Рис.11.:

graph_11.gif

Рис.11. Гистограмма


В начало

Параграф 4. Круговые диаграммы и элементы 3D графики

Построение круговых диаграмм с элементами 3D графики требует несколько больших затрат по сравнению с рассмотренным выше материалом. Прежде всего, необходимо определить дополнительные переменные для величин: оси эллипса (vfDiamX, vfDiamY), центр круговой диаграммы (vfXcirc, vfYcirc). Кроме того, если мы хотим, что бы в легенде (пояснению к графику) цвета надписей соответствовали цветам секторов диаграммы, то потребуется задать массив цветов однозначно соответствующий массиву цветов кистей. Зададим в классе:

private float vfDiamX = 100;
private float vfDiamY = 100;
private float vfXcirc = 100;
private float vfYcirc = 100;
private Color[] color ={
Color.LightGreen,Color.Chartreuse,Color.LimeGreen,Color.Green,Color.DarkGreen,
Color.DarkOliveGreen,Color.LightPink ,Color.LightSeaGreen,Color.LightCoral ,Color.DarkCyan ,
Color.Crimson , Color.CornflowerBlue ,Color.Chocolate,Color.CadetBlue,Color.BlueViolet,
Color.Maroon,Color.Blue,Color.Brown,Color.DarkBlue, Color.Red,
Color.Coral,Color.DarkRed, Color.DarkMagenta, Color.DarkOrange,Color.DarkOrchid};

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

Рисовать диаграмму будем в несколько этапов:

  • Первый этап: Против часовой стрелки рисуем последний сектор и сектора, выходящие за границу 180 градусов, но не заходящие за границу 270 градусов. Рисовать будем кистью с прозрачностью, например 25%, и каждый из них со сдвигом на 1 пиксель вниз. Иначе, если толщина диаграммы задана 20 пикселей, то сектор потребуется нарисовать 20 раз, каждый раз сдвигая на 1 пиксель вниз (Рис.12.1.).

    graph_12_1.gif

    Рис.12.1. Первый этап создания круговой диаграммы

  • Второй этап: Накладываем на данную диаграмму сектора от 0 градусов до сектора, заходящего за 270 градусов, используя SolidBrush и не выполняя сдвиг - рисуем каждый сектор один раз из точки рисования всей диаграммы (Рис.12.2.).

    graph_12_2.gif

    Рис.12.2. Второй этап создания круговой диаграммы

  • Третий этап: по часовой стрелки рисуем сектора, начиная со второго, используя HatchBrush. Рисование выполняем до сектора заходящего за границу -90 градусов, со сдвигом на толщину диаграммы (Рис.12.3.).

    graph_12_3.gif

    Рис.12.3. Третий этап создания круговой диаграммы

  • Четвертый этап: По часовой стрелке накладываем без сдвига, начиная со второго сектора до сектора, заходящего за границу -90 градусов, используя SolidBrush (Рис.12.4.).

    graph_12_4.gif

    Рис.12.4. Четвертый этап создания круговой диаграммы

  • Отдельно рисуем первый сектор, сначала используя HatchBrush со сдвигом на толщину диаграммы, затем накладываем сектор SolidBrush без сдвига. Координаты сектора определяем с учетом параметров сдвига секторов (Рис.12.5.).

    graph_12_5.gif

    Рис.12.5. Пятый этап создания круговой диаграммы

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

Эти этапы рисования выполняет следующая функция:

#region  vDravCircle3D
//Параметры - Отступ от краев по X слева deltaaxisL, от краев по Y справа deltaaxisR,
//deltaaxisH - отступа сверху и снизу, толщина диаграммы viH, сдвиг сектора viDx, viDy
public void vDravCircle3D(int deltaaxisL, int deltaaxisR, 
                 int deltaaxisH, int viH, int viDx, int viDy)
{
 //Запоминаем отступы            
 viDeltaaxisL = deltaaxisL;
 viDeltaaxisR = deltaaxisR;
 viDeltaaxisH = deltaaxisH;
 float a = viX - (deltaaxisL + deltaaxisR);
 //Нужен ли выброс сектора
 int viMov = 1;
 if (viDx == 0 && viDy == 0)
 {
     viMov = 0;
 }
 //Запоминаем диаметр
 vfDiamX = a;
 vfDiamY = viY - 2 * viDeltaaxisH;
 //Запоминаем центр элипса
 vfXcirc = deltaaxisL + a / 2;
 vfYcirc = viY / 2;
 graph.SmoothingMode = SmoothingMode.AntiAlias;
 //Определяем сумму всех значений в массиве
 float fSum = 0;
 string s = string.Empty;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     fSum += float.Parse(s);
 }
 float f = 0;
 float fBSum = 0;
 float fDeltaGrad = (fSum / (float)360);
 SolidBrush objBrush = new SolidBrush(Color.Aqua);
 Random rand = new Random(DateTime.Now.Millisecond);
 float[] frgZn = new float[viMaxRg];
 float[] frgSumGr = new float[viMaxRg];
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     frgZn[i] = float.Parse(s);
     if (i == 0) frgSumGr[i] = 0;
     else frgSumGr[i] = frgZn[i] + frgSumGr[i - 1];
 }
 for (int i = viMaxRg - 1; i >= 0; i--)
 {
  if (i != viMaxRg - 1 && fBSum < 90) break;
  //f в градусах  fBSum в градусах
  f = frgZn[i] / fDeltaGrad;
  //fBSum = frgSumGr[i] / fDeltaGrad;
  if (i == viMaxRg - 1)
  {
      fBSum = 360 - f;
  }
  else
  {
      fBSum -= f;
  }
  //Для цвета
  int j = i % br.Length;
  float k = f;
  if (f < 1) k = 1;
  //objBrush.Color = Color.FromArgb(rand.Next(255), rand.Next(255), rand.Next(255));
  if (i != 0)
  {
   if ((fBSum > 90 && fBSum < 180) || i == viMaxRg - 1)
   {
    for (int d = 0; d < viH; d++)
    {
       //Этап 1
       graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[j]/*objBrush.Color*/), 
             vfXcirc - a / 2, vfYcirc - vfDiamY / 2 + d,
               vfDiamX, vfDiamY, fBSum, k);
    }
   }
   objBrush.Color = color[j];
   //Этап 2
   graph.FillPie(objBrush, vfXcirc - a / 2, vfYcirc - vfDiamY / 2,
        vfDiamX, vfDiamY, fBSum, k);
  }
 }
 fBSum = 0;
 for (int i = viMov; i < viMaxRg; i++)
 {
  //f в градусах  fBSum в градусах
  f = frgZn[i] / fDeltaGrad;
  if (i == 1)
  {
      fBSum = frgZn[0] / fDeltaGrad;
  }
  //Для цвета
  int j = i % br.Length;
  float k = f;
  if (f < 1) k = 1;

  if (fBSum < 90)
  {
      for (int d = 0; d < viH; d++)
      {
          //Этап 3
          graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[j]), 
           vfXcirc - a / 2, vfYcirc - vfDiamY / 2 + d,
              vfDiamX, vfDiamY, fBSum, k);
      }
      objBrush.Color = color[j];
      //Этап 4
      graph.FillPie(objBrush, vfXcirc - a / 2, vfYcirc - vfDiamY / 2,
       vfDiamX, vfDiamY, fBSum, k);
  }
  else
  {
      break;
  }
  fBSum += f;
 }
 //Рисуем сдвинутым первый сектор 
 //Этап 5
 if (viMov == 1)
 {
  f = frgZn[0] / fDeltaGrad;
  fBSum = 0;
  float k1 = f;
  if (f < 1) k1 = 1;
  for (int d = 0; d < viH; d++)
  {
      graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[0]), 
       vfXcirc - a / 2 + viDx, vfYcirc - vfDiamY / 2 + d - viDy,
          vfDiamX, vfDiamY, fBSum, k1);
  }
  objBrush.Color = color[0];
  graph.FillPie(objBrush, vfXcirc - a / 2 + viDx, vfYcirc - vfDiamY / 2 - viDy,
  vfDiamX, vfDiamY, fBSum, k1);
 }
}
#endregion

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

#region vDravTextCircle
public void vDravTextCircle1(bool vfGde)
{
  float fSum = 0;
  string s = string.Empty;
  for (int i = 0; i < viMaxRg; i++)
  {
      s = rgsValues[i, 0];
      fSum += float.Parse(s);
  }
  float f = 0;
  float fBSum = 0;
  float f1Radian = (float)Math.PI / 180;
  float fDeltaGrad = fSum / 360;
  for (int i = 0; i < viMaxRg; i++)
  {
   s = rgsValues[i, 0];
   f = float.Parse(s);
   //f в градусах
   f = f / fDeltaGrad;
   int j = i % br.Length;
   //Угол в радианах
   float fRad = (f + fBSum) * f1Radian;
   float fty = 0;
   float ftx = 0;
   float fSin = (float)Math.Sin((360 - (f / 2 + fBSum)) * f1Radian);
   float fCos = (float)Math.Cos((360 - (f / 2 + fBSum)) * f1Radian);
   float c = (float)Math.Sqrt((vfDiamX / 2 * vfDiamX / 2 * vfDiamY / 2 * vfDiamY / 2) /
       (vfDiamY / 2 * vfDiamY / 2 * fCos * fCos + vfDiamX / 2 * vfDiamX / 2 * fSin * fSin));
   c -= 3 * objFont.Size;
   if (c < 0) c = 0;
   ftx = c * fCos;
   fty = c * fSin;
   ftx = vfXcirc + ftx;
   fty = vfYcirc - fty;
   if (vfGde)
   {
    graph.DrawString(Convert.ToString(i + 1), objFont, objBrush, ftx, fty);
   }
   else
   {
    graph.DrawString(rgsValues[i, 0], objFont, objBrush, ftx, fty);
   }
   fBSum += f;
  }
}
#endregion

#region Текст легенды
public void vDravTextKeyCircle(bool vfGde)
{
 float fSum = 0;
 float f = 0;
 string s = string.Empty;
 for (int i = 0; i < viMaxRg; i++)
 {
  s = rgsValues[i, 0];
  fSum += float.Parse(s);
 }
 //Сдвиг от круговой диаграммы
 float vfSdvig = vfXcirc + vfDiamX / 2;
 vfSdvig += (viX - vfSdvig) / 5;
 //Высота места для легенды
 //На одну строку по высоте отводится - +1 на заголовок
 float vfHg = viY / (viMaxRg + 2);
 vSetFont("Arial", 12, true);
 if (viMaxRg > 100)
 {
  graph.DrawString("Легенда не может быть размещена", 
   objFont, Brushes.DarkBlue, vfSdvig + (viX - vfSdvig) / 10, objFont.Size);
 }
 else
 {
  //Шрифт в 2 раза меньше места на строку надписи
  if (viMaxRg > 15)
  {
      vSetFont("Arial", (vfHg / 2), true);
  }
  else
  {
   if (viMaxRg > 10)
   {
       vSetFont("Arial", (vfHg / 3), true);
   }
   else
   {
       vSetFont("Arial", (vfHg / 6), true);
   }
  }
  if (vfGde)
  {
      graph.DrawString("Пояснения к графику", 
       objFont, Brushes.DarkBlue, vfSdvig /*+ (viX - vfSdvig) / 10*/, objFont.Size);
  }
  else
  {
      graph.DrawString("Пояснения к графику", 
        objFont, objBrush, vfSdvig/* + (viX - vfSdvig) / 10*/, objFont.Size);
  }
  if (viMaxRg > 15)
  {
      vSetFont("Arial", (vfHg / 2) + 1, true);
  }
  else
  {
   if (viMaxRg > 10)
   {
       vSetFont("Arial", (vfHg / 4) + 1, true);
   }
   else
   {
       vSetFont("Arial", (vfHg / 7) + 1, true);
   }
  }
  for (int i = 0; i < rgsValues.Length / 2; i++)
  {
   Brush brTxt = null;
   int j = i % br.Length;
   if (vfGde) brTxt = br[j];
   else brTxt = objBrush;
   graph.DrawString(Convert.ToString(i + 1), objFont, brTxt, vfSdvig, vfHg * (i + 2));
   f = float.Parse(rgsValues[i, 0]);
   f = (f * 100) / fSum;
   graph.DrawString(rgsValues[i, 0], objFont, 
       brTxt, vfSdvig + 1 * (viX - vfSdvig) / 5, vfHg * (i + 2));
   graph.DrawString(f.ToString("0.0") + "%", objFont, 
       brTxt, vfSdvig + 2 * (viX - vfSdvig) / 5, vfHg * (i + 2));
   graph.DrawString(rgsValues[i, 1], objFont, 
       brTxt, vfSdvig + 3 * (viX - vfSdvig) / 5, vfHg * (i + 2));
  }
 }
}
#endregion


#region Смена шрифта по секторам
private void vSetFont(string name, float size, bool bold)
{
 if (objFont != null) objFont = null;
 if (bold)
 {
     objFont = new Font(name, size, FontStyle.Bold);
 }
 else
 {
     objFont = new Font(name, size);
 }
}
#endregion

Оформим вызовы функций:

private void button3_Click(object sender, EventArgs e)
{
 viNumButton = 3;
 vCreateCircleDiagramm();
}
private void vCreateCircleDiagramm()
{
 //Создаем массив значений для вывода на графике
 vCreateRg();
 //Создаем класс и передаем ему размер холсты
 PaintCl clPaint = new PaintCl(pictureBox1.Width, pictureBox1.Height);
 //Фон холста
 clPaint.vSetBackground(Color.White);
 //Передаем значения массива в класс
 clPaint.RgValue = rgsValues;
 //Рисуем график. Параметры: отступ осей x слева, x справа ,
 //y от краев холста, толщина диаграммы,вынос сектора           
 clPaint.vDravCircle3D(20, 250, 50, 20, 20, 40);
 //Круговые надписи true цифры 1-20, false - значения
 clPaint.vDravTextCircle1(true);
 //false - Разноцветные надписи в легенде true - Цветом шрифта
 clPaint.vDravTextKeyCircle(true);
 //Принимаем нарисованное в pictureBox
 pictureBox1.Image = clPaint.Bmp;
}

Отметим, что при задании толщины диаграммы, равной нулю, получим обычную эллиптическую диаграмму, а при равенстве осей Х и Y - круговую.

Результат выполнения решения показан на Рис.12.6:

graph_12.gif

Рис.12.6. Круговая диаграмма

В заключении, еще раз повторим, что все параметры целесообразно иметь настраиваемыми, что позволяет быстро подобрать приемлемый вид графического отображения для демонстрации. Целесообразно также выполнить автономную настройку диаграмм по тестовым значениям (как это сделано в программе LitFregMeter - см. Параграф 2.). Тогда мы сможем быстро подбирать параметры, например так:

//Смена фона диаграммы
private void ColorBackGround_Click(object sender, EventArgs e)
{
 if (colorDialog1.ShowDialog() == DialogResult.OK)
 {
  //Переменная objColorBackGroung задана глобально, сохраняется в реестре
  //при закрытии приложения и передается в класс при рисовании диаграммы
  //clPaint.vSetBackground(objColorBackGroung);
  objColorBackGroung = colorDialog1.Color;
  vGhangeDiagramm();
 }
}
//Перерисовка конкретной диаграммы при настройке
private void vGhangeDiagramm()
{
 switch (viNumButton)
 {
  case 1:
   vCreateLinGr();
  break;
  case 2:
   vCreateRectangleDiagramm();
  break;
  case 3:
   vCreateCircleDiagramm();
   break;
 }
}


В начало

Параграф 5. Базовый класс для рисования графиков

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

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

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace graph1
{
 class PaintCl
 {
  //Основные объекты для рисования       
  private Bitmap bmp = null;
  private Graphics graph = null;
  private Font objFont = new Font("Arial", 8, FontStyle.Bold);
  private Brush objBrush = Brushes.Black;        
  private Pen objPenLine = new Pen(Color.Black, 1);

  //Размеры холста
  private int viX = 200;
  private int viY = 100;

  //Параметры для рисования круговой диаграммы
  private float vfDiamX = 100;
  private float vfDiamY = 100;
  private float vfXcirc = 100;
  private float vfYcirc = 100;

  //Массив предопределенных цветов для отображения легенды
  private Color[] color ={Color.LightGreen,Color.Chartreuse,Color.LimeGreen,
                          Color.Green,Color.DarkGreen,Color.DarkOliveGreen,
                          Color.LightPink,Color.LightSeaGreen,Color.LightCoral,
                          Color.DarkCyan,Color.Crimson,Color.CornflowerBlue,
                          Color.Chocolate,Color.CadetBlue,Color.BlueViolet,
                          Color.Maroon,Color.Blue,Color.Brown,Color.DarkBlue, 
                          Color.Red,Color.Coral,Color.DarkRed, Color.DarkMagenta, 
                          Color.DarkOrange,Color.DarkOrchid};

  //Массив предопределенных цветов для отображения круговой диаграммы и гистограммы
  private Brush[] br =   {Brushes.LightGreen,Brushes.Chartreuse,Brushes.LimeGreen,
                          Brushes.Green,Brushes.DarkGreen,Brushes.DarkOliveGreen,
                          Brushes.LightPink,Brushes.LightSeaGreen,Brushes.LightCoral,
                          Brushes.DarkCyan,Brushes.Crimson,Brushes.CornflowerBlue,
                          Brushes.Chocolate,Brushes.CadetBlue,Brushes.BlueViolet, 
                          Brushes.Maroon, Brushes.Blue,Brushes.Brown,Brushes.DarkBlue, 
                          Brushes.Red,Brushes.Coral,Brushes.DarkRed, 
                          Brushes.DarkMagenta, Brushes.DarkOrange,Brushes.DarkOrchid};

  //Массив значений для рисования графика
  private string[,] rgsValues = null;
  //Размер массива
  private int viMaxRg = 20;


  //Отступы от краев холста
  private int viDeltaaxisL = 50;
  private int viDeltaaxisR = 50;
  private int viDeltaaxisH = 20;


  #region Конструктор
  public PaintCl(int a, int b)
  {
   bmp = new Bitmap(a, b);
   graph = Graphics.FromImage(bmp);
   viX = a;
   viY = b;            
  }
  #endregion

  #region Установка цвета фона диаграммы
  public void vSetBackground(Color bcl)
  {
   graph.Clear(bcl);
  }
  #endregion

  #region Доступ к переменным класса
  public Bitmap Bmp
  {
   get { return bmp; }
  }
  public int MaxRg
  {
   set { viMaxRg = value; }
  }
  public string[,] RgValue
  {
   set { rgsValues = value; }
  }
  public Font font
  {
   set { objFont = value; }
  }
  public Brush  brush
  {
   set { objBrush  = value; }
  }
  #endregion

  #region Карандаш, шрифт, кисть
  //Цвет карандаша
  public void vSetPenColorLine(Color pcl)
  {
   if (objPenLine == null)
   {
    objPenLine = new Pen(Color.Black, 1);
   }
   objPenLine.Color = pcl;
  }
  //Установка толщина карандаша        
  public void vSetPenWidthLine(int penwidth)
  {
   if (objPenLine == null)
   {
    objPenLine = new Pen(Color.Black, 1);
   }
   objPenLine.Width = penwidth;
  }
  #endregion

  #region Рисование Осей
  //Отступ от левого края - deltaaxis, отступ от правого края - deltaaxisR,
  //отступ от нижнего и верхнего края - deltaaxisH, цвет оси - colorpenaxis,
  //толщина пера для оси - widthpen, наличие стрелок на оси - fArrow(true)
  public void vDravAxis(int deltaaxisL, int deltaaxisR, int deltaaxisH, 
                        Color colorpenaxis,int widthpen, bool fArrow)
  {
   //Запоминаем отступы                        
   viDeltaaxisL = deltaaxisL;
   viDeltaaxisR = deltaaxisR;
   viDeltaaxisH = deltaaxisH;
   //Запоминаем цвет осей и толщину
   vSetPenColorLine(colorpenaxis);
   if (widthpen > 0) vSetPenWidthLine(widthpen);
   //Точка начала рисования по х и y           
   int x = deltaaxisL;
   int y = viY - deltaaxisH;
   int x1 = viX - deltaaxisR;
   int y1 = deltaaxisH;
   int d = 0;
   if (fArrow) d = widthpen * 10;
   //Оси на d пикселей длинней для стрелок
   graph.DrawLine(objPenLine, x, y, x1 + d, y);
   graph.DrawLine(objPenLine, x, y, x, y1 - d);
   if (fArrow)
   {
    int a = 10 * (int)objPenLine.Width;
    int b = 2 * (int)objPenLine.Width;
    int x2 = x1 - a;
    int y2 = y + b;
    //Стрелки
    graph.DrawLine(objPenLine, x1 + 20, y, x2 + d, y2);
    y2 = y - b;
    graph.DrawLine(objPenLine, x1 + 20, y, x2 + d, y2);
    x2 = x - b;
    y2 = y1 + a;
    graph.DrawLine(objPenLine, x, y1 - d, x2, y2 - d);
    x2 = x + b;
    graph.DrawLine(objPenLine, x, y1 - d, x2, y2 - d);
   }   
  }
  #endregion

  #region Рисование сетки
  public void vDravGrid()
  {
   float x = viDeltaaxisL;
   float y = viY - viDeltaaxisH;
   float x1 = viX - viDeltaaxisR;
   float y1 = viDeltaaxisH;
   float f = (y - y1) / (float)viMaxRg;
   for (int i = 1; i < viMaxRg + 1; i++)
   {
    graph.DrawLine(objPenLine, x, y - f * i, x1, y - f * i);
   }
   f = (x - x1) / (float)(viMaxRg - 1);
   for (int i = 1; i < viMaxRg; i++)
   {
    graph.DrawLine(objPenLine, x - f * i, y, x - f * i, y1);
   }
  }
  #endregion

  #region Рисование линий графика для линейного графика
  public void vDrawGraphLines()
  {
   string s = string.Empty;
   string s1 = string.Empty;
   string s2 = string.Empty;
   float f = 0;
   float f1 = 0;
   float x1 = 0;
   float x = viDeltaaxisL;//-objPenLine.Width;
   float y = viY - viDeltaaxisH;// +objPenLine.Width;
   float x2 = 0;
   float fMax = float.MinValue;
   //float fMin = int.MaxValue;
   for (int i = 0; i < viMaxRg; i++)
   {
    s = rgsValues[i, 0];
    if (fMax < float.Parse(s)) fMax = float.Parse(s);
   }
   //Пикселей для рисования по оси х
   float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
   //Пикселей на один отсчет
   fdeltax = fdeltax / (float)(viMaxRg - 1);
   //Пикселей для рисования по оси y
   float fdeltay = viY - 2 * viDeltaaxisH;
   //Пикселей на одну единицу массива значений
   fdeltay = fdeltay / fMax;
   for (int i = 0; i < viMaxRg; i++)
   {
    //Первый раз запоминаем точку старта
    if (i == 0)
    {
     s = rgsValues[i, 0];
     s2 = rgsValues[i, 1];
     f = y - (float.Parse(s) * fdeltay);
     x1 = x;
    }
    else
    {
     s1 = rgsValues[i, 0];
     f1 = y - (float.Parse(s1) * fdeltay);
     x2 = x + (int)(fdeltax * i);
     graph.DrawLine(objPenLine, x1, f, x2, f1);
     s = rgsValues[i, 0];
     s2 = rgsValues[i, 1];
     f = f1;
     x1 = x + (int)(i * fdeltax);
    }
   }
  }
  #endregion

  #region Текст по оси X - цифры 
  //Параметр = false - без сдвига по оси Y, true - со здвигом по оси Y
  public void vDrawTextAxXNumber(bool f)
  {
   //Пикселей для надписей по оси х
   float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
   //Пикселей на один отсчет
   fdeltax = fdeltax / (float)(viMaxRg - 1);
   float x = viDeltaaxisL;
   float y = viY - viDeltaaxisH + objPenLine.Width;
   for (int i = 1; i < viMaxRg + 1; i++)
   {
    if (!f || i % 2 == 0)
    {
     graph.DrawString(Convert.ToString(i), objFont, 
                                         objBrush, x + (i - 1) * fdeltax, y);
    }
    else
    {
     graph.DrawString(Convert.ToString(i), objFont, 
                                        objBrush, x + (i - 1) * fdeltax, y + objFont.Size);
    }
   }
  }
  #endregion

  #region Текст по оси X 
  //Параметр = false - без сдвига по оси Y, true - со здвигом по оси Y
  public void vDrawTextAxXValues(bool f)
  {
   string s = string.Empty;
   //Пикселей для надписей по оси х
   float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
   //Пикселей на один отсчет
   fdeltax = fdeltax / (float)(viMaxRg - 1);
   float x = viDeltaaxisL;
   float y = viY - viDeltaaxisH;// +objPenLine.Width;
   for (int i = 0; i < viMaxRg; i++)
   {
    if (!f || i % 2 == 0)
    {
     graph.DrawString(rgsValues[i, 1], objFont, objBrush, x + i * fdeltax, y);
    }
    else
    {
     graph.DrawString(rgsValues[i, 1], objFont, 
                 objBrush, x + i * fdeltax, y + objFont.Size);
    }
   }
  }
  #endregion

  #region Текст по оси Y - Значения по сетке
  public void vDrawTextAxYValues()
  {
   string s = string.Empty;
   float f = 0;
   float fMax = float.MinValue;
   for (int i = 0; i < viMaxRg; i++)
   {
    s = rgsValues[i, 0];
    if (fMax < float.Parse(s)) fMax = float.Parse(s);
   }
   f = fMax / (float)(viMaxRg - 1);
   //Пикселей для надписей по оси х
   float fdeltay = viY - 2 * viDeltaaxisH;
   //Пикселей на один отсчет
   fdeltay = fdeltay / (float)(viMaxRg - 1);
   float y = viY - viDeltaaxisH - objFont.Size;
   for (int i = 0; i < viMaxRg; i++)
   {
    graph.DrawString(((float)(i * f)).ToString("0.00"), objFont, 
       objBrush, viDeltaaxisL - (objFont.Size) * 5 - 5, y - i * fdeltay);
   }
  }
  #endregion
  //*****************
#region Надписи по оси Y - Значения  над точкой
public void vDrawTextAxYValuesPoint(bool a, bool b)
{
 string s = string.Empty;
 float fMax = float.MinValue;
 float fSum = 0;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     fSum += float.Parse(s);
     if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 //Пикселей для надписей по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет по х
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 float x = viDeltaaxisL;
 float fdeltay = viY - 2 * viDeltaaxisH;
 float y = viY - viDeltaaxisH - objFont.Size;
 //Пикселей на одну единицу
 fdeltay = fdeltay / fMax;
 float fdelta = 0;
 for (int i = 0; i < viMaxRg; i++)
 {
  if (a)
  {
      if (i % 2 == 0) fdelta = objFont.Size;
      else fdelta = 2 * objFont.Size;
  }
  else
  {
      fdelta = objFont.Size;
  }
  if (b)
  {
      graph.DrawString(rgsValues[i, 0], objFont, objBrush, x + i * fdeltax,
          y - (float.Parse(rgsValues[i, 0]) * fdeltay) - fdelta);
  }
  else
  {
   float fp = float.Parse(rgsValues[i, 0]);
   fp = (fp * 100) / fSum;
   graph.DrawString(rgsValues[i, 0] + "-" + fp.ToString("0.0") + "%", 
                            objFont, objBrush, x + i * fdeltax,
                             y - (float.Parse(rgsValues[i, 0]) * fdeltay) - fdelta);
  }
 } 
}
#endregion


#region Рисование графика для графика прямоугольниками 
public void vDrawGraphRectangular(int a, int c)
{
 string s = string.Empty;
 string s1 = string.Empty;
 string s2 = string.Empty;
 float f = 0;
 float x1 = 0;
 float x = viDeltaaxisL;
 float y = viY - viDeltaaxisH;
 float fMax = float.MinValue;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     if (fMax < float.Parse(s)) fMax = float.Parse(s);
 }
 //Пикселей для рисования по оси х
 float fdeltax = viX - viDeltaaxisL - viDeltaaxisR;
 //Пикселей на один отсчет
 fdeltax = fdeltax / (float)(viMaxRg - 1);
 //Пикселей для рисования по оси y
 float fdeltay = viY - 2 * viDeltaaxisH;
 //Пикселей на одну единицу массива значений
 fdeltay = fdeltay / fMax;
 float fdx = 0;
 if (c != 0) fdx = (fdeltax * c / 100) / 2;
 Random rand = new Random(DateTime.Now.Millisecond);
 int arn = rand.Next((int)br.Length);
 objBrush = br[arn];
 for (int i = 0; i < viMaxRg - 1; i++)
 {
  s = rgsValues[i, 0];
  f = float.Parse(s);
  x1 = x + ((float)i * fdeltax);
  if (a == 0)
  {
      graph.FillRectangle(objBrush, x1 + fdx, y - fdeltay * f, fdeltax - 2 * fdx, fdeltay * f);
  }
  else
  {
   int b = i % br.Length;
   graph.FillRectangle(br[b], x1 + fdx, y - fdeltay * f, fdeltax - 2 * fdx, fdeltay * f);
  }
  if (i == viMaxRg - 2)
  {
   int b = (i + 1) % br.Length;
   s = rgsValues[i + 1, 0];
   f = float.Parse(s);
   x1 = x + ((float)(i + 1) * fdeltax);
   if (a == 0)
   {
    graph.FillRectangle(objBrush, x1 - 2, y - fdeltay * f, 4/*fdeltax*/, fdeltay * f);
   }
   else
   {
    graph.FillRectangle(br[b], x1 - 2, y - fdeltay * f, 4/*fdeltax*/, fdeltay * f);
   }
  }
 }
}
#endregion 

#region  vDravCircle3D
//Параметры - Отступ от краев по X слева deltaaxisL, от краев по Y справа,
//deltaaxisH - отступа сверху и снизу, толщина диаграммы viH, сдвиг сектора viDx, viDy
public void vDravCircle3D(int deltaaxisL, int deltaaxisR, int deltaaxisH, int viH, int viDx, int viDy)
{
 //Запоминаем отступы            
 viDeltaaxisL = deltaaxisL;
 viDeltaaxisR = deltaaxisR;
 viDeltaaxisH = deltaaxisH;
 float a = viX - (deltaaxisL + deltaaxisR);
 //Нужен ли выброс сектора
 int viMov = 1;
 if (viDx == 0 && viDy == 0)
 {
     viMov = 0;
 }
 //Запоминаем диаметр
 vfDiamX = a;
 vfDiamY = viY - 2 * viDeltaaxisH;
 //Запоминаем центр элипса
 vfXcirc = deltaaxisL + a / 2;
 vfYcirc = viY / 2;
 graph.SmoothingMode = SmoothingMode.AntiAlias;
 float fSum = 0;
 string s = string.Empty;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     fSum += float.Parse(s);
 }
 float f = 0;
 float fBSum = 0;
 float fDeltaGrad = (fSum / (float)360);
 SolidBrush objBrush = new SolidBrush(Color.Aqua);
 Random rand = new Random(DateTime.Now.Millisecond);
 float[] frgZn = new float[viMaxRg];
 float[] frgSumGr = new float[viMaxRg];
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     frgZn[i] = float.Parse(s);
     if (i == 0) frgSumGr[i] = 0;
     else frgSumGr[i] = frgZn[i] + frgSumGr[i - 1];
 }
 //Рисуем диаграмму против часовой стрелки со штриховкой до 90градусав по часовой
 //Штриховка убирается каждым следующим сектором от сектора предыдущего
 //В первом нарисованном секторе она сохраняетсяна случай сдвига первого по часовой стрелке
 for (int i = viMaxRg - 1; i >= 0; i--)
 {
 if (i != viMaxRg - 1 && fBSum < 90) break;
 //f в градусах  fBSum в градусах
 f = frgZn[i] / fDeltaGrad;
 //fBSum = frgSumGr[i] / fDeltaGrad;
 if (i == viMaxRg - 1)
 {
     fBSum = 360 - f;
 }
 else
 {
     fBSum -= f;
 }
 //Для цвета
 int j = i % br.Length;
 float k = f;
 if (f < 1) k = 1;
 //objBrush.Color = Color.FromArgb(rand.Next(255), rand.Next(255), rand.Next(255));
 if (i != 0)
 {
  if ((fBSum > 90 && fBSum < 180) || i == viMaxRg - 1)
  {
   for (int d = 0; d < viH; d++)
   {
    graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[j]/*objBrush.Color*/), 
          vfXcirc - a / 2, vfYcirc - vfDiamY / 2 + d,
                 vfDiamX, vfDiamY, fBSum, k);
   }
  }
    objBrush.Color = color[j];
   graph.FillPie(objBrush, vfXcirc - a / 2, vfYcirc - vfDiamY / 2,
          vfDiamX, vfDiamY, fBSum, k);
  }
 }
 //Рисуем до 90градусов без первого сегмента в случае необходимости 
 //сдвига первого по часовой стрелке
 fBSum = 0;
 for (int i = viMov; i < viMaxRg; i++)
 {
  //f в градусах  fBSum в градусах
  f = frgZn[i] / fDeltaGrad;
  if (i == 1)
  {
      fBSum = frgZn[0] / fDeltaGrad;
  }
  //Для цвета
  int j = i % br.Length;
  float k = f;
  if (f < 1) k = 1;

  if (fBSum < 90)
  {
   for (int d = 0; d < viH; d++)
   {
    graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[j]), vfXcirc - a / 2, 
        vfYcirc - vfDiamY / 2 + d,
          vfDiamX, vfDiamY, fBSum, k);
   }
   objBrush.Color = color[j];
   graph.FillPie(objBrush, vfXcirc - a / 2, vfYcirc - vfDiamY / 2,
   vfDiamX, vfDiamY, fBSum, k);
  }
  else
  {
      break;
  }
  fBSum += f;
 }
 //Рисуем сдвинутый сектор при необходимости
 if (viMov == 1)
 {
  f = frgZn[0] / fDeltaGrad;
  fBSum = 0;
  float k1 = f;
  if (f < 1) k1 = 1;
  for (int d = 0; d < viH; d++)
  {
      graph.FillPie(new HatchBrush(HatchStyle.Percent25, color[0]), 
       vfXcirc - a / 2 + viDx, vfYcirc - vfDiamY / 2 + d - viDy,
          vfDiamX, vfDiamY, fBSum, k1);
  }
  objBrush.Color = color[0];
  graph.FillPie(objBrush, vfXcirc - a / 2 + viDx, vfYcirc - vfDiamY / 2 - viDy,
  vfDiamX, vfDiamY, fBSum, k1);
 }
}
#endregion

#region vDravTextCircle
public void vDravTextCircle1(bool vfGde)
{
 float fSum = 0;
 string s = string.Empty;
 for (int i = 0; i < viMaxRg; i++)
 {
  s = rgsValues[i, 0];
  fSum += float.Parse(s);
 }
 float f = 0;
 float fBSum = 0;
 float f1Radian = (float)Math.PI / 180;
 float fDeltaGrad = fSum / 360;
 for (int i = 0; i < viMaxRg; i++)
 {
  s = rgsValues[i, 0];
  f = float.Parse(s);
  //f в градусах
  f = f / fDeltaGrad;
  int j = i % br.Length;
  //Угол в радианах
  float fRad = (f + fBSum) * f1Radian;
  float fty = 0;
  float ftx = 0;
  float fSin = (float)Math.Sin((360 - (f / 2 + fBSum)) * f1Radian);
  float fCos = (float)Math.Cos((360 - (f / 2 + fBSum)) * f1Radian);
  float c = (float)Math.Sqrt((vfDiamX / 2 * vfDiamX / 2 * vfDiamY / 2 * vfDiamY / 2) /
      (vfDiamY / 2 * vfDiamY / 2 * fCos * fCos + vfDiamX 
            / 2 * vfDiamX / 2 * fSin * fSin));
  c -= 3 * objFont.Size;
  if (c < 0) c = 0;
  ftx = c * fCos;
  fty = c * fSin;
  ftx = vfXcirc + ftx;
  fty = vfYcirc - fty;
  if (vfGde)
  {
   graph.DrawString(Convert.ToString(i + 1), objFont, objBrush, ftx, fty);
  }
  else
  {
   graph.DrawString(rgsValues[i, 0], objFont, objBrush, ftx, fty);
  }
  fBSum += f;
 }
}
#endregion

#region Смена шрифта
private void vSetFont(string name, float size, bool bold)
{
 if (objFont != null) objFont = null;
 if (bold)
 {
  objFont = new Font(name, size, FontStyle.Bold);
 }
 else
 {
  objFont = new Font(name, size);
 }
}
#endregion

#region Текст легенды
public void vDravTextKeyCircle(bool vfGde)
{
 float fSum = 0;
 float f = 0;
 string s = string.Empty;
 for (int i = 0; i < viMaxRg; i++)
 {
     s = rgsValues[i, 0];
     fSum += float.Parse(s);
 }
 //Сдвиг от круговой диаграммы
 float vfSdvig = vfXcirc + vfDiamX / 2;
 vfSdvig += (viX - vfSdvig) / 5;
 //Высота места для легенды
 //На одну строку по высоте отводится - +1 на заголовок
 float vfHg = viY / (viMaxRg + 2);
 vSetFont("Arial", 12, true);
 if (viMaxRg > 100)
 {
  graph.DrawString("Легенда не может быть размещена", 
   objFont, Brushes.DarkBlue, vfSdvig + (viX - vfSdvig) / 10, objFont.Size);
 }
 else
 {
  //Шрифт в 2 раза меньше места на строку надписи
  if (viMaxRg > 15)
  {
      vSetFont("Arial", (vfHg / 2), true);
  }
  else
  {
   if (viMaxRg > 10)
   {
    vSetFont("Arial", (vfHg / 3), true);
   }
   else
   {
    vSetFont("Arial", (vfHg / 6), true);
   }
  }
  if (vfGde)
  {
   graph.DrawString("Пояснения к графику", objFont, 
       Brushes.DarkBlue, vfSdvig /*+ (viX - vfSdvig) / 10*/, objFont.Size);
  }
  else
  {
   graph.DrawString("Пояснения к графику", objFont, 
       objBrush, vfSdvig/* + (viX - vfSdvig) / 10*/, objFont.Size);
  }
  if (viMaxRg > 15)
  {
   vSetFont("Arial", (vfHg / 2) + 1, true);
  }
  else
  {
   if (viMaxRg > 10)
   {
       vSetFont("Arial", (vfHg / 4) + 1, true);
   }
   else
   {
       vSetFont("Arial", (vfHg / 7) + 1, true);
   }
  }
  for (int i = 0; i < rgsValues.Length / 2; i++)
  {
   Brush brTxt = null;
   int j = i % br.Length;
   if (vfGde) brTxt = br[j];
   else brTxt = objBrush;
   graph.DrawString(Convert.ToString(i + 1), objFont, brTxt, vfSdvig, vfHg * (i + 2));
   f = float.Parse(rgsValues[i, 0]);
   f = (f * 100) / fSum;
   graph.DrawString(rgsValues[i, 0], objFont, 
        brTxt, vfSdvig + 1 * (viX - vfSdvig) / 5, vfHg * (i + 2));
   graph.DrawString(f.ToString("0.0") + "%", objFont, 
        brTxt, vfSdvig + 2 * (viX - vfSdvig) / 5, vfHg * (i + 2));
   graph.DrawString(rgsValues[i, 1], objFont, 
       brTxt, vfSdvig + 3 * (viX - vfSdvig) / 5, vfHg * (i + 2));
  }
 }
}
#endregion
 }
}

Молчанов Владислав 21.09.2005г. Материал переработан 14.10.2008г.

В начало

В начало книги

На главную страницу


Сайт управляется системой uCoz