Introduction
This program is more an exploration of the Compact Framework than an actual useful application. My first goal was to have some fun while getting away from the everyday "Hello World" type apps that are ordinarily used to illustrate features and techniques. When I was a teenager, I had a friend who was obsessed with airline flight schedules. He would spend countless hours drawing maps of airline routes by hand. They were not terribly useful (unless you were an airline industry analyst), but they were pretty cool to look at.
I decided to create a PocketPC solution for drawing airline maps. The "Flight Tracker", as I dubbed it, would included the ability to enter and maintain data for individual airports, airlines and flights. I limited my map area to North America, and only entered the data for a subset of the Southwest Airlines flight schedule. A comprehensive database would have taken months for data entry or required the purchase of commercial flight databases. This project would require the use of basic .NET form controls, as well as the Graphics
interface and .NET XML classes for data storage.
Managing Flight Data
The first task was to compile and manage the data needed to draw flight maps. I created a class, FlightData.vb
, to contain all data-related structures, arrays and methods. I prefer to use structures and arrays of structures to hold program data because it makes it so easy to see what you're doing in the code. This arrangement is also very efficient in terms of memory and processor usage. I wanted to store the data in an open, cross-platform format, so I decided to try out the XML class in .NET.
The System.XML.XmlDocument
class makes it easy to load and save the contents of XML files. You simply instantiate the XmlDocument
object and then call its Load or Save method with the desired filename. The data is stored in XmlNode
and XmlAttribute
objects within the XmlDocument
. Each XmlNode
contains a collection of child XmlNodes and a collection of XmlAttributes. I included in each of my structure definitions a method to load a new structure's member values from an XmlNode
object and a method to return a new XmlNode
containing the member values. Here I've included the simplest of my structures to illustrate this configuration:
Public Structure AirlineStruct
Dim Airline As String
Public Sub New(ByRef XmlNode As Xml.XmlNode)
Dim xAttribute As Xml.XmlAttribute
For Each xAttribute In XmlNode.Attributes
Select Case xAttribute.Name
Case "Airline"
Airline = xAttribute.Value
End Select
Next
End Sub
Public Function GetXmlNode(ByRef Doc As Xml.XmlDocument) _
As Xml.XmlNode
Dim Node As Xml.XmlNode = Doc.CreateNode( _
Xml.XmlNodeType.Element, "AIRLINE", "")
Node.Attributes.Append(GetXmlAttribute(Doc, "Airline", Airline))
Return Node
End Function
End Structure
One interesting limitation to the XmlNode
and XmlAttribute
classes is that they can only be instantiated by an XmlDocument
object. This was a little annoying in my case since I had to pass a pointer to the XmlDocument
to my structure's GetXmlNode
method just for this purpose.
With these methods in place, saving the class's data consists of creating an XmlDocument
and adding an XmlNode
for each structure stored in the class's arrays. When XmlNodes have all been added, simply call the XmlDocument
's Save
method.
Public Sub Save()
Dim xDoc As New Xml.XmlDocument
Try
xDoc.AppendChild(xDoc.CreateElement("FlightData"))
Dim Flight As FlightStruct
For Each Flight In Flights
xDoc.FirstChild.AppendChild(Flight.GetXmlNode(xDoc))
Next
Dim Airline As AirlineStruct
For Each Airline In Airlines
xDoc.FirstChild.AppendChild(Airline.GetXmlNode(xDoc))
Next
Dim Airport As AirportStruct
For Each Airport In Airports
xDoc.FirstChild.AppendChild(Airport.GetXmlNode(xDoc))
Next
xDoc.Save("\Program Files\Flight Tracker\FlightData.xml")
HasUnsavedChanges = False
Catch ex As Exception
MsgBox("Error saving XML data file.", _
MsgBoxStyle.Exclamation, "File Error")
End Try
End Sub
Note that the all the XmlDocument
's other nodes are typically located within the document's first child node, which you must create and add yourself if when assembling the new file's contents.
Loading is easily accomplished by loading the XmlDocument
from file and then sequencing through the collection of child nodes, loading each one into the appropriate structure type and adding the structure its corresponding array.
Public Sub Load()
Dim xDoc As New Xml.XmlDocument
Try
Dim Flight As FlightStruct
Dim Airline As AirlineStruct
Dim Airport As AirportStruct
ReDim Flights(-1)
ReDim Airlines(-1)
ReDim Airports(-1)
xDoc.Load("\Program Files\Flight Tracker\FlightData.xml")
Dim Root As Xml.XmlNode = xDoc.FirstChild
Dim Child As Xml.XmlNode
For Each Child In Root.ChildNodes
Select Case Child.Name
Case "FLIGHT"
Flight = New FlightStruct(Child)
ReDim Preserve Flights(UBound(Flights) + 1)
Flights(UBound(Flights)) = Flight
Case "AIRLINE"
Airline = New AirlineStruct(Child)
ReDim Preserve Airlines(UBound(Airlines) + 1)
Airlines(UBound(Airlines)) = Airline
Case "AIRPORT"
Airport = New AirportStruct(Child)
ReDim Preserve Airports(UBound(Airports) + 1)
Airports(UBound(Airports)) = Airport
End Select
Next
HasUnsavedChanges = False
Catch ex As Exception
MsgBox("Error loading XML data file.", _
MsgBoxStyle.Exclamation, "File Error")
End Try
End Sub
The rest of the data management portion of the program consisted of a tabbed dialog with ListView
controls for Flights, Airlines and Airports, as well as buttons and menus for creating, editing, deleting and sorting the list elements.
Drawing The Flight Map
The first thing I needed was a background image for my map. I found a simple outline of the North American continent on a CorelDraw 8 clip art CD (I assume that it is royalty-free, but I couldn't find any documentation to verify this). I changed the color scheme of the map and converted it to a 240 x 274 JPG. Everything was going fine until I tried to run the program on my iPAQ 3765 (what I use for development purposes at work). The program started to load...and then exited with an ArgumentException
! I tried to load the image dynamically at runtime but still ended up with the same error message. After a couple of hours scouring the internet for an answer, I finally found the following post on the OpenNetCF.org forums: 16bpp Bitmap allways an error - Terry Mohre. The posting stated that, "16bpp ALWAYS raises an 'ArgumentException' and fails to instantiate the Bitmap class". I checked my JPG and ,sure enough, it was in 16 bits. I converted it to 24 bit and it loaded without a hitch.
Next, as I began to fill in the Paint event handler for my map, I realized that all the familiar .NET desktop Graphics
features had been pared down to a tiny subset in the Compact Framework. I set out in my web browser once again to see what solutions were available in place of the GDI+ double-buffering and anti-aliasing features. I quickly found a solid example for a double-buffering technique on MSDN: How to Create a Microsoft .NET Compact Framework-based Image Button - Alex Yakhnin. The first part of this technique was to override the OnPaintBackground
event handler in the .NET Control
class. Because this event handler is a protected class member, you must create a derived class to perform the override. This new class is used wherever a double buffered drawing surface is needed throughout the program.
Public Class MapCanvas
Inherits System.Windows.Forms.Control
Protected Overrides Sub OnPaintBackground(ByVal e As _
System.Windows.Forms.PaintEventArgs)
End Sub
End Class
Although this class worked fine in the compiled program, I must say that it wreaked havoc in the development environment. Maybe this is somehow related to the fact that the Compact Framework does not support the UserControl
class that is normally used for this sort of thing? As a result, I was constantly having to delete and re-create the canvas objects in my forms. Sometimes I had to restart Visual Studio to get it to recognize that the MapCanvases were valid objects.
The second part of the double-buffering technique was to draw everything in an off-screen bitmap first, and then copy the bitmap onto the MapCanvas
object. This is illustrated by the Paint handler from the airport location picking dialog:
Private Sub Canvas_Paint(ByVal sender As Object, ByVal e _
As System.Windows.Forms.PaintEventArgs) Handles Canvas.Paint
Dim bg As Graphics = Graphics.FromImage(bmBuffer)
bg.DrawImage(pbMap.Image, 0, 0)
bg.DrawLine(Pen, 0, YPos, 240, YPos)
bg.DrawLine(Pen, XPos, 0, XPos, 272)
bg.Dispose()
e.Graphics.DrawImage(bmBuffer, 0, 0)
End Sub
You can make the Paint handler run faster by maintaining graphics objects at the form-level...
Private Brush As New SolidBrush(Color.Blue)
Private Pen As New Pen(Color.Blue)
Private bmBuffer As Bitmap
As for anti-aliasing, there doesn't appear to be any support for it in the PocketPC's GDI. I'm sure there are probably third-party libraries that could provided features equivalent to the desktop's GDI+, but I didn't spend any time looking for them.
Once my double-buffering code was in place, I processed the contents of the flight data class to fill an array of RectangleF
objects, each of which represented a single flight path. A corresponding array of integers was used to store heading information which determined which corners of the rectangle were the origination point and destination point. In order to animate the flight paths on the map, a Timer
was used to increment the rectangle sizes from 0 to 100% in a succession of screen refreshes. The timer and rectangles were a quick and dirty approach, but they worked rather well.
Using the Program
The program is easy to use. Just copy the .NET CF executable to your PocketPC (You must put it in "\Program Files\Flight Tracker". Create if necessary), along with the FlightData.xml file. The program will initially show a map of airport locations. Click Show/Flights to see the animated map of airline routes. Clicking Tools/Edit Flight Data will open the tabbed dialog where you can add the flight info for your favorite airline!