Article source code: chordfinder.zip
The .NET Framework includes a much improved library of graphic functions
developers can utilise to display dynamic images in their applications. This
article looks at how you can use an XML file to produce an image that you can
change at runtime based on a selection from a Listbox control. All the code
is in VB.net but is easily transposed into any .NET language. To explain the
workings of XML would take a large book so this article will presume basic knowledge
of XML, XPath and the .NET implimentation.
The example application is a chord book for budding guitar players. The interface
consists of an image of the guitar fingerboard generated at runtime using the
GDI+ classes and a ListBox Control containing chord names for the user to select
the chord they wish to view. On selecting a chord the finger positions are displayed
on the fingerboard image. The chord information is stored in an XML file called
Chords.xml. In the example of this file below you can see that each chord contains
a collection of notes. Each note has two attributes which contain the x and
y positions on screen.
<?xml version="1.0" encoding="utf-8" ?>
<GDIChords>
<GDIHeader>
<DateLastModified>30-10-2002</DateLastModified>
</GDIHeader>
<Chord name='A Minor'>
<Note PosX='182' posY='35'/>
<Note PosX='140' posY='85'/>
<Note PosX='96' posY='85'/>
</Chord>
<Chord name='G Major'>
<Note PosX='224' posY='138'/>
<Note PosX='56' posY='85'/>
<Note PosX='15' posY='138'/>
</Chord>
</GDIChords>
The following image shows the first chord in the file (A Minor) displayed in
the application. No picture resources are used to create this image, it is completely
generated with code. Next I will explain how I used the GDI+ classes to produce
it.
The main point to remember when using the GDI+ library is that
it is stateless. If your form is minimalized or covered by another application,
all your hard work will disappear. As the programmer you must take responsibility
for refreshing the image if these re-paint events occur. The fact that the GDI+
is stateless can be viewed as an advantage. For instance if you want to clear
your canvas all you need to do is refresh the form or control you are drawing
on. Create a new Windows Form project in VS.net and delete the default form
it creates for you. Add a new windows form and name it frmMain.vb with a FixedBorderStyle
of Fixed3D and place a ListBox control at the bottom. We will be
using the form surface itself as a canvas but you could use a Panel or any other
control which as a re-paint event. Change the form BackColor to LemonChiffon
or whatever you like but please no bright pink ones!
Before we can create the image we need to program the class which
will do all the drawing for us so add a new Class file to the project and call
it GDIDraw.vb. Place the following code inside this file.
Imports System.Drawing
Imports System.Drawing.Drawing2D
Public Class GDIDraw
Private mobGraphics As Graphics
Private mobPen As Pen
Public Sub New(ByVal obGraphics As Graphics)
MyBase.new()
Try
mobGraphics = obGraphics
mobPen = New Pen(Color.Transparent)
Catch obEx As Exception
Throw obEx
End Try
End Sub
Public Sub DrawSphere(ByVal vnX As Integer, _
ByVal vnY As Integer, _
ByVal vnX2 As Integer, _
ByVal vnY2 As Integer, _
ByVal vColOutline As Color, _
ByVal vColorA As Color, _
ByVal vColorB As Color)
Dim obBrush As SolidBrush
Dim obLBrush As LinearGradientBrush
Dim obRect As Rectangle
Try
'//DRAW SPHERE.
mobPen.Color = Color.Black
obBrush = New SolidBrush(vColOutline)
obRect = New Rectangle()
With obRect
.X = vnX
.Y = vnY
.Width = vnX2
.Height = vnY2
End With
obLBrush = New LinearGradientBrush(obRect, vColorA, vColorB, _
LinearGradientMode.ForwardDiagonal)
mobGraphics.FillEllipse(obLBrush, obRect)
'//DRAW HIGHLIGHT.
With obRect
.X = vnX + 4
.Y = vnY + 4
.Width = vnX2 \ 6
.Height = vnY2 \ 6
End With
mobPen.Color = Color.White
obBrush.Color = Color.White
mobGraphics.DrawEllipse(mobPen, obRect)
mobGraphics.FillEllipse(obBrush, obRect)
Catch obEx As Exception
Throw obEx
End Try
End Sub
Public Sub DrawGradientRectangle(ByVal vsngX As Single, _
ByVal vsngY As Single, _
ByVal vsngX2 As Single, _
ByVal vsngY2 As Single, _
ByVal vColorA As Color, _
ByVal vColorB As Color, _
ByVal vMode As LinearGradientMode)
Dim obBrush As LinearGradientBrush
Dim obRect As RectangleF
Try
obRect = New RectangleF()
With obRect
.X = vsngX
.Y = vsngY
.Width = vsngX2
.Height = vsngY2
End With
obBrush = New LinearGradientBrush(obRect, vColorA, vColorB, _
vMode)
mobGraphics.FillRectangle(obBrush, obRect)
obBrush.Dispose()
obBrush = Nothing
Catch obEx As Exception
Throw obEx
End Try
End Sub
End Class
There are only two subs within this class plus the constructor which takes
as a parameter a graphics object which represents the drawing surface. The DrawSphere
sub is responsible for drawing gradient circles which also draws a white highlight
to add to the 3D effect. This works by creating a rectangle to act as a holding
box for an ellipse which is filled using a brush object. A smaller ellipse is
placed over this to create the highlight. The DrawGradientRectangle sub is used
to draw everything else including the fingerboard, frets and strings. This works
by simply creating a rectangle and filling with a gradient brush. You can see
how much easier this is than the old API calls in VB6. Don`t be afraid to experiment
using different colors and GradientModes etc. This is all part of the
fun of using the GDI+.
Next add a module file to the project, name it modMain.vb an place the following
code inside.
Imports System.Drawing
Imports System.Xml
Module Main
Public gfrmMain As frmMain
Private mobGDIDraw As GDIDraw
Private mobXmlDocument As XmlDocument
'//APPLICATION STARTS HERE.
Public Sub Main()
Try
gfrmMain = New frmMain()
gfrmMain.ShowDialog()
Catch obEx As Exception
MsgBox(obEx.ToString, MsgBoxStyle.Critical)
End
End Try
End Sub
Public Sub DrawGuitarFingerBoard(ByRef obCanvas As Form)
Try
Dim obGraphics As Graphics = gfrmMain.CreateGraphics
mobGDIDraw = New GDIDraw(obGraphics)
With mobGDIDraw
'//DRAW GUITAR FINGERBOARD.
.DrawGradientRectangle(20, 20, 220, 240, _
Color.Cornsilk, _
Color.BurlyWood, _
Drawing.Drawing2D.LinearGradientMode.Horizontal)
'//DRAW GUITAR FRETS.
.DrawGradientRectangle(20, 20, 220, 8, _
Color.White, _
Color.DimGray, _
Drawing.Drawing2D.LinearGradientMode.Vertical)
.DrawGradientRectangle(20, 70, 220, 8, _
Color.White, _
Color.DimGray, _
Drawing.Drawing2D.LinearGradientMode.Vertical)
.DrawGradientRectangle(20, 120, 220, 8, _
Color.White, _
Color.DimGray, _
Drawing.Drawing2D.LinearGradientMode.Vertical)
.DrawGradientRectangle(20, 170, 220, 8, _
Color.White, _
Color.DimGray, _
Drawing.Drawing2D.LinearGradientMode.Vertical)
.DrawGradientRectangle(20, 220, 220, 8, _
Color.White, _
Color.DimGray, _
Drawing.Drawing2D.LinearGradientMode.Vertical)
'//DRAW STRINGS.
.DrawGradientRectangle(23, 20, 8, 240, _
Color.Yellow, _
Color.DimGray, _
Drawing.Drawing2D.LinearGradientMode.Horizontal)
.DrawGradientRectangle(235, 20, 3, 240, _
Color.Yellow, _
Color.DimGray, _
Drawing.Drawing2D.LinearGradientMode.Horizontal)
.DrawGradientRectangle(64, 20, 7, 240, _
Color.Yellow, _
Color.DimGray, _
Drawing.Drawing2D.LinearGradientMode.Horizontal)
.DrawGradientRectangle(106, 20, 6, 240, _
Color.Yellow, _
Color.DimGray, _
Drawing.Drawing2D.LinearGradientMode.Horizontal)
.DrawGradientRectangle(150, 20, 6, 240, _
Color.Yellow, _
Color.DimGray, _
Drawing.Drawing2D.LinearGradientMode.Horizontal)
.DrawGradientRectangle(193, 20, 4, 240, _
Color.Yellow, _
Color.DimGray, _
Drawing.Drawing2D.LinearGradientMode.Horizontal)
End With
Catch obEx As Exception
Throw obEx
End Try
End Sub
Public Function GetChord(ByVal vsChordName As String) As XmlNodeList
Dim nCount As Integer
Dim nNodeCount As Integer
Dim sNodeXPath As String
Dim nYPosition As Integer
Dim nXPosition As Integer
Dim obNode As XmlNode
Dim obNodeList As XmlNodeList
Try
sNodeXPath = "GDIChords/Chord[@name='" & vsChordName & "']/Note"
obNodeList = mobXmlDocument.SelectNodes(sNodeXPath)
If Not obNodeList Is Nothing Then
For nCount = 0 To obNodeList.Count - 1
nXPosition = CType(obNodeList.Item(nCount).Attributes( _
0).InnerText, Integer)
nYPosition = CType(obNodeList.Item(nCount).Attributes( _
1).InnerText, Integer)
mobGDIDraw.DrawSphere(nXPosition, nYPosition, 25, 25, _
Color.Black, Color.White, Color.Black)
Next
End If
Catch obEx As Exception
Throw obEx
End Try
End Function
Public Sub LoadChordXML()
Try
Dim sFilename As String = Application.StartupPath & "\Chords.xml"
mobXmlDocument = New XmlDocument()
mobXmlDocument.Load(sFilename)
Catch obEx As Exception
Throw obEx
End Try
End Sub
Public Sub GetChordList(ByRef rlstBox As ListBox)
Dim nCount As Integer
Try
Dim obNodeList As XmlNodeList = mobXmlDocument.SelectNodes( _
"GDIChords/Chord")
If Not obNodeList Is Nothing Then
For nCount = 0 To obNodeList.Count - 1
rlstBox.Items.Add(CType(obNodeList.Item( _
nCount).Attributes(0).InnerText, String))
Next
End If
Catch obEX As Exception
Throw obEX
End Try
End Sub
End Module
You will notice in this module that we are controlling the execution of this
application from a sub main procedure so right-click on your project, select
properties and choose Sub Main as your Start Up Object. This module
also contains procedures for reading in the Chord.xml file so you should create
the XML file at the top of this article now using NotePad or your favorite Text
or XML Editor. Save this XML file in the output directory (usually 'bin\Debug\'
or 'bin\Release\' or just 'bin') of your project.
Next we need to add code to our frmMain to make things happen! It is not absolutely
neccesary to add the Exit and About Menus so I will leave this for you to decide.
Rename your ListBox control 'lstChords' and add the following code to
your form.
Imports System.Environment
Imports System.Text
Public Class frmMain
Inherits System.Windows.Forms.Form
Private mobChordName As String
Protected Overrides Sub OnPaint( _
ByVal e As System.Windows.Forms.PaintEventArgs)
Try
DrawGuitarFingerBoard(Me)
GetChord(mobChordName)
Catch obEx As Exception
Throw obEx
End Try
End Sub
Private Sub mnuAbout_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles mnuAbout.Click
Try
Dim obStringBuilder As New StringBuilder()
With obStringBuilder
.Append(Chr(32), 4)
.Append(Me.Text)
.Append(vbCrLf)
.Append("CLR Version: ")
.Append(Environment.Version.ToString)
MsgBox(.ToString, MsgBoxStyle.Information, Me.Text)
End With
obStringBuilder = Nothing
Catch obEx As Exception
MsgBox(obEx.Message.ToString, MsgBoxStyle.Critical, Me.Text)
End Try
End Sub
Private Sub lstChords_SelectedIndexChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles lstChords.SelectedIndexChanged
Try
mobChordName = CType(Me.lstChords.SelectedItem, String)
Me.Refresh()
DrawGuitarFingerBoard(Me)
GetChord(mobChordName)
Catch obEx As Exception
MsgBox(obEx.Message.ToString, MsgBoxStyle.Critical, Me.Text)
End Try
End Sub
Private Sub mnuExit_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles mnuExit.Click
Try
Me.Close()
Application.Exit()
Catch obEx As Exception
MsgBox(obEx.Message.ToString, MsgBoxStyle.Critical, Me.Text)
End Try
End Sub
Private Sub frmMain_Closed(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles MyBase.Closed
Try
Application.Exit()
Catch obEx As Exception
MsgBox(obEx.Message.ToString, MsgBoxStyle.Critical, Me.Text)
End
End Try
End Sub
End Class
Open up the 'Windows Form Designer generated code' and modify the Sub New as below:
Public Sub New()
MyBase.New()
'This call is required by the Windows Form Designer.
InitializeComponent()
'Add any initialization after the InitializeComponent() call
Try
Me.Text = "GDI+ Chord Finder " & _
Application.ProductVersion.Substring(0, 3)
LoadChordXML()
GetChordList(Me.lstChords)
Catch obEx As Exception
MsgBox(obEx.Message.ToString, MsgBoxStyle.Critical, Me.Text)
End Try
End Sub
You should add an AssemblyInfo file and set the Version Attribute if you wish
to display this correctly. Feel free to modify/experiment with the procedures
in this example. I hope it will inspire you to use the GDI+ in your applications.