Дата публикации статьи: 01.09.2006 21:26

Удачные диаграммы: GDI+ Работа с графикой. Часть 1

Автор: Ged Mead
Перевод: Виталий Готовцов
[Оригинал Статьи] [Обсудить в форуме]

Давайте не будем изобретать колесо.

    Во многих ситуациях знакомая круговая диаграмма является хорошим способом представления данных в иллюстрированной форме. Класс Graphics в .NET предлагнает нам легкий способ создавать этот стиль диаграмм для демонстрации миру фактов и цифр.

    Вышерасположенная диаграмма может быть создана всего несколькими строками кода.  Однако это, очевидно, очень ограничивает ее самостоятельное использование. Картинка красочна, но нам необходимо знать, что представляют сегменты. Давайте рассмотрим основной, но очень полезный способ этого – использование ключа диаграммы:

    С таким ключом, как показанный выше, пользователю очень легко определить, какая часть круговой диаграммы представляет какую компанию. Чтобы сделать данные еще более полезными, общее значение каждой отдельной компании тоже показано в ключе. Вы увидите, что класс Graphics в .NET Framework делает совсем легким создание круговой диаграммы и ключей подобного рода. Так что мы будем использовать встроенные возможности DrawPie и DrawString и избежим изобретения колеса.

Прежде всего: Данные

    Очевидно, нам будут нужны данные для создания нашей диаграммы-примера. Поскольку эта статья направлена на то, чтобы показать вам способы использования GDI+ и класса Graphics, я хочу тратить как можно меньше времени на ту часть проекта, которая занимается сбором данных.
    По этой причине я выбрал создание данных во время проектирования. Хотя это может подходить в реальных ситуациях, я уверен, что большую часть времени вы будете вынуждены получать данные из других источников, таких как файлы с данными, базы данных, непосредственный ввод пользователем. Мы планируем посмотреть на эти другие источники в будущих статьях.
    В настоящее же время я остановился на использовании простой Структуры (Structure), которая создает определенный пользователем Тип (Type), называемый GraphData.
Это Тип будет содержать три Поля (Fields) – Amount, Clr и Description. Мы будем создавать данные для некоторых вымышленных компаний, и три поля будут представлять:
Amount – Ежегодный товарооборот $K
Clr – цвет, используемый в диаграмме для представления компании
Description – название компании.
Для создания структуры GraphData поместите следующий код в вашу форму, убедившись, что он помещен вне процедур.

Structure GraphData
           Dim Amount As Single
           Dim Clr As Color
           Dim Description As String
   
           '  Создает конструктор 
           Sub New(ByVal amt As Integer, ByVal col As Color, _
                ByVal desc As String)
               Me.Amount = amt
               Me.Clr = col
               Me.Description = desc
           End Sub
      End Structure

    Конструктор New включен только для уменьшения количества строк, необходимых во время «производства» данных, что является нашим следующим шагом. Для удобства обработки, данные будут помещены в массив. Массив может содержать любое количество объектов GraphData. Фактически мы собираемся создать только четыре объекта для демонстрации.
    Объявите и создайте массив, поместив следующую строку в начале вашего кода, и снова вне любой процедуры.

      Dim Companies As New System.Collections.ArrayList 

    Теперь нужно создать данные. Мы создадим четыре компании и присвоим значения их полям Amount, Col и Description. Этот код в событии Form Load – это все, что нужно:

   Companies.Add(New GraphData(50, Color.Blue, "Muir Inc."))
   Companies.Add(New GraphData(75, Color.Yellow, "Philmas Co."))
   Companies.Add(New GraphData(62, Color.Red, "Xamco"))
   Companies.Add(New GraphData(27, Color.LightGreen, "Wright plc"))

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

Рисование и Упорство


    Одна из распространенных проблем для новичков в рисовании в .NET - это хитрость постоянства. Пока вы не получите правильный рисующий код сразу размещенным в правильном месте, рисунок может исчезнуть полностью или частично при перемещении или если другое окно помещено выше него и, фактически, в некоторых других ситуациях.
    Разные сценарии призывают к решению этой проблемы. При рисовании непосредственно на форме, вы часто сможете избегать проблемы «исчезновения графики» просто помещая свой код в событии формы OnPaint. Этот подход мы будем использовать для демонстрации нашей круговой диаграммы.
    Если вы еще не достигли OnPaint, вы можете найти его, выбирая «(Overrides)» слева в открывающемся списке в Окне Кода и перемещаясь вниз, довольно долго, пока не найдете OnPaint.
В этой серии статей мы будем много раз обращаться к теме рисования и обновления. Пока же мы просто оставим все простым.

Объект Graphics


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

Начинаем

Во-первых, мы создаем переменную и присваиваем ей объект формы Graphics:

Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
   Dim g As Graphics = e.Graphics

    Если вы проходите этот код, шаг за шагом, с помощью Visual Studio, при чтении этой статьи, то попробуйте начать новую строку кода и напечатать “g.”. Intellisense немедленно покажет вам внушительный диапазон методов и свойств под рукой, для использования и удовольствия.
    Однако мы, пока, собираемся ограничиться установкой свойства SmoothingMode. Выбор значения SmoothingMode равным HighQuality уменьшит зубчатый эффект линий до минимума. Улучшения до высшего качества приходят с повышением их стоимости, и очень часто эта цена уменьшает скорость отображения. Однако наш пример не требователен к ресурсам, так что это вряд ли будет фактором, о котором вам придется беспокоиться.
    Добавьте эту строку в событие OnPaint:

  g.SmoothingMode = Drawing2D.SmoothingMode.HighQuality
Расположение и Размер

Сначала мы должны решить две вещи:
1.Где на форме мы хотим расположить нашу диаграмму
2.Какого размера диаграмму мы хотим.

    Как только эти решения будут приняты, мы задействуем еще один объект .NET класса Graphics, чтобы использовать эту информацию. Этот объект – Rectangle (Прямоугольник).
Здесь необходимо иметь в виду ключевое понятие – Rectangle является фактическим объектом, а не просто формой, которую он представляет. Легко запутаться с такими объектами, как Rectangle, Point, Size (Прямоугольники, Точки, Размеры) и т.д., потому что часто мы имеем тенденцию видеть их взглядом нашего ума, скорее абстрактно, чем физически. Но объект Rectangle класса Graphics является таким же реальным объектом, как и любой другой в .NET Framework.
Передача нашего выбора местоположения и размера объекта Rectangle требует только одной строки:

Dim rect As Rectangle = New Rectangle(100, 105, 150, 150)

В вышеуказанном коде четыре значения представляют:
100 – Позиция X Прямоугольника,
т.е. количество пикселей от левого края формы до левого края Прямоугольника
105 – Позиция Y Прямоугольника.
Количество пикселей от верхнего края формы до верхнего края Прямоугольника.
150 – Width (Ширина) Прямоугольника.
Поскольку круговая диаграмма займет все доступное пространство в этом Прямоугольнике, следует вывод, что это также будет ширина диаграммы.
150 – Height (Высота) Прямоугольника.
Поэтому также высота диаграммы.
    Хотя традиционно для круговой диаграммы используют круг, это не обязательно. Вы можете предпочесть, например, создать овальную диаграмму, в этом случае вам нужно изменить только значения Width и Height по вашему предпочтению.

Вычислите Общее значение

    Давайте остановимся здесь и проверим, чем является то, что мы делаем. Мы хотим показать «долю» каждой компании на диаграмме так, чтобы каждая размещалась в реальной пропорции. Так, если к примеру, общее количество четырех Amount было 1000, и первая компания имела свою Amount равным 250, то вы, конечно, ожидаете, чтобы ее сегмент на диаграмме занимал 25%, четверть, доступного пространства. В реальной жизни, числа редко бывают так дружественны к пользователю, но мы можем использовать некоторую базовую математику, чтобы заставить .NEТ выполнять грубую работу для нас.
    Расчет общего значения достаточно легок. Перечислите каждое отдельное значение, которое мы поместили в массив, по очереди добавляя каждое отдельное значение к общему:

  Dim TotalCount As Single
  For Each gd As GraphData In Companies
      TotalCount += gd.Amount
  Next

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

Вычислите Доли и Нарисуйте Диаграмму

Мы собираемся написать код, который вычисляет эти доли, которые представляют каждую компанию на диаграмме, а затем рисует соответствующие цветные сегменты.
Критическая часть информации, которую мы должны передать рисующему коду, это сообщить:
1.Где начать рисование следующего сегмента и
2.Насколько большим должен быть этот сегмент.
Чтобы сделать это мы должны понимать две концепции, которые иногда представляют трудность для новичков в GDI+. Это StartAngle и SweepAngle.
a. StartAngle. Круговая Диаграмма – это эллипс, который, как вам известно, имеет 360 градусов. Хотя вы можете ожидать, что самая верхняя точка эллипса может быть 0 градусов, в GDI+ это не соответствует действительности. Фактически, точка 0 градусов находится в крайней правой точке эллипса, на горизонтальной линии, проведенной через центр.
Это звучит сложнее, чем является на самом деле, и чтобы доказать это есть картинка, которая действительно стоит тысячи слов, вот она:

    Значение StartAngle возрастает, когда вы движетесь по кругу по часовой стрелке от точки 0 градусов. Вы можете видеть это на рисунке выше.
StartAngle это просто точка в градусах на эллипсе, от которой начинается любая дуга. Так, в условиях круговой диаграммы, которую мы здесь создаем, мы будем заинтересованы в StartAngle (или начальной точке в градусах) для сегмента каждой компании.
    b. SweepAngle. SweepAngle, вероятно, менее запутывающий. Дуга измеряется в градусах. SweepAngle – это число градусов, которое охватывает любая дуга. В примере, указанном выше, выделенный сегмент имеет SweepAngle около 45 градусов (а StartAngle – 200).
    Снова, имея дело с нашей диаграммой компаний, SweepAngle представляет число градусов сегмента диаграммы, в котором мы размещает каждую отдельную компанию. То есть пропорциональную часть от 360 градусов, которая должна быть выделена для отдельной компании.

Рисуем диаграмму

Вооружившись нашими цифрами, мы теперь можем нарисовать нашу круговую диаграмму. Продолжаем наше OnPaint:

   '  Создает переменные для хранения значений углов 
   Dim StartAngle As Single = 0
   Dim SweepAngle As Single = 0

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

  For Each gd As GraphData In Companies
    SweepAngle = 360 * gd.amount / TotalCount
    g.FillPie(New SolidBrush(gd.Clr), rect, StartAngle, SweepAngle)
    StartAngle += SweepAngle
  Next

Вышеуказанный цикл проходит следующим образом:
1.Выбрать следующую компанию в массиве-списке
2.Вычислить, какую часть из 360 градусов он занимает
3.Нарисовать сегмент:
• Заполнение сегмента цветом этой компании,
• Помещение диаграммы в прямоугольник,
• Начало сегмента в правильной точке эллипса,
• Продолжение для количества градусов, вычисленных для этой компании
4.Перемещение исходной позиции для следующего сегмента, добавляя количество градусов от текущего сегмента.

Улучшите диаграмму

    Мы уже улучшили вид диаграммы, установив SmoothingMode равным HighQuality, но я также люблю заканчивать отображение, поместив линии края вокруг каждого сегмента. Это очень легко сделать. Вставьте этот дополнительный код сразу после метода FillPie в верхнем отрывке кода:

  g.DrawPie(New Pen(Color.Brown), rect, StartAngle, SweepAngle)

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

Заголовок

    Текст для ключа диаграммы создается с помощью другого метода Graphics – DrawString. Мы уже видели объекты Pen и Brush в действии в коде, написанном выше; Brush для заполнения сегментов, Pen для рисования внешних линий. Когда вы рисуете текст с помощью метода DrawString, вы можете ожидать использования объекта Pen для этого. Другой причудой графического класса является то, что вам нужен объект Brush, а не Pen, для рисования текста. Вы вспомните, что мы создали объект Brush для рисования сегментов диаграммы «на лету» в этой строке кода:

 g.FillPie(New SolidBrush(gd.Clr), rect, StartAngle, SweepAngle)

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

'  Создает Brush для рисования текста 
  Dim TextBrsh As Brush = New SolidBrush(Color.Black)
'  создает объект Font для отображения текста
  Dim TextFont As New Font("Arial", 12, FontStyle.Bold)

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

 g.DrawString("Chart Key", TextFont, TextBrsh, 310, 100)  

Два значения в конце вышеуказанной строки кода являются координатами X иY позиции начала текста (т.е. левая верхняя позиция на форме).

Пули (Bullets) и Информация О Компании

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

  Dim pxFromTop As Integer = 135

    Я поместил первую строку информации о компании на 35 пикселей ниже, чем Заголовок, на 135 пикселей ниже верхнего края формы. Теперь мы снова можем перечислить массив и использовать его информацию для создания деталей ключа диаграммы. Вы можете увидеть из комментариев, как мы достигли этого.

  For Each gd As GraphData In Companies
   '  Рисуем пулю (bullet)
   g.FillEllipse(New SolidBrush(gd.Clr), 310, pxFromTop, 15, 15)
   '  Рисуем линию вокруг пули (bullet).
   g.DrawEllipse(New Pen(Color.Black), 310, pxFromTop, 15, 15)
   '  Рисуем текст – закодированный цветом 
   g.DrawString(gd.Description & " (" & gd.Amount & ")", TextFont,    
       TextBrsh, 360, pxFromTop)
   '  Увеличиваем промежуток от верха до следующей строки 
    pxFromTop += 30  
  Next

Единственная часть кода, которая нуждается в дополнительном объяснении – значения:
310 в первых двух строках – позиция X круглой пули (bullet)
15, 15 в первых двух строках представляют ширину и высоту эллипса.
(Если они будут равны – то в результате получится круг)
360 в третьей строке это позиция X, где мы хотим поместить начало Названия Компании.
Я думаю, мы охватили варианты всех других настроек в предыдущих отрывках кода.

Расположите После Использования

    Все, что осталось сделать – это «домоводство» – удаление всех удаляемых графических объектов, которые мы создавали специально, когда рисовали.

     TextBrsh.Dispose()
   TextFont.Dispose()

    Заметьте, что мы не пытаемся избавиться от всех объектов Brush и Pen, которые мы создавали «на лету» в коде и не избавляемся от объекта Graphics в этом особенном примере. Это находится в области, которую мы рассмотрим детальнее в дальнейших статьях, если ещё будем нуждаться в этом.

Все сделано!

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

Резюме

    В этой первой статье нам представили объекты Graphics и Rectangle. Мы применили методы DrawPie и FillPie и посмотрели, как эти методы используют настройки Rectangle, StartAngle и SweepAngle для создания необходимого нам окончательного изображения. Мы использовали объект Brush для заполнения цветных сегментов, а так же рисования текста; объект Pen был использован для рисования линий, окружающих сегменты диаграммы и пуль (bullets). Мы видели, что шрифт (Font) также является объектом и мы могли использовать его Конструктор (Constructor) для создания нового экземпляра, основываясь на предпочитаемых названии, размере и стиле шрифта.
    Метод DrawString был использован для отображения текста в виде шрифта и цвета по нашему выбору. Мы использовали методы FillEllipse и DrawEllipse для создания круглых окрашенных пуль (bullets) в ключе. Мы увидели, что если мы помещаем рисующий код в событие OnPaint, то он будет перерисован всякий раз, когда поверхность формы будет открыта, скрыта или если на нее будет оказано иное визуальное воздействие. Мы узнали, что хорошее «домоводство» включает в себя избавление от доступных объектов, когда необходимость в них заканчивается.
Итак, хотя количество кода, используемого в проекте, относительно невелико, он включает несколько ключевых графических техник, включая:

  • Объекты Brush
  • Метод DrawEllipse
  • FillEllipse
  • Dispose
  • DrawLine
  • DrawPie
  • DrawString
  • FillPie
  • Объект Font
  • Постоянство использования OnPaint
  • Объект Rectangle
  • SolidBrush
  • StartAngle
  • SweetAngle
  • Использование события OnPaint, чтобы сохранить рисунок

    То, что мы сделали здесь, затрагивает только самую верхушку айсберга .NET Graphics. Мощь, возможности и потенциал графических инструментов, которые доступны вам, позволят внедрить ваши приложения в жизнь таким способом, который был бы труден, если не невозможен, любым другим способом.
    В будущих статьях мы продолжим использовать часть этой мощи. Попутно я надеюсь помочь в прояснении некоторых трудных условий и тайного синтаксиса, что заставляет многих разработчиков видеть в Graphics и GDI+ что-то от черной магии. В этом есть такой большой потенциал, что было бы стыдно не использовать хотя бы часть его, и – кто знает? – через время вы сможете преуспеть в превращении Графического Ученика в полностью квалифицированного Волшебника!