Дата публикации статьи: 08.01.2007 06:52

Удачные диаграммы. Часть 5 - Линейные графики

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

    Как и в предыдущих статьях, диаграмма будет показана в элементе управления PictureBox. Хотя было бы легко – легче, чем некоторыми способами – просто налепить диаграмму на поверхность самой формы, я думаю, реалистичнее будет поселить ее внутри элемента-контейнера, потому что диаграмма чаще всего является лишь частью всей информации, показанной пользователю. Также это облегчает ситуацию, когда вы хотите получить пользовательский ввод с экрана и использовать эти данные непосредственно для рисования диаграммы. Для удобства в начале проекта мы создадим некоторые данные. В реальном мире вы, вероятно, использовали бы данные, полученные их другого источника, или те, которые были раньше сохранены в файле, или непосредственно от пользователя. Если вы хотите приспособить код этой статьи для получения данных во время выполнения, вам придется найти пример того, как это можно сделать в Части 3 этой серии.

Начнем

    Добавьте в проект форму и назовите ее LineChartDemo. Придайте форме размер около 640 на 480. Перетащите PictureBox на форму с панели ToolBox и растяните его левую, верхнюю и правую границы так, чтобы они были близки к ее краям. Перетяните нижний край элемента PictureBox так, чтобы его высота составляла приблизительно 80% высоты формы. Установите свойство Anchor элемента PictureBox так, чтобы оно включало все четыре стороны - верхнюю, нижнюю, левую и правую. Установите свойство Border по своему усмотрению; я использовал Fixed3D в образцах. Назовите его «PBLineChart».
Теперь добавьте кнопку на форму и поместите ее в нижнем правом углу. Измените ее свойство Anchor от предложенного по умолчанию на Bottom и Right. Эти изменения свойств Anchor приведут к тому, что PictureBox будет изменять размер при изменении размера формы, а кнопка будет оставаться привязанной в левом углу, где вам хотелось бы.

В самом верху формы вставьте следующее:

    Option Strict On
   Imports System.Drawing.Drawing2D

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

Данные для образца

    Наши данные очень просты – 12 месяцев года будут отображены на горизонтальной оси и цифры, представляющие продажи за месяц, будут отображены на вертикальной оси. Два набора данных для удобства могут быть помещены в два отдельных массива; массив «Months» для названий, массив «Sales» – для цифр. Как указывалось выше, это лишь удобство, дающее нам возможность рисовать, не отвлекаясь на обработку данных.
    Внутрь кода класса «Public Class LineChartDemo» вставьте следующий код, который создает два массива и наполняет их данными за один шаг:

    Dim Months() As String = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", _
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
    Dim Sales() As Integer = {835, 314, 672, 429, 715, 642, _
    153, 699, 622, 901, 345, 655}
    
Переменные

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

     '  ~~  Переменные для полей и контуров и т.д. ~~
    '  # пиксели 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 VertLineLength As Integer
    '  Переменная, хранящая длину горизонтальной оси 
    Dim BaseLineLength As Integer
    '  Переменная, содержащая длину каждого сегмента
    Dim LineWidth As Double
    

    Как и в предыдущих примерах, мы собираемся применить технику двойной буферизации для создания диаграммы в фоновом режиме и «вклеивания» ее в PictureBox за один раз, когда все будет готово. Конечно, в действительности мы не вклеиваем ничего в традиционном смысле; все, что мы делаем – это присваиваем готовое изображение диаграммы изображению на PictureBox. Если вы хотите разобраться, как это работает, вы можете найти дополнительное объяснение на Странице 3 Части 2 этой серии.
    Мы создадим объект Graphics и объект Bitmap в следующих строках кода:

    '  ~~  объекты Bitmap и Graphics ~~
    '  Переменная для хранения объекта Graphics 
    Dim g As Graphics
    '    Далее, создаем объект Bitmap который имеет
    '    тот же размер и разрешение, что и PictureBox
    Dim bmap As Bitmap
    

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

Внешние границы диаграммы

    Мы начинаем рисовать оси X и Y диаграммы, включая отметки и числа вертикальной оси. Это код содержится в процедуре, названной DrawOutline.

Объект Graphics

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

 Private Sub DrawOutline()
   ' Инициируем bmap и присваиваем ему ширину и высоту как у PictureBox.
   ' Также устанавливаем разрешение bitmap равным PictureBox
   bmap = New Bitmap(PBLineChart.Width, _ PBLineChart.Height, PBLineChart.CreateGraphics)
    '    Присваиваем Bitmap объекту Graphics.  
    g = Graphics.FromImage(bmap)
    
Вертикальная ось

    Код вертикальной оси тот же, что мы использовали в некоторых из предыдущих статей. Мы рисуем вертикальную линию, а затем вставляем 10 маленьких горизонтальных отметок на равных расстояниях одна от другой. Для простоты мы снова используем значения от 0 до 1000. Если вам нужно применить другие максимальные значения (а во многих случаях так и будет), то вы увидите описание того, как это сделать в статье Части 4 здесь.
Первым делом нужно рассчитать точки начала и конца вертикальной линии. Вы увидите, что мы делаем это с помощью переменных, установленных раньше для полей.

  '   Рисуем линию для вертикальной оси.
        Dim StartPoint As New Point(LeftMargin, PBLineChart.Height - BaseMargin)
        Dim EndPoint As New Point(LeftMargin, TopMargin)
        '  Основное перо для рисования внешних линий 
        Dim LinePen As New Pen(Color.Black, 2)
        '  Рисуем вертикальную линию 
        g.DrawLine(LinePen, StartPoint, EndPoint)
        

Чтобы добавить отметки и значения (100, 200 и т.д. до 1000) используйте следующий код.

     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 и скрыто используем это изображение. (В самом деле, технически правильно – как объяснялось раньше – мы рисуем на GraphicObject Bitmap, но эффект тот же). На этом уровне мы еще не присвоили это изображение PictureBox в качестве его изображения, поэтому PictureBox остается пустым.

Отметки

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

    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 позиция (исходная точка) нарисованной строки является самой верхней точкой воображаемого прямоугольника, в котором изображен текст. Поэтому, чтобы поместить середину текста приблизительно на один уровень с отметкой, эта исходная точка должна быть смещена на пять пикселей вверх на странице.

Горизонтальная ось (a.k.a. базовая линия)

    Код для Горизонтальной (X) Оси выглядит сложным, но в действительности совсем прост. Используя перо, которое мы создали раньше (LinePen), мы рисуем прямую линию, перемещаясь от начальной до конечной точек линии на значения, рассчитанные от полей, которые мы установили раньше.

  g.DrawLine(LinePen, LeftMargin, PBBarChart.Height - BaseMargin, _
     PBBarChart.Width - RightMargin, PBBarChart.Height - BaseMargin)

    Что вы думаете о показе сейчас?

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

  • Проверьте, что за строками кода, указанными выше, следует строка «End Sub» процедуры DrawLine.
  • Создайте новую процедуру, как показано ниже:
   Private Sub TempDisplay()
        PBLineChart.Image = bmap
    End Sub
    

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

    Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
        DrawOutline()
        TempDisplay()
    End Sub 
    

    Сохраните проект и запустите его. Если вы вставили код правильно, вы будете награждены чем-то вроде этого:

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

Рисование Линии (Линий)

    Код для рисования линий будет помещен в отдельную процедуру, которая называется DrawTheLine. Первым делом необходимо вычислить длину каждого сегмента всей линии графика. Я хочу сказать, что число продаж каждого месяца должно быть представлено сегментом линии; нам нужно вычислить, сколько пространства PictureBox доступно для сегмента каждого месяца.
    Это действительно простая математика:

           '  Вычисляем длину базовой линии, нарисованной в предыдущей процедуре 
        BaseLineLength = PBLineChart.Width - (LeftMargin + RightMargin)
        '  Вычисляем ширину каждого сегмента 
        LineWidth = (BaseLineLength / Sales.Length)
        

    Sales.Length – это, конечно, количество элементов в массиве, который содержит цифры продаж. Мы используем всю доступную ширину базовой линии и делим ее на количество сегментов, которыми мы хотим заполнить ее.

Расчет вертикальной шкалы

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

    Dim VertScale As Double
   VertScale = VertLineLength / 1000  
   

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

Создаем и рисуем первый сегмент линии

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

        Dim XPosStart As Integer = CInt(LeftMargin + 30)
        Dim XPosEnd As Integer = CInt(XPosStart + LineWidth)
        Dim YPosStart As Integer = CInt(Sales(0) * VertScale)
        Dim YPosEnd As Integer = CInt(Sales(1) * VertScale)

Названия четырех переменных говорят сами за себя. Арифметика понятна:
XPosStart - значение X начала сегмента линии и помещено в 30 пикселях от левого поля.
XPosEnd - значение X конца сегмента линии, и вычислено добавлением ширины сегмента (или длины, если вы предпочитаете так думать об этом) к значению начальной точки.
YPosStart - значение Y начала сегмента линии. Оно равно значению первого числа продаж (Jan) умноженному на коэффициент вертикального масштаба, который будет создан через мгновение.
YPosEnd - значение Y конца сегмента линии. Оно равно значению следующего числа продаж, которым в предыдущем коде случилось быть числу Feb, второго элемента массива.

Превращающаяся и исчезающая проблема линий

    Вы ожидаете, что мы могли бы теперь нарисовать набор линий, одну за другой, используя стандартный метод DrawLine. Мы могли бы, но вы не получили бы результат, который ожидаете. Причина в том, что когда мы видим график, мы ожидаем, что начальная точка графика (т.е. точка 0, 0) находится в нижнем левом углу. Это стандарт и вы будете правы в этих ожиданиях, как мне кажется.
    Проблема в том, что элемент PictureBox имеет собственную точку 0, 0 в верхнем левом углу. Поэтому если бы мы рисовали линию, скажем от 0, 0 к 100, 100, эта линия шла бы под углом вниз из верхнего левого угла. Это противоположно тому, чего мы ожидаем – в действительности мы хотим линию, которая выходит из нижнего левого угла и идет вверх.
Есть несколько решений этой проблемы. В этой статье мы будем использовать такой способ – создадим серию сегментов линии в памяти, которые мы поместим в объект класса Drawing, называемый GraphicsPath. Затем мы повернем и выровняем начальную позицию линии «вверх тормашками» в этом GraphicsPath, чтобы она появилась так, как мы хотим.
    Объект GraphicsPath подвижный и совсем не труден для применения. Преобразования и вращения, однако, не совсем просты для понимания. Фактически, они могут быть совершенно ошеломляющими, если у вас нет математического мышления (которого у меня нет). Целью этой статьи является создание графика, поэтому я собираюсь показать вам необходимый код, но объяснение, как он работает, будет шагом в сторону. В следующей статье я вернусь к этой тернистой теме и попытаюсь объяснить, по шагам, в нематематических терминах, как это происходит. Если вы сами пробовали преобразования, то, вероятно, уже столкнулись с проблемой невидимой линии – которая такова, что вы вращаете линию и она просто исчезает!
    Код, который мы используем, решит все указанные трудности. Создаем новый объект GraphicsPath:

        Dim MyPath As New GraphicsPath
        
Первый сегмент линии

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

      MyPath.AddLine(XPosStart, YPosStart, XPosEnd, YPosEnd)
      

    Обратите внимание, что метод AddLine принимает четыре параметра – два значения X и два значения Y. Нам не нужно определять перо для рисования линии; об этом позаботятся, когда мы, наконец, будем рисовать полный Path (путь). AddLine делает то, что предполагает название; он добавляет детали сегмента линии GraphicsPath.

Остающиеся сегменты линии

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

            For i As Integer = 1 To UBound(Sales) - 1
                '  Обновляем значения X и Y позиции для следующего значения:
                '  Перемещаем начальную точку вправо на ширину одной линии 
                XPosStart = XPosEnd
                '  Перемещаем конечную точку вправо на ширину одной линии 
                XPosEnd = CInt(XPosStart + LineWidth)
                '  Присваиваем YPosStart старое значение Y
                YPosStart = YPosEnd
                '  Присваиваем YPosEnd следующее значение числа Sales 
                YPosEnd = CInt(Sales(i + 1) * VertScale)
                '  Добавляем этот сегмент линии в GraphicsPath
                MyPath.AddLine(XPosStart, YPosStart, XPosEnd, YPosEnd)
            Next
            

Вот и все, сегменты линии созданы и помещены в GraphicsPath. Теперь немного этого хитрого преобразования (Transformation).

Преобразования

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

         '  Мы хотим, чтобы линия изменила направление на противоположное и для этого вращаем ее на 180 градусов
        g.RotateTransform(180)
        '  Из-за того, что вращение также удаляет линию из поля зрения (влево), поэтому 
        '  нам нужно отрегулировать x так, чтобы он переместился в положительную сторону от
        '  вертикальной оси.
        '  Значение Y остается неизменным, поэтому используется масштаб «1» 
        '  (т.е. не производится никаких изменений)
        g.ScaleTransform(-1, 1)
        ' Перемещаем начальную точку к нижнему левому углу 
        ' Значение X остается неизменным 
        ' Y перемещается вниз, к концу вертикальной оси, приравнивая 
        ' коэффициент 10, чтобы компенсировать вертикальное смещение.
        g.TranslateTransform(0, VertLineLength + 10, MatrixOrder.Append)
Рисуем преобразованную линию

    Рисование полной линии, которую мы только что создали и преобразовали, является простым делом рисования GraphicsPath на объекте Graphics. GraphicsPath, конечно, содержит все данные, которые мы ввели для сегментов линии.

        Dim MyPen As Pen = New Pen(Color.Blue, 3)
        g.DrawPath(MyPen, MyPath)
        

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

        g.ResetTransform()
Проверяем

    И снова, мы можем проверить, что все происходит по плану. В событии Click кода кнопки введите это:

    Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
        DrawOutline()
        DrawTheLine()
        TempDisplay()
    End Sub
    

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

Отображаем месяцы

    Чтобы быть полезными и пригодными к употреблению, нам нужно показать названия месяцев, продажи которых представляют числа. Чтобы сделать это, мы используем метод DrawString тем же способом, что и в прошлых статьях. Это значит, что мы «пишем» текст, рисуя его кистью. Это странный для людей способ решения задачи написания, но совершенно логичный для компьютера; буквы алфавита, в конце концов, являются просто фигурами – рисунками, которым случилось превратиться во что-то осмысленное в наших головах.
Во-первых, мы определяем начальную точку для первой буквы первого слова. Это 18 пикселей внутрь от левого поля. Затем мы создаем Кисть (Brush), чтобы рисовать, а так же определяем детали шрифта (Font), который мы хотели бы применить.

   '  Устанавливаем начальную точку первого слова
        Dim TextStartX As Integer = CInt(LeftMargin + 18)
        '  Создаем кисть Brush для рисования текста 
        Dim TextBrsh As Brush = New SolidBrush(Color.Black)
        '  Создаем экземпляр объекта Font для отображения текста 
        Dim TextFont As New Font("Arial", 10, FontStyle.Regular)
        

    Мы все установили и теперь можем продолжать двигаться вперед и рисовать названия месяцев. Возможно, вы помните, что эти названия были помещены в массив Month в самом начале этого проекта:

        For i As Integer = 0 To Months.Length - 1
            '  Рисуем название месяца 
            g.DrawString(Months(i), TextFont, TextBrsh, TextStartX, _
                CInt(PBLineChart.Height - (BaseMargin - 4)))
            '  Перемещаем начальную точку для следующего названия вправо 
            TextStartX += CInt(LineWidth)
        Next
        

    Теперь мы закончили с объектами Brush и Font, нам следует избавиться от них, что будет хорошей практикой. Это не критично для такого маленького проекта, как этот, но это хорошая привычка.

        TextBrsh.Dispose()
        TextFont.Dispose()
Отображаем результат

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

  • Отображает законченную диаграмму
  • Избавляется от объекта Graphics

Вот эта процедура:

       Private Sub FinalDisplay()
           PBLineChart.Image = bmap
           g.Dispose()
       End Sub
       

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

    Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
        DrawOutline()
        DrawTheLine()
        ShowMonths()
        FinalDisplay()
    End Sub
 

 

  Все сделано… или нет? Диаграмма вверху в порядке, но все ли на этом? Насколько лучше было бы, если бы соединения сегментов линий были бы более ясно отмечены. И если бы были линии сетки на диаграмме, чтобы было удобнее смотреть значения на вертикальной оси. Добавим эти улучшения на диаграмму? Конечно!

Соединения линий

    Мощь GraphicsPath теперь становится очевидной. Чтобы добавить маленький кружочек – или прямоугольник, квадрат или даже текст – между каждым сегментом линии нам нужно только три маленьких добавления кода. Сначала нужно добавить начальный кружок в самом начале линии диаграммы; затем в цикле добавим кружок в начале каждого последующего сегмента линии; третье – вставляем кружок в конце последнего сегмента линии.
    Проверяем следующий кусок кода, который формирует часть процедуры DrawTheLine. Я выделил красным три новые строки. Весь остальной код остался без изменений.

 '  Создаем  GraphicsPath, чтобы хранить информацию о линии
        Dim MyPath As New GraphicsPath
        ' Вручную добавляем первый кружок Path
        MyPath.AddEllipse(XPosStart - 2, YPosStart - 2, 4, 4)
        '  Вручную добавляем первую линию в Path
        MyPath.AddLine(XPosStart, YPosStart, XPosEnd, YPosEnd)
      
            For i As Integer = 1 To UBound(Sales) - 1
                '  Изменяем позиции X и Y для следующего значения:
                '  Перемещаем начальную позицию на ширину одной линии вправо
                XPosStart = XPosEnd
                '  Перемещаем конечную точку на ширину одной линии вправо 
                XPosEnd = CInt(XPosStart + LineWidth)
                ' Присваиваем YPosStart «старому» значению Y 
                YPosStart = YPosEnd
                ' Присваиваем YPosEnd следующему числу продаж 
                YPosEnd = CInt(Sales(i + 1) * VertScale)
                '  Добавляем новый кружок 
                MyPath.AddEllipse(XPosStart - 2, YPosStart - 2, 4, 4)
                '  Добавляем следующий сегмент линии в GraphicsPath
                MyPath.AddLine(XPosStart, YPosStart, XPosEnd, YPosEnd)
            Next
            '  Наконец, вручную добавляем последний кружок 
            MyPath.AddEllipse(XPosEnd - 2, YPosEnd - 2, 4, 4)

    Метод AddEllipse добавляет кружок диаметром 4 пикселя в GraphicsPath. Позиции X и Y кружка смещены на 2 пикселя, чтобы быть помещенными по центру между концом одного сегмента и началом следующего. Вы можете заменить кружочки квадратиками, заменив каждую строку AddEllipse на эту:

          MyPath.AddRectangle(New Rectangle(XPosEnd - 2, YPosEnd - 2, 4, 4))
Линии сетки

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

Горизонтальные линии сетки

    Основываясь на том, что мы выяснили в этой статье, я надеюсь, вы узнаете следующую процедуру, которая рисует горизонтальные линии сетки, очень легкую для повторения:

   Private Sub DrawHorizontalLines()
        '  Вычисляем вертикальные равные промежутки 
        Dim VertGap As Integer = CInt(VertLineLength / 10)
        '  Устанавливаем начальную и конечную точки 
        '  = левому и правому полям на базовой линии 
        Dim StartPoint As New Point(LeftMargin + 3, PBLineChart.Height - BaseMargin)
        Dim EndPoint As New Point(PBLineChart.Width, PBLineChart.Height - BaseMargin)
        '  Начальные настройки 
        Dim LineStart As New Point(StartPoint.X, StartPoint.Y - VertGap)
        Dim LineEnd As New Point(EndPoint.X, StartPoint.Y - VertGap)
        Dim ThinPen As New Pen(Color.LightGray, 1)
        For i As Integer = 1 To 10
            '  Рисуем линию 
            g.DrawLine(ThinPen, LineStart, LineEnd)
            '  Сбрасываем начальную и конечную позиции Y, перемещая вертикальную линию 
            LineStart.Y -= VertGap
            LineEnd.Y -= VertGap
        Next
        ThinPen.Dispose()
    End Sub
    

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

Вертикальные линии сетки

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

 Private Sub DrawVerticalGridLines()
        Dim ThinPen As New Pen(Color.Bisque, 1)
        '  Вычисляем длину базовой линии, нарисованной кодом, написанным выше 
        BaseLineLength = PBLineChart.Width - (LeftMargin + RightMargin)
        '  Вычисляем ширину каждого сегмента линии 
        LineWidth = (BaseLineLength / Sales.Length)
        '  Устанавливаем начальную току первой строки 
        Dim LineStartX As Integer = CInt(LeftMargin + 30)
        For i As Integer = 0 To Months.Length - 1
            g.DrawLine(ThinPen, LineStartX, TopMargin, LineStartX, PBLineChart.Height - (BaseMargin + 4))
            '  Двигаем начальную точку вправо 
            LineStartX += CInt(LineWidth)
        Next
        ThinPen.Dispose()
    End Sub
    

    Единственное существенное отличие заключается в замене DrawString на DrawLine, присвоившее соответствующие значения параметров.

Последнее отображение

    Добавьте вызов этих двух процедур в событие кнопки Click.

    Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
        DrawOutline()
        DrawHorizontalLines()
        DrawVerticalGridLines()
        DrawTheLine()
        ShowMonths()
        FinalDisplay()
    End Sub
    

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

Итоги

    Вы можете быть удивлены, какое большое количество кода вам пришлось написать, чтобы получить эту довольно просто выглядящую диаграмму. Со многими вещами это происходит при передаче идей с бумаги на экран, вещи не так просты, насколько кажутся. Эта хорошая мысль, которая требует 30 секунд для планирования в нашем мозгу и завершается через 30 часов упорной работы для внедрения в жизнь – о! радость программирования!
    В этой статье мы узнали или использовали следующее:

  • Bitmap
  • Brush
  • Создали Graphics
  • Двойная буферизация
  • Рисовали линию
  • Рисовали строку
  • Font
  • GraphicsPath
  • Объект Graphics
  • Перо
  • Точка
  • Измерение
  • Преобразования
  • Позиция X
  • Позиция Y