Удачные диаграммы. Часть 6
- Динамическая линейная диаграмма
Автор:
Ged Mead
Перевод: Виталий
Готовцов
[Оригинал
Статьи] [Обсудить в форуме]
Скачать исходный код к статье
Существуют некоторые весьма продвинутые возможности, которые могут быть включены в динамическую диаграмму, в зависимости от того, откуда поступают данные. В этой статье мы собираемся начать с простого демонстрационного примера. Затем перейдем к более сложному, и, наконец, рассмотрим дальнейшие улучшения, которые могут быть применены в этом типе диаграмм.
Итак, давайте установим сцену. Что мы хотим сделать здесь? Хорошо, первая линейная динамическая диаграмма, которую я собираюсь сделать, будет получать некоторые данные от пользователя и отображать значения так, как они будут выбраны на диаграмме. По мере того, как будет выбрано значение, диаграмма будет расширяться и перерисовываться, чтобы показать изменения, которые были сделаны. Когда экран будет заполнен, более ранние данные будут удалены с поля зрения по мере того, как диаграмма будет прокручиваться справа налево.
Должен быть создан быстрый и легкий демонстрационный пример, на форму нужно поместить scrollbar и использовать его изменяющиеся значения в качестве данных для диаграммы. Посмотрите на помещенный ниже образец формы.
Элементы управления формы
Серебряно-серая поверхность – это PictureBox. Изменяющиеся значения Scrollbar представляются в виде новых значений графика. Я применяю здесь Vertical Scrollbar, но вы можете так же легко применить Horizontal Scrollbar. В любом случае логика кода будет одной и той же.
Вот шаги создания формы.
1. Добавьте на форму элемент Scrollbar. Установите его свойство Minimum равным 0, а свойство Maximum равным 104. (Если вам интересно, почему я выбрал такое максимальное значение, посмотрите заметки ниже на этой странице.) Установите свойство LargeChange равным 5; оставьте свойство SmallChange равным 1. Измените название элемента на SBUserValue, чтобы отобразить его цель – ScrollBar (полоса прокрутки) для определения выбранных пользователем значений (User’s Chosen Values).
2. Добавьте PictureBox и назовите его picGraph. Установите его свойство height равным 400. Почему? Это действительно способ сохранить код размеров настолько легким, насколько это возможно. Значения пользователя будут значениями от 0 до 100, поэтому будет очень легко умножать значения на 4, чтобы автоматически отмерять их вертикально на графике на PictureBox. Реальная жизнь почти наверняка не будет такой простой, но это поможет нам сконцентрироваться на важности рисования для этой первой демонстрации. Измените название PictureBox’а на picGraph и установите цвет фона Gainsborough (или другой, который вам хочется).
3. И, наконец, добавьте элемент Label (метку) для отображения изменяющегося выбранного значения. Эта метка не играет никакой роли в создании диаграммы; я включил ее для того, чтобы вы могли проверить, что любые введенные данные правильно предоставлены на диаграмме. Назовите ее lblValue.
4. Все остальные элементы управления, показанные на скриншоте вверху, являются опциональными и не требуют никакого кода.
Инициирующий код
Поместите этот оператор Imports вверху формы:
Imports System.Drawing.Drawing2D
Как вам известно, если вы читали предыдущие статьи этой серии, это позволит нам использовать в коде любые методы и свойства класса Drawing.Drawing2D не записывая полные имена каждый раз.
В следующем шаге мы будем строить код, который выполняет все рисование в диаграмме, динамически изменяя дисплей каждый раз, когда в ScrollBar выбирается новое значение.
Причина установки значения Maximum равным 104, (если мы хотим в действительности масштаб от 0 до 100) заключается в маленьком недостатке элемента ScrollBar. Если вы установите Maximum равным 100 и LargeChange равным 5, то наивысшее значение, которое вы сможете получить, переместив ползунок или щелкнув на стрелке ScrollBar’а, будет равно 96. Возможно, это официальная установка, но я решил, что смещение Maximum в соответствии с числом (LargeChange-1) добивается цели.
Рисование направляющих линий диаграммы
Следующий шаг – рисование горизонтальных направляющих линий. Они будут нарисованы, как только загрузится форма и (как вы вскоре увидите) перерисовываются всякий раз, когда диаграмма изменяется с добавлением новых данных.
В этом первом примере, чтобы нарисовать направляющие линии, мы будем использовать данные, жестко закодированные в процедуре. Это делается для того, чтобы упростить код и сократить объяснения. Позже в статье мы будем применять более подвижный, модульный и более реалистичный подход.
В окне Code формы добавьте следующую процедуру:
Private Sub DrawGuidelines(ByVal PicBox As PictureBox)
' Создаем bitmap, чтобы рассовать и захватывать объект Graphics
Dim bmp As New Bitmap(PicBox.Width, PicBox.Height)
Dim gr As Graphics = Graphics.FromImage(bmp)
' Рисуем направляющие линии на этом объекте graphics.
For i As Integer = 40 To 400 Step 40
gr.DrawLine(Pens.WhiteSmoke, 0, i, PicBox.Width, i)
Next i
' Присваиваем bitmap в качестве изображения PictureBox.
PicBox.Image = bmp
End Sub
Как это работает
Давайте проанализируем, что в действительности делает этот код. (Если вы читали предыдущие статьи, это уже знакомо вам, но если вы перепрыгнули сюда, в Часть 6, я уверен, что некоторые объяснения будут полезны для вас).
Первые две строки кода в процедуре:
Dim bmp As New Bitmap(PicBox.Width, PicBox.Height)
Dim gr As Graphics = Graphics.FromImage(bmp)
могут быть разделены так:
1.
Dim bmp As New Bitmap
Создает объект Bitmap.
2.
(PicBox.Width, PicBox.Height)
Устанавливает размер этого Bitmap таким же,
как и PictureBox, на котором он будет изображен.
3.
Dim gr As Graphics: Создает объект Graphics.
(Объект Graphics можно рассматривать, как
своего рода «холст», который лежит на элементе и
на котором могут быть нарисованы фигуры и линии).
4.
= Graphics.FromImage(bmp)
Назначьте объект Graphics объекту Bitmap,
который мы только что создали.
Мы будем рисовать линии на объекте Graphics
Мы начали. Давайте проверим его. Вставьте в событие формы Load следующий код, который вызовет вышеуказанную процедуру и нарисует десять линий на равном расстоянии друг от друга на PictureBox:
Private Sub _1stDemo_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
' Рисуем горизонтальные линии на Picturebox
DrawGuidelines(Me.picGraph)
End Sub
Отображаем значения
Следующая часть кода является ключом к динамизму диаграммы. Во время того, как пользователь будет изменять записанные данные, прокручивая ScrollBar, вот что мы будем делать:
1. Возьмем «снимок» («snapshot») предыдущего изображения на дисплее, захватывая текущее изображение с PictureBox’а в котором создана диаграмма.
2. Растягиваем направляющие линии в правой части изображения. (Зачем – мы увидим через минуту).
3. «Вставляем» снимок предыдущего дисплея обратно в объект Graphics Bitmap’а, НО помещаем его немного левее так, чтобы часть снимка выпадала из обзора PictureBox’а.
(Теперь в правой части появился пустой промежуток, если мы не продлили направляющие линии в предыдущем шаге.)
4. Вычисляем точку Y (вертикальная ось), в которой должно быть отмечено выбранное значение.
Заметьте, что мы должны пересчитывать его, чтобы использовать всю доступную высоту PictureBox’а. В нашем примере, в котором мы заботливо установили высоту PictureBox’а равным 400 и максимальное значение ScrollBar’а равным 100, нам придется учитывать масштаб 4:1 (т.е. каждое повышение на 1 значения в ScrollBar, будет представлено 4 вертикальными пикселями PictureBox’а).
5. Перемещаем начальную точку от начального положения в верхнем левом углу PictureBox’а, чтобы теперь начинать рисование в нижнем левом углу PictureBox’а.
Важно понимать, что когда вы рисуете линию в PictureBox’е, исходная точка – точка со значением (0, 0) – всегда находится в верхнем левом углу.
Затем, когда вы рисуете линию из этой точки – в точку (50, 100), к примеру – перо перемещается на 50 пикселей в сторону и на 100 пикселей вниз.
Это хорошо для большинства задач в рисовании, но в диаграмме мы ожидаем, что линия будет двигаться вверх, при росте значения, а не вниз. Мы хотим, поэтому, чтобы линия нашей диаграммы начиналась в нижнем левом углу, чтобы представлять значение нуль (Точка 0, 0) и повышающиеся значения были представлены движением линии вверх.
6. Рисуем следующий сегмент. Эта линия будет тянуться от точки, где было отмечено последнее значение в точку, которая должна будет представлять текущее значение.
Имея в виду то, что было сказано выше о линиях, идущих сверху вниз, мы также должны перевернуть линию, чтобы она стала нарисованной снизу вверх (иначе она будет нарисована за пределами видимости).
В этом примере, это достигается очень просто, вычерчивая отрицательные эквиваленты значений.
7. Все задачи рисования для последнего значения теперь выполнены, так что мы можем присвоить наш только что нарисованный Bitmap свойству Image PictureBox’а.
Это обновит экран, и пользователь увидит результаты наших предыдущих действий.
8. Наконец, хорошей практикой считается устранение объекта Graphics и мы должны закончить этим цикл действий.
Следующий код делает эту работу: -
Мы должны установить несколько переменных. Первая представляет длину каждого сегмента графика – т.е. число пикселей, используемых горизонтально на экране, для каждой новой рисуемой части графика. Добавьте нижележащий код вверху кода формы:
Dim Xmove As Integer = 3
Из-за того, что мы собираемся рисовать график каждого изменяемого набора значений, нам необходимо иметь возможность ссылаться на последнее использованное значение так же, как на текущее. Две переменные, показанные ниже, будут использованы для этой задачи:
Private OldValue As Single = 0
Private NewValue As Single = 0
Теперь фактическая процедура, которая производит данные. Она называется DrawGuidelinesAndChart:
Private Sub DrawGuidelinesAndChart(ByVal PicBox As PictureBox)
' Шаг 1
' Захватываем текущее изображение (последняя версия диаграммы)
Dim bm As New Bitmap(PicBox.Width, PicBox.Height)
Dim gr As Graphics = Graphics.FromImage(bm) ' Шаг 2
' Отмечаем пропущенные направляющие линии в правой стороне объекта Graphics.
For i As Integer = 40 To 400 Step 40
gr.DrawLine(Pens.WhiteSmoke, PicBox.Width - Xmove, i, PicBox.Width, i)
Next i
' Шаг 3
' Рисуем это захваченное изображение, помещая его в Xmove пикселей в левой
' части рисунка. (т.е., первые несколько пикселей будут невидимы из-за
' того, что они теперь находятся вне видимой области picturebox'а)
gr.DrawImage(PicBox.Image, -Xmove, 0)
' Заметьте, что мы все еще рисуем на объекте Graphics.
' На PictureBox еще не появилось никаких изменений.
' Шаг 4
' Отображаем новое значение. Т.е. вычисляем, где на оси Y
' (вертикальной плоскости) должно быть отмечено это новое значение.
' Это вычисление принимает во внимание масштаб значений min/max
' в диапазоне относительно общей высоты PictureBox.
' В этом примере он составляет 4:1 (Высота PictureBox 400:Max значение ScrollBar - 100)
' поэтому мы умножаем выбранное значение на 4
NewValue = SBUserValue.Value * 4 ' Step 5
' Перемещаем начальную точку с верхней левой точки в нижнюю левую.
gr.TranslateTransform(0, picGraph.Height)
' Шаг 6
' Рисуем следующий сегмент линии на объекте Graphics.
' Заметьте, что ОТРИЦАТЕЛЬНЫЕ значения OldValue и NewValue
' используются для позиций Y.
' Это будет обратная позиция этих точек и заставит их
' появится в PictureBox. Если вы не сделаете этого, они будут
' нарисованы ниже нижнего края PictureBox и поэтому
' вне видимости пользователем.
gr.DrawLine(Pens.Black, _
picGraph.Width - 1 - Xmove, -OldValue, _
picGraph.Width - 1, -NewValue)
OldValue = NewValue
' Шаг 7
' Отображаем результаты, присваивая этот редактированный Bitmap в качестве
' Image PictureBox’а.
picGraph.Image = bm
' Шаг 8
' Все сделано
gr.Dispose()
' Для подтверждения целей отображаем текущее выбранное значение в label
lblValue.Text = SBUserValue.Value.ToString
End Sub
Использование Vertical ScrollBar
Как упоминалось раньше, вертикальный ScrollBar, используемый в этой демонстрации, является просто легким способом генерации набора быстроизменяемых данных для отображения в графике. Теперь нам нужно написать немного кода, который отмечает изменения, сделанные пользователем в ScrollBar. Место для этого кода – событие ValueChanged ScrollBar’а.
Private Sub SBUserValue_ValueChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles SBUserValue.ValueChanged
DrawGuidelinesAndChart(picGraph)
End Sub
Проверяем
Теперь запускаем проект и перемещаем ScrollBar. Каждое движение ScrollBar’а приводит к тому, что результат будет отображен на диаграмме. Вы можете проверить, что значение, которое было выбрано последним, будет показано в label.
Как вы увидите, Vertical ScrollBar создает более высокие значения при движении вниз; более низкие значения при движении вверх. Если это смущает вас, вы легко можете заменить вертикальный ScrollBar горизонтальным. В этом случае меньшие значения вы будете выбирать при прокручивании влево, а большие значения – справа, что может показаться вам более логичным.
Итоги первой демонстрации
Относительно маленьким кодом мы создали полезную динамичную линейную диаграмму. Вы можете заменить ScrollBar, который использовал я, любым другим способом получения данных, который будет уместен для вашего проекта, то ли пользовательский ввод, системно производимая информация, внешние данные, основанные на времени данные и т.д.
Конечно, вы не всегда можете гарантировать, что используемые точно данные войдут в диапазон от 1 до 100, который мы использовали в первом примере, поэтому мы вкратце посмотрим, как можно редактировать код, чтобы сделать его более уступчивым.
Прежде всего, я хочу рассмотреть пару способов производства интересных альтернативных изображений.
Затененный график
Если вы хотите диаграмму, которая выглядит так:
то вы можете достичь такого эффекта маленьким улучшением кода. Замените Шаг 6 в процедуре DrawGuidelinesAndChart блоком кода, помещенным ниже:
' Шаг 6
' Рисуем следующий сегмент линии на объекте Graphics.
gr.DrawLine(Pens.Black, _
picGraph.Width - 1 - Xmove, -OldValue, _
picGraph.Width - 1, -NewValue)
OldValue = NewValue
' Рисуем следующий сегмент линии, но в этот раз оставляем значение Y
' OldValue положительным.
gr.DrawLine(Pens.LightBlue, _
(picGraph.Width - 1) - Xmove, OldValue, _
picGraph.Width - 1, -NewValue)
OldValue = NewValue
Как вы можете видеть, единственное изменение в блоке sub, которое снова использует Drawline, чтобы рисовать второй сегмент линии на объекте Graphics. Отличие здесь заключается в том, что начальная точка этого сегмента является положительным значением OldValue, которое вы будете вызывать, подразумевается, что она будет находиться в нижней части PictureBox’а. Поэтому рисование этой второй линии имеет эффект создания линии, которая выходит снизу PictureBox’а и поднимается, пока не встречает точку, в которой мы отметили отрицательное значение NewValue.
Это звучит сложно, я знаю, но если вы пройдете через это постепенно, я надеюсь, это приобретет смысл.
Или что-то более замечательное:
И снова, здесь требуется просто маленькое изменение Шага 6. В этот раз, однако, вместо того, чтобы рисовать линии, мы нарисовали многоугольники. Верхний многоугольник является линией, которая точно такая же линия, что и та, которую мы рисовали в предыдущем примере; остаток многоугольника создается рисованием двух вертикальных линий, направленных вниз от конца этой линии к низу PictureBox, и горизонтальной линии вдоль нижней части.
Метод FillPolygon берет эти четыре точки и рисует многоугольник, заполненный цветом по нашему выбору.
Вот замена в Шаге 6:
' Шаг 6
' Рисуем Polygon, чтобы заполнить площади ниже нарисованной линии.
Dim topLeft As New Point((picGraph.Width - 1) - Xmove, -OldValue)
Dim topRight As New Point((picGraph.Width - 1), -NewValue)
Dim bottomRight As New Point(picGraph.Width - 1, picGraph.Height)
Dim bottomLeft As New Point((picGraph.Width - 1) - Xmove, picGraph.Height)
Dim PolyPoints As Point() = {topLeft, topRight, bottomRight, bottomleft}
gr.FillPolygon(Brushes.Red, PolyPoints)
OldValue = NewValue
Финальный вариант для вас:
Исправленный Шаг 6 похож на это:
' Шаг 6
' Рисуем Polygon, чтобы заполнить площади ниже нарисованной линии.
Dim topLeft As New Point((picGraph.Width - 1) - Xmove, -OldValue)
Dim topRight As New Point((picGraph.Width - 1), -NewValue)
Dim bottomRight As New Point(picGraph.Width - 1, picGraph.Height)
Dim bottomLeft As New Point((picGraph.Width - 1) - Xmove, picGraph.Height)
Dim PolyPoints As Point() = {topLeft, topRight, bottomRight, bottomLeft}
Dim hb As New Drawing.Drawing2D.HatchBrush(Drawing2D.HatchStyle.Trellis, Color.Black, Color.White)
' Альтернатива: Раскомментируйте эту строку для получения более тонкой версии:
' Она изменяет свойства ForeColor и BackColor для большего отличия
gr.FillPolygon(hb, PolyPoints)
OldValue = NewValue
Вы можете играться с этими вариациями часами! Если вы хотите уменьшить эффект зазубренности на начерченных линиях, попробуйте увеличить значение XMove.
Значения вертикальной оси
Пока пользователь мог видеть текущее значение, отображенное на метке (Label), которое было полезным, но не всегда реалистичным. Однако на самой диаграмме текущее значение ограничено серыми горизонтальными линиями. В большинстве случаев мы хотим что-нибудь более определенное, чем это. Особенно было бы лучше получить значение в конце каждой направляющей линии, где хотелось бы нашему пользователю.
Всегда есть несколько способов достичь этого, один из самых простых – получить второй PictureBox, который содержит эти значения и поместить его рядом с picGraph PictureBox.
Добавьте на форму второй PictureBox. Поместите его слева от оригинального PictureBox’а picGraph. Установите его размеры 30, 400. Это сделает его той же высоты, что и PictureBox picGraph. Назовите его picValue.
Легкий способ выровнять два PictureBox – применить меню Format в Visual Studio IDE. Вероятно, вы уже знаете все об этом, но просто на всякий случай:
-
Щелкните левой кнопкой мыши на одном из PictureBox’ов.
-
Нажмите на клавишу Control.
-
Кликните на втором PictureBox’е удерживая клавишу Control нажатой.
-
Выберите Format – Alighn – Tops в меню IDE:
Код Вертикальных Значений
Процедура, которая размещает линии и значения в новом PictureBox’е называется DrawVerticalValues. Вот она:
Private Sub DrawVerticalValues(ByVal PB As PictureBox)
' Рисуем вертикальные значения (снова применяем двойную буферизацию,
' хотя в этом нет необходимости)
' Шаг 1
Dim bmp As New Bitmap(PB.Width, PB.Height)
Dim gv As Graphics = Graphics.FromImage(bmp)
' Шаг 2
' Рисуем направляющие линии на полосках значений
For i As Integer = 40 To 400 Step 40
gv.DrawLine(Pens.WhiteSmoke, 0, i, PB.Width, i)
Next i
' Шаг 3
Dim NextMarker As Integer = 100
For i As Integer = 0 To 360 Step 40
gv.DrawString(CStr(NextMarker), New Font("Verdana", 8, FontStyle.Regular), Brushes.Black, 1, i)
NextMarker -= 10
Next
' Шаг 4
PB.Image = bmp
' Шаг 5
gv.Dispose()
End Sub
Шаг 1 и Шаг 2 те же, что и обсуждавшиеся раньше, поэтому я не буду на них останавливаться. Шаг 3 имеет задачу – заполнение PictureBox’а значениями.
- Первое, он создает переменную, которая называется NextMarker и присваивает ей инициирующее значение 100. Это значение отражает значение Maximum ScrollBar.
-
Вторая строка в Шаге 3 устанавливает цикл. Из-за того, что значения Шага используются в цикле, в действительности это значит, что цикл будет выполнен десять раз.
-
В цикле DrawString применяется для того, чтобы писать следующую отметку значения на объекте Graphics PictureBox’а. Первое значение, которое будет написано, должно быть 100, следующее – 90, затем 80, и т.д. Причина того, что значения записываются в порядке убывания, заключается в том, что процедура работает, перемещаясь сверху к низу PictureBox’а и нам приходится уменьшать значения с каждым шагом.
- Следующая строка кода просто уменьшает значение, которое мы собираемся отобразить следующим. В этом примере мы уменьшаем значение на 10 каждый раз.
- Цикл завершается с ключевым словом Next.
Шаги 4 и 5 снова просто повторяют логику, которую мы использовали в предыдущих процедурах рисования.
Так что теперь у вас есть довольно простой способ вставить значения в вертикальный список показываемых значений. Затем нам нужно рассмотреть более реалистичную ситуацию, в которой диапазон значений, которые могут быть отображены, не должен быть точно предустановлен и рассчитан в PictureBox’е.
Подвижные направляющие линии
Мы сделали жизнь относительно легче для себя, поместив заранее установленные и легкие для использования значения. Пока это удобно для целей статьи, я уверен, что вы захотите знать, как вы можете преобразовать логику, что позволит вам создать диаграммы для других диапазонов данных и других размеров дисплеев.
Вот что мы увидим теперь.
Отображаем направляющие линии
Вместо использования процедуры, которая использует значения, жестко закодированные в нее, мы перейдем к такой, которая принимает параметры. Мы изменим Sub на Function, что позволит нам передать в нее необходимые параметры и получить взамен вновь созданный объект Bitmap.
Вот замещающая функция:
Private Function DisplayGuidelines(ByVal PicBox As PictureBox, ByVal chunks As Integer) As Bitmap
' Шаг 1
' Создаем bitmap для рисования и захвата его объекта Graphics
Dim bm As New Bitmap(PicBox.Width, PicBox.Height)
Dim gr As Graphics = Graphics.FromImage(bm)
' Шаг 2
' Рисуем направляющие линии на главной диаграмме.
' Получаем общую доступную высоту и делим ее на части
Dim total As Integer = PicBox.Height
Dim chunk As Single = total / chunks
' Шаг 3
For i As Single = chunk To total Step chunk
gr.DrawLine(Pens.WhiteSmoke, 0, i, PicBox.Width, i)
Next i
' Шаг 4
' возвращаем результаты.
Return bm
' Шаг 5
gr.Dispose()
End Function
Давайте посмотрим на ключевые изменения. Шаг 2 вычисляет вертикальное пространство между каждой строкой. Это легко делается делением всей высоты PictureBox’а на требуемое количество линий. Шаг 3 является общей версией оригинального цикла, который рисует линии; значения больше не кодируются жестко. Хотя я использовал перо Pens.WhuteSmoke в этой процедуре, вы можете, конечно, добавить третий параметр в нее и позволить клиентскому коду установить цвет пера по выбору. Шаг 4 возвращает объект Bitmap. Он немного отличается от оригинального Sub, в котором мы присваиваем Bitmap PictureBox’у, чье имя закодировано. В этой заменяющей функции мы можем присвоить Bitmap любым, удобным для нас способом, любому элементу управления, который будет его использовать.
Мы вызываем эту функцию в Form_Load:
Private Sub _3rd_More_Generic_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
picGraph.Image = DisplayGuidelines(picGraph, 12)
End Sub
которая в этом примере создает 12 направляющих линий и рисует их на PictureBox’е, называемом picGraph.
Направляющие линии и отображенные значения
Давайте посмотрим на процедуру, которая отображает значения более удобные.
Вот замещающая процедура – и снова Function, а не Sub.
Private Function DisplayGuidelinesAndChart(ByVal PicBox As PictureBox, ByVal chunks As Integer,
_ ByVal XMove As Integer, ByVal NewValue As Single, ByVal Min As Single, ByVal Max As Single) As Bitmap
' Шаг 1
' Захватываем текущее изображение (самая последняя версия диаграммы)
Dim bm As New Bitmap(PicBox.Width, PicBox.Height)
Dim gr As Graphics = Graphics.FromImage(bm)
' Шаг 2
' Получаем общую доступную высоту и делим ее на части
Dim total As Integer = PicBox.Height
Dim chunk As Single = total / chunks
' Отмечаем пропущенные линии справа на объекте Graphics.
For i As Single = chunk To total Step chunk
gr.DrawLine(Pens.WhiteSmoke, PicBox.Width - XMove, i, PicBox.Width, i)
Next i
' Шаг 3
' Рисуем это захваченное изображение, помещаем его в XMove пикселей слева
If Not IsNothing(PicBox.Image) Then
gr.DrawImage(PicBox.Image, -XMove, 0)
End If
' Шаг 4
' Отображаем новое значение.
' Вычисляем требуемый масштаб и делаем полное использование высоты
' PictureBox’а
Dim ValueRange As Single = Max - Min
Dim vScale As Single = PicBox.Height / ValueRange
' Применяем масштаб к текущему значению
NewValue *= vScale
' Шаг 5
' Перемещаем начальную точку с верху слева в низ слева.
gr.TranslateTransform(0, picGraph.Height)
' Шаг 6
' Рисуем следующий сегмент объекта Graphics.
' Если Min > 0, то вам нужно переместить изображение вниз еще раз,
' в этот раз помещаем значение Min на горизонтальную ось
If Min > 0 Then gr.TranslateTransform(0, Min * vScale)
' Шаг 7
' Возвращаем Bitmap.
Return bm
' Шаг 8
' Все сделано
gr.Dispose()
End Function
Мы расширили параметры и теперь получаем значения от клиента для XMove, NewValue, Min и Max. XMove и NewValue играют ту же роль, что и раньше; передача их, как параметров, делает процедуру более общей. Мы использовали в предыдущей функции целое число «кусков» и их роль та же в этой процедуре.
В Шаге 3 я добавил код, чтобы поймать перемежающуюся ошибку, которая вызовет проблемы. Она может быть обработана, как исключение «Значение не может быть нулевым»; проблема может быть сконцентрирована в PicBox.Image в Form_Load. Я не мог устранить эту ошибку из-за ее неустойчивой природы. Альтернативным решением было бы поместить в метод DrawImage блок Try Catch; это обработает исключение без крушения приложения, но я решил, что при появлении новой формы это вызовет нежелательную задержку, поэтому предпочел IsNothing.
В Шаге 4 параметры Min и Max тоже новые. Они введены для того, чтобы дать вам возможность использовать такие диапазоны значений, в которых значение Minimum не закодировано жестко или не должно равняться нулю. Так же значением Maximum может быть любое число, которое может быть продиктовано любым источником данных. Имея эти два значения в виде параметров, функции позволяется вычислять масштаб, необходимый для заполнения всей доступной высоты PictureBox’а.
Также в Шаге 4 мы вычисляем подходящий масштаб, основываясь на доступной высоте PictureBox’а и определенного диапазона Maximum. В предыдущих примерах, как вы можете помнить, что жестко был закодирован масштаб 4:1. Теперь у нас есть бóльшая подвижность.
В Шаге 6, как должно быть ясно из комментариев, нам нужно переместить изображение так, чтобы минимальное значение (если оно есть) было помещено точно на линию горизонтальной оси. Метод TranslateTransform просто перемещает начальную точку рисования на соответствующее значение.
Вот и все, ничего нового больше нет.
Вертикальные значения
Наконец, нам нужно применить тот же метод мышления к процедуре, которая заполняет picValues PictureBox’а направляющими линиями и числами. Хотя код, расположенный ниже, со всеми его переменными, заместившими предыдущие, жестко вписанные, значения, кажется более сложным, логика кода, фактически, такая же. Надеюсь, комментарии и названия переменных помогут вам понять, как эта версия отражает версию с кодированными переменными.
Private Function DisplayVerticalValues(ByVal PB As PictureBox, ByVal HowManyChunks As Single, _
ByVal MinValue As Single, ByVal MaxValue As Single) As Bitmap
' Шаг 1
Dim bmp As New Bitmap(PB.Width, PB.Height)
Dim gv As Graphics = Graphics.FromImage(bmp)
' Шаг 2
' Рисуем направляющие линии с полосками значений
' Получаем общую доступную высоту и делим ее на части
' Это значение представляет количество пикселей
Dim TotalPixels As Integer = PB.Height
Dim SingleChunk As Single = TotalPixels / HowManyChunks
For i As Single = SingleChunk To TotalPixels Step SingleChunk
gv.DrawLine(Pens.WhiteSmoke, 0, i, PB.Width, i)
Next i
' Шаг 3
' Рисуем Numbers как текст, точно размещая по вертикали
' Начинаем с наибольшего допустимого значения
Dim NextMarker As Integer = MaxValue
Dim ValueRange As Single = MaxValue - MinValue
' В цикле рисуем числа пропорционально уменьшая их по одному
For i As Single = 0 To TotalPixels Step SingleChunk
gv.DrawString(CStr(NextMarker), New Font("Verdana", 8, FontStyle.Regular), Brushes.Black, 1, i)
NextMarker -= (ValueRange / HowManyChunks)
Next
' Шаг 4
Return bmp
' Шаг 5
gv.Dispose()
End Function
Итак, у вас есть различные изменения и улучшения, необходимые, чтобы сделать исходную идею более гибкой. Вы можете теперь развить общий подход для любого специфического источника данных так, как вам необходимо для достижения ваших целей.
Возможные улучшения
Следующим логическим шагом было бы создание классов Chart и Library, чтобы сделать ваш код более переносимым между приложениями. Следуя этому, если вы собираетесь использовать многие из этих видов диаграмм долгое время в будущем, было бы неплохо создать свой элемент управления и добавить его в Visual Studio Toolbox. В настоящее время нас устраивает этот набор процедур создающих линейную диаграмму. Класс, библиотека и пользовательский элемент будут ждать лучших времен. Тем не менее, мы раскрыли очень многое в этой статье и разработали удобный способ создания динамических линейных диаграмм. Мы прошли долгий путь от первого примера круговой диаграммы в Части 1!
Охваченные темы
В этой статье мы раскрыли следующие темы:
- Кисти (Brushes)
- Метод Dispose
- Двойная буферизация
- Метод DrawLine
- Метод DrawImage
- DrawString
- Метод FillPolygone
- Объект Font
- Элемент меню Format
- Метод Graphics.FromImage
- Перья (Pens)
- Масштабирование
- Метод TranslateTransform
Пример
Весь описанный код, включенный в приложенный образец решения, был создан с помощью VB.NET 2003. Скачать пример можно
по ссылке.