Дата публикации статьи: 19.09.2006 09:31

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

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

Нарисуем это!

    В Части 1 мы создавали круговую диаграмму, рисуя непосредственно на форме, с помощью принадлежащего форме объекта Graphics. В этой статье мы собираемся создать гистограмму, снова основанную на некоторых отвлеченных данных – цифрах продаж шести европейских стран. Однако в этот раз мы будем рисовать диаграмму внутри элемента PictureBox.

    Вообще, есть небольшое различие между двумя подходами – рисование непосредственно на форме или рисование внутри PictureBox. В этом проекте мы нарисуем диаграмму, когда пользователь щелкнет на кнопке. Как и в Части 1 дисплей будет перерисовываться, когда он будет закрыт или изменен (это известно, как «упорный» рисунок). В примере с круговой диаграммой мы использовали метод формы OnPaint для создания диаграммы заново каждый раз, когда требовалось перерисовать ее заново. Этот подход вполне подходит в большинстве ситуаций. Однако чтобы повысить уровень нашего мастерства, мы собираемся подойти к этой работе с другой стороны, в этот раз мы будем применять объект Bitmap. Если вы новичок в графике, вы можете найти некоторые темы запутанными. Иногда кажется, что вы полностью теряете возможность отследить действия множества графических объектов, отображений, картинок, рисованных поверхностей и их взаимодействия. Но с помощью этих статей все должно оказаться для вас на своих местах. Шаг за шагом я объясню графические процедуры, которые буду использовать для рисования диаграммы. Разделяя код на маленькие части, я надеюсь сделать разные методы легкими для понимания. Так же я включу полностью комментированную демонстрационную версию в Visual Studio Solution которая добавлена к этой статье.

Давайте начнем

    Итак, если мы не собираемся помещать код в событие OnPaint на этот раз, то куда он пойдет? Очевидно, один вариант – поместить его в событие кнопки click. Однако чтобы сохранить код в виде легких для следования кусков, я предпочел создать две отдельные процедуры, которые вызываются одна за другой в событии кнопки click. Задачи, предпринятые в соответствии с двумя этими процедурами:

  • Генерация данных
  • Рисование диаграммы

Теперь посмотрим на каждую из этих процедур по очереди.

Шаг 1: генерация данных

Ради непрерывности мы будем использовать Structure и ArrayList для данных, потому что этот подход мы использовали в Части 1.
Поместите эти операторы вверху формы:

    Option Strict On
    Imports System.Drawing.Drawing2D
    Imports System.Collections
    

    Создайте структуру, подобную той, что использовалась в Части 1 (поместите в код формы, но отдельно от процедур):

    Structure GraphData
        Dim Country As String
        Dim Sales As Short
        Sub New(ByVal country As String, ByVal sales As Short)
            Me.Country = country
            Me.Sales = sales
        End Sub
    End Structure
    

    Инициируйте ArrayList, чтобы хранить данные. И снова, поместите это в код формы вне процедур:

   Dim SalesData As New ArrayList
   

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

   Private Sub GetData() 
   SalesData.Clear() 
   '  Гарантируем, что сохраняется только один набор сгенерированных данных
        SalesData.Clear()
   '  Генерируем данные и помещаем их в массив
        SalesData.Add(New GraphData("Belgium", 834))
        SalesData.Add(New GraphData("Greece", 385))
        SalesData.Add(New GraphData("Portugal", 672))
        SalesData.Add(New GraphData("Spain", 429))
        SalesData.Add(New GraphData("Turkey", 715))
        SalesData.Add(New GraphData("UK", 942))    
 End Sub
 

Теперь у нас есть данные, которые можно применить для создания нашей диаграммы.

Шаг 2: Получаем элемент управления

    Диаграмма будет изображаться в PictureBox и рисование будет начинаться после щелчка на кнопке. Настало время добавить эти элементы на форму.
Добавьте элемент PictureBox на форму и назовите его PBBarChart. PictureBox должен занимать около 80% поверхности формы. Затем добавьте кнопку где-нибудь на оставшемся свободном месте на форме и назовите ее btnDraw. Цвет фона установите светлым на свой выбор. Мы готовы начать создавать диаграмму.

Повторно используемые значения

    Как мы узнали в Части 1, большая часть процесса рисования форм в Windows требует игр с установкой начальных и конечных точек линий, фигур, текста и т.д. Обычно это происходит, потому что мы руководим построением большей части того, что видит пользователь; поэтому мы должны кодировать точные позиции и размеры всего, что мы создаем.
    Подразумевая повторное использование, я включил некоторые переменные в статьи и примеры, и мы будем использовать их для помещения выше указанных значений. Надеюсь, это даст нам два преимущества.
    Во-первых, вы легко сможете менять установки, чтобы законченный продукт имел тот вид, который вам хотелось бы. Во-вторых, этот сделает большую часть кода легким для повторения; если мы будем применять понятные названия для наших переменных, то это поможет сделать каждую часть каждого кода понятнее, чем она является на самом деле.

Шаг 3: Установка полей, промежутки и размеры

    Диаграмма будет нарисована внутри PictureBox, поэтому нам следует создать поле между самой диаграммой и внешними краями PictureBox. Мы применим четыре переменных, чтобы хранить эти значения. Названия их говорят сами за себя:

    '  # пиксели Y-оси вставки PicBox слева
    Dim LeftMargin As Integer = 35
    '  # пиксели оставшиеся неиспользованными справа от PicBox
    Dim RightMargin As Integer = 15
    '  # пиксели над базой picturebox X-ось помещена
    Dim BaseMargin As Integer = 35
    '  Поле вверху
    Dim TopMargin As Integer = 10
    

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

    Dim BarGap As Integer = 12
    
Шаг 4а: Объекты Graphics и Bitmap

    Мы касались объекта Graphics в Части 1 и видели, что к нему можно относиться, как к холсту, на котором мы можем рисовать то, что хотим видеть на поверхности формы или другом элементе управления. Мы рисовали прямо на ней нашу круговую диаграмму.
    Так как в этой статье мы будем иметь дело с PictureBox, подход будет немного отличаться.
Вот что мы делаем:

  • Объявляем объект Graphics
  • Также создаем объект Bitmap. Этот Bitmap будет того же размера, что и PictureBox и будет приписан объекту Graphics элемента PictureBox
  • Чтобы получить доступ к методам рисования класса Graphics, мы приписываем Bitmap объекту Graphics
  • Когда рисование будет закончено, мы получим Bitmap, который отредактируем графическими инструментами и используем, как изображение в PictureBox’е

    Запутались? Не переживайте! Это кажется сложными только при первой встрече, но скоро все начнет становиться на свои места. В сущности, мы создаем объект Graphics, который имеет ту же форму, размер и разрешение, что и PictureBox. Мы можем позаимствовать это, а позже мы применим наш законченный рисунок, как изображение для PictureBox’а.
Весь рисующий код входит в одну процедуру, названную DrawChart. Вот код для указанных выше действий (конечно, это первая часть всей процедуры):

Private Sub DrawChart()
   Dim g As Graphics
   Dim bmap As Bitmap
   bmap = New Bitmap(PBBarChart.Width,  PBBarChart.Height, _
           PBBarChart.CreateGraphics)
   g = Graphics.FromImage(bmap)
   
Шаг 4b: Вертикальная ось

    Вертикальная ось диаграммы (в технике известная, как ось Y) будет нарисована внутри PictureBox. Мы отодвинем ее от границы, используя значения, которые мы присвоили переменным TopMargin, LeftMargin и BaseMargin.
    У этой оси будут маленькие точечные отметки с равными промежутками, каждый будет обозначать 100 продаж. В дополнение, значение каждой отметки будет написано возле отметки. Рисунок ниже показывает пример части вертикальной оси. Всю ее вы можете видеть на рисунке вначале статьи:

Следующий код устанавливает начальную точку и конечную точку вертикальной оси:

     Dim StartPoint As New Point(LeftMargin, PBBarChart.Height - BaseMargin)
   Dim EndPoint As New Point(LeftMargin, TopMargin)
   

    Названия переменных StartPoint и EndPoint не нуждаются в объяснении, но понятие Point (точка) стоит упоминания. Нам хорошо знакомо понятие точки – это определенное место, которое может быть определено определенным способом. В графике .NET точка (Point) идентифицируется ее X-положением (как далеко она отдалена от левого края) и ее Y-положением (как далека она от верха).
    В данном проекте мы измеряем эти два значения в пикселях, потому что эта единица используется для определения позиций на экране. Важно понимать, что Point – это не просто положение в пространстве, фактически это Объект, завершенный в своих свойствах и методах.
    Вы видите, что StartPoint объекта Point имеет свою X-позицию, установленную в строке с левым полем, которое мы установили раньше; ее Y-позиция вычислена перемещением от основания PictureBox на расстояние, равное BaseMargin.
    EndPoint оси имеет X-значение, такое же, как и у StartPoint, чего можно было ожидать (в конце концов, это вертикальная линия). Ее Y-значение установлено в точке, которая отступает от верхнего края PictureBox на значениие TopMargin.
Вооружившись начальной и конечно точками, мы берем перо и рисуем линию:

    Dim LinePen As New Pen(Color.Black, 2)
    g.DrawLine(LinePen, StartPoint, EndPoint)
    

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

Шаг 4c: Отметки и текст

    Из-за того, что мы сами генерировали данные во время разработки, мы знаем, что максимальное число продаж равно 942. Поэтому мы заранее знаем, что будет безопасно нарисовать вертикальную ось, которая будет содержать значения от 0 до 1000, и все значения продаж будут находиться в промежутке между этими двумя крайними значениями. В дальнейшем мы будем принимать пользовательские данные во время выполнения и динамически рассчитывать, сколько будет нужно отметок. Но пока что мы будем делать это легким (ну ладно, более легким) способом.
    Чтобы решить, как растягивать пространство десяти отметок, нам нужно прежде всего узнать длину вертикальной оси:

    Dim VertLineLength As Integer = PBBarChart.Height - (BaseMargin + TopMargin)
    

    Это даст нам возможность рассчитать точное расстояние между отметками. Размещая метки от 0 до 1000 с интервалом 100, запоминаем:

    Dim VertGap As Integer = CInt(VertLineLength / 10)
    

    Я произвольно выбрал установить (горизонтальную) длину каждой отметки равной 5 пикселей. Тогда следует, что начальная точка каждой отметки должна быть в 5 пикселях слева от вертикальной оси. Конечная точка каждой отметки будет на самой вертикальной оси. Это и есть необходимые X-позиции и они являются одними и теми же для каждой отметки. Y-позиции (которые вы получите, отсчитав количество пикселей вниз от верха) должны меняться для каждой отметки, потому что мы рисуем десять их на равном расстоянии на вертикальной оси. Расстояние между ними, следовательно, должно быть значением, которое мы только что вычислили – VertGap.

  Dim TickSP As New Point(LeftMargin - 5, StartPoint.Y - VertGap)
  Dim TickEP As New Point(LeftMargin, StartPoint.Y - VertGap)

У нас есть вся необходимая информация для создания отметок и текста 100, 200, 300 и т.д. Давайте установим шрифт для текста:

  Dim ValueFont As New Font("Arial", 8, FontStyle.Regular)

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

  For i As Integer = 1 To 10
   '  Отметка 
   g.DrawLine(New Pen(Color.Black), TickSP, TickEP)
   '  Значение отметки в виде текста
   g.DrawString(CStr(i * 100), ValueFont, Brushes.Black, 2, TickSP.Y - 5)
   '  Сброс y-позиции, перемещение на 10% вверх по вертикальной линии 
   TickSP.Y -= VertGap
   TickEP.Y -= VertGap
 Next
 

    Единственный код, который требует дополнительного объяснения, это тот, который использует DrawString, чтобы нарисовать текст возле отметок. Вы можете заметить, что Y-позиция начала текста была смещена на 5 пикселей вверх, чем начало самой отметки (TickSP, Y-5). Это просто, потому что X,Y позиция (исходная точка) нарисованной строки является самой верхней точкой воображаемого прямоугольника, в котором изображен текст. Поэтому, чтобы поместить середину текста приблизительно на один уровень с отметкой, эта исходная точка должна быть смещена на пять пикселей вверх на странице.

Шаг 4d: Время для проверки

    Нам пришлось написать больше рисующего кода прежде, чем мы достигли чего-то, это могло бы быть полезным для быстрого продвижения вперед, и можно взглянуть на результаты. Чтобы сделать это, добавьте этот код в самый конец DrawChart Sub:

          PBBarChart.Image = bmap
     g.Dispose()
     LinePen.Dispose()
    End Sub
    

    Я объясню этот код позже, но сейчас вы можете запустить проект и увидеть вертикальную ось и отметки. Просто обеспечьте, чтобы код, который мы создадим на следующих страницах, был введен выше этих трех строк, иначе будут проблемы.
    Кнопка используется для запуска рисующего кода, поэтому событие button_click кнопки btnDraw нуждается в вызове двух процедур, которые мы создали:

    Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
        '  Генерирует данные 
        GetData()
        '  Затем рисует диаграмму 
        DrawChart()
   End Sub
   

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

Шаг 5: Горизонтальная ось и панели

Логика Горизонтальной (X) оси та же, что мы использовали при рисовании Вертикальной Оси:

  '  Сначала рисуем линию оси X
  g.DrawLine(LinePen, LeftMargin, PBBarChart.Height - BaseMargin, _
     PBBarChart.Width - RightMargin, PBBarChart.Height - BaseMargin)
  '  Вычисляем длину базовой линии, нарисованной кодом выше 
  Dim BaseLineLength As Integer = _
     PBBarChart.Width - (LeftMargin + RightMargin)
     
5b. Панели

    Вычисление ширины каждой панели снова является простой математикой: разделите длину базовой линии на число панелей (которое является тем же, что и число элементов массива SalesData – по одной на каждую страну). Вычтите ширину, которую вы решили раньше вставить между панелями.

   Dim BarWidth As Double = (BaseLineLength / SalesData.Count) - BarGap
   

    Каждая панель, конечно, является прямоугольником, поэтому для этой цели мы создаем объект Rectangle. Чтобы определить X-позицию первой панели мы начнем на пересечении осей X и Y, а затем будем двигаться вправо на расстояние, равное ширине, которую мы решили вставить между панелями. Y-позиция устанавливается на один пиксель выше базовой линии:

  Dim BarRect As Rectangle
  Dim BarStartX As Integer = LeftMargin + BarGap
  Dim BarStartY As Integer = PBBarChart.Height - (BaseMargin + 1)
  

Далее, создаем кисть, которой будем рисовать и закрашивать прямоугольные панели:

   Dim BarBrush As New SolidBrush(Color.BurlyWood)
   

    Так много для ширины. Теперь нам нужно определить высоты панелей. Очевидно, высота каждой панели будет определяться цифрами продаж страны. Мы уже конфигурировали вертикальную ось так, чтобы она отмечала максимум 1000 единиц значений продаж.
    Вертикальная ось имеет так же другое значение – число пикселей, которое составляет ее длину. Чтобы гарантировать, что высота каждой панели нарисована пропорционально общему числу доступных пикселей, мы должны вычислить масштаб, т.е. сколько продаж представлено в каждом пикселе вертикальной линии. Это измерение будет использовано, чтобы гарантировать, что панели нарисованы пропорционально в пределах доступной им высоты.
    Это может только звучать сложно. На самом деле таковым не является и само вычисление очень простое:

    Dim VertScale As Double
   VertScale = VertLineLength / 1000   
   

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

  • Вычислить высоту панели

  • Создать прямоугольник, представляющий панелью

  • «Тянуть» Y-позицию вверх, пока она не достигнет места, где должен быть верхний левый уровень прямоугольной панели. Почему мы должны это делать? Потому что прямоугольник рисуется «наружу и вниз», а не «наружу и вверх». Если мы не сделаем этого изменения, панель будет нарисована ниже базовой линии, а не выше ее

  • Рисуем и закрашиваем прямоугольник, чтобы создать панель

  • Перемещаем X-позицию на рассчитанное значение вправо

И вот код, который все это делает:

  For Each gd As GraphData In SalesData
    '  Вычисляем высоту панели
    Dim BarHeight As Integer = CInt(gd.Sales * VertScale)
    '  Создаем Rectangle для панели
    BarRect = New Rectangle(BarStartX, BarStartY, CInt(BarWidth), _
          BarHeight)
    '  Тащим Y-точку вверх чтобы панель (прямоугольник) 
    '  растянулась вниз к базовой линии во время рисования 
    BarRect.Offset(0, -BarHeight)
    '  закрашиваем панель
    g.FillRectangle(BarBrush, BarRect)
    '  Опционально, рисуем линию вокруг панели 
    g.DrawRectangle(LinePen, BarRect)
    '  Повышаем X-значение ширины панели плюс промежуток 
    '  готовясь к рисованию следующей панели.
    BarStartX += CInt(BarWidth + BarGap)
 Next
 
5c. Названия стран

    Названия стран отображены внизу каждой панели. В одной из следующих статей мы узнаем более интересные и иллюстративные способы, но пока нас устроит простой текст, использующий DrawString.
    Так как процесс подобен тому, который мы уже использовали в этом проекте, комментированный код, я надеюсь, будет иметь смысл для вас на этом уровне:

   '  Устанавливаем начальную точку первой строки
   Dim TextStartX As Integer = LeftMargin + BarGap + 4
   '  Создаем кисть для рисования текста 
   Dim TextBrsh As Brush = New SolidBrush(Color.Black)
   '  Создаем экземпляр объекта Font для отображения текста
   Dim TextFont As New Font("Arial", 11, FontStyle.Bold)
   For Each gd As GraphData In SalesData
       '  Рисуем название страны 
       g.DrawString(gd.Country, TextFont, TextBrsh, TextStartX, _
          CInt(PBBarChart.Height - (BaseMargin - 4)))   
       '  Перемещаем начальную точку вправо 
       TextStartX += CInt(BarWidth + BarGap)
   Next  
   
Шаг 6: Все показано!

    Вы, возможно, уже вставили следующую строку кода, если вы выбрали «пробовать все на ходу», чтобы увидеть, что диаграмма растет. Если вы этого не сделали, то настало время для этого.
    Это не кажется важным, но на самом деле, это ключевая строка в процессе. Она берет объект Bitmap, на котором вы рисовали свою диаграмму и, наконец, показывает ее пользователю. Это происходит присваиванием Bitmap свойству Image элемента PictureBox.

   '  Назначаем изображение нарисованной диаграммы изображению picturebox
  PBBarChart.Image = bmap
  

    Как мы узнали в Части 1, вам следует избавиться от всех доступных объектов, когда они будут не нужны. Этот код решает такую задачу:

        '  Избавляемся от рисующих объектов 
        g.Dispose()
        LinePen.Dispose()
        BarBrush.Dispose()
        ValueFont.Dispose()
        TextFont.Dispose()
     End Sub
     

Снова, если вы не проверяли код во время чтения, то вам нужно добавить код для события click кнопки:

   Private Sub btnDraw_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnDraw.Click ' Генерируем данные GetData() ' Затем рисуем диаграмму DrawChart() End Sub

И вот у вас есть ваша первая простая, но работающая, гистограмма.

Итоги

    Эта статья построена на основных возможностях и техниках, охваченных в Части 1, а также добавила некоторые новые. Мы видели, что с очень маленьким количеством кода гистограмма может быть создана на пустом месте.
    Вычисляя масштаб, мы гарантировали, что панели точно представляют цифры продаж, используя всю высоту доступной для рисования области. Благодаря использованию переменных для представления значений, таких, как поля вокруг диаграммы, код стал легче для чтения и редактирования.
    Благодаря использованию двойной буферизации, диаграмма была нарисована на объекте Bitmap вне поля зрения пользователя, а затем изображение было применено к PictureBox после ее завершения. Эта техника уменьшила мерцание, а также гарантировала, что после того, как диаграмма была показана, она будет сохранена (это значит, что она будет перерисована, когда бы не была скрыта или изменена каким-либо образом).
    Хотя завершенный код совсем короток, были использованы несколько важных методов класса Graphics и других навыков рисования: -

  • Объект Bitmap
  • Кисти
  • Создание объекта Graphics из Image
  • Избавление от объекта Graphics
  • Рисование на элементе PictureBox
  • Рисование линии
  • Рисование прямоугольника
  • Рисование строки
  • Закрашивание прямоугольника
  • Объект Font
  • Метод Offset
  • Кисти
  • Точки
  • Объект Rectangle
  • Масштабирование

    В Части 3 мы вернемся к круговой диаграмме и будем применять пользовательский ввод во время исполнения для создания диаграммы, основываясь на пользовательских данных и покажем результат. Сплошные цвета диаграммы также будут заменены HatchStyle выбранным пользователем.