October 22, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Managed C++: Rubber-banding and Cropping Images

  • November 29, 2004
  • By Tom Archer
  • Send Email »
  • More Articles »
My previous article illustrated how to load, display, and dynamically resize images. This article demonstrates how to allow a user to select an area of an image with their mouse (sometimes called rubber-banding) and crop the image to that selected region.



Click here for larger image

Step-by-Step Instructions

  1. Define the form-level members.
    First, define the variables that will be used to control how and where the rectangle is drawn. The cropRect defines the exact coordinates of the cropped image (the x and y coordinates as well as the width and height). Use the cropping boolean value to determine when the user has the mouse button down so that the user doesn't draw a rectangle when he or she simply is moving the mouse across the image. Finally, use the cropOriginX and cropOriginY to determine the original starting position (You'll see why these values need to be stored separately from the cropRect member shortly.):
    public __gc class Form1 : public System::Windows::Forms::Form
    {	
    protected:
      Rectangle cropRect;
      bool cropping;
      int cropOriginX, cropOriginY;
    
    ...
    
  2. Implement an event handle for the PictureBox control's MouseDown event.
    The user pressing the mouse button constitutes the start of a new rectangle. The first thing to verify is that an image file has been loaded. The PictureBox control has a member called Image that represents the image being displayed, so the code can easily check for its existence in a boolean statement. If an image is being displayed, the method first calls the PictureBox::Refresh method to remove the previously drawn rectangle. If you're familiar with the famous MFC Scribble example application, you'll recall that to erase the rectangle, it simple drew the rectangle using the client area background color. However, that obviously won't work here because the program has drawn on an image (instead of a simple blank background). Therefore, the quickest and easiest way to remove the previous rectangle is by refreshing the image.

    Once the image has been refreshed, the method need only initialize the variables that define where the rectangle is drawn and set a boolean flag to indicate that the user is currently cropping the image:

    private: System::Void picImage_MouseDown(System::Object *  sender, System::Windows::Forms::MouseEventArgs *  e)
    {
      if (picImage->Image)
      {
        picImage->Refresh();
    
        cropOriginX = e->X;;
        cropRect.X = e->X;
        cropRect.Height = 0;
    
        cropOriginY = e->Y;
        cropRect.Y = e->Y;
        cropRect.Width = 0;
    
        cropping = true;
      }
    }
    
  3. Implement an event handle for the PictureBox control's MouseMove event.
    As you might imagine, this is where most of the action occurs. The mousemove method first verifies that the user is cropping the image. If so, it refreshes the image (to erase any pre-existing rectangle), calls SetCroppingRectangle (to properly set up the rectangle to be drawn), and then calls the PictureBox::Invalidate method to cause the picture box to be repainted:
    private: System::Void picImage_MouseMove(System::Object *  sender, System::Windows::Forms::MouseEventArgs *  e)
    {
      if (cropping)
      {
        picImage->Refresh();
    
        SetCroppingRectangle(e->X, e->Y);
        picImage->Invalidate();
      }
    }
    
    The SetCroppingRectangle receives the coordinates where the mouse is on the picture box. The first thing the method does is make sure that these coordinates are not outside of the image's dimensions. If either the x or the y coordinate is outside the image's dimensions, then the value is modified so that it represents the extreme in that direction. In other words, if an image is 20 (width) x 40 (height) and the user attempts to move the mouse to pixel position 41 on the x axis, then the value representing the x coordinate is set to 40. Likewise, the code ensures that the user can't move to a value below 0. These numbers are then massaged to ensure that the full rectangle can be viewed within the picture box. (I used a hard-coded value of 6, which worked for me when using a pen size of 3. However, you might want to play around with them to ensure perfect accuracy for your application.)

    You would think that when the user moves the mouse you need only increment the Width and Height members of your cropRect object. However, when you draw a rectangle in .NET, it is drawn relative to an upper-left hand corner. This causes a problem if the user moves the mouse from right to left and/or from bottom to top. As a result, you need code to handle that situation such that the rectangle properly reflects not the direction the user moves the mouse, but the coordinates that are necessary for drawing the rectangle.

    As an example, suppose the user starts moving the mouse from 5,5 and to 15,15. This is easy. The x and y members are each equal to 5 and the width and height are each equal to 10. In this situation, you simply leave the x and y alone and change the width and height to the current position (15,15) minus the starting position (5,5), which gives you a width and height of 10,10. However, if the user moves left and/or up, you need to set the starting position of the rectangle to the point where the user is now and then set the width and height equal to that original starting position (this is why you needed to define the cropOriginX and cropOriginY members) minus the current positions:

    private: System::Void SetCroppingRectangle(int moveToX, int moveToY)
    {
      // Account for user cropping outside of image dimensions
      moveToX = Math::Max(0, Math::Min(moveToX, 
                                       picImage->Image->Width));
      moveToY = Math::Max(0, Math::Min(moveToY, 
                                       picImage->Image->Height));
      
    
      // Make sure that rectangle is viewable within the picturebox
      // (Note: If the form is scrollable and the picturebox is set
      // to autosize, then the rectangle can be drawn beyond the user's
      // view.
      if (moveToX == 0) moveToX = 6;
      if (moveToX >= picImage->Image->Width)
        moveToX -= 6;
      if (moveToY == 0) moveToY = 6;
      if (moveToY >= picImage->Image->Height)
        moveToY -= 6;
    
      if (moveToX >= cropRect.X) // moving right
        cropRect.Width = moveToX - cropOriginX;
      else // moving left
      {
        cropRect.X = moveToX;
        cropRect.Width = cropOriginX - moveToX;
      }
    
      if (moveToY >= cropRect.Y) // moving down
        cropRect.Height = moveToY - cropOriginY;
      else // moving up
      {
        cropRect.Y = moveToY;
        cropRect.Height = cropOriginY - moveToY;
      }
    }
  4. Implement an event handle for the PictureBox control's MouseUp event.
    At this point, you have the rectangle to draw. All you need to do is turn off the cropping boolean flag and, in the case of the demo, enable the button that allows the user to invoke the cropping of the image. As you can see, I enable the button based on whether or not a rectangle is present:
    private: System::Void picImage_MouseUp(System::Object *  sender, System::Windows::Forms::MouseEventArgs *  e)
    {
      cropping = false;
      btnCropImage->Enabled = (cropRect.Width > 0 && cropRect.Height > 0);
    }
    
  5. Implement a means of the user invoking the crop function.
    In the article demo, the user first draws the rectangle and then clicks a button labeled "Crop Image". The method first disables the crop button, and then it adjusts the rectangle to make sure that if the user has moused to the edge of the image, the edge is also cropped.

    Once that's done, you're finally finished with the rectangle drawing and can crop the image. Do this by creating a Bitmap object that is the width and height of the user's drawn rectangle. Then obtain a Graphics object for drawing on the bitmap. Next, use the Graphics::DrawImage method. This method allows you to specify a source image, destination coordinates, and source coordinates. As you can see, the source image is the image in the PictureBox control. The destination coordinates are defined by the cropRect object that has been used to draw the rectangle on the image, and the source coordinates start at 0 and 0 (top left-hand corner of the new image) with a width and height equal to that of the drawn rectangle. The PictureBox control's Image property is set to the newly created image and the rectangle is initialized:

    private: System::Void btnCropImage_Click(System::Object *  sender, System::EventArgs *  e)
    {
      if (picImage->Image 
      && cropRect.Width > 0 
      && cropRect.Height > 0)
      {
        btnCropImage->Enabled = false;
    
        if (cropRect.X <= 6) cropRect.X = 0;
        if (cropRect.X + cropRect.Width >= picImage->Image->Width)
          cropRect.Width += 6;
        if (cropRect.Y <= 6) cropRect.Y = 0;
        if (cropRect.Y + cropRect.Height >= picImage->Image->Height)
          cropRect.Height += 6;
    
        Bitmap* croppedImage = new Bitmap(cropRect.Width, cropRect.Height);
        Graphics* g = Graphics::FromImage(croppedImage);
        g->DrawImage(picImage->Image, 
                     Rectangle(0, 0, 
                               cropRect.Width, 
                               cropRect.Height), 
                     cropRect, 
                     GraphicsUnit::Pixel);
    
        picImage->Image = croppedImage;
    
        cropRect.X = 0;
        cropRect.Width = 0;
        cropRect.Y = 0;
        cropRect.Height = 0;
      }
    }
    
  6. Implement a Paint method for the PictureBox control.
    This is done so that if the user switches away from the application and then back again, the rectangle (if drawn) is always present. As you can see, you simply check to see if an image and valid rectangle coordinates are present. If they are, you create a pen and draw the rectangle:
    private: System::Void picImage_Paint(System::Object *  sender, System::Windows::Forms::PaintEventArgs *  e)
    {
      if (picImage->Image 
      && cropRect.Width > 0 
      && cropRect.Height > 0)
      {
        Pen* pen = new Pen(Brushes::Red, 3);
        Graphics* g = e->Graphics;
        g->DrawRectangle(pen, cropRect);
      }
    }
    

Download the Code

To download the accompanying source code for the demo, click here.

About the Author

The founder of the Archer Consulting Group (ACG), Tom Archer has been the project lead on three award-winning applications and is a best-selling author of 10 programming books as well as countless magazine and online articles.






Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel