Data Source
First, let’s generate some data to display via the chart. We will create it at design time to allow us to get on with the core Graphics Class parts of the project. If you prefer to have the data input by the user at run time then you can adapt the techniques covered in Part 3.
Create Data
Shown below is what has become our standard code to create initial variables and to generate and store some data. I won’t trundle through the details as they are fully covered in earlier articles:
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
' Create Variables
‘ # of pixels vertical Axis is inset from PictureBox Left
Dim LeftMargin As Integer = 35
' # of Pixels left unused at right side of PictureBox
Dim RightMargin As Integer = 15
' The number of pixels above bottom of baseline
Dim BaseMargin As Integer = 35
' Margin at Top
Dim TopMargin As Integer = 10
' Set size of gap between each bar
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
' Minor changes for this version:
' Declare a variable to hold a Graphics object
Dim g As Graphics
' Variable for Bitmap to be displayed in (PictureBox) control
Dim bmap As Bitmap
' Length of vertical axis
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
' Generate some data and store it in the arraylist
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
Controls
Add a PictureBox named PBBarChart and a Button named btnDraw to the Form.
Place the button somewhere down in the bottom corner of the Form.
Stretch the PictureBox so that it is close to the top, right and left edges of the form. Set its Anchor property so that it is anchored to all four sides. Doing this will allow the user to click the button for a resized version of the chart to be displayed whenever the form size is altered . (As we will see in later articles, there are more dynamic ways of achieving this, but we’ll stick with this method for now.)
Graphics Object and Bitmap
Next, we set up Graphics and Bitmap objects. Put this initialising code in a separate procedure named GetGraphics:
Private Function GetGraphics() As Graphics
' Make bmap the same size and resolution as the PictureBox
bmap = New Bitmap(PBBarChart.Width, PBBarChart.Height, PBBarChart.CreateGraphics)
' Assign the Bitmap object to the Graphics object
' and return it
Return Graphics.FromImage(bmap)
End Function
Vertical Axis
If you have read the previous articles, you may notice that the various stages are now being broken up into smaller code chunks and put into separate procedures.
The code for the vertical axis is now placed in its own procedure and remains largely unchanged from previous versions. The first part looks like this:
Private Sub DrawVerticalAxis(ByVal g As Graphics)
' Draw a line for the Vertical Axis.
Dim StartPoint As New Point(LeftMargin, PBBarChart.Height - BaseMargin)
Dim EndPoint As New Point(LeftMargin, TopMargin)
' Basic Pen
Dim LinePen As New Pen(Color.Black, 2)
' Draw the vertical line (without tick marks)
g.DrawLine(LinePen, StartPoint, EndPoint)
' Draw the Tickmarks and Display Numbers
' Calculate length of the vertical axis
VertLineLength = PBBarChart.Height - (BaseMargin + TopMargin)
' Identify the highest sales figure
For Each gd As GraphData In SalesData
If gd.Sales > HighSale Then HighSale = gd.Sales
Next
' : Scaled Tick Marks code follows
Scaled Tick Marks
Here’s a change from the earlier article. In Part 2, we fixed the maximum number of sales at a figure of 1000. Not very realistic, but helped to keep the code less complicated. This time round we will adjust the maximum sales figure (and therefore the number and the spacing of tick marks) in accordance with whatever value is held in the HighSale variable above.
This means that you can change the sales data to allow a total sales value of more than 1000 at any time in the future and your graph won’t become distorted.
To do this, we identify the highest sales figure, then round up until we get to the next round value of 100. For example, if the HighSale figure is 1675 then the maximum value on the scale will be set to 1700.
Here is the code that does this:
' DrawVerticalAxis procedure continued:
' Round up to next hundred above highest sales figure
Dim NextCent As Integer = CInt(HighSale)
Do While NextCent Mod 100 <> 0
NextCent += 1
Loop
' Identify how many TickMarks required (one per hundred):
Dim TotalTicks As Integer = CInt(NextCent / 100)
Now we know how many hundreds we have to allow for, we can divide the vertical axis proportionately into 100s, draw the tick marks and the values as text, just as we did in Part 2:
' Calc gaps between vertical tick marks
Dim YPos As Integer = CInt(VertLineLength / TotalTicks)
' Variables for Start and End Points of Tick Marks
Dim TickSP As New Point(LeftMargin - 5, StartPoint.Y - YPos)
Dim TickEP As New Point(LeftMargin, StartPoint.Y - YPos)
' Font for values - declared here for readability
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
' Tick Values as text :
g.DrawString(CStr(i * 100), ValueFont, Brushes.Black, 2, TickSP.Y - 5)
' Resetx, y positions, proportionately up vertical line
TickSP.Y = CInt(TickSP.Y - (VertLineLength / TotalTicks))
TickEP.Y -= CInt(VertLineLength / TotalTicks)
Next
End Sub
Well, to be strictly accurate, it’s more a case of “Testing 1, 2, ... 1500”. If at this stage you want to see if the code so far is working, put the next code snippet in the btnDraw’s Click event and then click the button.
Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
' The various actions are now split into separate procedures to make
' the project more modular
' 1. Get the data
GetData()
' 2. Get a Graphics object to use for the drawing methods
g = GetGraphics()
' 3. Draw the Vertical Axis
DrawVerticalAxis(g)
' : More code to come ....
' n. Assign the bitmap with the vertical axis drawn to the PictureBox
PBBarChart.Image = bmap
End Sub
As you will see, the Tick Marks now range from 100 to 1500, reflecting the current highest sales figure of 1472 (Turkey).
You can tweak these values, run the project again and you will see that the vertical axis adjusts accordingly.
Baseline (The X Axis)
You can include a horizontal axis if you like the look of it. Create a new procedure named Draw3DBars.
Private Sub Draw3DBars()
g.DrawLine(New Pen(Color.Black), LeftMargin, PBBarChart.Height - BaseMargin, _
PBBarChart.Width - RightMargin, PBBarChart.Height - BaseMargin)
' Calculate length of baseline drawn by the code above
BaseLineLength = pbBarChart.Width - (LeftMargin + RightMargin)
' More code to follow
3D Bars
I came across a C# method for building up bars with a 3D effect on codeproject.com. The author, Michael Damron, had used the simple but effective idea of building a series of polygons one above the other.
Topping the bar off with a lighter coloured polygon completes the effect. So I’ve re-jigged his original basic idea for this project.
Here’s the way I’ve gone about it: The first few code items deal with some of the core calculations required. For example, we can calculate the optimum width of each bar by using the total width available and dividing it by the number of bars; not forgetting to take into account the required gaps between each bar.
Most of these are the same as were used in Part 2:
' Calculate width of each bar making full use of space available
BarWidth = CInt((BaseLineLength / SalesData.Count) - BarGap) ' Set the start point for the first bar
Dim BarStartX As Integer = LeftMargin + BarGap
' Set the baseline of the graph
Dim BaseLine As Integer = PBBarChart.Height - BaseMargin
' Calc scaling (new in this project)
VertScale = VertLineLength / HighSale
' : More code to follow
- the only new item being the VertScale variable, which is used to ensure that the bars are drawn to the correct height, allowing for the scaling we applied in the DrawVerticalAxis procedure earlier.
This procedure continues with the following block of code which draws each of the bars in turn. I have added quite a lot of commenting and (especially if you have read any of the previous articles) it should all make sense to you.
For Each gd As GraphData In SalesData
' Set the positions of the four points of the bottom-most
' parallelogram and store in an array
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)
' Calculate the height of this bar, taking scale into account:
Dim BarHeight As Integer = CInt(gd.Sales * VertScale)
' Create brushes to draw the bars
' Colors will change according to settings in GraphData
Dim barmainBrush As New HatchBrush(HatchStyle.Percent50, gd.BarColor)
Dim bartopBrush As New SolidBrush(gd.BarColor)
' Draw one complete bar
For i As Integer = 0 To BarHeight - 11
‘ (“BarHeight - 11” might be confusing. This makes allowance for
‘ a. the 10 pixels which are added to create the 3D depth effect
‘ plus
‘ b. the final rhombus is to be drawn in a lighter color, so we
‘ need to stop drawing these hatched ones 1 pixel below the
‘ total bar height.
' Fill next polygon using the hatchbrush
g.FillPolygon(barmainBrush, Corners)
' Move all the Y positions up the picture box by 1 pixel
Corners(0).Y -= 1
Corners(1).Y -= 1
Corners(2).Y -= 1
Corners(3).Y -= 1
Next
' Finally, top it off with a lighter rhombus
g.FillPolygon(bartopBrush, Corners)
' Move the startpoint for the next bar
BarStartX += CInt(BarWidth + BarGap)
‘ Dispose of brushes
barmainBrush.Dispose()
bartopBrush.Dispose()
Next
End Sub
(If you want to test the above procedure, don't forget to add a call to it in the btnDraw_Click event. )
Just to finish it off, the names of the countries are written below each bar.
This is very similar to what we did in Part 2, with the addition of a code line that adjusts the font size according to the width available.
Private Sub WriteTheNames()
' X position for start of country name(s). It is placed
' under the left edge of the bar, plus 5 pixels for better look
Dim TextStartX As Integer = LeftMargin + BarGap + 5
' Create a Brush to draw the text
Dim TextBrsh As Brush = New SolidBrush(Color.Black)
' Create a Font object instance for text display
' dynamically adjusted font size would be useful:
Dim fntSize As Integer = CInt(BarWidth / 7)
Dim TextFont As New Font("Verdana", fntSize, FontStyle.Bold)
' Write them:
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
Again, you will need to add a call to this procedure in the Draw Button’s click event. The complete code for that event would therefore look like this:
Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
' The various actions are now split into separate procedures to make
' the project more modular
' 1. Get the data
GetData()
' 2. Get a Graphics object
g = GetGraphics()
' 3. Draw the Vertical Axis
DrawVerticalAxis(g)
' 4. Draw bars
Draw3DBars()
' 5. Write the titles
WriteTheNames()
' All Done! Assign the bitmap with the vertical axis drawn to the picturebox
pbBarChart.Image = bmap
g.Dispose()
End Sub
This project creates a more versatile and better looking bar chart. It splits the main drawing tasks into separate procedures. However, the code is still rather unwieldy and this makes a strong case for creating a 3DBar class with settable properties for width, depth, height, color, etc. It would then be possible to include properties well beyond the UI choices we have used here, and of course it conforms more closely to the OOP approach to developing in VB.NET.
In this article I explained or used the following:
- Bitmap object
- Brushes
- Dispose
- Do While Loop
- DrawLine
- DrawString
- FillPolygon
- Font object
- Graphics Object
- Pen
- Structure
In the next article in this series, we will move on to another, sometimes more dynamic, type of chart - the line chart.
评论