Managed C++: Rubber-Banding and Cropping Images

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.

Step-by-Step Instructions

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;

...

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 simply 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 needs only to 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;
  }
}

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, 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, 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;
  }
}

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);
}

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;
  }
}

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);
  }
}


About the Author

Tom Archer - MSFT

I am a Program Manager and Content Strategist for the Microsoft MSDN Online team managing the Windows Vista and Visual C++ developer centers. Before being employed at Microsoft, I was awarded MVP status for the Visual C++ product. A 20+ year veteran of programming with various languages - C++, C, Assembler, RPG III/400, PL/I, etc. - I've also written many technical books (Inside C#, Extending MFC Applications with the .NET Framework, Visual C++.NET Bible, etc.) and 100+ online articles.

Downloads

Comments

  • crops the area right of the selected rectangle

    Posted by sanjeet.uchil on 10/17/2008 06:16am

    i dnt knw why this is happening...on clicking crop button....image area right to the selected area gets cropped instead....please help...i have translated ur code into c#.

    • can you share your code?

      Posted by niketrk on 11/21/2008 02:11am

      Hi, Can you share your code here so other can try to help you? otherwise one good ready-made control, I found at http://futurefiesta.com/downloads.htm . Though it is not free but it has got many features related to crop control. It is in form of activex so it is easy to integrate in application. It is simple but effective. anyway please share your code, so I can try to fix it.

      Reply
    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • The open source cloud computing project OpenStack has come a long way since NASA and Rackspace launched it in 2010. Backed by leading technology infrastructure providers including Cisco, Dell, EMC, HP, IBM, Intel, and VMware, OpenStack underpins significant workloads at an increasingly diverse set of organizations, including BWM, CERN, Comcast, eBay, and Wal-Mart. For CIOs engaged in broader programs to win, serve, and retain customers -- and refocus business technology (BT) spend -- a planned and pragmatic …

  • For many organizations, moving enterprise applications to the public cloud can be a very attractive proposition, but planning the best way to move your applications is mission–critical. As an alternative to the costly option of re–architecting the application for a cloud environment, you can follow a "lift and shift" model that's significantly cheaper and almost always a lot quicker. In order to have a successful "lift and shift" migration, read this white paper to learn a few rules you should …

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date