Drag a map to scroll in C#

[drag a map]

This example shows how you can allow a user to drag a map to scroll to hidden parts of it. Scroll bars provide a simple method for allowing the user to view parts of a map that don’t fit on the form. This example shows a second method.

The basic idea is to keep track of where the map image’s upper left corner should be relative to the visible map area. Then you draw the part of the map that appear in the visible area. The idea isn’t too complicated, but the details are a bit involved. The rest of this post groups the code into two sections: those that deal with drawing the map and those that deal with dragging the map.

Drawing the Map

When you drag a map, the example uses the following code to keep track of the map’s visible part.

private Bitmap OriginalImage;
private float CurrentScale;
private Bitmap VisibleImage = null;
private Graphics VisibleGraphics = null;

// Upper left corner of the image in the PictureBox.
private int PicX = 0, PicY = 0;

The OriginalImage field holds the full map image, which is stored in a resource named Map. The program could just use Properties.Resources.Map instead of using this variable. The variable just makes the code a bit easier to read.

The CurrentScale variable keeps track of the map’s current scale. See the post Display a scalable map with hotspots in C# for more information on setting the map scale.

The VisibleImage field is a bitmap that holds the part of the map that should be visible. The VisibleGraphics variable is a Graphics object associated with the bitmap. The program uses VisibleGraphics to draw on the visible bitmap.

Finally the variables PicX and PicY give the coordinates of the full map’s upper left corner in its current scale. These values will both be less than or equal to 0.

The following code shows the program’s first interesting pieces.

// Initially display at full scale.
private void Form1_Load(object sender, EventArgs e)
{
    // Use a grabbing hand cursor.
    picMap.Cursor = new Cursor("hand2.cur");

    // Get the map image.
    OriginalImage = Properties.Resources.Map;

    // Get ready to draw.
    PrepareGraphics();

    // Start at full scale.
    mnuScale_Click(mnuScaleFull, null);
}

// Make a display Bitmap and Graphics.
private void Form1_Resize(object sender, EventArgs e)
{
    PrepareGraphics();
    DrawMap();
}

The form’s Load event handler makes the picMap PictureBox use a custom grabbing hand cursor. It saves a reference to the map image resource in OriginalImage and calls PrepareDraw to get ready to draw. It then calls the Scale menu event handler mnuScale_Click to initially display the map at full scale.

The form’s Resize event handler calls PrepareGraphics to get ready to draw. It then calls DrawMap to actually draw the visible part of the map.

The Resize event handler is stranger than it might seem. You might think that you can move this code into a Resize event handler for the visible PictureBox. After all, it’s when we resize that control that we need to change the size of the bitmap that it displays. Even though the control has SizeMode set to Normal, setting its Image property seems to confuse its resizing features so the control reverts to its original size. To work around this, the program needs to handle resizing of the PictureBox in the form’s Resize event.

The following code shows the PrepareGraphics method.

private void PrepareGraphics()
{
    // Skip it if we've been minimized.
    if ((picMap.ClientSize.Width == 0) ||
        (picMap.ClientSize.Height == 0)) return;

    // Free old resources.
    if (VisibleGraphics != null)
    {
        picMap.Image = null;
        VisibleGraphics.Dispose();
        VisibleImage.Dispose();
    }

    // Make the new Bitmap and Graphics.
    VisibleImage = new Bitmap(
        picMap.ClientSize.Width,
        picMap.ClientSize.Height);
    VisibleGraphics = Graphics.FromImage(VisibleImage);
    VisibleGraphics.InterpolationMode = InterpolationMode.High;

    // Display the Bitmap.
    picMap.Image = VisibleImage;
}

This code first checks whether the map PictureBox has zero width or height. If it does, probably because the form was minimized, the method returns.

Next if the program has previously created a visible bitmap and associated Graphics object, the method disposes of them to release resources. It then creates a new bitmap and Graphics object for the PictureBox object’s new size.

The method finishes by displaying the new bitmap in the PictureBox.

The following code shows the Scale menu’s event handler.

// Set the scale.
private void mnuScale_Click(object sender, EventArgs e)
{
    // Check the selected scale item.
    ToolStripMenuItem item = sender as ToolStripMenuItem;
    foreach (ToolStripMenuItem menu_item in mnuScale.DropDownItems)
        menu_item.Checked = (menu_item == item);

    // Set the selected scale.
    CurrentScale = float.Parse(item.Tag.ToString());

    // Draw.
    DrawMap();
}

All of the menu items in the Scale menu invoke this event handler. It gets the menu item that invoked it. It then parses the menu item’s Tag property. The Tag property contains the menu item’s scale. For example, the value for the Full Scale menu item is 1 and the value for the 1/2 menu item is 0.5.

After saving the new scale, the code calls the following DrawMap method to display the visible part of the map.

// Draw the image at the correct scale and location.
private void DrawMap()
{
    // Validate PicX and PicY.
    SetOrigin();

    // Get the destination area.
    float scaled_width = CurrentScale * OriginalImage.Width;
    float scaled_height = CurrentScale * OriginalImage.Height;
    PointF[] dest_points =
    {
        new PointF(PicX, PicY),
        new PointF(PicX + scaled_width, PicY),
        new PointF(PicX, PicY + scaled_height),
    };

    // Draw the whole image.
    RectangleF source_rect = new RectangleF(
        0, 0, OriginalImage.Width, OriginalImage.Height);

    // Draw.
    VisibleGraphics.Clear(picMap.BackColor);
    VisibleGraphics.DrawImage(OriginalImage,
        dest_points, source_rect, GraphicsUnit.Pixel);

    // Update the display.
    picMap.Refresh();
}

This code first calls SetOrigin (described shortly) to ensure that PicX and PicY are valid. It then gets the map’s scaled width and height. It then draws the map. The source area includes the whole map. The destination area has upper left corner at position (PicX, PicY) and has the map’s scaled size. The result is that the part of the map that should be visible is drawn on the PictureBox and the rest of the map is clipped off.

The method finishes by refreshing the PictureBox to show the new image.

The final piece of code used to draw the map is the following SetOrigin method.

// Set the PictureBox's position.
private void SetOrigin()
{
    // Keep x and y within bounds.
    float scaled_width = CurrentScale * OriginalImage.Width;
    int xmin = (int)(picMap.ClientSize.Width - scaled_width);
    if (xmin > 0) xmin = 0;
    if (PicX < xmin) PicX = xmin;
    else if (PicX > 0) PicX = 0;

    float scaled_height = CurrentScale * OriginalImage.Height;
    int ymin = (int)(picMap.ClientSize.Height - scaled_height);
    if (ymin > 0) ymin = 0;
    if (PicY < ymin) PicY = ymin;
    else if (PicY > 0) PicY = 0;
}

The SetOrigin method ensures that the PicX and PicY values are within the appropriate bounds. For example, if PicX was greater than 0, then the map would start shifted to the right of the edge of the PictureBox. Also if PicX was too small, the whole map would be hidden off of the left edge of the PictureBox.

The method first calculates the map’s current width. From that it determines the minimum value that PicX could have without displaying whits space to the right of the map in the PictureBox. If the PictureBox is bigger than the scaled map, then the xmin value will be greater than 0. In that case the code sets it equal to 0 so the map won’t shift to the right of the control’s left edge.

Finally the method ensures that PicX is between xmin and 0. (Note that xmin will be 0 if the whole map fits horizontally in the PictureBox.)

The method repeats the same steps for PicY.

To summarize, the methods have the following purposes:

  • Form1_Load – Get ready. Set scale to 1.
  • Form1_Resize – Resize the visible bitmap to fit the PictureBox.
  • PrepareGraphics – Make a bitmap and Graphics object for the PictureBox object’s current size.
  • mnuScale_Click – Set the current scale.
  • SetOrigin – Make sure PicX and PicY are valid.
  • DrawMap – Draw the map.

Dragging the Map

The following code shows how the program lets you drag a map.

// Let the user drag the image around.
private bool Dragging = false;
private int LastX, LastY;

private void picMap_MouseDown(object sender, MouseEventArgs e)
{
    LastX = e.X;
    LastY = e.Y;
    Dragging = true;
}

private void picMap_MouseMove(object sender, MouseEventArgs e)
{
    if (!Dragging) return;

    PicX += e.X - LastX;
    PicY += e.Y - LastY;
    LastX = e.X;
    LastY = e.Y;

    DrawMap();
}

private void picMap_MouseUp(object sender, MouseEventArgs e)
{
    Dragging = false;
}

When you press the mouse down on the PictureBox, the MouseDown event handler saves the mouse position and sets Dragging = true so we know a drag is in progress.

When the mouse moves, the MouseMove event handler returns if no drag is in progress. If a drag is in progress, the code moves PicX and PicY by the amount that the mouse has moved. It then updates the stored mouse position and calls DrawIimage to redraw the map.

When the mouse is released, the MouseUp event handler sets Dragging = false to end the current drag.


Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.

This entry was posted in drawing, graphics, image processing, mathematics and tagged , , , , , , , , , , , , , , , . Bookmark the permalink.

3 Responses to Drag a map to scroll in C#

  1. archistico says:

    Very good!!
    ps. And scale with the mouse wheel?

    private void ZoomPlus()
    {
        private double ZOOMFACTOR = 1.20; 
        private int MINMAX = 5; 
    
        if ((PBox.Width < (MINMAX * OuterPanel.Width)) &&
            (PBox.Height < (MINMAX * OuterPanel.Height)))
        {
            PBox.Width = Convert.ToInt32(PicBox.Width * ZOOMFACTOR);
            PBox.Height = Convert.ToInt32(PicBox.Height * ZOOMFACTOR);
            PBox.SizeMode = PictureBoxSizeMode.StretchImage;
        }
    }
    
    private void PBox_MouseWheel(object sender, MouseEventArgs e)
    {
        if (e.Delta < 0)
        {
            ZoomPlus();
        }
        else
        {
            ZoomMinus();
        }
    }

Leave a Reply

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