Programming MapPoint via .NET
This article explains how to use VB.NET to create an improved location sensor for MapPoint. DnClipPos can copy the current lat/lon to the clipboard, and supports configurable display formats.
Introduction
Microsoft's .NET initiative has gotten a lot of press lately, and some may have
been led to think that it is all only about the Internet and this new
thing called web services exclusively, and hence not of interest for developers
of desktop apps - e.g. those using MapPoint 2002 in their applications.
Not so. While the .NET vision is indeed about revolutionizing the Internet with
web services, and the implementation Microsoft delivered has great tools to
help doing so, these tools can also be used to make the development of desktop
applications simpler, faster, more elegant and more fun. If some of you have
heard the words "Internet" and "web service" one time too often, I hereby
promise not to mention them for the rest of this article, OK ?
Another common misconception is that Visual Studio.NET is required to create
and compile Visual Basic.NET programs. Again - not so. Everything needed to
compile and run VB.NET applications can be downloaded from Microsoft for free
in an admittedly quite large download. (See "Prerequisites" below)
In the following article, I'd like to illustrate how MapPoint can be programmed
using the free commandline version of VB.NET. The small sample program we'll
write is called DnClipPos (Dn standing for DotNet) - an improved location
sensor with configurable coordinate display format and the ability to copy the
current position to the clipboard when the middle mouse button is pressed.
Prerequisites
Before we can start implementing the new location sensor with VB.NET, we need
to make sure all the prerequisites are met.
You'll need:
-
a copy of MapPoint 2002, obviously
-
if you just want to run the compiled program: the .NET
framework redistributable (20MB download, to be found
here )
(you will not need to download and install this again if you already did so for
another .NET application, or have Visual Studio .NET)
-
if you want to be able to compile the source code and write VB.NET programs
yourself:
-
the .NET
platform SDK (130MB download - this includes the
framework redistributable
, so you do not need that download. If you have Visual Studio .NET,
you do not need to download the platform SDK either)
-
a text editor, like Notepad or
UltraEdit
Downloadables
Here's a summary of the downloadable material mentioned in this article:
-
DnClipPos
archive: contains the compiled executable as well as the full source code.
-
the
.NET framework redistributable
- required if you just want to run DnClipPos.exe and this is the first .NET
program you use.
-
the
.NET platform SDK - required if you want to compile the source code and
do not have Visual Studio.NET.
Installation
Well, the good news is that this program does not require any installation
beyond the
framework redistributable, and that is true for most .NET programs that
you will write. Just download the
archive that contains both the sourcecode as well as the executable,
extract to a local directory on your machine, and run DnClipPos.exe to give it
a try. If MapPoint is already running, the program will attach to it. If it
isn't, it will launch a new instance.
Using DnClipPos
If all goes well, DnClipPos displays a small window that stays on top, and will
show coordinates in its default format (decimal degrees with a single-letter
compass point).
To copy the current position to the clipboard, press the middle mouse button or
the wheel on your mouse while over the map. (For best precision, zoom in).
If you don't like the coordinate format DnClipPos uses, you can easily change
it - either by changing the default format in the source code, or using the
following instructions:
-
right-click on DnClipPos.exe and select "create shortcut".
-
select the shortcut and open its properties dialog
-
in the target field, after the name of the EXE, add the pattern string to use
for formatting.
Pattern strings are explained in detail in my
DmsFormat article (which DnClipPos uses internally), but here's a table
that lists the placeholders it understands:
Place- holder |
Description |
Result for latitude 1.23456 south |
Default format |
<Di> |
Integer (whole) degrees, unsigned
|
1 |
"##0" |
<Mi> |
Integer (whole) degrees, unsigned
|
14 |
"#0" |
<Si> |
Integer (whole) seconds, unsigned
|
4 |
"#0" |
<Dd> |
Decimal degrees, unsigned
|
1.23456 |
"##0.0####" |
<Md> |
Decimal minutes, unsigned
|
14.074 |
"#0.0##" |
<Sd> |
Decimal seconds, unsigned
|
4.4 |
"#0.0" |
<Cp> |
Compass point (W, E, N, or S)
|
S |
"W|E|S|N" |
For example, to get the classic "degrees, minutes, decimal seconds, compass
point" format, you'd use <Di> <Mi> <Sd> <Cp>
as the format string.
Creating the DnClipPos binaries yourself
Before I explain how DnClipPos works, here's how to create your own version
using the commandline Visual Basic compiler included in the platform SDK. All
you'll need is the two source files: DnClipPos.vb and MpToolbox.vb, and the
"response file" DnClipPos.rsp.
So copy them to a new directory, and open a command prompt window. (If you have
Visual Studio.NET installed, select "Start / Programs / Microsoft Visual Studio
.NET / Visual Studio .NET Command Prompt" to open one that has the correct
environment variables set. If you do not have Visual Studio .NET, download and
install the
.NET Platform SDK as described under "Prerequisites" above.
To check if the prerequisites are met, enter "vbc" (without the quotes) at the
command prompt, and press RETURN. The Visual Basic compiler should be launched,
and output a few screenfuls of commandline options.
Since programs talking to MapPoint use COM to do so, and .NET does not use
components entered in the registry directly, we need a way to inform the VB.NET
compiler about the MapPoint objects and their methods, properties and events,
.NET style. Fortunately, this is simple since the platform SDK includes a tool
to do so. (Visual Studio.NET does this automatically simply set a reference to
MapPoint using the COM tab).
The tool in question is called TLBIMP (type library import), and all it needs
is the COM type library that contains the above information. For MapPoint 2002
North America, this file is called MPNA81.TLB and is located in your MapPoint
installation directory. For MapPoint 2002 Europe, its name is MPEU81.TLB.
Assuming MapPoint is installed in e.g. "C:\Program Files\MapPoint 2002 North
America", enter the following at the commandline:
TLBIMP "C:\Program Files\MapPoint 2002 North America\MPNA81.TLB"
(The quotes are important this time, especially if the path includes spaces)
The result should be the creation of MapPoint.dll in the current directory
(where you also copied the *.vb and the DnClipPos.rsp response file.
To compile the *.vb source file into DnClipPos.exe, use the following command:
VBC @DnClipPos.rsp
If all goes well, DnClipPos.exe should be created in the current directory.
There's nothing special about the response file, it is a simple ASCII text file
containing all the VBC commandline parameters that you would've had to enter by
hand otherwise.
Porting existing VB code to VB.NET
DnClipPos needs to be able to retrieve the latitude and longitude of
the mouse cursor, and to display it in a format specified by the
user. Fortunately, I already had existing code available that came in
handy here: the CalcPos
and DmsFormat routines.
This was VB6 code though, so I was a bit worried if I would be able to use it
under VB.NET. The good news is that DmsFormat worked without any change, and
the changes required to CalcPos were minimal - the mathematical functions Cos
and Sin now reside in the System.Math namespace, hence adding an "Imports
System.Math" directive at the top was all that was required.
Pleasant surprises ...
The VB6 version of CalcPos required me to write a helper function to compute
the arccosine - the Math namespace of the .NET library includes a
native Acos function, so I could get rid of the helper function and use
the built-in one instead. I could also remove my own constant definition for PI
and replace it with the one now built into Math.PI.
... and a gotcha
There's only one occasion where I was bitten by VB.NET's new syntax. The
default parameter passing mechanism in VB6 has been "by reference" - which
means that by default, any subroutine can inadvertantly change the variables of
its caller. This default was changed to "by value" in VB.NET, and it is a
welcome change since it usually helps to prevent havoc. In my case, though, it
helped create it - in my original code I had not specified the "ByRef", but
relied on it to be the default. Moreover, I did not use the conversion wizard
to port the code, but simply pasted it into VS.NET via the clipboard. This
caused VS.NET to silently add "ByVal" to the parameters not specified with
neither "ByVal" nor "ByRef". Since CalcPos returns the computed position by
changing two doubles passed by the caller, it now stopped doing so, and
appeared broken. Took me some time before I noticed - but you now have
been warned.
Dissecting DnClipPos
DnClipPos will be explained "by dissection" in the following paragraphs -
explaining its workings a few lines at a time.
Let's have a look at the main source code module, DnClipPos.vb now:
These two statements help save typing - VB.NET functionality is available
through classes organized in namespaces - to avoid having to write the entire
hierarchy each time you want access to a given class or method, use "Imports".
It can be roughly compared to the VB6 "With" construct. As soon as you have
e.g. an "Imports System.Windows.Forms", instead of having to type
"System.Windows.Forms.Label", you can simply use "Label".
It is important to realize that "Imports" statements will not make additional
objects (classes) available to your program. This is done by
setting references - quite similar to the way they are used in VB6. So to be
able to access objects of a given namespace, find out which assemblies they are
part of (e.g. System.Drawing.dll, System.Xml.dll etc.), and add a reference to
that assembly - either interactively in VS.NET, or by using the /reference:
commandline instruction. Once you have the reference working, you can save
typing by using "Imports" statements. To find out which DLL is required for a
given type, check out the documentation overview of the type - the name of
its assembly (aka DLL, in .NET speak) is usually listed at the very bottom
of the page.
You can see a list of the references used by DnClipPos by inspecting the
above-mentioned response file, DnClipPos.rsp - each one of the lines starting
with "/r:" introduces a reference. But let's continue examining the
DnClipPos.vb source code.
A new class "CoordDisplay" (obviously meant to display coordinates) is
introduced here, and inherits the methods and properties of methods of the
regular label control. We have just created a new type of control, derived from
the label control. This is powerful stuff - while you could create custom
controls in VB6, it involved some "behind the scenes" magic, and there was no
way to inherit from an existing control just like that.
This is where the new "label control that can display coordinates" keeps its
internal information - two doubles to represent the coordinates (in MapPoint's
internal format), and a DmsFormat compatible format string that represents the
way the user wants to see them. Note that the private variables are not only
being defined, but initialized at the same time - something not possible in VB
until now.
The "New" subroutine (aka 'constructor') is called when an instance of a class
is created in VB. Here, we set "our" (the label from which we inherit, in fact)
"AutoSize" property to True, which will cause the label to automatically
increase its size to fit the text it contains. The initial text the label
should display is taken from an argument passed to the constructor - not
previously possible in VB either.
UpdateDisplay() is a working horse helper function of the CoordDisplay control
- it sets the text property of the label control it is derived from, according
to the private latitude and longitude values formatted with the help of
DmsFormat, according to the private format string.
This subroutine is called by users of our coordinate control to have it copy
the current coordinates, according to the currently set format, to the
clipboard.
That's all as far as the functionality of CoordDisplay is concerned - the
remaining code for that class are properties that can be read and set from
outside.
A user of our CoordDisplay object can control how it shows the latitude and
longitude values by setting the "Format" property. The "Get" part simply
returns the private variable m_strFmt. (The "Return" statement is new in VB.NET
too, btw - an elegant way to return a value from e.g. a function, without
requiring the assignment to the name of the function VB6 needs. Nice if you
decide on another name for the function later, for example).
The "Set" part is almost as straightforward - the only additional statement is
to call "UpdateDisplay" so that the change to the coordinate display format is
effective immediately. A slight change to the former property syntax here is
that the Get/Set parts are grouped inside one construct - easier to keep track
of.
These two properties are - you guessed it the "accessors" for latitude and
longitude. Note the call to "UpdateDisplay" when setting the Longitude from
outside - it automatically causes the new coordinates to display. Why is this
call missing in the corresponding Latitude property ? The answer is: for
performance reasons - by convention, Latitude should be set first, then
Longitude. This way, we can save one superfluous update.
With the
End Class
we have finally reached the end of the CoordDisplay class - our own, lat/lon
aware label control !
LocationSensor is the tiny window that will be used to display the coordinates.
We make this class behave like a VB.NET Form and have all its functionality
simply by stating "Inherits Form". This form should contain the coordinate
display label control detailed above - this is done here by adding a public
member variable of this type to the LocationSensor class. Since its constructor
wants an initial text to display, we make it display a welcome message.
The constructor of the LocationSensor class accepts a format string, which, if
not empty, will be used to set the format of the coordinate label control. It
then adds said control to the forms "Controls" collection, so that it gets
properly shown and managed. The "TopMost" attribute of the form is set to true,
which will make the window stay on top of MapPoint (and indeed of any other
window). Finally, the window title is set to "Location Sensor", and it is made
just large enough to contain the coordinate control. And that's all of the code
for the LocationSensor class.
The DnClipPos module contains the rest of the code we will examine. It holds
member variables for an instance of the MapPoint application, the MapPoint map,
and our LocationSensor form. The LocationSensor gets initialized with whatever
got passed as commandline arguments - this is the way the user can control the
format used to display coordinates.
Sub Main is the entry point of the program. An attempt is made to find a
running instance of MapPoint, and if that fails, to create a new instance. Note
that VB.NET's new structured error handling (the Try/Catch/End Try construct)
is used here instead of the dreaded "On Error Goto" device.
After creating an instance of MapPoint or attaching to one, it is made sure it
is visible, and won't disappear when the LocationSensor is closed. After
retrieving the active map, control is passed to the Run method of the
Application class, passing it our frmSensor form. This method will take care of
keeping the form running, and will only return when it is closed.
This is the event handler for the maps click event. It only reacts to the
middle mouse button and therefore exits if any other button was pressed. If it
was indeed the middle mouse button (or the wheel button), the coordinates
control is asked to copy its text to the clipboard.
The mouse move events are handled here. oMap.XYToLocation converts screen
coordinates (X and Y, the position of the mouse) into a location object -
exactly what the CalcPos routine needs to set the values of two doubles passed
by reference to the computed latitude and longitude of the spot. Note that the
magic that calls the "Set" methods of the lat/lon properties of the coordinate
control also works here without the obvious presence of an assignment
statement.
In this last event handler, we react to the closing of the application object,
release our references to MapPoint objects, and close the form window -
wouldn't make much sense to have the location sensor survive the closing of
MapPoint.
You may have noticed the difference in names between this last event handler
and the two previous ones - the last one conforms to the classic naming
convention "<VariableName>_<EventName>" while the previous ones
don't - why do they work nevertheless ? The answer is that the name is no
longer important to VB.NET - what matters is the additional "Handles
<VariableName>.<EventName>". Several handlers can handle events of
a single object this way, or a single handler can handle events from multiple
sources (by listing them after the "Handles" keyword).
The "End Module" is the last line of the program.
Conclusion
I hope I was able to show that you can indeed use MapPoint via COM
interop using the free, commandline based .NET platform SDK tools.
DnClipPos was one of the first programs I wrote using VB.NET, and I liked what
I discovered. Not only did I find a few pleasant surprises when porting
CalcPos, while writing the rest of the code I found major improvements and
changes for the better all over the place: inheritance, flexible event
handlers, modern error handling, an amazingly vast and well thought-out class
library, variable initialization, New() with parameters ... and so on. I did
not plan on writing a ".NET feature demo" - it just happened that I was able to
make use of the new features without intending to do so in advance. If you
were'nt sure .NET is for you as a VB desktop developer, I'd urge you to give it
a try - using the freely available tools if necessary. A similar quantum leap
has happened in the IDE, but that's another story. Oh, and did I mention
that it is a great tool to develop web services that can be accessed over
the Internet ? (oops ...)
Mapping and especially GPS-related topics are a hobby - Gilles enjoys developing solutions for Microsoft MapPoint and his favorite outdoor occupation is confluence hunting.