Удачные диаграммы: GDI+ Работа с графикой. Часть 4
Автор:
Ged Mead
Перевод: Виталий
Готовцов
[Оригинал
Статьи] [Обсудить в форуме]
Настройка
Первым делом давайте сгенерируем данные для отображения в диаграмме. Мы будем создавать их во время разработки, чтобы позволить себе продолжать разрабатывать основные части класса Graphics. Если вы предпочитаете получать данные, вводимые пользователем в режиме выполнения, то вы можете адаптировать технику, раскрытую в Части 3.
Создаем данные
Ниже показано то, каким должен стать наш стандартный код, чтобы создать переменные и генерировать и поместить данные. Я не стану ковыряться в деталях, потому что они полностью раскрыты в предыдущих статьях:
Option Strict On
Imports System.Drawing.Drawing2D
Imports System.Collections
Structure GraphData
Dim Country As String
Dim Sales As Integer
Dim BarColor As Color
Sub New(ByVal country As String, ByVal sales As Short, _
ByVal barcol As Color)
Me.Country = country
Me.Sales = sales
Me.BarColor = barcol
End Sub
End Structure
' Создаем переменные
‘ # пиксели вертикальной оси установлены из PictureBox Left
Dim LeftMargin As Integer = 35
' # пиксели оставшиеся неиспользованными с правого края PictureBox
Dim RightMargin As Integer = 15
' Число пикселей выше верхней базовой линии
Dim BaseMargin As Integer = 35
' Верхнее поле
Dim TopMargin As Integer = 10
' Устанавливает размер пространства между панелями
Dim BarGap As Integer = 12
Dim SalesData As New ArrayList
Dim HighSale As Double ' Maximum sales figure
Dim VertScale As Double ' Scaling used for bar heights
' Младшие изменения для этой версии:
' Объявляет переменную для хранения объекта Graphics
Dim g As Graphics
' Переменная для Bitmap который должен быть отображен в PictureBox
Dim bmap As Bitmap
' Длинна вертикальной оси
Dim VertLineLength As Integer
Dim BarWidth As Integer ' width of bars
Dim BaseLineLength As Integer ' X Axis length
Private Sub GetData()
SalesData.Clear() ' Avoid data duplication
' Генерирует данные и помещает их в массив
SalesData.Add(New GraphData("Belgium", 934, Color.Blue))
SalesData.Add(New GraphData("Greece", 385, Color.DarkOrange))
SalesData.Add(New GraphData("Portugal", 1029, Color.Green))
SalesData.Add(New GraphData("Spain", 729, Color.IndianRed))
SalesData.Add(New GraphData("Turkey", 1472, Color.Tomato))
SalesData.Add(New GraphData("UK", 1142, Color.Aquamarine))
End Sub
Элементы управления
Добавьте PictureBox (Графическое окно) PBBarChart и кнопку btnDraw на форму.
Поместите кнопку куда-нибудь вниз, в углу формы.
Растяните PictureBox так, чтобы он закрывал верхний, правый и левый края формы. Установите его свойство Anchor так, чтобы оно было привязано ко всем четырем сторонам. Это позволит пользователю щелкнуть на кнопке, чтобы версия диаграммы с измененными размерами отображалась, какими бы ни стали размеры формы. (Как мы увидим в дальнейших статьях, есть более динамичные способы достичь этого, но мы пока будем придерживаться этого метода.)
Затем мы установим объекты Graphics и Bitmap. Поместите этот инициализирующий код в отдельную процедуру, названную GetGraphics:
Private Function GetGraphics() As Graphics
' Сделайте bmap того же размера и разрешения, чито и PictureBox
bmap = New Bitmap(PBBarChart.Width, PBBarChart.Height, _
PBBarChart.CreateGraphics)
' Присвойте объект Bitmap объекту Graphics
' и верните его
Return Graphics.FromImage(bmap)
End Function
Вертикальная ось
Если вы читали предыдущие статьи, вы можете заметить, что разные этапы теперь разделены на меньшие куски кода и помещены в отдельные процедуры.
Код для вертикальной оси теперь помещен в свою процедуру и остается, в значительной степени, неизменен относительно предыдущих версий. Первая часть его выглядит так:
Private Sub DrawVerticalAxis(ByVal g As Graphics)
' Рисует линию вертикальной оси.
Dim StartPoint As New Point(LeftMargin, _
PBBarChart.Height - BaseMargin)
Dim EndPoint As New Point(LeftMargin, TopMargin)
' Основной Pen
Dim LinePen As New Pen(Color.Black, 2)
' Рисуем вертикальную линию (без точечных меток)
g.DrawLine(LinePen, StartPoint, EndPoint)
' Рисуем Tickmarks и отображаем числа
' рассчитанной длинны вертикальной оси
VertLineLength = PBBarChart.Height - (BaseMargin + TopMargin)
' Идентифицируем число самой высшей продажи
For Each gd As GraphData In SalesData
If gd.Sales > HighSale Then HighSale = gd.Sales
Next
' : Код точечных отметок шкалы последует
Точечные отметки шкалы
Здесь есть отличие от предыдущей статьи. В Части 2 мы отмечали максимальное число продаж равным 1000. Не очень реалистично, но это помогало сделать код менее сложным. На этот раз мы будем регулировать число максимальных продаж (а соответственно и число, и пространство точечных меток) в соответствии с любым значением переменной HighSale.
Это значит, что вы можете изменить данные продаж, чтобы позволить значение общих продаж превышающим 1000 в любое время в будущем, и ваша диаграмма при этом не будет искажаться.
Чтобы сделать это, мы идентифицируем число максимальных продаж, затем округляем его пока не получим следующее круглое значение, кратное 100. На пример, если число HighSale равно 1675, следующее максимальное значение на шкале будет установлено 1700.
Вот код, который делает это:
' Процедура DrawVerticalAxis продолжена:
' Округляем до следующей сотни высшее число продаж
Dim NextCent As Integer = CInt(HighSale)
Do While NextCent Mod 100 <> 0
NextCent += 1
Loop
' Определяем, сколько точечных отметок
' (TickMarks) требуется (одна на сотню):
Dim TotalTicks As Integer = CInt(NextCent / 100)
Теперь, когда мы знаем, сколько сотен мы должны допустить, мы можем разделить вертикальную ось пропорционально на сотни, нарисовать точечные отметки и текстовые значения, точно так же, как мы делали в Части 2:
' Рассчитываем промежутки между вертикальными отметками
Dim YPos As Integer = CInt(VertLineLength / TotalTicks)
' Переменные для точек Start и End
Dim TickSP As New Point(LeftMargin - 5, StartPoint.Y - YPos)
Dim TickEP As New Point(LeftMargin, StartPoint.Y - YPos)
' Шрифт для значений – объявляем здесь для читабельности
Dim ValueFont As New Font("Arial", 8, FontStyle.Regular)
For i As Integer = 1 To TotalTicks
g.DrawLine(New Pen(Color.Black), TickSP, TickEP) ' Tick mark
' Значения точек как текст:
g.DrawString(CStr(i * 100), ValueFont, Brushes.Black, 2, TickSP.Y - 5)
' Сбрасываем позиции x, y пропорционально вертикальной линии
TickSP.Y = CInt(TickSP.Y - (VertLineLength / TotalTicks))
TickEP.Y -= CInt(VertLineLength / TotalTicks)
Next
End Sub
Проверяем 1, 2, 3
Ладно, чтобы быть совсем точным, правильнее было бы сказать: «Проверяем 1, 2, … 1500». Если вы сейчас хотите убедиться, что код все еще работает, поместите следующий отрывок кода в событие Click кнопки btnDraw, а затем щелкните на кнопке.
Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles btnDraw.Click
' Различные действия теперь разделены в разные процедуры чтобы сделать
' проект более модульным
' 1. Получаем данные
GetData()
' 2. Получаем объект Graphics и его методы для рисования
g = GetGraphics()
' 3. Рисуем вертикальную ось
DrawVerticalAxis(g)
' : Еще добавим код позже ....
' n. Назначаем bitmap вертикальной оси, проведенной к PictureBox
PBBarChart.Image = bmap
End Sub
Вы видите, что теперь точки отмечают значения от 100 до 1500, отображая высшее текущее значение продаж – 1472 (Турция).
Вы можете изменить значения, перезапустить проект снова и вы увидите, что вертикальная ось изменяется пропорционально.
Базовая линия (ось X)
Вы можете вставить горизонтальную ось, если хотите видеть ее. Создайте новую процедуру и назовите ее Draw3DBars.
Private Sub Draw3DBars()
g.DrawLine(New Pen(Color.Black), LeftMargin, PBBarChart.Height - BaseMargin, _
PBBarChart.Width - RightMargin, PBBarChart.Height - BaseMargin)
' Вычисляем длину базовой линии, нарисованной кодом сверху
BaseLineLength = pbBarChart.Width - (LeftMargin + RightMargin)
' Еще код будет дальше
3D Панели
Я натолкнулся на метод C# для рисования панелей с 3D эффектом на codeproject.com. Автор Michael Damron использовал простую, но очень эффективную идею надстраивания серии многоугольников один над другим.
Многоугольник более светлого цвета завершает эффект при помещении его сверху панели. Я переделал его оригинальную мысль для этого проекта. Вот путь, которым я шел: первые несколько элементов кода работают с некоторыми из основных требуемых вычислений. Например, мы можем рассчитать оптимальную ширину каждой панели, применяя общую доступную ширину и разделяя ее на количество панелей; не забудьте учесть требуемое смещение между панелями.
Большая часть этого повторяет то, что мы делали в Части 2:
' Вычисляет ширину каждой панели, делая доступным все возможное пространство
BarWidth = CInt((BaseLineLength / SalesData.Count) - BarGap)
' Устанавливает начальную точку первой панели
Dim BarStartX As Integer = LeftMargin + BarGap
' Устанавливает базовую линию графика
Dim BaseLine As Integer = PBBarChart.Height - BaseMargin
' Вычисляет масштаб (новый в этом проекте)
VertScale = VertLineLength / HighSale
' : Дальнейший код последует
- единственный новый элемент – переменная VertScale, которая обеспечивает, что панели будут нарисованы правильной высоты, позволяя масштабирование, которое мы применили в процедуре DrawVerticalAxis раньше.
Эта процедура продолжается следующим блоком кода, который рисует по очереди каждую панель. Я добавил достаточно много комментариев и (особенно если вы прочли предыдущие статьи) это должно иметь смысл для вас.
For Each gd As GraphData In SalesData
' Устанавливаем позиции четырех точек самого нижнего
' параллелограмма и помещаем в массив
Dim Corners(3) As Point
Corners(0) = New Point(BarStartX, BaseLine - 10)
Corners(1) = New Point(BarStartX + BarWidth - 5, BaseLine - 10)
Corners(2) = New Point(BarStartX + BarWidth, BaseLine)
Corners(3) = New Point(BarStartX + 5, BaseLine)
' Вычисляем высоту этой панели, принимая в расчет масштаб:
Dim BarHeight As Integer = CInt(gd.Sales * VertScale)
' Создаем кисти для рисования панелей
' Цвета будут изменяться, согласно GraphData
Dim barmainBrush As New HatchBrush(HatchStyle.Percent50, gd.BarColor)
Dim bartopBrush As New SolidBrush(gd.BarColor)
' Рисуем одну завершенную панель
For i As Integer = 0 To BarHeight - 11
‘ (“BarHeight - 11” может быть непонятным. Это делает возможным:
‘ a. 10 пикселей, которые добавляются для создания эффекта 3D глубины
‘ плюс
‘ b. последний ромб должен быть нарисован более светлым, поэтому нам
‘ нужно прекратить рисовать такой штриховкой на 1 пиксель ниже
‘ высоты всей панели.
' Заполняем следующий многоугольник с помощью hatchbrush
g.FillPolygon(barmainBrush, Corners)
' Поднимаем все позиции Y картинки на 1 пиксель
Corners(0).Y -= 1
Corners(1).Y -= 1
Corners(2).Y -= 1
Corners(3).Y -= 1
Next
' Наконец, увенчиваем его более светлым ромбом
g.FillPolygon(bartopBrush, Corners)
' Перемещаем начальную точку следующей панели
BarStartX += CInt(BarWidth + BarGap)
‘ Избавляемся от кистей
barmainBrush.Dispose()
bartopBrush.Dispose()
Next
End Sub
(Если хотите проверить эту процедуру, не забудьте вызвать ее в событии btnDraw_Click).
Последние штрихи
Чтобы закончить, нужно написать названия стран под панелями. Это очень близко тому, что мы делали в Части 2, с дополнением строки кода, которая регулирует размер шрифта согласно доступной ширине.
Private Sub WriteTheNames()
' Позиция X для начала названия стран(ы). Она помещается
' под левым краем панели, плюс 5 пикселей для того, чтобы
' лучше выглядеть
Dim TextStartX As Integer = LeftMargin + BarGap + 5
' Создаем кисть для рисования текста
Dim TextBrsh As Brush = New SolidBrush(Color.Black)
' Создаем экземпляр объекта Font для отображения текста
' был бы полезен динамически приспосабленный размер шрифта:
Dim fntSize As Integer = CInt(BarWidth / 7)
Dim TextFont As New Font("Verdana", fntSize, 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
End Sub
Снова вам нужно добавить обращение к этой процедуре в событии щелчка кнопки btnDraw. Полный код этого события будет выглядеть так:
Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)_
Handles btnDraw.Click
' Разные действия теперь разделены в отдельные процедуры,
' чтобы сделать
' проект более модульным
' 1. Получаем данные
GetData()
' 2. Получаем объект Graphics
g = GetGraphics()
' 3. Рисуем вертикальную ось
DrawVerticalAxis(g)
' 4. Рисуем панели
Draw3DBars()
' 5. Пишем подписи
WriteTheNames()
' Все сделано! Выравниваем bitmap с вертикальной осью нарисованного
' picturebox
pbBarChart.Image = bmap
g.Dispose()
End Sub
Резюме
Этот проект создает более универсальную и лучше выглядящую гистограмму. Она разделяет главные задачи рисования в разные процедуры. Однако код все еще является довольно громоздким и это создает причину для создания класса 3DBar с регулируемыми свойствами ширины, глубины, высоты, цвета и т.д.. Впоследствии, будет можно подключить пользовательский интерфейс для выбора свойств, который мы здесь использовали, и, конечно, это соответствует подходу ООП к разработке в VB.NET.
В этой статье я объяснил или использовал следующее:
- Объект Bitmap
- Brushes
- Dispose
- Do While Loop
- DrawLine
- DrawString
- FillPolygon
- Font object
- Объект Graphics
- Pen
- Structure
В следующей статье в этой серии мы перейдем к другому, иногда более динамичному типу диаграмм – линейчатой диаграмме.