Calling Web Services from Visual Basic 6, the Easy Way
Swigart Consulting LLC
Visual Basic 6
Visual Basic .NET
Visual Basic 2005
Visual Studio 2005
Summary: This article shows you how to build an application that downloads satellite photos of a given street address from Visual Basic 6. (12 printed pages)
Where Art Thou?
A View from Beyond
Click here to download the code sample for this article.
Download the sample
Websites expose all sorts of useful information. Through Amazon, you can look up book ranks and make purchases; through Google, you can search the World Wide Web; with MapPoint, you can look up addresses and get directions; and through EBay, you can buy or sell, well, just about anything.
The problem with websites is that they don't expose information in a way that's easy for an application to consume. For example, you might want your customer service agents to have an application that lets them locate the nearest store, so that they can provide directions for customers who call in. Building an application that screen-scrapes MapPoint or MapQuest (1) would be hard, and (2) could break at any moment if those websites change in any way.
The answer is Web services. Web services typically let an application make requests over the Web, and rather than getting the data back as an HTML document, the data is returned as XML. This makes it very easy for the application to consume the information. Web services have emerged as the way that applications communicate with each other. In some cases, legacy back-end systems are wrapped with Web services in order to easily expose their data and functionality to the rest of the company. In other cases, Web-based providers (such as those listed above) add a Web service to allow programmatic access to the data and functionality that they currently provide to Web browsers.
For this article, I wanted to build an application that would download satellite photos of a given street address. I know that the satellite photos are available through the TerraServer Web service. I also know that you can convert a street address to a latitude/longitude through the MapPoint Web service and through Geocoder, so I know that all the building blocks are available.
First, I'll show you the completed application (see Figure 1), and then we'll examine how this was accomplished from Visual Basic 6.
Figure 1. Satellite photo of an address from Visual Basic 6
There are two ways in which you can call a Web service from Visual Basic 6. You can use the Web Services Toolkit, but when you call a Web service that takes complex data, this leaves you dealing with a lot of raw XML. In addition, the satellite image will be returned from TerraServer as a series of image tiles that must be pieced together. This means that your application would get XML documents with Base64-encoded image tiles. The application would have to decode these and somehow stitch them together to make the complete image. From Visual Basic 6, this would not be fun.
With Visual Basic .NET, however, this is very simple. Visual Basic .NET can get the image tile and convert it to a bitmap, with just one line of code. It can also easily put the tiles together to make the complete image. In fact, you will find that, in most cases, the best way to call a Web service from Visual Basic 6 is to write a little bit of Visual Basic .NET code to make the call, and return the results as standard COM that Visual Basic 6 can consume.
Where Art Thou?
The first step is to convert a street address into the latitude and longitude coordinates needed by TerraServer. There are a couple ways to go about this. The "best" way would be to use something like the MapPoint Web service. This is a highly reliable Web service provided by Microsoft that can quickly convert the address to a latitude and longitude (a process known as "geocoding"). MapPoint is designed to be high-performance, scalable, and reliable enough that you can depend on it for mission-critical applications. However, MapPoint has a couple of downsides that make it less than ideal for casual use. First, you must register and obtain an evaluation user ID and password. This authentication information is used to make calls to the MapPoint service. This free evaluation ID is valid for only 45 days. Also, after you register, it can take a couple of days for you to receive your ID. I wanted to provide you with a sample application that you could just download and run, without any special registration or setup; therefore, in addition to MapPoint, I included another option for geocoding the address.
The Geocoder.us website also provides functionality for turning a street address into lat/long coordinates. If you send the address as part of the query string, the site will return an XML document containing the lat/long information. To begin, I created a very simple class to hold the lat/long results (see Listing 1).
Listing 1. Class to hold lat/long information
<ComClass(LatLongInfo.ClassId, LatLongInfo.InterfaceId, _ LatLongInfo.EventsId)> _ Public Class LatLongInfo #Region "COM GUIDs" Public Const ClassId As String = "4362FF28-FFF7-4BCE-B844-8CF4208536AC" Public Const InterfaceId As String = "4A8FC896-6A9D-4F85-8B55-104CE4211829" Public Const EventsId As String = "B9108846-8A2C-4698-831B-D3BC197168C5" #End Region Public Sub New() MyBase.New() End Sub Private _lat As Double Public Property Lat() As Double Get Return _lat End Get Set(ByVal Value As Double) _lat = Value End Set End Property Private _long As Double Public Property [Long]() As Double Get Return _long End Get Set(ByVal Value As Double) _long = Value End Set End Property End Class
This class just contains properties for the latitude and longitude. In addition, the class contains attributes so that it will be exposed as a COM object that can be used directly from Visual Basic 6.
The Geocoder site can then be used to look up an address and populate a LatLongInfo object with the information (see Listing 2).
Listing 2. Using Geocoder to get the lat/long for an address
Public Function AddressToLatLongGeocoder(ByVal address As String) _ As LatLongInfo Dim wc As New WebClient Dim geocodeStream As Stream = _ wc.OpenRead("http://geocoder.us/service/rest?address=" _ & address) Dim xr As New XmlTextReader(geocodeStream) Try Dim ll As New LatLongInfo While xr.Read() If xr.Name = "geo:lat" Then ll.Lat = CDbl(xr.ReadInnerXml) End If If xr.Name = "geo:long" Then ll.Long = CDbl(xr.ReadInnerXml) End If End While wc.Dispose() Return ll Catch Return Nothing End Try End Function
The thing I love about .NET is that it makes interacting with the Web so easy. The WebClient class lets you "open" a URL as though it were a file. Since this "file" is an XML document, I can just use the XmlTextReader to look through the results for XML elements with certain names. When I find them, I just grab the values and put them into my LatLongInfo class. If the website is unable to resolve the address, this function will return Nothing; otherwise, it will return a LatLongInfo class with the latitude and longitude of the street address.
I also mentioned that you could use the MapPoint Web service to get the lat/long for an address. The code for this is even simpler that using Geocoder (see Listing 3).
Listing 3. Geocoding an address using the MapPoint Web service
Public Function AddressToLatLongMapPoint(ByVal address As String, _ ByVal username As String, ByVal password As String) As LatLongInfo Dim fs As New MapPoint.FindServiceSoap fs.Credentials = New System.Net.NetworkCredential(username, password) fs.PreAuthenticate = True Dim addressInfo As New MapPoint.Address addressInfo.FormattedAddress = address Dim fas As New MapPoint.FindAddressSpecification fas.InputAddress = addressInfo fas.DataSourceName = "MapPoint.NA" Dim results As MapPoint.FindResults = fs.FindAddress(fas) Dim ll As New LatLongInfo If results.Results.Length = 0 Then Return Nothing Else With results.Results(0).FoundLocation.LatLong ll.Lat = .Latitude ll.Long = .Longitude End With End If Return ll End Function
The first few lines of this function just set up the user ID and password so that they will be sent to the MapPoint Web service. To make any calls to the MapPoint Web service, you must have already created an account. If you don't have an account, and you want to use MapPoint instead of Geocoder, you can register for an account here.
The code then creates a MapPoint Address object and populates it with the address supplied by the user. MapPoint has a number of data sources (North America, Europe, and so on), and so the FindAddressSpecification.DataSourceName is set to MapPoint.NA, indicating that North America will be searched.
Finally, the call is made to FindAddress. This returns a FindResult object that contains the latitude and longitude information. This information is then just copied into my custom LatLongInfo class and returned.
A View from Beyond
Now that you know the latitude and longitude for the street address, TerraServer can return a satellite image of that location. The image comes in the form of image "tiles" that you must splice together to make the complete image. The code to do this is provided on the TerraServer website. I lifted this code and converted it to Visual Basic .NET. The code begins by simply calculating the pixel width and height of the desired image (see Listing 4).
Listing 4. Calculating image dimensions in Visual Basic .NET
Public Function DownloadSatteliteImage(ByVal latLong As LatLongInfo, _ ByVal scale As Integer, _ ByVal widthTwips As Double, _ ByVal heightTwips As Double) As Object Dim center As New LonLatPt Dim theme As Integer Dim mapWidth As Integer Dim mapHeight As Integer center.Lon = latLong.Long center.Lat = latLong.Lat theme = 1 mapWidth = Compatibility.VB6.TwipsToPixelsX(widthTwips) mapHeight = Compatibility.VB6.TwipsToPixelsY(heightTwips)
Visual Basic .NET contains some helper functions that make it easy to convert from Visual Basic 6 twips to standard pixels. This allows the Visual Basic 6 application to pass in the height and width of the Image control as twips, and the Visual Basic .NET code is able to calculate the pixel dimensions so that it can call the TerraServer service and correctly obtain the image.
Next, the code calls TerraServer with the image dimensions, and TerraServer returns a list of tile IDs for the area that you want to view (see Listing 5).
Listing 5. Obtaining tile IDs for the area you want to view
Dim ts As New TerraService Dim abb As AreaBoundingBox = ts.GetAreaFromPt(center, _ theme, scale, mapWidth, mapHeight)
The AreaBoundingBox contains a list of all the image tiles that will need to be downloaded in order to create the complete image for the requested area. As each tile is downloaded, it will be drawn into a .NET Framework Bitmap object, which will eventually hold the completed image. The Bitmap class makes it astonishingly easy to draw and work with graphics (see Listing 6).
Listing 6. Creating a Bitmap object to hold the satellite photo
Dim pf As PixelFormat = PixelFormat.Format32bppRgb Dim compositeImage As Bitmap = New Bitmap(mapWidth, mapHeight, pf) Dim compositeGraphics As Graphics = Graphics.FromImage(compositeImage)
At this point, each tile can be downloaded and drawn to form the composite image (see Listing 7).
Listing 7. Downloading each tile and adding it to the composite image
Dim xStart As Integer = abb.NorthWest.TileMeta.Id.X Dim yStart As Integer = abb.NorthWest.TileMeta.Id.Y For x As Integer = xStart To abb.NorthEast.TileMeta.Id.X For y As Integer = yStart To abb.SouthWest.TileMeta.Id.Y Step -1 Dim tid As TileId = abb.NorthWest.TileMeta.Id tid.X = x tid.Y = y Dim tileImage As Image = Image.FromStream( _ New MemoryStream(ts.GetTile(tid))) compositeGraphics.DrawImage(tileImage, _ (x - xStart) * tileImage.Width - _ CInt(abb.NorthWest.Offset.XOffset), _ (yStart - y) * tileImage.Height - _ CInt(abb.NorthWest.Offset.YOffset), _ tileImage.Width, tileImage.Height) tileImage.Dispose() Next Next
Here, each tile is downloaded and drawn on the composite image in the correct location. When this loop is finished, the entire satellite photo for the requested area will be downloaded, and the compositeImage object will contain the complete image. The only thing left to do is return this image so that it can be assigned to a Visual Basic 6 Image or PictureBox control.
I have to admit, the only part of this application that had me worried was returning the image to Visual Basic 6. I knew that the .NET Bitmap class, and the Visual Basic 6 Image controls were not directly compatible, and I thought that converting the Bitmap to something that Visual Basic 6 could use would be a pain. I spent some time panning for answers in the stream of Google, looking for Win32 API calls or libraries that had already been written. But then, just to make sure I wasn't overlooking something obvious, I looked through the Microsoft.VisualBasic.Compatibility.VB6 object. This is the same object that provided the twips to pixels conversion. Sure enough, it had an ImageToIPictureDisp method. The Picture property of the Image and PictureBox controls is an IPictureDisp, so this accomplished the conversion with one line of code (see Listing 8).
Listing 8. Returning the completed image from Visual Basic .NET to Visual Basic 6
From Visual Basic 6, you can call the DownloadSatelliteImage method, pass in the lat/long and size of the image control, and get a satellite photo that just fits (see Listing 9).
Listing 9. Calling DownloadSatelliteImage from Visual Basic 6
Dim msw As MapServiceWrapper.SatImageService Set msw = New MapServiceWrapper.SatImageService Dim satImage As Object Set satImage = msw.DownloadSatteliteImage(latLong, hscrScale.Value, _ imgSatPhoto.Width, imgSatPhoto.Height) Set imgSatPhoto.Picture = satImage
You can see that you just call DownloadSatelliteImage, passing in the lat/long, the scale (how zoomed-in you want the image to be), and the height and width. The Visual Basic .NET code does the work of calling TerraServer and composing the full image (see Figure 2). The image is returned, and it can just be assigned to the Picture property of the Visual Basic 6 Image control.
Figure 2. Image downloaded and displayed in a Visual Basic 6 application
Building this application with just Visual Basic 6 would have been really hard. To download a Web page, I would have needed to use a Web browser control, and then extract the document information. This is overkill compared to the simple classes that the .NET Framework provides for this task. The Web Services Toolkit would have been needed in order to call the Web services, leaving me to deal with a lot of raw XML. I don't know how I would have decoded the Base64-encoded image tiles or stitched them together.
However, by extending Visual Basic 6 with Visual Basic .NET, this was really easy to do. Hopefully, this article has illustrated how you could take an existing Visual Basic 6 application, and then extend it with Visual Basic .NET in order to pull real-time information from Web services. The Web services might be hosted inside of your organization to expose back-end systems, or they may exist on the Web—for example, Google, eBay, Amazon, or MapPoint. There are Web services that expose stock prices, weather information, and just about any other data. And, with the combination of Visual Basic 6 and Visual Basic .NET, they are all at your disposal.
About the Author
Scott Swigart spends his time consulting, authoring, and speaking about emerging and converging technologies. Scott has worked with a wide range of technologies over his career, beginning with Commodore 64 programming at the age of 12, writing hardware diagnostics for UNIX systems in C++, and building Windows desktop and Web applications. Over the years, Scott has worked with component development, XML technologies, .NET, Web services, and other languages, platforms, and paradigms. With this experience, Scott has seen how technology evolves over time, and he is focused on helping organizations get the most out of the technology of today while preparing for the technology of tomorrow. Scott is also a Microsoft MVP, and co-author of numerous books and articles. Scott can be reached at firstname.lastname@example.org.