Display a scalable map with hotspots in C#

[scalable map]

Making a scalable map with hotspots is surprisingly easy, although getting the details right is a bit 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 such as opening a form or browser page for the hotspot.

At design time, I created a Scale menu with entries Full Scale, 1/2, and 1/4. You could add other entries if they make sense for your scalable map.

I set each scale entry’s Tag property to the corresponding scale factor. Full Scale gets the value 1, 1/2 gets the value 0.5, and 1/4 gets the value 0.25. All of these entries use the same Click event handler and that code uses the Tag entry to see how it should scale the scalable 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 scalable 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 named 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<Rectangle> Hotspots = new List<Rectangle>();

// 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 scalable map image from the resource. Next if the DRAW_HOTSPOTS preprocessor directive is defined (it’s commented out in the code that 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 scalable 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)
{
    // Select the correct menu item.
    foreach (ToolStripMenuItem item in
        scaleToolStripMenuItem.DropDownItems)
            item.Checked = (item == checked_item);

    // 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 checks the desired menu item and unchecks the other items. It then parses the selected 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 0.5 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 control'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 (100 / 0.5, 50 / 0.5) = (200, 100) on the map.

The code then loops through the Hotspots list looking for a hotspot 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 control, 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 scalable 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.


Download Example   Follow me on Twitter   RSS feed   Donate




This entry was posted in algorithms, graphics, mathematics and tagged , , , , , , , , , , , , , . Bookmark the permalink.

5 Responses to Display a scalable map with hotspots in C#

  1. archistico says:

    How to pan e scale with the mouse?
    thank you

  2. Pingback: Define map hotspots in C# - C# HelperC# Helper

  3. Pingback: Drag a map to scroll in C# - C# HelperC# Helper

Leave a Reply

Your email address will not be published. Required fields are marked *