DevCity.NET - http://devcity.net
Chart Success: GDI+ Graphics At Work. Part 2
http://devcity.net/Articles/138/1/article.aspx
Ged Mead

Ged Mead (XTab) is a Microsoft Visual Basic MVP who has been working on computer software and design for more than 25 years. His journey has taken him through many different facets of IT. These include training as a Systems Analyst, working in a mainframe software development environment, creating financial management systems and a short time spent on military laptop systems in the days when it took two strong men to carry a 'mobile' system.

Based in an idyllic lochside location in the West of Scotland, he is currently involved in an ever-widening range of VB.NET, WPF and Silverlight development projects. Now working in a consultancy environment, his passion however still remains helping students and professional developers to take advantage of the ever increasing range of sophisticated tools available to them.

Ged is a regular contributor to forums on vbCity and authors articles for DevCity. He is a moderator on VBCity and the MSDN Tech Forums and spends a lot of time answering technical questions there and in several other VB forum sites. Senior Editor for DevCity.NET, vbCity Developer Community Leader and Admin, and DevCity.NET Newsletter Editor. He has written and continues to tutor a number of free online courses for VB.NET developers.

 
by Ged Mead
Published on 3/16/2005
 

In this series of articles, designed for newcomers to graphics in VB.NET, you will see how to create various kinds of charts. Charts are a great way of  displaying data in a way that is easy to understand, as well as being a useful way to become familiar with many of the core drawing methods which are available in the Graphics Class. In this article, we will build a Bar Chart from the ground up using double buffering to create a chart to display in a PictureBox control.


Picture This

 In Part 1   we created a Pie Chart by drawing directly on to the surface of the form using the form’s Graphics object.   In this article, we are going to create a Bar Chart, again based on some notional sample data - sales figures from six European countries.     However, this time we will draw the Bar Chart inside a PictureBox control.  

 In general, there is little difference between the two approaches – drawing on the form itself or drawing inside the picturebox.

 In this project, we will draw the chart once the user has clicked a Button.  As in Part 1, the display will be redrawn whenever it has been obscured or changed (this is known as "persisting" the drawing).

     In the Pie Chart example we used the form’s OnPaint method to recreate the chart every time a redraw was required.   This approach is absolutely fine  in most situations .  However, in order to add to our range of graphics skills, we are going to tackle the job from another angle, this time using a Bitmap object.  

   If you are completely new to graphics, you may well find some of the topics confusing.  Sometimes there seems to be so many graphics objects, bitmaps, images, drawing surfaces being manipulated one after the other that you completely lose track of what each of them is supposed to be doing.  But with the help of these articles it should all fall into place for you in the end.

   I will explain step by step the graphics procedures which are used to draw the chart.   By breaking much of the code down into quite small steps, I hope to make the various methods easier to follow.   I have also included a fully commented demonstration version in the Visual Studio Solution which is attached to this article.

 

 

 

 

Let's Get Started

A Step at a Time

  So, if we are not going to put the code in the OnPaint event this time, where will it go?   One obvious option would be to place it in the button’s click event.    However, in order to keep the code in easier to follow chunks I have elected to create two separate procedures which are called one after the other in the button’s click event.  The tasks undertaken by the two procedures are:

  • Generate the Data
  • Draw the Chart

Let’s look at each of those procedures in turn now.

Step 1: Generate the Data
Data Content   
    For continuity’s sake we will use a Structure and an ArrayList for the data because this is the approach we used in Part 1. 

  Put these Statements at the very top of the form:

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

   Create a Structure similar to that used in Part 1 (placed in the Form’s code area, but outside any procedures):

    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

 Instantiate an ArrayList to hold the data.  Again, place this in the form’s code area, outside any procedures:

   Dim SalesData As New ArrayList

    The GetData procedure generates some sample data: six countries and six sales figures.   (Looking ahead, in Part 3 we will see how to replace this with code which reads data from file  or takes input from the user.)

   Private Sub GetData()

   SalesData.Clear()
   '  Ensure that only one set of generated data is held
        SalesData.Clear()
  
'  Generate some data and store it in the arraylist
        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


   We now have some data to use to create our chart.

Step 2: Get Control

The chart will be displayed in a PictureBox and the drawing will be fired by the click of a button.  Time then to add these controls to the form. 

   Add a PictureBox control on to the form and name it PBBarChart.  Cover approx 80% of the form with the PictureBox.    Then add a Button control somewhere in the remaining surface of the form and name this btnDraw
Set the backcolor of the Form to a light color of your choice. 

    We are ready to begin creating the chart.

Reusable Values

  As we discovered in Part 1, much of the process of drawing in Windows Forms requires fiddling around with setting the start and end points of lines, shapes, text and so on.   This is obviously going to be the case because we are hand building most of the things that the user sees; we therefore have to code the exact positions and sizes of those entities we create.

   With reusability in mind, I have included some variables in the articles and examples and we will use these to store the above kinds of values.   Hopefully this will have two advantages. 

   First, it will enable you to change the settings quite easily so that the finished product looks just as you want.  Secondly, it should make much of the drawing code easier to follow; if we use understandable names for our variables then it may help make clearer what each part of each code line actually does.

Step 3: Set the Margins, Gaps and Scaling

  The chart will be drawn inside a PictureBox, so we should create a margin between the chart itself and the outer edges of the PictureBox.    We will use four variables to hold these values.  The names are self-explanatory:

 '  # of pixels Y-Axis is inset from PicBox Left
    Dim LeftMargin As Integer = 35
    '  # of Pixels left unused at right side of PicBox
    Dim RightMargin As Integer = 15
    '  # of pixels above base of picturebox the X-Axis is placed
    Dim BaseMargin As Integer = 35
    '  Margin at Top
    Dim TopMargin As Integer = 10

 When we come to draw the bars of the chart, they will look better with a small gap between them.  The next variable contains that setting:

    Dim BarGap As Integer = 12

 

Draw The Chart : Verticals

Step 4a:  Graphics and Bitmap Objects

   We touched on the role of the Graphics object in Part 1 and saw that it can be thought of as a canvas on which we can draw what we want to see on the surface of the form or other control.     We drew directly on to it for our Pie Chart.

   As we are dealing with a PictureBox in this article, the approach will be slightly different.   
This is what we do:

  • Declare a Graphics object
  • Also create a Bitmap object.   This Bitmap will be same size as the PictureBox and will be assigned the PictureBox’s Graphics object.  
  • To give us access to the drawing methods of the Graphics class, we assign the Bitmap to the Graphics object.
  • Once the drawing is complete we take the Bitmap that we have edited with the graphics tools and use that as the PictureBox’s Image.

   Confused?  Don't worry!  It’s tricky stuff when you first meet it,  but after a while it does begin to fall into place.   Essentially, we have created a Graphics object which is the same shape, size and resolution as the PictureBox.   We can draw on this and later we will use our completed drawing as the image for the PictureBox.

   All the drawing code goes into one procedure named DrawChart.  Here is the code for the above actions (This is of course just the first part of the complete procedure):

Private Sub DrawChart()
   Dim g As Graphics
   Dim bmap As Bitmap
   bmap = New Bitmap(PBBarChart.Width,  PBBarChart.Height, _
           PBBarChart.CreateGraphics)

   g = Graphics.FromImage(bmap)

Step 4b: Vertical Axis

  The vertical axis of the chart (technically known as the Y-Axis) will be drawn inside the PictureBox.  We move it in from the boundaries, using the values we have assigned to the TopMargin. LeftMargin and BaseMargin variables.

  This axis will have small tick marks evenly spaced, each space representing 100 sales.   In addition, the value of each tick mark will be written at the side of mark.   The image below shows a sample part of the vertical axis.  You can see the full thing in the picture at the start of this article:

The following code sets the start point and end point of the vertical axis:

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

   The names of the variables StartPoint and EndPoint need no explanation but the concept of a Point might be worth a mention.  We are all familiar with the idea of a point – that is, a specific location which can be identified in some way.  In .Net graphics, a Point is identified by its x position (how far it is from the left hand side) and its y position (how far down it is from the top).  

  In the current project we measure these two values in pixels, because that is the unit used to track positions on the screen.   It is important to understand that the Point isn’t just a position in space, it is in fact an Object, complete with its own properties and methods.

   You see that the StartPoint Point has its x position set in line with the left margin we set earlier;  its y position is calculated by moving up from the bottom of the picture box by the distance equal to the BaseMargin.

   The EndPoint of the axis has an x value the same as the StartPoint, as you would expect (it is a vertical line, after all).  Its y value is set to a point which is shy of the top of the PictureBox by the value of the TopMargin.

   Armed with our start and end points, we take a pen and draw the line: 

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

   If you are entering the code as you read the article, you may be wondering why you haven't see any drawing appear.   The reason is that we have drawn the line on an object that isn’t visible  yet  to the user.   Effectively we are using a technique known as double buffering, where we create a bitmap object the same size as the picturebox and behind the scenes we draw on this bitmap. At this stage though we haven't assigned this bitmap back into the PictureBox as its image.

Step 4c: Tick Marks and Text

   Because we generated the data ourselves at design time, we know that the maximum sales figure is 942.   So we know in advance that it will be safe to draw a vertical axis which stretches from 0 to 1000 and all the sales data will fit inside those two extremes.    In a later article we will  take user data at runtime and calculate dynamically how many tick marks will be needed.   But for now we’ll do it the easy (well, easier) way.

  To decide how to space out the ten Tick marks we first of all need to know the length of the vertical axis:

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

 This will enable us to calculate the correct gap between each of the Ticks.  The layout is from  0 to 1000 in intervals of 100, remember:

   Dim VertGap As Integer = CInt(VertLineLength / 10)
      
    I have arbitrarily chosen to set the (horizontal) length of each Tick mark as 5 pixels.   It follows then that the start point of each Tick must be 5 pixels to the left of the vertical axis.   The end point of each Tick will be on the vertical axis itself.  Those are the x positions needed and are the same for each Tick.

   The y positions (which you will recall are the number of pixels counting down from the top) must change for each Tick mark, because we are drawing ten of them equally spaced all the way up the vertical axis.   The spacing between them therefore must be the value we have just calculated – VertGap.

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

 We have all the information needed to create the Tick marks and the 100, 200, 300, etc text.   Let’s set up a Font for the text:

  Dim ValueFont As New Font("Arial", 8, FontStyle.Regular)
     
 And now we can loop 10 times through a code block which will draw out Tick marks from bottom to top and add the 100s as text at the same time:

  For i As Integer = 1 To 10
   '  Tick mark
   g.DrawLine(New Pen(Color.Black), TickSP, TickEP)
   '  Tick Values as text
   g.DrawString(CStr(i * 100), ValueFont, Brushes.Black, 2, TickSP.Y - 5)
   '  Reset y positions, moving 10% up vertical line
   TickSP.Y -= VertGap
   TickEP.Y -= VertGap
 Next

  The only code which might need additional explanation is the one which uses DrawString to draw the text next to the Tick marks.   You may have noticed that the y-position of the start of the text has been shifted 5 pixels higher than the start of the Tick mark itself (TickSP.Y – 5).   This is simply because the x,y position (the point of origin) of the drawn string is the very top left point of the imaginary rectangle in which the text is drawn.  So, to bring the middle of the text approximately in line with the tick mark, that point of origin has been nudged five pixels further up the page.

         
Step 4d:  Time For A Test

   
    Although we have more drawing code to write before we are done, it might be useful to fast-forward and have a look at the results so far.   To do this, add the following code to the very end of the DrawChart Sub:

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

  I will explain this code later, but for now it will enable you to run the project and see the vertical axis and the Tick marks.  Just be sure to remember that the code we create in the following pages is entered above those three lines, otherwise there will be problems.

   The button is used to fire the drawing code, so the button_click event of btnDraw needs to call the two procedures we have created:

    Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
        '  Generate the Data
        GetData()
        '  Then Draw the Chart
        DrawChart()
   End Sub

  You may be thinking that this isn’t much to show for several pages of explanation.   Bear in mind though that the actual code used is really quite short.   Because this series of articles is aimed squarely at graphics newbies,  I’ve erred on the side of too much explanation, rather than too little.  

 


    

 


 

  

 

 

Draw The Chart: Horizontals

Step 5:  Horizontal Axis and the Bars

5a.  Base Line
  The logic for the Horizontal (X) Axis is similar to that used to draw and measure the Vertical Axis:

  '  First draw the X (horizontal) axis line
  g.DrawLine(LinePen, LeftMargin, PBBarChart.Height - BaseMargin, _
     PBBarChart.Width - RightMargin, PBBarChart.Height - BaseMargin)
  '  Calculate length of baseline drawn by the code above
  Dim BaseLineLength As Integer = _
     PBBarChart.Width - (LeftMargin + RightMargin)

 5b.  The Bars
     Calculating the width of each bar again is simple math: Divide the length of the baseline by the number of bars (which is the same as the number of elements in the SalesData arraylist – one per country).  Subtract the width you decided earlier to insert between bars.  

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

    Each bar is of course rectangular, so we will create a Rectangle object for this purpose.   To locate the  X-position of the first bar we start at the intersection of the x and y axis and then move to the right a distance equal to the width we decided to insert between bars.      The Y-position is set one pixel above the baseline:

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

  Next, create a brush with which to draw and fill the bar rectangles:

   Dim BarBrush As New SolidBrush(Color.BurlyWood)

  So much for the width.  Now we need to consider the heights of the bars.  Obviously the height of each bar will represent the sales figure for a country.   We have already configured the vertical axis so that it is marked out to a maximum of 1000 units of sales values. 

   The vertical axis also has another value – the number of pixels that make up its length.      In order to ensure that each bar’s height is drawn proportionately to the total pixels available we must calculate the scale, i.e. how many Sales are represented by a single pixel up that vertical line.    This measurement will be used to ensure that the bars are drawn proportionately within the overall height available for them.

   This may sound complicated.  It really isn’t and the calculation itself is very simple:

    Dim VertScale As Double
   VertScale = VertLineLength / 1000
  

   By enumerating through the information stored in the arraylist, we can pull out the sales figures one after the other.   We use this figure to calculate the height of the bar .
  
  There are several ways of approaching the task of drawing the bars in the correct positions, some easier than others.    The approach I have taken for this article is as follows.  For each bar:-

  • Calculate the Bar’s height.
  • Create a rectangle to represent the Bar.
  • “Pull” the Y-position upwards until it reaches to place where the top left of the rectangular bar should be.   Why do we have to do this?  Because the rectangle is drawn “outwards and downwards”, not “outwards and upwards”.  If we don’t make this change, the bars will be drawn below the baseline, not above it.
  • Draw and Fill the Rectangle to create the bar
  • Move the X-Position by the calculated amount to the right.

   And here is the code which does exactly that:

  For Each gd As GraphData In SalesData
    '  Calculate Bar Height
    Dim BarHeight As Integer = CInt(gd.Sales * VertScale)
    '  Create a rectangle for the Bar
    BarRect = New Rectangle(BarStartX, BarStartY, CInt(BarWidth), _
          BarHeight)
    '  Pull the Y point upwards so that the bar (rectangle) will
    '  stretch back down to the baseline when drawn

    BarRect.Offset(0, -BarHeight)
    '  Fill the Bar
    g.FillRectangle(BarBrush, BarRect)
    '  Optionally draw a line round the bar
    g.DrawRectangle(LinePen, BarRect)
    '  Increase the X value by bar width plus gap
    '  ready for next bar to be drawn.
    BarStartX += CInt(BarWidth + BarGap)
 Next

5c.   Country Names

  The names of the countries are displayed at the bottom of each bar.   In a later article we will investigate more challenging and pictorial ways of doing this but for now simple text using DrawString will suffice.

  As the process is similar to those we have used already in this project, the commented code will, I hope, make sense to you at this stage:
 
   '  Set the start point of the first string
   Dim TextStartX As Integer = LeftMargin + BarGap + 4

   '  Create a Brush to draw the text
   Dim TextBrsh As Brush = New SolidBrush(Color.Black)
   '  Create a Font object instance for text display
   Dim TextFont As New Font("Arial", 11, FontStyle.Bold)

   For Each gd As GraphData In SalesData
       '  Draw the name of the country
       g.DrawString(gd.Country, TextFont, TextBrsh, TextStartX, _
          CInt(PBBarChart.Height - (BaseMargin - 4)))  
        '  Move start point along to the right
        TextStartX += CInt(BarWidth + BarGap)
   Next
 

 

All Is Revealed!

Step 6:  All is Revealed!

   You may already have inserted this next code line if you elected to “try as you go”  to see your chart building up.   Just in case you didn’t, though,  this is the time and place to do so.

    It doesn’t seem much, but it’s actually a key line in the process.   This takes the Bitmap object on which you have drawn your chart and finally shows it to the user.   It does this  by assigning the Bitmap to the Image property of the PictureBox.

   '  Assign the chart drawing bitmap as the image for the picturebox
  PBBarChart.Image = bmap

  As we learned in Part 1, you should dispose of any disposable objects once they are finished with.    This code deals with that task:

        '  Dispose of drawing objects
        g.Dispose()
        LinePen.Dispose()
        BarBrush.Dispose()
        ValueFont.Dispose()
        TextFont.Dispose()
     End Sub

  Again, if you haven't been testing the code while reading, then you will need to add the code for the button click event:

   Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
        '  Generate the Data
        GetData()
        '  Then Draw the Chart
        DrawChart()
    End Sub

   And there you have your first, basic but working, bar chart.

 

Summary

  This article built on the basic skills and techniques covered in Part 1 and also added some new ones.     We saw that with a very small amount of code a bar chart can be created completely from scratch.  

   By calculating the scaling, we ensured that that the bars accurately represented the sales figures, making use of the full height of the drawing area available.    Using variables to represent values, such as the margins around the chart,  the code is made easier to read and edit.

   Using double buffering, the chart was drawn on a Bitmap object out of sight of the user and then the bitmap was applied to a PictureBox once it was complete.  This technique reduced flicker and also ensured that once chart was displayed, it will persist (that is, it will be redrawn whenever it has been obscured or changed in some way).

      Although the completed code is quite short, several important Graphics Class methods and other drawing skills have been used:-

• Bitmap object
• Brushes
• Create a Graphics object FromImage
• Disposing of Graphics objects
• Drawing on a PictureBox control
• DrawLine
• DrawRectangle
• DrawString
• FillRectangle
• Font object
• Offset method
• Pens
• Points
• Rectangle object
• Scaling

  In Part 3, we will return to the Pie Chart and will take user input at runtime to create a chart based on the user’s data and display decisions.  The solid colors of the pie will also be replaced with HatchStyles selected by the user.