Draw a heartagram in C#

[example]

The heartagram is a symbol created by vocalist Ville Valo and used by the band HIM. I’m not particularly a fan, but I think the heartagram shape is interesting so I wanted to make one with a C# program.

The most interesting part of the program is the following MakeHeartagram method, which creates a GraphicsPath that defines the heartagram.

// Return a GraphicsPath representing a heartagram.
private GraphicsPath MakeHeartagram(PointF center,
    float radius, float pen_width)
{
    // Define scales to place the points the right
    // distances from the edges of the circle.
    float[] scales =
    {
        1 - 1.75f * pen_width / radius,
        1 - 1.75f * pen_width / radius,
        0.9f,
        0.9f,
        1 - 1.75f * pen_width / radius,
    };

    // Find the points of a pentagon within the area.
    double angle = Math.PI / 2.0;
    double dtheta = Math.PI * 2.0 / 5.0;

    PointF[] points = new PointF[5];
    for (int i = 0; i < points.Length; i++)
    {
        points[i] = new PointF(
            (float)(center.X + radius * Math.Cos(angle) * scales[i]),
            (float)(center.Y + radius * Math.Sin(angle) * scales[i]));
        angle += dtheta;
    }

    // Find the top intersection point.
    PointF top = new PointF(
        center.X,
        center.Y - radius * 0.8f);

    // Build the GraphicsPath.
    GraphicsPath path = new GraphicsPath();
    path.AddLine(points[4], points[1]);

    float tension = 1f;
    PointF[] curve1_points =
    {
        points[1],
        points[3],
        points[0],
    };
    path.AddCurve(curve1_points, tension);

    PointF[] curve2_points =
    {
        points[0],
        points[2],
        points[4],
    };
    path.AddCurve(curve2_points, tension);

    // Close the figure so the last corner is mitered.
    path.CloseFigure();

    return path;
}

The method first creates a scales array. The method uses the values in that array to define points arranged in a pentagon. The scale values move the pentagon’s points closer or farther from the heartagram’s circumference to give a good result.

[example]

The program uses the variable angle to loop over points that define the pentagon. Each angle is 2 π / 2 radians after the angle before it. As the program loops over the points’ angles, it multiplies the heartagram’s radius by the angle’s scale factor and the sine of cosine of the angle to get the point’s location. When the loop is finished, the points array holds the pentagon’s points. The picture on the right shows the pentagon with its corners labeled. Notice that the scale values result in a non-centered pentagon. The points must be moved away from the heartagram’s circumference to make the final shape’s corners just touch the circumference.

The program then creates a GraphicsPath object to hold the heartagram and adds the pieces of the shape to it. It first adds a line from point 4 to point 1.

Next, the code creates a smooth curve starting at point 1, moving to point 3, and ending at point 0. That creates the smooth lobe on the heartagram’s upper right.

The code then creates a second smooth curve starting at point 0, moving to point 2, and ending at point 4. That creates the left lobe.

[example]

The method calls the GraphicsPath object’s CloseFigure method to make the figure a closed curve. If you skip that step, the GraphicsPath treats this as an open curve so its other corners are mitered but the final corner is not as shown in the picture on the right.

Finally, the method returns the GraphicsPath object.

The program uses the following method to draw the heartagram’s background, enclosing ellipse, and the heartagram itself.

private void DrawHeartagram(Graphics gr, PointF center,
    float radius, Brush brush, Pen pen)
{
    gr.FillEllipse(brush,
        center.X - radius,
        center.Y - radius,
        2 * radius,
        2 * radius);
    gr.DrawEllipse(pen,
        center.X - radius,
        center.Y - radius,
        2 * radius,
        2 * radius);

    // Make a GraphicsPath to represent the heartagram.
    GraphicsPath path = MakeHeartagram(center, radius, pen.Width);

    // Draw the GraphicsPath.
    gr.DrawPath(pen, path);
}

This code fills and outlines an ellipse with the brush and pen that are passed into the method. In this example, the brush is a linear gradient brush and the pen is a thick, black pen.

After it draws the ellipse, the program calls the MakeHeartagram method described earlier to get a GraphicsPath that defines the heartagram. The code then draws that path with the pen.

Download the example to experiment with it and to see additional details.

The result is pretty good, but it’s not exactly the same as the shape drawn by Ville Valo. If you look closely at the heartagram drawn by this program, you’ll see that its edges curve in to the shape’s points. In other words, they are not straight. You can also see that the rounded lobes in this program’s heartagram are narrower than they are in heartagrams that you can find online. Both of those issues arise because this program uses a smooth curve to define the lobes. The heartagrams online seem to use circular arcs for the lobes.

In my next post, I’ll describe an improved version of the program that uses circular arcs for the heartagram’s lobes.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics | Tagged , , , , , , , , , | Leave a comment

Connect two line segments with a circular arc in C#


[circular arc]

Recently I wanted to make a circular arc that connected two line segments. The .NET DrawCurve method lets you connect points with a smooth curve relatively easily, but the curve is a spline and not a circular arc. (I’ll post examples showing both versions of the program I was making a bit later.) This example explains how you can find a circular arc that connects two line segments.

First suppose the line segments are tangent to some circle as shown in the following picture.


[circular arc]

In this case, you can use some geometry (which I’ll described later) to find the circular arc. Unfortunately if you let the user define the line segments by clicking with the mouse, it’s very likely that the line segments won’t be tangents of any circle. In that case you will need to extend one of the segments as shown in the following picture.


[circular arc]

Shortly I’ll show how you can extend one of the segments to make it tangent to a circle. However, first notice that it is not always possible to extend a segment to a tangent, at least in the way that we want. For example, the following picture shows two line segments that cannot be extended in a simple way so they become tangents of a circle. You can make these segments tangents, but not in the simple way that I want.


[circular arc]

I won’t talk further about these stranger arcs, although you could find them if you want to connect segments with that kind of circular arc.

The following three sections explain:

  • How to extend one of the segments so both segments are tangent to a circle
  • How to find the circle
  • How to find the circular arc
  • The program’s C# code

Extending a Segment

Extend the two line segments until they intersect as shown in the following picture.


[circular arc]

There are infinitely many circles tangent to the two extended line segments. The circle that we want is the one that is tangent at one of the original segments’ end points. That end point will be the one that is closer to the lines’ point of intersection (POI). The other tangent point is the same distance away from the POI, and its line segment must be extended to that point.

Here’s the algorithm for extending one of the line segments until they are both tangent to a circle.

  1. Find the POI between the two extended line segments.
  2. On each segment, find the end point that is closer to the POI.
  3. Of the two closer points, find the one that is closer to the POI. Call that point P1. Let D be the distance from point P1 to the POI.
  4. Find the point on the other extended line segment that is distance D from the POI. Call that point P2.

If the two line segments are parallel, then they do not intersect. You can still find a circular arc to connect the segments, but you’ll need to use a slightly different method, which I won’t cover here. Again, this isn’t the kind of circular arc I need.

Finding the Circle

Now that you have the points P1 and P2, the following picture shows how you can find the circle.


[circular arc]

The two segments are tangent to the circle so lines that are pependicular to those segments at the tangent points will pass through the center of the circle. Make two lines that are perpendicular to points P1 and P2, and find the intersection of those two lines. That is the circle’s center.

Now calculate the distance between the center and either point P1 or P2 to get the circle’s radius.

Together the circle’s center and radius define the circle.

Finding the Circlular Arc

After you know the circle’s center, you can calculate the start angle and sweep angle for the circular arc. (The DrawArc method uses a start angle and sweep angle so that’s what we need to find.)

First, subtract P1s X and Y coordinates from the coordinates of the circle’s center to get the distances dx and dy from the circle’s center to point P1. Now use Math.ATan2(dy, dx) to find the angle from the circle’s center to point P1.

Use similar steps to find the angle to point P2. Finally, subtract the two angles to get the sweep angle for the circular arc.

The circle’s center and radius gives us the bounding rectangle that should contain the circular arc. The start and sweep angles tell where the circular arc should start and how far it should extend. Those are all of the values that we need to draw the circular arc.

C# Code

STOP: Be sure you understand the previous sections before you look at the code. The code just implements the techniques described in the previous discussion, so it will be a lot easier to follow if you understand that discussion.

Saving Points

When you click on the program’s form, the following code springs into action.

private Listlt;PointF> Seg1Points = new List();
private Listlt;PointF> Seg2Points = new List();

// Save a new point.
private void Form1_MouseClick(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        if (Seg1Points.Count == 2) Seg1Points = new List();
        Seg1Points.Add(e.Location);
        Refresh();
    }
    else
    {
        if (Seg2Points.Count == 2) Seg2Points = new List();
        Seg2Points.Add(e.Location);
        Refresh();
    }
}

This code adds the clicked point to either the Seg1Points or Seg2Points list, depending on whether you left- or right-click. If the correct list already contains two points, the code replaces it with a new list. It then adds the new point and refreshes to redraw the picture.

Drawing the Picture

The following Paint event handler draws the picture.

private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

    // See if both segments are defined.
    if ((Seg1Points.Count == 2) &&
        (Seg2Points.Count == 2))
    {
        // Both segments are defined.
        // Find the arc.
        RectangleF rect;
        float start_angle, sweep_angle;
        PointF s1_far, s1_close, s2_far, s2_close;
        FindArcFromSegments(
            Seg1Points[0], Seg1Points[1],
            Seg2Points[0], Seg2Points[1],
            out rect, out start_angle, out sweep_angle,
            out s1_far, out s1_close, out s2_far, out s2_close);

        using (Pen thick_pen = new Pen(Color.Green, 2))
        {
            // Draw the revised segments.
            e.Graphics.DrawLine(thick_pen, s1_far, s1_close);
            e.Graphics.DrawLine(thick_pen, s2_far, s2_close);

            // Draw the arc.
            thick_pen.Color = Color.Red;
            e.Graphics.DrawArc(thick_pen, rect, start_angle, sweep_angle);

            // Draw the returned points that connect to the arc.
            e.Graphics.FillPoint(Brushes.Red, s1_far, 5);
            e.Graphics.FillPoint(Brushes.Red, s1_close, 5);
            e.Graphics.FillPoint(Brushes.Red, s2_far, 5);
            e.Graphics.FillPoint(Brushes.Red, s2_close, 5);
        }
    }
    else
    {
        // Both segments are not defined.
        using (Pen thick_pen = new Pen(Color.Green, 2))
        {
            // Draw the segments.
            if (Seg1Points.Count == 2)
                e.Graphics.DrawLine(thick_pen,
                    Seg1Points[0], Seg1Points[1]);
            if (Seg2Points.Count == 2)
                e.Graphics.DrawLine(thick_pen,
                    Seg2Points[0], Seg2Points[1]);
        }
    }

    // Draw the user-selected points. This will
    // overwrite all but one of the returned points.
    foreach (PointF point in Seg1Points)
        e.Graphics.FillPoint(Brushes.Green, point, 5);
    foreach (PointF point in Seg2Points)
        e.Graphics.FillPoint(Brushes.Green, point, 5);
}

The code first determines whether both lists contain two points. If they do, then you have fully specified both line segments so the code can connect them with a circular arc.

If both lists contain two points, then the code calls the FindArcFromSegments method to get the circular arc parameters. The code then draws the picture. First it draws the line segments returned by the FindArcFromSegments method. That method sets its s1_far, s1_close, s2_far, and s2_closeparameters to the updated end points of the two segments. Three of those points are points that the user clicked. The fourth is the one labeled P2 in the earlier picture that was moved so its segment was tangent to the circle.

Next the code draws the arc. This piece of code finishes by drawing dots at the revised segments’ end points. Note that the code uses the FillPoint extension method to draw the points. That extension method and a few others are useful but not very complicated or interesting, so I won’t describe them here. Download the example to see how they work.

If the two point lists are not both full, the program draws either of the two line segments that are defined.

The code finishes by drawing any points that were selected by the user. If all four points are defined, then that will draw over three of the four points drawn earlier; all except the one labeled P2 earlier.

FindArcFromSegments

The following FindArcFromSegments method performs the calculations described earlier.

// Find a circular arc connecting the segments.
// Return the arc's parameters. Also return new points
// to define the segments so you can draw
// s1_far -> s1_close -> arc -> s2_close -> s2_far.
// Three os those points will be original segments points.
private void FindArcFromSegments(
    PointF s1p1, PointF s1p2,
    PointF s2p1, PointF s2p2,
    out RectangleF rect,
    out float start_angle, out float sweep_angle,
    out PointF s1_far, out PointF s1_close,
    out PointF s2_far, out PointF s2_close)
{
    // See where the segments intersect.
    PointF poi;
    bool lines_intersect, segments_intersect;
    PointF close1, close2;
    FindIntersection(s1p1, s1p2, s2p1, s2p2,
        out lines_intersect, out segments_intersect,
        out poi, out close1, out close2);

    // See if the lines intersect.
    if (!lines_intersect)
    {
        // The lines are parallel. Find the 180 degree arc.
        throw new NotImplementedException("The segments are parallel.");
    }

    // Find the point on each segment that is closest to the poi.
    float close_dist1, close_dist2, far_dist1, far_dist2;

    // Make s1_close be the closer of the points.
    if (s1p1.Distance(poi) < s1p2.Distance(poi))
    {
        s1_close = s1p1;
        s1_far = s1p2;
        close_dist1 = s1p1.Distance(poi);
        far_dist1 = s1p2.Distance(poi);
    }
    else
    {
        s1_close = s1p2;
        s1_far = s1p1;
        close_dist1 = s1p2.Distance(poi);
        far_dist1 = s1p1.Distance(poi);
    }

    // Make s2_close be the closer of the points.
    if (s2p1.Distance(poi) < s2p2.Distance(poi))
    {
        s2_close = s2p1;
        s2_far = s2p2;
        close_dist2 = s2p1.Distance(poi);
        far_dist2 = s1p2.Distance(poi);
    }
    else
    {
        s2_close = s2p2;
        s2_far = s2p1;
        close_dist2 = s2p2.Distance(poi);
        far_dist2 = s1p1.Distance(poi);
    }

    // See which of the close points is closer to the poi.
    if (close_dist1 < close_dist2)
    {
        // s1_close is closer to the poi than s2_close.
        // Find the point on seg2 that is distance
        // close_dist1 from the poi.
        s2_close = PointAtDistance(poi, s2_far, close_dist1);
        close_dist2 = close_dist1;
    }
    else
    {
        // s2_close is closer to the poi than s1_close.
        // Find the point on seg1 that is distance
        // close_dist2 from the poi.
        s1_close = PointAtDistance(poi, s1_far, close_dist2);
        close_dist1 = close_dist2;
    }

    // Find the arc.
    FindArcFromTangents(
        s1_close, s1_far,
        s2_close, s2_far,
        out rect, out start_angle, out sweep_angle);
}

The code first calls the FindIntersection method to find the point where the lines containing the line segments intersect. You can learn about that method in my post Determine where two lines intersect in C#.

If the lines do not intersect, then the segments are parallel. You can write code to handle that situation if you like. This example just throws an exception.

Next, the method determines for each segment which end point is closer and which is farther from the POI. To do that, the code uses the Distance extension method. (Download the example to see how it works.)

The code then determines which of the segments’ close end points is closest to the POI. The distance from that point to the POI is the value D shown on the earlier pictures. The code moves the other segment’s close point so it is that same distance D from the POI. The code uses the PointAtDistance helper method described shortly to move the point.

Now the points s1_close, s1_far, s2_close, and s2_far define two segments that are tangent to the circle so we can find the arc. The code does that by calling the FindArcFromTangents method that is described in the following section. that method returns the values that this method (FindArcFromSegments) needs to return, so this method is finished.

The following PointAtDistancehelper method returns a point along a segment that is a specified distance away from the segment’s first end point. (The program uses this method to find a point distance D away from the segments’ POI.)

// Find a point on the line p1 --> p2 that
// is distance dist from point p1.
private PointF PointAtDistance(PointF p1, PointF p2, float dist)
{
    float dx = p2.X - p1.X;
    float dy = p2.Y - p1.Y;
    float p1p2_dist = (float)Math.Sqrt(dx * dx + dy * dy);
    return new PointF(
        p1.X + dx / p1p2_dist * dist,
        p1.Y + dy / p1p2_dist * dist);
}

This method calculates the horizontal and vertical distances dx and dy between the segment’s two end points. It divides those distances by the segment’s length and multiples by the desired distance from the first end point. The method adds the result to the first end point’s coordinates and that gives the desired point.

FindArcFromTangents

The following code shows the FindArcFromTangents method.

// Find the arc that connects points s1p2 and s2p2.
private void FindArcFromTangents(
    PointF s1_close, PointF s1_far,
    PointF s2_close, PointF s2_far,
    out RectangleF rect,
    out float start_angle, out float sweep_angle)
{
    // Find the perpendicular lines.
    PointF perp_point1, perp_point2;

    float dx1 = s1_close.X - s1_far.X;
    float dy1 = s1_close.Y - s1_far.Y;
    perp_point1 = new PointF(
        s1_close.X - dy1,
        s1_close.Y + dx1);

    float dx2 = s2_close.X - s2_far.X;
    float dy2 = s2_close.Y - s2_far.Y;
    perp_point2 = new PointF(
        s2_close.X + dy2,
        s2_close.Y - dx2);

    // Find the point of intersection between segments
    // s1_close --> perp_point1 and
    // s2_close --> perp_point2.
    bool lines_intersect, segments_intersect;
    PointF poi, close_p1, close_p2;
    FindIntersection(
        s1_close, perp_point1,
        s2_close, perp_point2,
        out lines_intersect, out segments_intersect,
        out poi, out close_p1, out close_p2);

    // Find the radius.
    float dx = s1_close.X - poi.X;
    float dy = s1_close.Y - poi.Y;
    float radius = (float)Math.Sqrt(dx * dx + dy * dy);

    // Create the rectangle.
    rect = new RectangleF(
        poi.X - radius,
        poi.Y - radius,
        2 * radius, 2 * radius);

    // Find the start, end, and sweep angles.
    start_angle = (float)(Math.Atan2(dy, dx) * 180 / Math.PI);
    dx = s2_close.X - poi.X;
    dy = s2_close.Y - poi.Y;
    float end_angle = (float)(Math.Atan2(dy, dx) * 180 / Math.PI);

    // Make the angle less than 180 degrees.
    sweep_angle = end_angle - start_angle;
    if (sweep_angle > 180)
        sweep_angle = sweep_angle - 360;
    if (sweep_angle < -180)
        sweep_angle = 360 + sweep_angle;
}

The method first finds two line segments that are perpendicular to the original segments at the points P1 and P2 as shown in the earlier pictures. If a segment has X and Y components <dx, dy>, then the two sets of components <dy, -dx> and <-dy, dx> define two perpendicular segments. The code finds the original vectors’ components and switches them to get perpendicular components. It adds those components to the two points P1 and P2 to get the ends of the desired perpendicular segments. For example, the first perpendicular segment starts at point s1_close and ends at point perp_point1.

Next, the method uses the FindIntersection method to see where the two perpendicular lines intersect. That point is the center of the circle.

Now the code finds the distance from the center of the circle to the point P1. that gives the circle’s radius. Now that we know the circle’s center and radius, we can define the bounding rectangle for the circular arc.

The last thing the method needs to do is to calculate the start and sweep angles. It uses Meth.Atan2 to calculate the start angle (for point P1) and the end angle (for point P2). It then sets the sweep angle equal to the difference.

Because we want to use the smaller arc around the circle, the code then checks that the sweep angle is less than 180 degrees. If the angle is greater than 180 degrees, the code resets it to that angle minus 360 to make the arc go the right direction around the circle.

Similarly if the sweep angle is less than -180 degrees, the code resets it to 360 plus the angle, again to make the arc go the right direction around the circle. (try commenting out those if statements and experimenting to see what they do.)

Summary

This program still only draws one kind of circular arc. For example, it doesn’t draw any of the kinds of circular arcs shown in the following picture.


[circular arc]

With some work, you may be able to use the techniques described in this post to find those other kinds of circular arc if you need them some day. Meanwhile, download the example program and give it a try.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics | Tagged , , , , , , , , | Leave a comment

Crop images to specific sizes in C#

[example]

Sometimes I need to crop images to specific sizes or aspect ratios. For example, the following list shows some recommended image sizes for Google Business.

  • Profile image – 250×250 pixels
  • Cover photo – 1080×608 pixels
  • Shared images – 497×373 pixels

Unfortunately if you use a program like MS Paint to crop the image, it’s hard to get a desired size that is nicely centered on an area of interest. This example shows how you can crop images by positioning a selection rectangle over a desired part of the image.

Use the File menu’s Open command to open an image file. Next, enter the width and height of the area that you want to select. Then click and drag the selection rectangle to select the part of the image that you want to keep. Finally, use the File menu’s Save As command to save the selected part of the image into a new file.

The following sections describe the main pieces of the program’s code.

Dragging the Selection Rectangle

The program uses the following variables to keep track of the selection rectangle.

private Rectangle SelectedRect = new Rectangle();
private bool Dragging = false;
private Point LastPoint;

The variable SelectedRect gtracks the currently selected rectangle. The value Dragging is true while a drag is in progress. The point LastPoint stores the last mouse position while a drag is in progress.

When you press the mouse button down on the picImage PictureBox, the following event handler executes.

// Start dragging.
private void picImage_MouseDown(object sender, MouseEventArgs e)
{
    // If the mouse is not inside the
    // selection rectangle, do nothing.
    if (!SelectedRect.Contains(e.Location)) return;

    // Start the drag.
    LastPoint = e.Location;
    Dragging = true;
}

This code calls the SelectedRect rectangle’s Contains method to determine whether the mouse’s location is inside the rectangle. If the mouse is not inside the rectangle, the method returns without doing anything.

If the mouse is inside the rectangle, the code saves the mouse location in variable LastPoint and sets Dragging to true.

If you move the mouse over the PictureBox, the following event handler executes.

// Continue dragging.
private void picImage_MouseMove(object sender, MouseEventArgs e)
{
    // See if we are dragging.
    if (Dragging)
    {
        // We are dragging. Move the selected rectangle.
        SelectedRect.X += e.Location.X - LastPoint.X;
        SelectedRect.Y += e.Location.Y - LastPoint.Y;
        LastPoint = e.Location;
        picImage.Refresh();
    }
    else
    {
        // We are not dragging. Update the cursor.
        if (SelectedRect.Contains(e.Location))
            Cursor = Cursors.SizeAll;
        else
            Cursor = Cursors.Default;
    }
}

This code checks the Dragging variable to see if a drag is in progress.

If a drag is in progress, the code subtracts the coordinates of the previous mouse location (stored in the variable LastPoint) from the mouse’s current location. It uses the difference in coordinates to update the upper left corner of the selection rectangle. It then saves the mouse’s current location in variable LastPoint and refreshes the PictureBox to display new selection rectangle.

If no drag is in progress, the code calls the selection rectangle’s Contains method to see if the mouse if over the currently selected rectangle. If the mouse is over the rectangle, the program displays the SizeAll cursor. If the mouse is not over the rectangle, the program displays the default cursor.

When you release the mouse button, the following event handler executes.

// Stop dragging.
private void picImage_MouseUp(object sender, MouseEventArgs e)
{
    Dragging = false;
}

This code simply sets Dragging to false to end the current drag.

Drawing the Selection Rectangle

The following code shows how the program’s PictureBox draws the selection rectangle.

// Draw the selection rectangle.
private void picImage_Paint(object sender, PaintEventArgs e)
{
    try
    {
        // Draw the selection rectangle.
        using (Pen pen = new Pen(Color.Red, 2))
        {
            e.Graphics.DrawRectangle(pen, SelectedRect);

            pen.Color = Color.Yellow;
            pen.DashPattern = new float[] { 5, 5 };
            e.Graphics.DrawRectangle(pen, SelectedRect);
        }
    }
    catch
    {
    }
}

This code creates a two-pixel-wide red Pen and uses it to draw the selection rectangle.

The code then changes the pen’s color to yellow, gives it a dash pattern, and redraws the rectangle with the modified pen. The result is the thick, dashed, red and yellow rectangle shown in the picture at the top of the post.

Saving the Selected Area

When you invoke the File menu’s Save As command, the following code executes.

// Save the selected area.
private void mnuFileSaveAs_Click(object sender, EventArgs e)
{
    if (sfdImage.ShowDialog() == DialogResult.OK)
    {
        try
        {
            // Copy the selected area into a new Bitmap.
            Bitmap bm = new Bitmap(
                SelectedRect.Width,
                SelectedRect.Height);
            using (Graphics gr = Graphics.FromImage(bm))
            {
                gr.DrawImage(picImage.Image, 0, 0,
                    SelectedRect, GraphicsUnit.Pixel);
            }

            // Save the new Bitmap.
            SaveImage(bm, sfdImage.FileName);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }
}

This code displays a SaveFileDialog to let you pick the file that should contain the selected part of the image. If you pick a file and click Save, the code creates a new Bitmap that is the same size as the rectangle and makes a Graphics object associated with the Bitmap. The code then uses the Graphics object’s DrawImage method to draw the selected part of the image on the Bitmap.

The code finishes by using the SaveImage method to save the bitmap into the file with the appropriate file format (JPG, PNG, BMP, etc.). For information on the SaveImage method, see my post Save images with an appropriate format depending on the file name’s extension in C#.

Resizing the Rectangle

If you change the value in the width or height text box, one of the following event handlers executes.

private void txtWidth_TextChanged(object sender, EventArgs e)
{
    // Get the new value.
    int value;
    if (!int.TryParse(txtWidth.Text, out value)) value = 0;
    SelectedRect.Width = value;
    picImage.Refresh();
}

private void txtHeight_TextChanged(object sender, EventArgs e)
{
    int value;
    if (!int.TryParse(txtHeight.Text, out value)) value = 0;
    SelectedRect.Height = value;
    picImage.Refresh();
}

These methods try to parse the width and height values. If a value is not a valid integer, the program sets that value to zero. It then sets the selection rectangle’s corresponding size accordingly and refreshes the PictureBox to show the resized rectangle.

Conclusion

This example lets you crop images to a specific size relatively easily. A nice enhancement would be to let you change the size of the selection rectangle while preserving its aspect ratio. Maybe I’ll add that feature some day if I’m really bored.

Download the example to experiment with it and to see additional details.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, image processing, tools | Tagged , , , , , , , , , , | Leave a comment

Draw on top of a background image in C#


[background image]

Drawing on a background image is actually very easy. It seems to be a common question on the internet, however, so I decided to make this example.

This example draws on a background image in two ways. If you just move the mouse around, it draws the smiley face shown above. If you click and drag the mouse, the program draws an area selection rectangle as shown in the following picture.


[background image]

All you need to do to draw on a background image is to set a PictureBox control’s Image property and then draw in its Paint event handler without calling the Graphics object’s Clear method. If that’s all you wanted to know, you’re done with this post. If you want to learn a bit more about what the example program does, read on.

The program does all of its drawing in a PictureBox control’s Paint event handler. Before I explain how that works, I’ll explain the mouse events that control the area selection rectangle. (If you only want to know how to draw on the background image, skip to the section “Drawing on a Background Image.”)

Mouse Events

The program uses the following variables to track the selection rectangle.

private bool SelectingArea = false;
private Point StartPoint, EndPoint;

The program sets the SelectingArea to true while you are selecting an area. The StartPoint and EndPoint values keep track of where you press the mouse button and the mouse’s current location.

When you press the mouse button over the PictureBox, the following event handler executes.

// Start drawing.
private void picCanvas_MouseDown(object sender, MouseEventArgs e)
{
    StartPoint = e.Location;
    EndPoint = e.Location;
    SelectingArea = true;
    this.Cursor = Cursors.Cross;

    // Refresh.
    picCanvas.Refresh();
}

This code saves the mouse’s location in the StartPoint and EndPoint variables. It then sets SelectingArea to true to indicate that an area selection is in progress. It sets the cursor to a crosshair and finishes by refreshing the PictureBox to make it redraw itself.

When you move the mouse, the following event handler executes.

// Continue drawing.
private void picCanvas_MouseMove(object sender, MouseEventArgs e)
{
    // Update the end point.
    EndPoint = e.Location;

    // Refresh.
    this.Refresh();
}

This code simply saves the mouse’s new position and refreshes the PictureBox to make it redraw itself. Note that this code does not check the SelectingArea variable; it executes whether of not the mouse is pressed. That allows the Paint event handler to draw either the smiley face or the selection rectangle.

When you release the mouse button, the following event handler executes.

// Continue drawing.
private void picCanvas_MouseUp(object sender, MouseEventArgs e)
{
    SelectingArea = false;
    this.Cursor = Cursors.Default;

    // Do something with the selection rectangle.
    Rectangle rect = MakeRectangle(StartPoint, EndPoint);
    Console.WriteLine(rect.ToString());
}

This code first sets SelectingArea to false to indicate that the area selection is over. It then calls the MakeRectangle helper method described shortly to create a rectangle determined by the start and end points. It then displays the rectangle in the Output window. If you really wanted to allow the user to select an area, you would make the program do something with the rectangle here.

The following code shows the MakeRectangle helper method.

// Make a rectangle from two points.
private Rectangle MakeRectangle(Point p1, Point p2)
{
    int x = Math.Min(p1.X, p2.X);
    int y = Math.Min(p1.Y, p2.Y);
    int width = Math.Abs(p1.X - p2.X);
    int height = Math.Abs(p1.Y - p2.Y);
    return new Rectangle(x, y, width, height);
}

This method finds the two points’ minimum X and Y coordinates and the area’s width and height. It then uses those values to create and return a new Rectangle.

Drawing on a Background Image

To draw on the background image, first set the PictureBox control’s Image property to the background image. You may also want to set its SizeMode property to AutoSize to make the control fit the image.

Now simply draw on the control in the control’s Paint event handler. The Paint event automatically resets the control’s image whenever it needs to redraw so you don’t need to clear the event handler’s Graphics object. In fact, if you call the e.Graphics.Clear method, you will erase the background image.

The following code shows the picCanvas control’s Paint event handler.

// Draw the selection rectangle.
private void picCanvas_Paint(object sender, PaintEventArgs e)
{
    if (SelectingArea)
    {
        using (Pen pen = new Pen(Color.Yellow, 2))
        {
            e.Graphics.DrawRectangle(pen,
                MakeRectangle(StartPoint, EndPoint));

            pen.Color = Color.Red;
            pen.DashPattern = new float[] { 5, 5 };
            e.Graphics.DrawRectangle(pen,
                MakeRectangle(StartPoint, EndPoint));
        }
    }
    else
    {
        DrawSmiley(e.Graphics, EndPoint, 50);
    }
}

If you are currently selecting an area, then the code creates a thick yellow pen. It uses the MakeRectangle helper method to define the currently selected rectangle and draws it.

Next, the code changes the pen’s color to red and gives it a {5, 5} dash pattern so it draws 5 units and then skips 5 units. (Here a unit is a multiple of the pen’s width, so in this example the pen draws 10 pixels and then skips 10 pixels.) The code redraws the rectangle with the new pen parameters to give it a yellow and red dashed appearance.

(Multi-colored dashed lines show up relatively well no matter what colors the background image has. The dash pattern’s origin also changes as the mouse moves, giving the rectangle making the “marching ants” effect and making it even easier to see.)

If you are not currently selecting an area, the event handler simply calls the DrawSmiley method to draw a smiley face at the mouse’s current location, which is stored in variable EndPoint. That method is straightforward so I won’t describe it here. Download the example program to see how it works.

That’s all there is to drawing over the background. Just draw without erasing anything.

Drawing on Forms

You can draw on a form’s background image much as you can draw on a PictureBox, but you need to deal with two potential problems: tiling and tearing.

Tiling

A form does not have an Image property so you can’t place the background image there. You can set the form’s BackgroundImage property to the image, but then by default the form tiles the image to fill itself with copies of the image. There are a few ways that you can handle that.

The easiest approach is to just let the form tile its background image and not worry about it.

A second approach is to size the form to fit its image and then set its FromBorderStyle property to prevent the user from resizing the form and displaying the tiled copies. If you like, you can use the following Load event handler to fit the form to its background image and make it non-resizable.

// Size the form to fit its bacground image.
private void Form1_Load(object sender, EventArgs e)
{
    ClientSize = BackgroundImage.Size;
    FormBorderStyle = FormBorderStyle.FixedDialog;
}

A third approach is to change the form’s BackgroundImageLayout property to prevent the form from tiling its background image. If you set this property to None, Center, Stretch, or Zoom, then the form won’t tile the image.

The fourth and final approach that I’ll mention is to place the image in a PictureBox that is on the form.

Tearing

If you modify the example program slightly so it draws on the form’s background, the result suffers from some extremely annoying image tearing. As you move the mouse, the image flickers madly, often displaying only part of the background image and pieces of the drawing. For example, the image might sometimes look like the following.


[background image]

Fortunately you can make this problem disappear if you simply set the form’s DoubleBuffered property to true. That makes the form buffer its image until the image is complete before it displays the result.

Summary

All you really need to do to draw on a background image is to set a PictureBox control’s Image property and then draw in its Paint event handler without calling the Graphics object’s Clear method. Download the example and give it a try. For some extra practice, remove the PictureBox, display the image on the form, and attach the form’s events to the provided event handlers to see what happens.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, image processing | Tagged , , , , , , , , , , | Leave a comment

Make a Word document with one picture on each page in C#


[Word document]

This example shows how you can make a program create a Word document that contains a picture on each page.

Before you start writing code, open the Add References dialog, click on the COM tab, and add a reference to “Microsoft Word 12.0 Object Library” (or whatever version you have installed on your system).

When you click the Select Pictures button, the program executes the following code.

// Let the user select the pictures.
private void btnSelectPictures_Click(object sender, EventArgs e)
{
    if (ofdPictures.ShowDialog() == DialogResult.OK)
        lstFiles.DataSource = ofdPictures.FileNames;
}

This code displays the OpenFileDialog named ofdPictures. If you select one or more files and click Open, the program sets the DataSource property for the lstFiles ListBox equal to the dialog’s FileNames array. That makes the ListBox display the names of the selected files.

You can then review the list of files and click the Create Document button to execute the following code.

// Create the Word document.
private void btnCreateDocument_Click(object sender, EventArgs e)
{
    // Get the Word application object.
    Word._Application word_app = new Word.ApplicationClass();

    // Make Word visible (optional).
    word_app.Visible = true;

    // Create the Word document.
    object missing = Type.Missing;
    Word._Document word_doc = word_app.Documents.Add(
        ref missing, ref missing,
        ref missing, ref missing);

    // Make one page per picture.
    object collapse_end = Word.WdCollapseDirection.wdCollapseEnd;
    object page_break = Word.WdBreakType.wdPageBreak;
    for (int i = 0; i < lstFiles.Items.Count; i++)
    {
        // Get the file's name.
        string filename = (string)lstFiles.Items[i];

        // Go to the end of the document.
        range = word_doc.Paragraphs.Last.Range;

        // Add the picture to the range.
        Word.InlineShape inline_shape =
            range.InlineShapes.AddPicture(
                filename, ref missing, ref missing,
                ref missing);

        // Add a paragraph.
        Word.Range range.InsertParagraphAfter();

        // Add a caption.
        FileInfo file_info = new FileInfo(filename);
        range.InsertAfter("Picture " +
            i.ToString() + ": " + file_info.Name);

        // If this isn't the last page, insert a page break.
        if (i < lstFiles.Items.Count - 1)
        {
            range.Collapse(ref collapse_end);
            range.InsertBreak(ref page_break);
        }
    }

    // Save the document.
    object doc_filename = Path.GetFullPath(
        Path.Combine(Application.StartupPath, "..\\..")) +
        "\\pictures.docx";
    word_doc.SaveAs(ref doc_filename, ref missing, ref missing,
        ref missing, ref missing, ref missing, ref missing,
        ref missing, ref missing, ref missing, ref missing,
        ref missing, ref missing, ref missing, ref missing,
        ref missing);

    // Close.
    object save_changes = false;
    word_doc.Close(ref save_changes, ref missing, ref missing);
    word_app.Quit(ref save_changes, ref missing, ref missing);

    MessageBox.Show("Done");
}

This code creates a Word application object (the Word server) and makes it visible. It then uses the server to create a new Word document.

Next the program loops through the file names in the program’s ListBox. Inside the loop, the program uses the document’s Paragraphs.Last.Range value to get a Range object that represents the end of the document.

The code then creates a new InlineShapes object holding the picture in the range. It adds a paragraph after the picture and then adds a text caption after that.

Next, if this is not the last picture, the program collapses the range so it represents the spot after the range and adds a page break.

After it has finished creating all of the pages, the program saves the Word document.

As is usually the case with Word automation, the hardest part is figuring out what Word objects and methods to use. Download the example to experiment with the program and to see additional details.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in Office, Word | Tagged , , , , , , , , | Leave a comment

See which section is under the mouse in a sunburst chart in C#


[sunburst chart]

My earlier post Make a sunburst chart in C#, Part 4 shows how you can draw a sunburst chart. This example shows how you can tell which section the mouse is over when you move the mouse over the chart. It also shows how you can draw a wedge for a “missing” element.

Finding the Wedge Under the Mouse

To keep track of the wedges, the program uses the following Wedge class.

public class Wedge
{
    public GraphicsPath Path;
    public Color FgColor, BgColor;
    public string Text;
    public bool IsHidden;

    public Wedge(GraphicsPath path, Color fg_color,
        Color bg_color, string text, bool is_hidden)
    {
        Path = path;
        FgColor = fg_color;
        BgColor = bg_color;
        Text = text;
        IsHidden = is_hidden;
    }

    // Return true if the Wedge contains this point.
    public bool ContainsPoint(Point point)
    {
        return Path.IsVisible(point);
    }

    // Return the Wedge's text.
    public override string ToString()
    {
        return Text;
    }
}

This class stores the GraphicsPath object used to draw the wedge. It also stores the wedge’s foreground and background colors, text, and an IsHidden value that indicates whether the wedge should be drawn. I’ll say more about that in the following section, which deals with hidden edges.

The wedge-finding code really only needs the Wedge class’s GraphicsPath object. The other values are just there so the program can display something. You could add other pieces of information to identify the wedge if you like. For example, you could add a Name property to the wedges in the XML data and then make the program save that data in the Wedge objects.

The Wedge class’s ContainsPoint method returns true if a given point lies within the wedge. To do that, it simply calls the GraphicsPath object’s IsVisible method and returns the result.

The last piece of the class overrides its ToString method to return the wedge’s text. Overriding this method is useful because it allows the debugger to display a Wedge object by using its text instead of the default, which is the class name.

The program uses the following code to define two objects used to keep track of the wedges.

// The items' wedges.
private List Wedges = null;

// The Wedge that is currently under the mouse.
private Wedge WedgeUnderMouse = null;

The Wedges list holds a Wedge object for each of the wedges. As you can probably guess, the WedgeUnderMouse field holds a reference to the wedge that is currently under the mouse.

When the program starts or is resized, it draws the wedges needed to make the sunburst chart. The previous version of the program just drew the chart’s wedges on a bitmap. The new version also creates a new Wedge object to represent each wedge. For example, it uses the following statement to create a Wedge for the chart’s root.

wedges.Add(new Wedge(path, fg_color, bg_color,
    XmlDoc.DocumentElement.Name,
    IsHidden(XmlDoc.DocumentElement)));

The program uses the following code to create a Wedge object for a child wedge.

// Make the item's wedge.
wedges.Add(new Wedge(path, fg_color, bg_color, text, is_hidden));

Most of the parameters to the Wedge class’s constructor were already found by the program’s previous version. The final parameter indicates whether the wedge should be drawn. I’ll say more about how that is used and the IsHidden method in the following section

When the mouse moves over the sunburst chart’s PictureBox, the following event handler executes.

// Display information about the wedge under the mouse.
private void picSunburst_MouseMove(object sender, MouseEventArgs e)
{
    // Find the wedge under the mouse.
    foreach (Wedge wedge in Wedges)
    {
        if (wedge.ContainsPoint(e.Location))
        {
            DisplayWedgeInfo(wedge);
            return;
        }
    }

    // We didn't find a wedge containing
    // the mouse. Clear the info.
    DisplayWedgeInfo(null);
}

This method simply loops through the objects in the Wedges list calling their ContainsPoint methods. If it finds an object that contains the mouse’s location, the method calls the DisplayWedgeInfo method to display that wedge’s information and then returns. If none of the Wedge objects contains the mouse’s location, the method calls the DisplayWedgeInfo method passing it null to clear any previously displayed information.

The following code shows the DisplayWedgeInfo method.

// If this is a new Wedge under the mouse,
// display its information.
private void DisplayWedgeInfo(Wedge wedge)
{
    // If the Wedge under the mouse has
    // not changed, do nothing.
    if (wedge == WedgeUnderMouse) return;
    WedgeUnderMouse = wedge;

    // See if the FgColor is Transparent.
    if ((wedge == null) || (wedge.IsHidden))
    {
        // It's null or Transparent. Clear the label.
        lblWedgeUnderMouse.Text = "";
    }
    else
    {
        // It's not Transparent.
        // Display the Wedge's information.
        lblWedgeUnderMouse.Text = wedge.Text.Replace("\\n", " ");
    }
}

This method compares its parameter to the previously displayed Wedge stored in the WedgeUnderMouse variable. If the new object is the same as the old one, then that object’s information is already shown so the method returns.

If the new Wedge object is different from the previously displayed one, the code saves the new object in the variable WedgeUnderMouse. Next, if the wedge is null or hidden, the method clears the text in the lblWedgeUnderMouse label.

If the wedge is not null and not hidden, the code displays its text in the label. The code also replaces the \n escape sequence with a space so it can display multi-line text.

Drawing Empty Wedges

One approach you can use to draw an empty wedge is to make an entry that has the same FgColor and BgColor values. Then the entry’s text and background have the same color, so you can’t see the text.

Unfortunately the program makes descendant elements inherit the foreground and background colors of their parents. If you give an element matching foreground and background colors, then its descendants inherit those colors. To make them visible, you would need to explicitly assign them new colors. That’s not the end of the world, but it removes the advantage of inherited colors and clutters the XML file.

To avoid those problems, I modified the program to recognize a new IsHidden attribute. For example, the following code shows the part of the example program’s XML data that defines the fruit section of the sunburst chart.

<Fruit BgColor="Orange" IsHidden="true">
  <Banana />
  <Peach />
  <Frog />
</Fruit>

The first line defines the Fruit element. It gives that element an orange background color so its children inherit that color. The Fruit element has IsHidden="true", so that element is marked as hidden.

When the program is loading an element, it uses the following IsHidden method to determine whether the wedge should be hidden.

// Return true if the wedge should be hidden.
private bool IsHidden(XmlNode node)
{
    if (node.Attributes["IsHidden"] == null) return false;
    return (bool.Parse(node.Attributes["IsHidden"].Value));
}

This method gets an XML node’s IsHidden attribute. If the attribute is not present, then the method returns false to indicate that the node should not be hidden. If the attribute is present, then the method parses it ass a Boolean value and returns the result.

The last interesting new piece in the example is where it decides whether it should draw a wedge. The DrawSunburstChild method fills the wedge’s GraphicsPath and draws its text. Before it does so, it checks whether the wedge should be hidden. For example, the following code snippet fills and outlines the wedge’s GraphicsPath.

// See if this wedge should be hidden.
bool is_hidden = IsHidden(node);

bg_color = GetNodeColor(node, "BgColor", default_bg_color);
if (!is_hidden)
{
    using (Brush brush = new SolidBrush(bg_color))
    {
        gr.FillPath(brush, path);
    }
    gr.DrawPath(Pens.Black, path);
}

This code calls the IsHidden method to see if the wedge should be hidden. It then gets the wedge’s background color. If the wedge should not be hidden, the code fills the wedge’s GraphicsPath and outlines it in black.

Summary

To determine the wedge under the mouse, the program uses a list of Wedge objects. When the mouse moves, it simply loops through the objects to see if any contain the mouse’s location. This is a simple and very versatile strategy that you can use to determine what the mouse is above in many programs.

To avoid drawing a wedge, the program allows its XML elements to have a new IsHidden attribute. When it processes an element, the code now saves that value in the Wedge class’s IsHidden field. It also uses the value to decide whether it should draw the wedge. You can use a similar technique to add other attributes to the XML elements so they can hold other information.

Because the program now uses Wedge objects to store information about the wedges, you could modify it to do other things to the wedges. For example, you could redraw any individual wedge to give it new colors, perhaps when the mouse moves over it, when the user clicks on it, or when the user selects the corresponding item in the TreeView control on the program’s left side.

This example only includes a few snippets of code because most of the code is the same as in the earlier post. See that post and download this example to see all of the code, to experiment with the program, and to learn about other details.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, drawing, graphics | Tagged , , , , , , , , , , , | 3 Comments

Let the user drag ListBox items in C#

[drag ListBox items]

This example uses drag-and-drop to allow the user to drag ListBox items from one position to another. To use the program, right-click on an item and then drag it to hits new position. If you drag an item beyond the last item in the list, the item is moved to the end of the list.

The techniques used here are actually relatively simple, although it can be hard to figure out exactly what you need to do.

AllowDrop

The first step is to set the ListBox control’s AllowDrop property to true. This is a very easy step to forget and, if you do, nothing happens and you’ll probably be left wondering why. This is such an important step that I decided to do it in the following Form_Load event handler instead of setting the property in the Form Designer.

private void Form1_Load(object sender, EventArgs e)
{
    lstAnimals.AllowDrop = true;
}

Tracking Data

Usually a program uses drag-and-drop to let the user drag text, images, or some other simple data from one control to another. It also usually doesn’t care where the data comes from or where it ends up. For this example, I want to add the following restrictions.

  • Allow only Move operations, not Copy or something else.
  • Do not drag something other than an item onto the ListBox.
  • Allow items to be dragged only to the control that started the drag.

The program uses the following DragItem class to keep track of the data and where it started.

public class DragItem
{
    public ListBox Client;
    public int Index;
    public object Item;

    public DragItem(ListBox client, int index, object item)
    {
        Client = client;
        Index = index;
        Item = item;
    }
}

This class simply stores the control that started the drag, the index where the dragged item began, and the item itself.

Notice that the Item field is a generic object. Many ListBox controls contain strings, but the list can actually hold any object. Using the object data type allows the DragItem object to represent any data.

The program uses the ListBox control’s MouseDown, DragEnter, DragOver, and DragDrop methods to control the drag.

MouseDown

When the user presses the right mouse button down over a ListBox item, the following code executes.

// On right mouse down, start the drag.
private void lst_MouseDown(object sender, MouseEventArgs e)
{
    ListBox lst = sender as ListBox;

    // Only use the right mouse button.
    if (e.Button != MouseButtons.Right) return;

    // Find the item under the mouse.
    int index = lst.IndexFromPoint(e.Location);
    lst.SelectedIndex = index;
    if (index < 0) return;

    // Drag the item.
    DragItem drag_item = new DragItem(lst, index, lst.Items[index]);
    lst.DoDragDrop(drag_item, DragDropEffects.Move);
}

This code first verifies that the user has pressed the right mouse button and exits if some other button is pressed.

Next, the code uses the control’s IndexFromPoint method to find the index of the item under the mouse. The code selects that item. Then if the index is -1, which indicates that the mouse is not over any item, the method exits.

Finally, if the user has pressed the right mouse button down over an actual item, the code creates a DragItem object holding the control, item index, and item. It then calls the control’s DoDragDrop method to start the drag.

DragEnter

When the drag enters the control, its DragEnter event fires. The event handler can decide whether the control should allow this kind of drop.

The following code shows the example’s DragEnter event handler.

// See if we should allow this kind of drag.
private void lst_DragEnter(object sender, DragEventArgs e)
{
    ListBox lst = sender as ListBox;

    // Allow a Move for DragItem objects that are
    // dragged to the control that started the drag.
    if (!e.Data.GetDataPresent(typeof(DragItem)))
    {
        // Not a DragItem. Don't allow it.
        e.Effect = DragDropEffects.None;
    }
    else if ((e.AllowedEffect & DragDropEffects.Move) == 0)
    {
        // Not a Move. Do not allow it.
        e.Effect = DragDropEffects.None;
    }
    else
    {
        // Get the DragItem.
        DragItem drag_item = (DragItem)e.Data.GetData(typeof(DragItem));

        // Verify that this is the control that started the drag.
        if (drag_item.Client != lst)
        {
            // Not the congtrol that started the drag. Do not allow it.
            e.Effect = DragDropEffects.None;
        }
        else
        {
            // Allow it.
            e.Effect = DragDropEffects.Move;
        }
    }
}

This method looks scarier but is really fairly simple. First, it checks that the drag is dragging a DragItem object. If this is some other kind of drag (for example, the user is dragging text or an image), the method sets e.Effect = DragDropEffects.None to not allow the drag on this control.

Next, the program determines whether the drag is a Move operation. If the drag is not a Move, the program does not allow it.

If the drag looks okay so far, the code then gets the DragItem object being dragged. If that object’s Client is different from the current control, the code again does not allow the drag.

Finally, if the drag contains DragItem data, is a Move, and was started by this control, then the code sets e.Effect = DragDropEffects.Move to allow the drag,

DragOver

The DragOver event occurs when the user moves the drag around on the control. The program can use that event to provide feedback such as showing where the item would land if dropped.

When the drag moves over the example’s ListBox, the following code executes.

// Select the item under the mouse during a drag.
private void lst_DragOver(object sender, DragEventArgs e)
{
    // Do nothing if the drag is not allowed.
    if (e.Effect != DragDropEffects.Move) return;

    ListBox lst = sender as ListBox;

    // Select the item at this screen location.
    lst.SelectedIndex =
        lst.IndexFromScreenPoint(new Point(e.X, e.Y));
}

This code first checks the current drag effect, which was set by the DragEnter method. If the effect is not Move, then this drag is not allowed so the code simply returns. It does not provide feedback because the drag is not allowed on this control.

If the effect is Move, the code gets the control that raised the event. It uses the IndexFromScreenPoint extension method described shortly to see what item is below the mouse and selects that item to highlight it.

The DragEventArgs object includes the mouse’s coordinates (e.X, e.Y) during the drag. Unfortunately those coordinates are relative to the screen instead of the current control. The following IndexFromScreenPoint extension method uses those coordinates to find the ListBox item under the mouse.

// Return the index of the item that is
// under the point in screen coordinates.
public static int IndexFromScreenPoint(this ListBox lst, Point point)
{
    // Convert the location to the ListBox's coordinates.
    point = lst.PointToClient(point);

    // Return the index of the item at that position.
    return lst.IndexFromPoint(point);
}

This method uses the ListBox control’s PointToClient method to convert the screen coordinates to the control’s coordinate system. It then uses the control’s IndexFromPoint method to get the index of the item beneath the mouse and returns that index.

Note that this method returns -1 if the mouse is below the last item in the ListBox.

DragDrop

When the user drops the drag on the control, the following event handler executes. Note that the event does not occur unless the drag is allowed. If the user drops the drag over a control that does not allow it, then the event does not occur.

// Drop the item here.
private void lst_DragDrop(object sender, DragEventArgs e)
{
    ListBox lst = sender as ListBox;

    // Get the ListBox item data.
    DragItem drag_item = (DragItem)e.Data.GetData(typeof(DragItem));

    // Get the index of the item at this position.
    int new_index =
        lst.IndexFromScreenPoint(new Point(e.X, e.Y));

    // If the item was dropped after all
    // of the items, move it to the end.
    if (new_index == -1) new_index = lst.Items.Count - 1;

    // Remove the item from its original position.
    lst.Items.RemoveAt(drag_item.Index);

    // Insert the item in its new position.
    lst.Items.Insert(new_index, drag_item.Item);

    // Select the item.
    lst.SelectedIndex = new_index;
}

This code gets the ListBox control that is catching the drop and the dragged DragInfo object. It calls the IndexFromScreenPoint method to get new_index, the index of the item on which the data is being dropped.

If new_index is -1, then the user is dropping beyond the last item in the ListBox. In that case, the code sets new_index to the index of the last item in the list so the item will be moved to the end.

The code then removes the item from its current position in the list and inserts it at the position below the mouse. The code finishes by selecting the dropped item.

Summary

You may have noticed that the event handlers shown here use their sender objects instead of hard-coded ListBox objects such as lstAnimals and lstFoods. That allows the event handlers to work with any number of controls. Both of this example’s ListBox controls use the same event handlers so the user can drag to rearrange items in either list. The DragEnter event handler prevents the user from dragging items from one list to another.

Download the example to see additional details and to experiment with it.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in controls, user interface | Tagged , , , , , , , , | Leave a comment

Let the user arrange ListBox items in C#

[ListBox items]

This example demonstrates one way that you can let the user arrange ListBox items. To rearrange the items, the user can click on an item and then use the program’s buttons to move the item to the top of the list, up one position, down one position, or to the bottom of the list.

When the user selects an item, the following code enables and disables the appropriate buttons.

// Enable and disable the appropriate buttons.
private void lstAnimals_SelectedIndexChanged(object sender, EventArgs e)
{
    btnUp.Enabled = (lstAnimals.SelectedIndex > 0);
    btnToTop.Enabled = btnUp.Enabled;
    btnDown.Enabled =
        (lstAnimals.SelectedIndex < lstAnimals.Items.Count - 1);
    btnToBottom.Enabled = btnDown.Enabled;
}

If the selected index is greater than 0 (the first item has index 0), then the item is not at the top of the list. In that case, the code enables the Move Up and Move To Top buttons.

If the selected index is less than the number of items minus one (the last item has index lstAnimals.Items.Count - 1), then the item is not at the end of the list. In that case, the code enables the Move Down and Move To Bottom buttons.

After it moves the item, the button selects it. That is less confusing than leaving no item selected, it lets the user click the buttons multiple times to move the item repeatedly, and it ensures that the item remains visible if it moves off the bottom or top of the visible list.

The basic approach that each button uses to move the selected item is the same: save the item’s index if necessary, save the item, remove the item from the list box, and insert the item in its new position.

The following code shows how the buttons work.

// Move the selected item to the top of the list (index 0).
private void btnToTop_Click(object sender, EventArgs e)
{
    object item = lstAnimals.SelectedItem;
    lstAnimals.Items.RemoveAt(lstAnimals.SelectedIndex);
    lstAnimals.Items.Insert(0, item);
    lstAnimals.SelectedIndex = 0;
}

// Move the selected item up one position.
private void btnUp_Click(object sender, EventArgs e)
{
    int index = lstAnimals.SelectedIndex;
    object item = lstAnimals.SelectedItem;
    lstAnimals.Items.RemoveAt(lstAnimals.SelectedIndex);
    lstAnimals.Items.Insert(index - 1, item);
    lstAnimals.SelectedIndex = index - 1;
}

// Move the selected item down one position.
private void btnDown_Click(object sender, EventArgs e)
{
    int index = lstAnimals.SelectedIndex;
    object item = lstAnimals.SelectedItem;
    lstAnimals.Items.RemoveAt(lstAnimals.SelectedIndex);
    lstAnimals.Items.Insert(index + 1, item);
    lstAnimals.SelectedIndex = index + 1;
}

// Move the selected item to the end of the list.
private void btnToBottom_Click(object sender, EventArgs e)
{
    object item = lstAnimals.SelectedItem;
    lstAnimals.Items.RemoveAt(lstAnimals.SelectedIndex);
    lstAnimals.Items.Add(item);
    lstAnimals.SelectedIndex = lstAnimals.Items.Count - 1;
}

That’s all there is to it. This method is cumbersome if you want to move ListBox items a long way through the list, but it’s easy enough if the list is short or if the user doesn’t need to move items very far.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in controls, user interface | Tagged , , , , , , , , , , | Leave a comment

Make a Pinterest-style diagonal picture montage in C#


[picture montage]

This example was inspired by a picture montage generated by Pinterest. Several months ago, my girlfriend and I started a bakery (see the website here), and a cool image appeared on our Pinterest page (which is here). Pinterest had taken random pictures from our page and arranged them in diagonal slices similar to those shown at the top of this post.

I thought that looked pretty interesting, so I decided to write this example to do something similar. I think it’s a pretty interesting example because it requires you to do several moderately (but not extremely) difficult things. The explanation is fairly long, but you should be able to handle it if you take it slowly in pieces. It’s also worth the effort because it will explain how to:

  • Rotate graphics
  • Size and crop an image to completely fill an area
  • Map points clicked by the user back to non-rotated coordinates
  • Determine whether a point lies within a rotated area

It’s also useful if you need to create a rotated picture montage.

The following section explains how to use the example program. The sections after that one explain how the program works.

Using the Program

Run the program and enter the following parameters:

  • Image width and height give the desired size for the picture whole montage.
  • Cell width and height indicate how wide and tall the rectangular picture cells should be.
  • Angle tells how many degrees the pictures should be rotated clockwise.

After you fill in those values, click Create to make a grid of empty cells similar to those shown in the following picture.


[picture montage]

If you want to change the grid parameters, you have to start over and will lose any pictures that you have already assigned. You can, however, change the width or color of the dividers between the cells without losing any picture assignments.

After you create the grid, click in a cell to open a file selection dialog. If you select an image file and click Open, the program displays that picture in the cell that you clicked. The program makes the picture as large as necessary to fill the width and height of the cell and then trims the picture to fit. Note that you can click on a cell again to change its picture. (Although I didn’t give the program the ability to clear a cell.)

After you have filled the cells the way you want to, use the File menu’s Save As command to save the picture montage. If you try to close the program or create a new grid and you have unsaved changes, the program asks if you want to save the changes.

Overview

This section provides a brief overview of the basic approach that the program uses to make a picture montage.

To make a rotated image, you first apply a rotation transformation to the Graphics object that you’re using to draw. You then draw the image non-rotated and the transformation rotates it. The following picture illustrates the idea.


[picture montage]

The program draws the cells on the left so they are oriented normally. The rotation transformation automatically tilts the cells to make the diagonal picture montage shown on the right.

The red dashed rectangle on the right shows the picture montage’s area. The dashed rectangle on the left shows the parts of the original image that were rotated to make the montage. The program doesn’t actually draw the red rectangle.

Notice that the grid cells on the left that become part of the picture montage are not nicely lined up in rows and columns. For example, they are not simply the cells in rows 1 through 3 and columns 2 through 7. Instead they are cells in different numbered rows for columns -1, 0, 1, 2, 3, 4, and 5.

One of the more interesting challenges for this program is figuring out which cells must be drawn to fill the dashed red rectangle. You could just draw a whole bunch of cells centered around the origin, but that would waste time.

We’re also going to need to be able to map mouse clicks to cells so you can click on a cell to assign its picture. To do that, we need to be able to map points in the picture on the right back to locations in the picture on the left. We can use that same method to figure out which cells we need to draw to fill the red dashed rectangle.

When the program draws a cell, it must decide how to scale the image to fill the cell. You could use any of the following approaches.

  • Draw the image in the cell’s upper left corner at full scale and truncate to fit.
  • Center the image at full scale and truncate to fit.
  • Stretch the image to fill the cell even if it distorts the image
  • Uniformly scale the image so it is as large as possible while still fitting in the cell, possibly leaving some blank space above/below or left/right of the image
  • Uniformly scale the image so it completely fills the cell, possibly truncating some of it on the top/bottom or left/right

This example uses the last approach. The following picture shows how a square picture would be scaled to fill a cell that was taller than it was wide.


[picture montage]

Here the original image was enlarged until it filled the height of the cell. The image was centered in the cell and its left and right sides were truncated to make it fit.

The last thing we need to do is draw the rectangles with rounded corners around the cells as shown in the picture at the top of the post. We can use techniques from an earlier post to do that.

Those are the basic tasks we need to handle to draw the picture montage. The following sections explain how the program accomplishes those tasks.

The Cell Class

The program uses a Cell class to store information about the cells. The following code shows the class’s main pieces of code.

class Cell
{
    public RectangleF Bounds;
    public Bitmap Picture = null;
    public Cell(RectangleF bounds)
    {
        Bounds = bounds;
    }

    // Draw the cell.
    public void Draw(Graphics gr, Pen pen,
        float cell_width, float cell_height)
    {
        // Draw the cell's picture.
        if (Picture != null)
        {
            // Find the part of the picture that we will draw.
            float pic_wid = Picture.Width;
            float pic_hgt = Picture.Height;
            float cx = pic_wid / 2f;
            float cy = pic_hgt / 2f;
            if (pic_wid / pic_hgt > Bounds.Width / Bounds.Height)
            {
                // The picture is too short and wide. Make it narrower.
                pic_wid = Bounds.Width / Bounds.Height * pic_hgt;
            }
            else
            {
                // The picture is too tall and thin. Make it shorter.
                pic_hgt = pic_wid / (Bounds.Width / Bounds.Height);
            }
            RectangleF src_rect = new RectangleF(
                cx - pic_wid / 2f, cy - pic_hgt / 2f, pic_wid, pic_hgt);

            // Draw the picture.
            PointF[] dest_points =
            {
                new PointF(Bounds.Left, Bounds.Top),
                new PointF(Bounds.Right, Bounds.Top),
                new PointF(Bounds.Left, Bounds.Bottom),
            };
            gr.DrawImage(Picture, dest_points, src_rect,
                GraphicsUnit.Pixel);
        }

        // Outline the cell.
        GraphicsPath path = MakeRoundedRect(Bounds,
            2 * pen.Width, 2 * pen.Width, true, true, true, true);
        gr.DrawPath(pen, path);
    }

    // Return true if the cell contains the point.
    public bool ContainsPoint(PointF point)
    {
        return Bounds.Contains(point);
    }

    // Draw a rectangle in the indicated Rectangle
    // rounding the indicated corners.
    private GraphicsPath MakeRoundedRect(...)
    {
        ...
    }
}

The class’s Bounds field indicates the rectangle where the cell’s picture should be drawn in the images on the left in the earlier pictures. The Picture field will hold the cell’s picture. Those are the only two things that a Cell object needs to know to draw itself.

The class’s constructor simply saves the cell’s bounds. It does not save a picture for the cell because this program assigns pictures later when you click on the cells.

The Draw method draws the cell. If the Picture field is not null, the code compares the picture’s aspect ratio (the width/height ratio) to the cell’s aspect ratio. If the picture is relatively short and wide compared to the cell’s bounds, the program makes the picture narrower. Similarly if the picture is relatively tall and thin compared to the cell’s bounds, the program makes the picture shorter.

After it has adjusted the picture’s width and height so it has the same aspect ratio as the cell, the program makes a rectangle of that size centered over the picture. that is the area that it will draw on the cell.

The code also makes an array of points holding the cell’s upper left, upper right, and lower left corners. That defines the area where we will draw the image. (Don’t blame me. A rectangle and three points is the goofy way that Microsoft decided you should specify where an image should be drawn.)

Finally, the program uses the rectangle and points to draw the cell. (The main program will apply a rotation transformation to the Graphics object before it calls this method so the cell will be rotated.)

After drawing the cell’s picture (if the Picture field isn’t null), the Draw method outlines the cell. It calls the MakeRoundedRect method described in my earlier post Draw rounded rectangles in C# to make a GraphicsPath that defines a rounded rectangle around the cell. See that post for details about how that method works. The code then draws the path with the Pen object that was passed into the method.

The Cell class’s ContainsPoint method returns true if the cell contains a particular point in non-rotated coordinates. To do that, it simply calls the Bounds rectangle’s Contains method passing it the point.

The Cell class stores a cell’s bounds and picture. The following section explains how the program builds the Cell objects that it needs.

Creating the Grid

The program uses the following variables to keep track of the grid’s geometry.

private int ImgWidth, ImgHeight;
private float CellWidth, CellHeight, Angle, DividerWidth;
private Color DividerColor;
private Matrix Transform = null, InverseTransform = null;
private List<Cell> Cells = null;
private bool DocumentModified = false;

The ImgWidth and ImgHeight values hold the dimensions of the whole picture montage. The CellWidth and CellHeight fields hold the size of the non-rotated cells. The value Angle indicates the angle by which the cells should be rotated.

The DividerWidth value holds the desired thickness of the dividers between the cells. As you can probably guess, DividerColor holds the desired divider color.

The Transform field is a Matrix that represents the desired rotation. The InverseTransform value represents the inverse of the transformation.

The first transformation maps the normal coordinate system to the rotated system as shown in the earlier picture that contained red dashed rectangles. The inverse transformation maps back from the rotated coordinate system to the original coordinate system. The first is useful for drawing the cells; the second is useful for figuring out where the user clicked on the rotated result.

The Cells field is a list that holds one Cell object for each of the cells that intersect the red dashed rectangle shown earlier.

Finally, the DocumentModified value indicates whether you have made changes to the picture montage since the last time you saved or created it. The program uses that value to decide whether it is safe to exit or to create a new picture montage. This is useful but it’s a bit off topic so I won’t cover it in detail. Download the example to see the details.

When you enter the grid parameters and click Create, the following code executes.

private void btnCreate_Click(object sender, EventArgs e)
{
    if (!DocumentIsSafe()) return;

    try
    {
        // Save the parameters.
        ImgWidth = int.Parse(txtWidth.Text);
        ImgHeight = int.Parse(txtHeight.Text);
        CellWidth = int.Parse(txtCellWidth.Text);
        CellHeight = int.Parse(txtCellHeight.Text);
        Angle = float.Parse(txtAngle.Text);
        DividerWidth = float.Parse(txtDividerWidth.Text);
        DividerColor = lblColor.BackColor;

        Transform = new Matrix();
        Transform.Rotate(Angle);
        InverseTransform = new Matrix();
        InverseTransform.Rotate(-Angle);

        // Make the cells.
        MakeCells();

        // Show the result.
        picCanvas.ClientSize = new Size(ImgWidth, ImgHeight);
        int margin = picCanvas.Left;
        int client_right = margin + Math.Max(
            picCanvas.Right, btnCreate.Right);
        int client_bottom = margin + picCanvas.Bottom;
        this.ClientSize = new Size(client_right, client_bottom);
        picCanvas.Visible = true;
        mnuFileSaveAs.Enabled = true;

        picCanvas.Refresh();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

This code first parses the values that you entered. It then creates a new Transform matrix and uses its Rotate method to make that matrix represent rotation through the desired angle. It similarly creates the InverseTransform matrix and makes it represent a rotation through the negative of the desired angle. (Alternatively you could copy the Transform matrix and then call its Invert method to invert the matrix. The inverse of a rotation is a rotation by the negative of the original angle, however, so this program just creates it directly.)

Next, the program calls the MakeCells method described in the following section to create the necessary Cell objects. It then sizes its picCanvas PictureBox to hold the picture montage and arranges the form to show the result. This code finishes by refreshing the picCanvas control to show the picture montage. That control’s Paint event handler is described in a later section.

The most interesting part of this code is its call to the MakeCells method described next.

Making Cells

The following code shows the MakeCells method, which creates the cells that intersect the red dashed rectangle shown in the earlier picture.

// Make the cells.
private void MakeCells()
{
    // Rotate the image's corners by -Angle degrees.
    PointF[] points =
    {
        new PointF(0, 0),
        new PointF(0, ImgHeight),
        new PointF(ImgWidth, ImgHeight),
        new PointF(ImgWidth, 0),
    };
    InverseTransform.TransformPoints(points);

    // Get the rotated image's bounds.
    float xmin = points[0].X;
    float ymin = points[0].Y;
    float xmax = xmin;
    float ymax = ymin;
    for (int i = 1; i < points.Length; i++)
    {
        if (xmin > points[i].X) xmin = points[i].X;
        if (xmax < points[i].X) xmax = points[i].X;
        if (ymin > points[i].Y) ymin = points[i].Y;
        if (ymax < points[i].Y) ymax = points[i].Y;
    }

    // Calculate the minimum and maximum rows
    // and columns that might be needed.
    int min_row = (int)(ymin / CellHeight) - 1;
    int max_row = (int)(ymax / CellHeight) + 1;
    int min_col = (int)(xmin / CellWidth) - 1;
    int max_col = (int)(xmax / CellWidth) + 1;

    // Make a GraphicsPath representing the rotated image bounds.
    GraphicsPath image_path = new GraphicsPath();
    image_path.AddPolygon(points);

    // Make a Graphics Object for use in IsEmpty.
    Graphics gr = CreateGraphics();

    // Loop over the possible rows and columns
    // and see which are actually needed.
    Cells = new List<Cell>();
    for (int row = min_row; row <= max_row; row++)
    {
        for (int col = min_col; col <= max_col; col++)
        {
            // See if this cell's rectangle intersects
            // the image's rotated bounds.
            Region rgn = new Region(image_path);
            float x = col * CellWidth;
            float y = row * CellHeight;
            if (Math.Abs(col % 2) == 1) y += CellHeight / 2f;
            RectangleF cell_rect = new RectangleF(
                x, y, CellWidth, CellHeight);
            rgn.Intersect(cell_rect);
            if (!rgn.IsEmpty(gr))
            {
                // Save this cell.
                Cells.Add(new Cell(cell_rect));
            }
        }
    }
    Console.WriteLine("# Cells: " + Cells.Count.ToString());
}

This method creates an array holding the points at the corners of the picture montage. The red dashed rectangle on the right side of the following picture shows the area that defines the picture montage.


[picture montage]

The code then calls the IntervseTransform object’s TransformPoints method to apply its transformation to those corner points. This maps the points from the rotated coordinate system on the right back to the non-rotated system on the left.

Next, the code loops through the transformed points to find their minimum and maximum X and Y coordinates. That gives a bounding area for the non-rotated points. The green dashed box on the left side of the preceding picture shows that bounding area.

The program also uses the minimum and maximum X and Y coordinates to calculate minimum and maximum row and column numbers for cells that might overlap the green dashed bounding area. It adds one to the maximums and subtracts one from the minimums so we are sure to get all of the cells that might overlap that area.

After all of this setup, the program is almost ready to start creating cells, but it still needs some additional values to help determine whether a cell actually intersects the red dashed rectangle. To detect those intersections, the program It makes a GraphicsPath representing the red dashed bounding rectangle’s transformed points (on the left). The program also makes a Graphics object for use with the IsEmpty method. (You’ll see how that works shortly.)

Now the program loops over the rows and columns that might intersect the green dashed area. It adds half of the cell height to the Y position of the cells in odd-numbered columns so they are offset vertically as shown in the picture on the left. (You could change this. For example, you could offset columns by a third, fourth, or some other fraction of the cell’s height to make other brick-like arrangements.)

For each row and column, the code determines whether the corresponding cell intersects the red dashed area. To do that, it creates a Region object that holds the GraphicsPath representing that area. (Remember the GraphicsPath we created earlier?) It also makes a rectangle holding the area occupied by the cell. It then calls the region’s Intersect method to make the region hold the intersection of its original contents (the red dashed rectangle) and the cell’s rectangle.

Finally, the program uses the region’s IsEmpty method to determine whether the result is empty. (Here’s where we use that Graphics object that we created earlier.) If the region is not empty, then the cell intersects the red dashed rectangle. In that case, the program creates a new Cell object to hold the cell’s bounds and adds it to the Cells list.

The method finishes by displaying the number of Cell objects that it created in the Console window so you can verify that it makes sense.

After the method finishes, the Cells list contains Cell objects representing cells that intersect the red dashed box in the preceding picture. After all of that work, it’s relatively easy to paint the picture montage and to let the user click on a cell to set its picture. The code that handles those tasks is described in the following sections.

Painting the Picture Montage

The picCanvas PictureBox control displays the picture montage. When it needs to refresh, the control’s Paint event handler simply calls the following DrawCells method.

private void DrawCells(Graphics gr)
{
    gr.SmoothingMode = SmoothingMode.AntiAlias;
    gr.InterpolationMode = InterpolationMode.High;
    gr.Clear(picCanvas.BackColor);

    gr.Transform = Transform;
    using (Pen pen = new Pen(lblColor.BackColor, DividerWidth))
    {
        foreach (Cell cell in Cells)
            cell.Draw(gr, pen, CellWidth, CellHeight);
    }
}

This method sets the Graphics object’s SmoothingMode property to produce smooth lines, sets InterpolationMode to resize images smoothly, and clears the drawing.

The program then sets the Graphics object’s Transform property to the Transform matrix that we created earlier. After this, any shapes that the program draws on the Graphics object are automatically rotated appropriately.

Next, the method creates a pen with the desired divider thickness and color. It then loops through the Cell objects in the Cells list and calls their Draw methods. The objects draw themselves and Robert’s your mother’s brother.

Handling Clicks

When the user clicks on the picture montage, the following event handler executes.

// Place a picture in this cell.
private void picCanvas_MouseClick(object sender, MouseEventArgs e)
{
    if (ofdCellPicture.ShowDialog() == DialogResult.OK)
    {
        try
        {
            // Find the clicked cell.
            // Inverse transform the clicked point.
            PointF[] points = { e.Location };
            InverseTransform.TransformPoints(points);

            // See which cell contains the inverted point.
            foreach (Cell cell in Cells)
            {
                if (cell.ContainsPoint(points[0]))
                {
                    cell.Picture = new Bitmap(ofdCellPicture.FileName);
                    DocumentModified = true;
                    break;
                }
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }

        picCanvas.Refresh();
    }
}

This code displays the file selection dialog named ofdCellPicture to let user pick an image file. If the user selects a file and clicks Open, the program creates an array named points that contains the point that the user clicked. It then calls the InverseTransform object’s TransformPoints method to map that point from the normal, rotated coordinates that the user sees to the pre-rotation coordinates. If you look at the following picture, the user clicks on the image on the right and the TransformPoints method maps that point to the corresponding location in the image on the left.


[picture montage]

The program then loops through the Cell objects in the Cells collection and calls each object’s ContainsPoint method to see if the point lies within that cell. If the point is inside the cell, the code loads the image file that the user selected and sets the cell’s Picture value to the resulting bitmap. The code then sets the DocumentModified value to true and breaks out of the loop.

The code finishes by refreshing the picCanvas control so the user can see the modified cell.

Summary

Hopefully you found the example interesting. It showed how you can do all of the following.

  • Rotate graphics (create a Matrix and apply it to the Graphics object, although note that this isn’t the only way you can do this)
  • Size and crop an image to completely fill an area (set the width or height to get the correct aspect ratio and then use DrawImage)
  • Map points clicked by the user back to non-rotated coordinates (use an inverse transformation Matrix to transform the clicked point)
  • Determine whether a point lies within a rotated rectangle (map the point back to non-rotated coordinates and then use the non-rotated rectangle’s Contains method)

The example program performs several other important tasks such as saving the picture montage into a file, ensuring that you don’t exit without saving your changes, letting the user click on the color sample to change the divider color, and drawing rounded rectangles. Download the example to see those details.

Feel free to experiment with the example. For example, try different angles of rotation or change the offsets between cells in adjacent columns. You could also let the user select multiple image files and then make the program place the images in random cells.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, image processing, tools | Tagged , , , , , , , , , , , , , | Leave a comment

Study a fascinating color illusion in C#


[color illusion]

This example shows how you can generate a fascinating color illusion that tricks your brain trick into thinking that a black and white picture is actually colored. All of the pictures shown above are black and white. All except the one on the lower right also contain colored lines or dots. Your brain uses the color information to infer a color for the black and white areas and produce the color illusion.

If you zoom in on the pictures, you can see that the areas between the colored lines or dots are actually black and white. Here’s a closeup of the picture on the upper left. You can clearly see that the picture is actually black and white.


[color illusion]

To create the color illusion, you can simply draw colored lines, dots, characters, or other small colored areas on top of the black and white image.

This example starts with a full-color image and creates a monochrome version of it. It then uses the color version to draw lines or dots on the monochrome version.

The program uses the following PlaceColorAreas to draw the lines or dots.

// Place areas of color on an image.
private Bitmap PlaceColorAreas(Bitmap bm, Bitmap brush_bm,
    AreaTypes area_type,
    float spacing, float thickness)
{
    // Make a monochrome copy of the image.
    Bitmap result = ToMonochrome(bm);

    // Draw colored stripes on the image.
    using (Graphics gr = Graphics.FromImage(result))
    {
        using (Brush brush = new TextureBrush(brush_bm))
        {
            using (Pen pen = new Pen(brush, thickness))
            {
                int wid = brush_bm.Width;
                int hgt = brush_bm.Height;

                switch (area_type)
                {
                    case AreaTypes.Stripes:
                        // Vertical and horizontal stripes.
                        for (float x = 0; x < wid; x += spacing)
                            gr.DrawLine(pen, x, 0, x, hgt);
                        for (float y = 0; y < hgt; y += spacing)
                            gr.DrawLine(pen, 0, y, wid, y);
                        break;

                    case AreaTypes.DiagonalStripes:
                        // Diagonal stripes.
                        wid *= 2;
                        hgt *= 2;
                        spacing *= (float)Math.Sqrt(2);
                        for (float i = 0; i < wid; i += spacing)
                            gr.DrawLine(pen, i, 0, 0, i);
                        for (float i = -hgt; i < hgt; i += spacing)
                            gr.DrawLine(pen, 0, i, hgt, i + hgt);
                        break;

                    case AreaTypes.Dots:
                        // Dots.
                        for (float x = -spacing / 2f; x < wid;
                                x += spacing)
                            for (float y = -spacing / 2f; y < hgt;
                                    y += spacing)
                                gr.DrawEllipse(pen, x, y,
                                    thickness, thickness);
                        break;

                    case AreaTypes.Text:
                        // Text.
                        using (Font font = new Font("Arial", spacing))
                        {
                            GraphicsUnit unit = GraphicsUnit.Pixel;
                            gr.DrawString(lorem_ipsum, font, brush,
                                brush_bm.GetBounds(ref unit));
                        }
                        break;
                }
            }
        }
    }
    return result;
}

This method first calls the ToMonochrome method to create a monochrome version of the original full-color image. You can read about that method in my earlier post Use an ImageAttributes object to convert an image to monochrome in C#.

Next, the code creates a Graphics object associated with the monochrome image. The program then makes a TextureBrush based on the full-color version of the picture. Anything that the program draws with this brush will copy pieces of the full-color image onto the monochrome version.

The method then uses the brush to create a pen with a desired thickness. Now lines drawn with the pen will show pieces of the TextureBrush, which are bits of the color image.

Now the method gets down to drawing. Depending on the area_type parameter, the method will draw horizontal/vertical stripes, diagonal stripes, dots, or text.

For the stripes, the code simply uses nested loops to draw the appropriate lines. To draw dots, the program uses nested loops to place dots in a grid across the picture.

To draw text, the program simply draws the text stored in the string named lorem_ipsum to it fills the bitmap’s bounds.

That’s about all there is to this example. Download it to experiment with the thickness and spacing of the lines or dots. You can also modify the code to try out filling the image with text. Or try performing the same color illusion on some other images.

[example]

You’ll find that the results are much duller than the original images. The monochrome areas remove a lot of the image’s brightness and the lines don’t restore all of it. For example, the original full-color picture that this example uses is shown on the right.

I tried several experiments that adjusted the colors. For example, I tried increasing the colored image’s brightness to make up for the reduced color in the monochrome areas. The things I tried were a lot of work but didn’t really improve the result very much so I removed them and went back to the basic code shown here.

For more information on this color illusion, see the article This Photo Is Black And White. Here’s The Science That Makes Your Brain See Colour.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics | Tagged , , , , , , , , , , | Leave a comment