Scale a drawing so it fits a target area in C#

example

Many drawing programs need to scale a drawing to fit a target area, either with or without stretching. The following MapDrawing method transforms a Graphics object so it maps a drawing rectangle to a rectangle on the Graphics object.

// Map a drawing coordinate rectangle to
// a graphics object rectangle.
private void MapDrawing(Graphics gr, RectangleF drawing_rect,
    RectangleF target_rect, bool stretch)
{
    if ((target_rect.Width < 1) ||
        (target_rect.Height < 1)) return;

    gr.ResetTransform();

    // Center the drawing area at the origin.
    float drawing_cx = (drawing_rect.Left + drawing_rect.Right) / 2;
    float drawing_cy = (drawing_rect.Top + drawing_rect.Bottom) / 2;
    gr.TranslateTransform(-drawing_cx, -drawing_cy);

    // Scale.
    // Get scale factors for both directions.
    float scale_x = target_rect.Width / drawing_rect.Width;
    float scale_y = target_rect.Height / drawing_rect.Height;
    if (!stretch)
    {
        // To preserve the aspect ratio,
        // use the smaller scale factor.
        scale_x = Math.Min(scale_x, scale_y);
        scale_y = scale_x;
    }
    gr.ScaleTransform(scale_x, scale_y,
        System.Drawing.Drawing2D.MatrixOrder.Append);

    // Translate to center over the drawing area.
    float graphics_cx = (target_rect.Left + target_rect.Right) / 2;
    float graphics_cy = (target_rect.Top + target_rect.Bottom) / 2;
    gr.TranslateTransform(graphics_cx, graphics_cy,
        System.Drawing.Drawing2D.MatrixOrder.Append);
}

The code starts by resetting the Graphics object’s transformations to remove any previous transformations.

Next the code finds the center of the drawing rectangle and makes a transformation that translates the drawing so it is centered at the origin. Then when the code scales the drawing in the next step, it remains centered at the origin.

The code then scales the drawing. It first finds scale factors that would make the drawing fill the target output area. Then if the stretch parameter is false, indicating that you don’t want to stretch the result, the code sets both scale factors to the smaller of the two. That makes the result as big as possible without distorting it.

The code then calls the Graphics object’s ScaleTransform method to scale the drawing. Notice the final parameter: Append. This makes the new scale transformation come after the earlier translation. This is very important because the order of the transformations is important. In general you cannot change the order of transformations and produce the same result. (I have no clue why Microsoft decided the default should be to prepend new transformation before older ones, but that’s what you get if you omit this final parameter.)

At this point the Graphics object is set up to make the drawing at its final scale centered at the origin. The code now adds a transformation to move the center of the drawing (the origin) to the center of the target area. Again notice that the code appends this transformation.

After you use this method to apply those transformations to the Graphics object, anything you draw on it will be appropriately mapped to the target rectangle.

The following code shows how the program uses the MapDrawing method to draw the picture shown above.

// Draw some smiley faces.
private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = 
        System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

    RectangleF smiley_rect = new RectangleF(-1, -1, 2, 2);
    float wid = (this.ClientSize.Width - 1) / 2;
    float hgt = (this.ClientSize.Height - 1) / 2;

    // Draw in the upper left quarter.
    MapDrawing(e.Graphics, smiley_rect,
        new RectangleF(0, 0, wid, hgt), false);
    DrawSmiley(e.Graphics);

    // Draw in the lower left quarter.
    MapDrawing(e.Graphics, smiley_rect,
        new RectangleF(0, hgt, wid, hgt), false);
    DrawSmiley(e.Graphics);

    // Draw in the right side.
    MapDrawing(e.Graphics, smiley_rect,
        new RectangleF(wid, 0, wid, 2 * hgt), true);
    DrawSmiley(e.Graphics);
}

The DrawSmiley method uses ellipses and an arc to draw a smiley face in the area (-1, -1)-(1, 1). It’s straightforward so it isn’t shown here. Download the example to see how it works.

The Paint event handler RectangleF named smiley_rect to represent the area where the smiley should be drawn. It also makes variables wid and hgt to measure half of the form’s client area (minus 1 so things fit at the edges).

Next the code calls MapDrawing and DrawSmiley to draw the smiley face on different parts of the form. Notice that the first two calls to DrawSmiley use false for their last parameters so those smiley faces are not distorted. In the final call to DrawSmiley, this parameter is true so the smiley is stretched to fill the entire target area.


Download Example   Follow me on Twitter   RSS feed   Donate




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

8 Responses to Scale a drawing so it fits a target area in C#

  1. Crashes on form minimization. Line 74. System Argument Exception: ‘Parameter is not valid’ Otherwise it compiles and runs beautifully in Visual Studio 2017, Windows 10. Thanks.

    • RodStephens says:

      True. Many of the examples have only minimal error checking so they don’t get too cluttered. This one is straightforward enough that I should have added at least this much.

      Drawing routines generally don’t like zero-sized things such as zero-width or zero-height rectangles. To fix it, I added this statement:

      if ((target_rect.Width < 1) ||
          (target_rect.Height < 1)) return;

      Thanks for pointing this out!

  2. Thanks. I had a more elaborate method but your’s is much simpler and works nicely.

    The biggest problem I have is that I have been unable to scale text down to a usable size. Using this code:

    Brush brush = new SolidBrush(Color.Blue);
    FontFamily ff = new FontFamily(“Arial”);
    Font font = new Font(ff, 1, FontStyle.Regular);
    gr.DrawString(“Main Title”, font, brush, new PointF(-1.0f, -1.0f));

    results in huge ‘Main’ that takes up upper half of the window. Nowhere have I found an understandable answer to this problem. I must be missing something. If one simply changes the font size to 0, appcrash occurs. Any ideas greatly appreciated.

  3. Never mind. I figured it out.

    Font font = new Font(ff, 0.1f, FontStyle.Regular);

    Just can’t get used to the float dimensions of everything.

  4. I have used your method to draw a grid inside the client area with the intention of creating a 2D plotter. All works well, but for weeks I have struggled with how to identify the grid rectangle inside the client rectangle. Any suggestions greatly appreciated.

  5. I apologize for describing my problem so poorly. Please have a look at this video I made to demo the problem. https://youtu.be/sA0pcMwDsSQ Note that the MouseMove tracker is supposed to post the x : y points on the grid. But so far I can only capture the Client Rectangle points. I have yet to figure out how to scale client xy to grid xy, even more challenging when scale changes as it does when a new data set is loaded, and when the client rect resizes when the Form does so. If I could determine the relation between client rect and grid rect points under varying conditions, I think I could figure it out.

Leave a Reply

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