Display a map at different scales with hotspots that the user can click in C#

This is a surprisingly simple program, although getting the details right was tricky. The program displays a map at several different scales. If the map won't fit on the form at the current scale, it displays scroll bars so you can move through the map. If you hover over a hotspot on the map (the cities in this example), the cursor changes to a hand. Finally if you click a hotspot, the program displays a message. In an actual application, you might want to take some other action.

At design time, I created a Scale menu with Full Scale, 1/2, and 1/4 entries. You could add other entries if they make sense for your map. I set each scale entry's Tag property to the corresponding scale factor. Full Scale gets 1, 1/2 gets 2, and 1/4 gets 4. All of these entries use the same Click event handler and it uses the Tag entry to see how it should scale the map. This makes it amazingly easy to add new scales. Simply create a new menu entry, set its Tag property, and make it use the same event handler.

Also at design time, I added the map's image to the program's resources. (Open the Project menu and select Properties. On the Resources tab, open the Add Resource dropdown, select Add Existing File, select the map image file, and click Add.)

The program's form contains a Panel control with AutoScroll = True. That control contains the PictureBox picMap, which displays the map image. If picMap is too big to fit in the Panel, the Panel automatically displays scroll bars and moves picMap appropriately when the user scrolls.

At run time, the code starts by defining some variables and by initializing the Hotspots list.

// The map.
private Bitmap Map;

// The hotspots.
private List Hotspots = new List();

// The current scale.
private float MapScale;


// Prepare the map for first viewing.
private void Form1_Load(object sender, EventArgs e)
{
// Initialize the hotspots.
Hotspots.Add(new Rectangle(88, 509, 22, 22));
Hotspots.Add(new Rectangle(140, 577, 20, 20));
Hotspots.Add(new Rectangle(161, 609, 20, 20));
...
Hotspots.Add(new Rectangle(1234, 1076, 16, 18));

// If we should draw the hotspots, add them to the map.
Map = Properties.Resources.GCMap;

#if DRAW_HOTSPOTS
using (Graphics gr = Graphics.FromImage(Map))
{
foreach (Rectangle hotspot in Hotspots)
{
gr.FillRectangle(Brushes.Blue, hotspot);
}
}
#endif

// Display the initial map.
picMap.SizeMode = PictureBoxSizeMode.Zoom;
picMap.Image = Map;

// Start at small scale.
SetMapScale(mnuScale4);
}

After defining the variables and initializing the Hotspots list, the code loads the map image from the resource. Next if the DRAW_HOTSPOTS preprocessor directive is defined (it's commented out in the code you can download), the program marks each hotspot with a blue rectangle.

The code sets the picMap control's SizeMode property to Zoom so the control makes its image fill the control's available area. The program then sets the control's Image property to the map's image.

The Form_Load event handler finishes by calling the SetMapScale method shown in the following code to display the map at the scale selected by the mnuScale4 menu item (1/4 scale).

// Scale the map.
private void mnuScaleMap_Click(object sender, EventArgs e)
{
SetMapScale(sender as ToolStripMenuItem);
}
private void SetMapScale(ToolStripMenuItem checked_item)
{
// Clear all scale menu choices.
foreach (ToolStripMenuItem item in scaleToolStripMenuItem.DropDownItems)
{
item.Checked = false;
}

// Check this item.
checked_item.Checked = true;

// Scale the map.
MapScale = float.Parse(checked_item.Tag.ToString());
picMap.Size = new Size(
(int)(Map.Width / MapScale),
(int)(Map.Height / MapScale));
}

The mnuScaleMap_Click method is the event handler used by all of the scale menu items. It simply calls SetMapScale, passing that method the menu item that raised the Click event.

The SetMapScale method unchecks all of the map scale menu items and then checks the selected item. It then parses the menu item's Tag property and saves it in the MapScale variable. It finishes by sizing the picMap control so it scales the map appropriately. For example, if we're viewing the map at 1/2 scale, then MapScale is 2 and the PictureBox is half as big as it would need to be to display the map at full size. The PictureBox automatically scales the map so it fits.

The only other pieces of code that I'm going to explain today deal with detecting hotspots as the mouse moves over them. The HotspotAtPoint method shown in the following code returns the index of the hotspot at a particular position.

// Return the index of the hotspot at this point
// or -1 if there is no hotspot there.
private int HotspotAtPoint(Point mouse_point)
{
// Adjust for the current map scale.
mouse_point = new Point(
(int)(mouse_point.X * MapScale),
(int)(mouse_point.Y * MapScale));

// Check the hotspots.
for (int i = 0; i < Hotspots.Count; i++)
{
if (Hotspots[i].Contains(mouse_point)) return i;
}

// We didn't find a hotspot that contains the point.
return -1;
}

The HotspotAtPoint method takes as a parameter a point in the PictureBox's coordinates. The method starts by scaling the point's X and Y coordinates to make it represent a point in the map's original coordinate system. For example, if the map is being displays at 1/2 scale, then the point (100, 50) in the PictureBox really represents the point (2 * 100, 2 * 50) = (200, 100) on the map.

The code then loops through the Hotspots list looking for one that contains the mouse's position. If it finds such a hotspot, the code returns its index. If there is no hotspot containing the point, the method returns -1.

For bonus points, you can use the following LINQ statement instead of this loop to find the hotspot that contains the mouse's position. The example code includes this statement commented out.

    return Hotspots.FindIndex(hotspot => hotspot.Contains(mouse_point));

The following code shows how the program uses the HotspotAtPoint method.

// See if we're over a hotspot.
private void picMap_MouseMove(object sender, MouseEventArgs e)
{
// See if we're over a hotspot.
if (HotspotAtPoint(e.Location) >= 0)
{
picMap.Cursor = Cursors.Hand;
}
else
{
picMap.Cursor = Cursors.Default;
}
}

// See if we clicked a hotspot.
private void picMap_MouseClick(object sender, MouseEventArgs e)
{
int i = HotspotAtPoint(e.Location);
if (i >= 0) MessageBox.Show("You clicked hotspot " + i);
}

When the mouse moves over the picMap PictureBox, the picMap_MouseMove event handler executes. It calls HotspotAtPoint to get the index of the hotspot under the mouse or -1 if no hotspot is there. if the mouse is over a hotspot, the code sets the cursor to a hand. If the mouse is not over a hotspot, the code sets the cursor to the default.

When the user clicks on the map, the picMap_MouseClick event handler calls HotspotAtPoint to get the index of the hotspot under the mouse. If the result is at least 0, indicating that the mouse is over a hotspot, the code displays a message giving the index of the hotspot. In a real application you might want the code to do something else such as looking up information about the hotspot and displaying it.

In the next blog entry, I'll explain how this program lets you easily define hotspots.

 

What did you think of this article?




Trackbacks
  • No trackbacks exist for this post.
Comments
  • No comments exist for this post.
Leave a comment

Submitted comments are subject to moderation before being displayed.

 Name

 Email (will not be published)

 Website

Your comment is 0 characters limited to 3000 characters.