Let the user scribble on a background image in C#

[scribble]

The post Let the user scribble on a PictureBox in C# shows how you can let the user draw with the mouse. This example lets you load a background image. It also lets you save the result into an image file.

Loading a Background Image

There are only two basic changes that you need to make to scribble on a background image. First, set the PictureBox control’s Image property to the image. Second, do not clear the control in the Paint event handler. This example also takes a few other actions to make the program more user-friendly.

When you use the File menu’s Load Background Image command, the following code executes.

private void mnuFileLoadBgImage_Click(object sender, EventArgs e)
{
    if (ofdBackground.ShowDialog() == DialogResult.OK)
    {
        picCanvas.SizeMode = PictureBoxSizeMode.AutoSize;
        picCanvas.Anchor =
            AnchorStyles.Top |
            AnchorStyles.Left;

        picCanvas.Image = LoadBitmapUnlocked(ofdBackground.FileName);
    }
}

This code displays a FileSelectionDialog. If you select an image file, the method sets the picCanvas control’s SizeMode property to AutoSize so it resizes to fit its picture. It also sets the control’s Anchor property to Top, Left so the control will not resize when the form resizes.

The code then displays the image in the PictureBox control’s Image property.

For information about the LoadImageUnlocked method, see Load images without locking their files in C#.

Starting a New Scribble

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

// Start a new drawing.
private void mnuFileNew_Click(object sender, EventArgs e)
{
    Polylines = new List<List<Point>>();
    picCanvas.Image = null;

    int margin = picCanvas.Left;
    int width = ClientSize.Width - 2 * margin;
    int height = ClientSize.Height - picCanvas.Top - margin;
    picCanvas.SizeMode = PictureBoxSizeMode.Normal;
    picCanvas.Size = new Size(width, height);
    picCanvas.Anchor =
        AnchorStyles.Top |
        AnchorStyles.Bottom |
        AnchorStyles.Left |
        AnchorStyles.Right;

    picCanvas.Refresh();
}

This code creates a new Polylines list to remove all of the old scribbles. It then sets the picCanvas control’s SizeMode property to Normal so it can resize when the form resizes. The code also resizes the control so it fills the form nicely. The event handler finishes by setting the control’s Anchor property so it resizes when the form resizes.

Drawing the Scribbles

The following code draws the scribbles.

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

    // Draw the polylines.
    foreach (List<Point> polyline in Polylines)
    {
        e.Graphics.DrawLines(Pens.Black, polyline.ToArray());
    }
}

This code is very similar to the code used by the previous version of the program. The only difference is that this version does not call e.Graphics.Clear before it draws. If it did call Clear, that would erase the image displayed in the background.

As you can see if you run the program, this example doesn’t need the call to Clear. I put it in the previous example mostly out of habit. You would need that call if the Paint event handler draws something that should move or resize when the PictureBox resizes. For example, suppose you want to draw an ellipse that just fills the PictureBox. Then you would need to call Clear before drawing so it can erase any previous ellipses that were drawn when the form had a different size.

Saving the Result

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

private void mnuFileSave_Click(object sender, EventArgs e)
{
    if (sfdResult.ShowDialog() == DialogResult.OK)
    {
        Bitmap bm = GetControlImage(picCanvas);
        SaveImage(bm, sfdResult.FileName);
    }
}

This code displays a SaveFileDialog. If you select a destination file and click Save, the code calls the GetControlImage method to get an image of the PictureBox. It then uses the SaveImage method to save the image with an appropriate file format. For example, if the file name’s extension is png, then SaveImage saves the image as a PNG file.

The following code shows the GetControlImage method.

// Return a Bitmap holding an image of the control.
private Bitmap GetControlImage(Control ctl)
{
    Bitmap bm = new Bitmap(ctl.Width, ctl.Height);
    ctl.DrawToBitmap(bm,
        new Rectangle(0, 0, ctl.Width, ctl.Height));
    return bm;
}

This code makes a bitmap large enough to hold the image. It then calls the control’s DrawToBitmap method to draw the control onto the bitmap and returns the result.

For more information on the SaveImage method, see the post Save images with an appropriate format depending on the file name’s extension in C#.

Conclusion

Displaying an image behind a drawing is fairly easy. Just set the PictureBox control’s Image property and don’t call Clear in the Paint event handler. This is an important technique and a lot of people don’t know about it, but the example demonstrates a couple of other useful techniques such as setting a PictureBox control’s SizeMode and Anchor properties.

Download the example to see additional details and to experiment with the program. For other examples that improve the basic scribble program, see these posts:


Download Example   Follow me on Twitter   RSS feed   Donate




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

Insert a table into a RichTextBox in C#

[table]

Someone recently asked me if I could write a program to insert a table in a RichTextBox control and give it all of the features of a table in Microsoft Word. The short answer is “no.” The RichTextBox control just doesn’t provide the support that you would need. You might be able to coerce it into a program into doing this, but I suspect it would be easier to build your own table editor from scratch rather than trying to cram one into a RichTextBox.

It would also be better to consider embedding another tool such as Word in the application. Or just using Word.

However, you can add a table to a RichTextBox with a little effort. You just won’t be able to interactively resize rows and columns, change cell alignments, add and remove cell borders, and everything else you would expect from a table editor.

Rich Text Format

One problem with Rich Text Format (RTF) is that it is, well, rich. It contains a lot of features so deciding which features to implement is difficult. To make matters worse, the RichTextBox control doesn’t seem to support all of the RTF features. Or at least I was unable to figure out how to use them.

A RTF table isn’t really a table; it’s just a sequence of rows. The syntax that I’m going to use for a row begins with \trowd to indicate that a row is starting.

The next entry is \trgaph followed by a measurement in twips. This measurement gives half of the amount of minimum space that you want between cells. A twip is 1440th of an inch. For example, \trgaph180 means there each cell should have a 180 twip or 180 / 1440 = 1/8 inch internal margin. Two adjacent cells each have an internal margin, so the contents of the cells are at least 1/4 inch apart. (All of those measurements are approximate, depending on your screen’s calibration.)

After the \trgaph element, the program includes a list of cell reaches. These are the distances from the left margin to the cell’s right edge, again in twips. Note that this is the text’s margin not the cell’s, so these are not cell widths. Each position consists of \cellx followed by the position. For example, \cellx1440 means the cell’s right edge is 1 inch from the left margin.

At this point, the program includes the data that should go inside the row’s cells. Each cell’s entry is written as \pard\intbl{<contents>}\cell. Here <contents> is whatever you want to put in the cell.

The row’s definition ends with \row.

The RtfTable Class

To make creating a table a bit easier, I made an RtfTable class. The following code shows the class’s fields and constructor.

public int InternalMargin = 180;
public int NumRows, NumCols;
public int[] ColumnWidths = null;
public string[,] Contents = null;

public RtfTable(int num_rows, int num_cols, int internal_margin)
{
    NumRows = num_rows;
    NumCols = num_cols;
    InternalMargin = internal_margin;
    ColumnWidths = Enumerable.Repeat(1440, NumCols).ToArray();

    Contents = new string[NumRows, NumCols];
    for (int r = 0; r < NumRows; r++)
        for (int c = 0; c < NumCols; c++)
            Contents[r, c] = "";
}

The InternalMargin value is the value used by the RTF \trgaph command. Obviously the NumRows and NumCols values give the numbers of rows and columns in the table.

The ColumnWidths array holds the widths of the columns in twips. (These are widths, not reaches as described earlier.)

Finally the two-dimensional Contents array holds the table’s contents.

The constructor saves the NumRows, NumCols, and InternalMargin values. It then uses the Enumerable class’s Repeat method to make a list containing the value 1440 once for each column. The code invokes the list’s ToArray method and saves the result in the ColumnWidths array.

The code then makes the Contents array, giving it the correct numbers of rows and columns. It finishes by looping through the rows and columns setting each cell’s value to an empty string.

The following method lets you set the table’s column widths.

public void SetColumnWidths(params int[] widths)
{
    for (int c = 0; c < NumCols; c++)
        ColumnWidths[c] = widths[c];
}

This method’s widths parameter uses the params keyword so it is a parameter list and can take any number of values. For example, the statement table.SetColumnWidths(1440, 770, 1440) passes the method three values.

The method simply loops through the widths values and copies them into the ColumnWidths array.

If the widths parameter contains fewer values than the number of table columns, then the last values in the ColumnWidths array remain unchanged. If the method receives too many values, the method crashes. (You can add code to check for that if you like.)

The last part of the following RtfTable class is the ToString method, which returns RTF codes to build the table.

public override string ToString()
{
    StringBuilder sb = new StringBuilder();
    string column_widths_string = ColumnWidthsString();

    for (int r = 0; r < NumRows; r++)
    {
        // Start the row.
        sb.Append(@"\trowd");
        sb.Append(@"\trgaph" + InternalMargin.ToString());

        // Column widths.
        sb.Append(column_widths_string);

        // Column contents.
        for (int c = 0; c < NumCols; c++)
        {
            sb.Append(@"\pard\intbl{" +
                Contents[r, c].Replace(@"\", @"\\") +
                @"}\cell");
        }

        // End the row.
        sb.Append(@"\row");
    }
    return sb.ToString();
}

The method first creates a StringBuilder object. Next the method calls the ColumnWidthsString method to get the column width commands and saves the result in a string. (I’ll describe that method shortly.)

The code then loops through the table’s rows. For each row, the program adds the \trowd command to the StringBuilder. It then adds the \trgaph command followed by the InternalMargin value. It then adds the column width string.

Next the code loops through the row’s columns and adds the value for this row and column to the StringBuilder. It begins each entry with \pard\intbl\{ and ends each entry with }\cell.

When it is done adding the row’s values, the method ends the row with \row.

After it has processed every row, the ToString method returns the text stored in the StringBuilder.

The following code shows the ColumnWidthsString method.

private string ColumnWidthsString()
{
    StringBuilder sb = new StringBuilder();
    int total = 0;
    for (int c = 0; c < NumCols; c++)
    {
        total += ColumnWidths[c];
        sb.Append(@"\cellx" + total.ToString());
    }
    return sb.ToString();
}

This method creates its own StringBuilder. It uses the variable total to keep track of total distance from the left margin to the right edge of the next column. After initializing total to 0, the code loops through the ColumnWidths array.

For each column, the code adds that column’s width to total. It then \cellx following by the new value in total to the StringBuilder.

After it has finished processing all of the columns, the method returns the string stored in the StringBuilder.

Using RtfTable

Having built an RTF string that defines a table, you need to insert it into the RichTextBox control. One way to do that is to remove the } at the end of the control’s RTF text, append the table’s code, and replace the } at the end. That works but it forces you to place the table at the end of the control’s contents.

Another approach would be to replace the control’s SelectedRtf value with the table’s code. Unfortunately I could not get that to work. If you figure out how to make it work, please post a note in the comments below.

A final technique would be to search for text within the control’s contents and insert the table code there. This example searches for the string “@@@” and replaces it with the table code.

The following code shows how the program inserts a sample table.

private void btnInsert_Click(object sender, EventArgs e)
{
    RtfTable table = new RtfTable(4, 3, 120);
    table.SetColumnWidths(720, 720, 720);

    for (int r = 0; r < 4; r++)
        for (int c = 0; c < 3; c++)
            table.Contents[r, c] =
                "(" + r.ToString() +
                ", " + c.ToString() + ")";

    // Insert the table at @@@.
    rtbTable.Rtf = rtbTable.Rtf.Replace("@@@", table.ToString());

    // Insert the table at the end.
    //rtbTable.Rtf =
    //    rtbTable.Rtf.Trim().TrimEnd('}') +
    //    table.ToString() +
    //    "}";
}

This code first creates an RtfTable object and initializes it to hold four rows and three columns with an internal margin of 180 twips. The code then uses the SetColumnWidths method to make every column 720 twips (1/2 inch) wide.

Next the program uses two nested loops to set the table’s contents. For example, the cell at row 3 column 1 contains the string (3, 1).

Having prepared the RtfTable object, the program gets the RichTextBox control’s RTF code and replaces the string @@@ with the table’s RTf code.

If you look at the code, you’ll see that it also contains a commented out statement that puts the table’s code at the end instead of replacing the string @@@.

Conclusion

This example creates a simple table and inserts it into the control. RTF supports many other features such as cell borders, different fonts, foreground and background colors, and more. The example doesn’t handle them.

This example also doesn’t provide extra features such as interactive column and row resizing. You might be able to add some of those features to the program, but it’s probably not worth the effort. If you need such advanced features, then you’re probably better off using Microsoft Word or some other word processor to create your documents.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in strings | Tagged , , , , , , , | Leave a comment

Draw a radar chart in C#

[radar chart]

A radar chart shows measurements along several axes that all radiate out from a common center. This lets you compare several properties for different objects at the same time. in a way, you can think of a radar chart as similar to a line graph that has been bent around so the X axis has bene condensed to a point.

The Main Idea

The main idea is relatively straightforward. Find the radar chart’s center point (cx, cy). Divide the circle into the right number of wedges for the desired number of properties that you want to graph. Then plot points with different radii from the center point in the directions of the wedges.

To be a bit more precise (but not too precise just yet), suppose you want to draw a radar chart with five properties or dimensions. Then we divide the circle into 360 / 5 = 72 degree wedges. You should draw each of the axes at 0, 72, 144, 216, and 288 degrees from the center of the radar chart. (This example actually rotates the angles back 90 degrees so the first axis is drawn vertically.)

Now suppose an object has the value R for a particular dimension and that dimension should be drawn at angle theta from the center of the radar chart. Then the corresponding point on the chart has coordinates (R * Cosine(theta), R * Sine(theta)).

You should pick the values R for the objects so they fill the drawing area nicely.

To draw the radar chart for a particular object, find the points for that object’s values and connect them to form a polygon.

If you make positive outcomes farther from the center, then better objects will produce larger polygons. Note that this does not mean you can blindly use the polygons’ areas to decide which object is better because the different dimensions may have different degrees of importance. For example, if you’re trying to decide which car to buy, price may be more important that number of cupholders. Still the radar chart lets you quicklyk compare the objects.

Storing Data

Probably the biggest issue when building this example is deciding how to store the data. This example uses two classes to store data about the objects and about the dimensions displayed on the chart.

The following CarData class holds data about cars.

public class CarData
{
    public string Name;
    public Color Color;
    public float[] Values;
    public PointF[] Points = null;

    public CarData(string name, Color color,
        params float[] values)
    {
        Name = name;
        Color = color;
        Values = (float[])values.Clone();
    }
}

This class simply holds a car’s name and values. The Values array holds the values. Notice that the class does not know what those values represent. That makes iterating over the values easier but would make using the class for other purposes harder. In this example the values are for luxury electric cars and store a car’s low-end price, high-end price, overall rating by Edmunds, range per charge, and miles per kilowatt-hour of charging. (I got the data from https://www.edmunds.com/electric-car/.)

The Points array holds the points that the program uses to draw the car’s radar chart. The program stores the points instead of generating them as needed so it can easily use them to display tooltips when the mouse hovers over one of the points.

The class’s constructor takes as parameters the car’s name, the color that should be used to draw the car’s radar chart, and a params array holding the values. The constructor simply saves the name and color. It then clones the values parameter and saves it in the object’s Values array.

The following AxisInfo class is even simpler.

public class AxisInfo
{
    public string Name, FormatString;
    public float Min, Max;

    public AxisInfo(string name, string format_string,
        float min, float max)
    {
        Name = name;
        FormatString = format_string;
        Min = min;
        Max = max;
    }
}

This class holds the name of an axis (such as PriceLow or Rating) and a format string that should be used to format values along the axis. (For example, price values should be formatted as currency.)

The class also stores Min and Max values that indicate how values on the axis should be scaled along the axis. For example, suppose the min and max values for a price axis are $40,000 and $100,000. Then the value $40,000 is mapped to the center of the radar chart and the value $100,000 is mapped to the end of the axis farthest from the center. You should select min and max so the axis’s values for different objects are spread out nicely. For example, if the actual car prices range from $60,000 to $70,000 and you set min = 0 and max = 100,000, then the cars’ values will be too close together to be very useful.

The AxisInfo class’s constructor simply saves the name, format string, min, and max values.

Initializing Data

When the program starts, the following code initializes the radar chart data.

// Initialize the car data.
// From https://www.edmunds.com/electric-car/.
private void Form1_Load(object sender, EventArgs e)
{
    Cars = new List<CarData>();
    Cars.Add(new CarData("Audi e-tron", Color.Red, 69850, 80900, 8.4f, 218, 100f / 44));
    Cars.Add(new CarData("Jaguar I-PACE", Color.Green, 39090, 44590, 8.2f, 234, 100f / 30));
    Cars.Add(new CarData("Polestar 2", Color.Blue, 59900, 59900, 8.2f, 275, 100f / 27));

    AxisInfos = new List<AxisInfo>();
    AxisInfos.Add(new AxisInfo("PriceLow", "c", 90000, 30000));
    AxisInfos.Add(new AxisInfo("PriceHigh", "c", 90000, 30000));
    AxisInfos.Add(new AxisInfo("Rating", "0.0", 0, 10));
    AxisInfos.Add(new AxisInfo("Range", "0", 0, 300));
    AxisInfos.Add(new AxisInfo("Miles/kWh", "0.00", 0, 5));
}

This code creates the Cars list and then adds three CarData objects to it to hold information about the three kinds of cars that the example uses. It then similarly creates a new AxisInfos list and adds AxisInfo objects to represent the five property axes that the program uses.

Notice that the min and max values for the PriceLow and PriceHigh axes are reversed. For example, the minimum PriceLow value is $90,000 and the maximum value is $30,000. Reversing those values makes the radar chart plot larger values close to the chart’s center and smaller values farther out. That makes all of the “good” values farther away from the center so it’s easier to tell which cars have better properties.

That’s the only really interesting setup. The remaining interesting pieces draw the radar chart and display tooltips.

Drawing the Radar Chart

It takes a few steps to draw the radar chart. The process starts when the picPlot PictureBox control receives a Paint event and executes the following event handler.

private void picPlot_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
    e.Graphics.Clear(picPlot.BackColor);

    // Draw the axes.
    DrawCharts(e.Graphics, chkFillAreas.Checked);
}

This code sets some Graphics object properties to produce smooth lines and text, clears the drawing, and then calls the following DrawCharts method.

// Draw the radar charts.
private void DrawCharts(Graphics gr, bool fill_areas)
{
    // Find the center and radii.
    float cx = picPlot.ClientSize.Width / 2f;
    float cy = picPlot.ClientSize.Height / 2f;
    float rx = cx - 20;
    float ry = cy - 20;

    // Find the angular distance between wedges.
    double dtheta = 2 * Math.PI / AxisInfos.Count;

    // Draw.
    DrawAxes(gr, cx, cy, rx, ry, dtheta);
    DrawLevels(gr, cx, cy, rx, ry, dtheta);
    DrawRadarCharts(gr, fill_areas, cx, cy, rx, ry, dtheta);
}

This code finds the center of the drawing area and calculates the chart’s X and Y radii, minus a margin. (If you want the chart’s drawing area to be square, make the two radii the same. For example, you can set them both equal to the smaller of the two, or ensure that the picPlot control is square.)

Next the code calculates the angular distance between the axes. This is simply 2π radians divided by the number of axes.

The method finishes by calling the DrawAxes, DrawLevels, and DrawRadarCharts methods described in the following sections to do the actual drawing.

DrawAxes

The following DrawAxes method draws the chart’s axes.

// Draw the axes.
private void DrawAxes(Graphics gr, float cx, float cy,
    float rx, float ry, double dtheta)
{
    double theta = -Math.PI / 2;
    using (Font font = new Font("Arial", 12))
    {
        for (int i = 0; i < AxisInfos.Count; i++)
        {
            double x = cx + rx * Math.Cos(theta);
            double y = cy + ry * Math.Sin(theta);
            gr.DrawLine(Pens.Black, cx, cy, (float)x, (float)y);

            x = cx + (rx + 10) * Math.Cos(theta);
            y = cy + (ry + 10) * Math.Sin(theta);
            DrawRotatedText(gr, font, Brushes.Black,
                AxisInfos[i].Name, x, y, theta + Math.PI / 2);

            theta += dtheta;
        }
    }
}

This code makes variable theta start at -π/2 radians (-90 degrees). That makes the first axis vertical and above the chart’s center. (Look at the picture at the top of the post.)

The code creates a font and then loops through the AxisInfos list.

For each axis, the method calculates the X and Y coordinates of the axis’s second end point. (The first end point is at the center.) It then draws a line between the center and the end point.

Next the code finds a point that lies 10 pixels beyond the axis end point. It then calls the DrawRotatedText method described next to draw the axis name at that point. The code adds &pi/2 radians (90 degrees) to the angle passed into the call to DrawRotatedText method to give the text the correct orientation.

The following code shows the DrawRotatedText method.

// Draw text rotated at the indicated point.
private void DrawRotatedText(Graphics gr, Font font,
    Brush brush, string text, double x, double y, double theta)
{
    GraphicsState state = gr.Save();
    gr.ResetTransform();

    gr.RotateTransform((float)(theta * 180 / Math.PI));
    gr.TranslateTransform((float)x, (float)y, MatrixOrder.Append);
    using (StringFormat sf = new StringFormat())
    {
        sf.Alignment = StringAlignment.Center;
        sf.LineAlignment = StringAlignment.Center;
        gr.DrawString(text, font, brush, 0, 0, sf);
    }

    gr.Restore(state);
}

The method first saves the Graphics object’s state and resets its transformation to remove any previous transformations. It then adds a rotation to give the text its desired orientation. It then translates the origin to the text’s desired final location (x, y).

The code then creates a StringFormat object to center text vertically and horizontally, and uses the DrawString method to draw the text at the origin. The StringFormat object centers the text over the origin. The Graphics object’s transformations then rotate the text and translate it to its desired final destination (x, y).

DrawLevels

The following DrawLevels method draws the black pentagons that show points 20%, 40%, 60%, 80% and 100% of the distances along each axis.

// Draw level polygons.
private void DrawLevels(Graphics gr, float cx, float cy,
    float rx, float ry, double dtheta)
{
    // Draw the level polygons.
    double theta = -Math.PI / 2;
    int num_levels = 5;
    double dfraction = 1.0 / num_levels;
    double fraction = dfraction;
    PointF[] points = new PointF[AxisInfos.Count];
    for (int level = 0; level < num_levels; level++)
    {
        for (int i = 0; i < AxisInfos.Count; i++)
        {
            double x = cx + fraction * rx * Math.Cos(theta);
            double y = cy + fraction * ry * Math.Sin(theta);
            points[i] = new PointF((float)x, (float)y);
            theta += dtheta;
        }
        gr.DrawPolygon(Pens.Black, points);
        fraction += dfraction;
    }
}

The method finds the number of levels that it should draw. It divides 1.0 by that number to get the amount by which each level should differ from the ones around it. This example shows five levels so they are separated by 0.2 times the length of the axes. initially the code sets the value fraction to the difference between the levels. (In this example, that’s 0.2.)

The code then loops through the levels. For each level, the method loops through the number of axes. For each axis, the code calculates the point along that axis that is fraction distance from the chart’s center to the end of the axis. It saves the point in the points array.

After it has found the points for all of the axes at this level, the program uses the points to draw a polygon. It then increases fraction so it is ready to draw the next polygon.

The method continues looping through the levels until it has drawn them all. If you like, you could skip the final level so the axes stick out past the outermost level polygon.

DrawRadarCharts

The following DrawRadarCharts method draws a radar chart for each car.

// Draw a radar chart for each car.
private void DrawRadarCharts(Graphics gr, bool fill_areas,
    float cx, float cy, float rx, float ry, double dtheta)
{
    // Plot the data.
    foreach (CarData car_data in Cars)
    {
        DrawRadarChart(car_data, gr, fill_areas, cx, cy, rx, ry, dtheta);
    }
}

This method simply loops through the car data and calls the following DrawRadarChart method for each car.

// Draw one car's radar chart.
private double DrawRadarChart(CarData car_data,
    Graphics gr, bool fill_areas,
    float cx, float cy, float rx, float ry, double dtheta)
{
    // Get this car's polygon.
    PointF[] points = new PointF[AxisInfos.Count];
    double theta = -Math.PI / 2;
    for (int i = 0; i < AxisInfos.Count; i++)
    {
        double frac =
            (car_data.Values[i] - AxisInfos[i].Min) /
            (AxisInfos[i].Max - AxisInfos[i].Min);
        double x = cx + frac * rx * Math.Cos(theta);
        double y = cy + frac * ry * Math.Sin(theta);
        points[i] = new PointF((float)x, (float)y);
        theta += dtheta;
    }

    // Save the points.
    car_data.Points = points;

    // Draw the polygon.
    if (fill_areas)
    {
        Color color = Color.FromArgb(64, car_data.Color);
        using (Brush brush = new SolidBrush(color))
        {
            gr.FillPolygon(brush, points);
        }
    }
    using (Pen pen = new Pen(car_data.Color, 3))
    {
        gr.DrawPolygon(pen, points);
    }
    return theta;
}

This method loops through the number of axes. For each axis, it gets the car’s corresponding Values entry and uses interpolation to see what fraction of the distances along the axis the value lies.

For example, if the value is close to AxisInfos[i].Min, then the fraction’s numerator is close to zero so the fraction is close to zero. If the value is close to AxisInfos[i].Max, then the fraction’s numerator is close to its denominator so the fraction is close to one.

Having found the fraction, the code uses it to find the corresponding point along the axis. (This is similar to the way the DrawLevels method worked except here the fraction varies with each of the car’s value. In the DrawLevels method the fraction was the same for every point at a particular level.)

After it has found all of the car’s points, the method saves them in the CarData object’s Points array for later use.

If the Fill Areas checkbox is checked, then the fill_areas parameter is true and the method creates a color that uses the car’s color but that has alpha (opacity) value value 64, making it largely transparent. The code uses the color to create a brush and then fills a polygon defined by the car’s points. The picture on the right shows the program displaying filled polygons.

[radar chart]

The method finishes by outlining the car’s polygon with a thick pen.

Displaying Toltips

When you move the mouse over the picPlot control, the following event handler executes.

// Display a tooltip if appropriate.
private void picPlot_MouseMove(object sender, MouseEventArgs e)
{
    string tip_text = "";
    foreach (CarData car in Cars)
    {
        // Skip this car if it has no points yet.
        if (car.Points == null) continue;

        // Check the car's points.
        for (int i = 0; i < car.Values.Length; i++)
        {
            if (PointIsClose(e.Location, car.Points[i], 8))
            {
                tip_text =
                    car.Name + " " +
                    AxisInfos[i].Name + ": " +
                    car.Values[i].ToString(AxisInfos[i].FormatString);
                break;
            }
            if (tip_text != "") break;
        }
    }

    if (tip_text != tipPoint.GetToolTip(picPlot))
        tipPoint.SetToolTip(picPlot, tip_text);
}

This code loops through the car data. For each car, it loops through the car’s points to see if the mouse’s position is close the the mouse. It does that by calling the PointIsClose method described shortly.

If PointIsClose method says that the mouse is close to one of the car’s points, the code composes some tooltip text and breaks out of its loops.

After the loops end, the event handler compares the new tooltip text to the picPlot control’s current tooltip. If the two are different, the code displays the new tooltip.

There are two interesting things to note here. First, the new tooltip text will be blank of the mouse is not near any of the cars’ points. That lets the program remove old tooltips.

Second, the code only updates the tooltip if it has changed. Once a tooltip is displayed, it remains unchanged even if the mouse moves slightly. To see the difference, comment out the statement if (tip_text != tipPoint.GetToolTip(picPlot)) and see what happens. You may prefer that effect.

The following code shows the PointIsClose method.

private bool PointIsClose(PointF point1, PointF point2, float radius)
{
    float dx = point1.X - point2.X;
    float dy = point1.Y - point2.Y;
    return (dx * dx + dy * dy) < (radius * radius);
}

This method simply calculates the distance squared between two points and returns true if the result is less than the given radius squared.

Conclusion

This example is somewhat complex, largely to support more advanced features like allowing you to easily change the names and number of axes, display more car objects, and show tooltips. Download the example to experiment with it and to see additional details.

Looking at the two pictures shown above, you can easily see that the Jaguar I-Pace covers the most area, so you may decide that it is the best choice. The Polestar 2 has slightly more area in the Range and Miles/kWh dimensions, so it may be better if you think those are much more important that price.

Strangely the Audi e-tron had worse scores on all dimensions except for the Edmunds overall rating. You’ll probably need to do some more reading to find out why Edmunds thought that was the better car even though it did worse on price, range, and Miles/kWh.


Download Example   Follow me on Twitter   RSS feed   Donate




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

Revised: Graph world total COVID-19 cases, deaths, and recoveries in C#


[COVID-19]

This post revises the example Graph world total COVID-19 cases, deaths, and recoveries in C#.

Ulrich Rosin left a comment on that post. He added a seven-day average. He also displayed tick marks for the beginning of each month instead of every day. The pandemic has been going on for quite a while since I posted the original example, so now the daily tick marks are too close together to be useful.

This revision makes two changes: it displays tick marks at the start of each month and it displays the names of the months.

Drawing Changes

The new version of the DrawAxes method uses the following code snippet to draw the tick marks and the month labels.

// Draw tick marks on the 1st of each month.
using (Font font = new Font("Arial", 10))
{
    using (StringFormat sf = new StringFormat())
    {
        sf.Alignment = StringAlignment.Near;
        sf.LineAlignment = StringAlignment.Far;

        for (int day_num = 0; day_num < num_cases; day_num++)
        {
            if (CountryData.Dates[day_num].Day == 1)
            {
                gr.DrawLine(pen, day_num, -tick_y, day_num, tick_y);

                state = gr.Save();
                gr.ResetTransform();

                string month =
                    CountryData.Dates[day_num].ToString("MMMyy");
                PointF[] p = { new PointF(day_num, 0) };
                Transform.TransformPoints(p);
                gr.DrawString(month, font, Brushes.Blue,
                    p[0].X, p[0].Y, sf);

                gr.Restore(state);
            }
        }
    }
}

This code creates a Font to draw the month names and a StringFormat to align them. It sets the StringFormat properties to draw text above and to the right of the drawing position.

Next the code loops through the days of data. It checks each day’s date, stored in the CountryData class’s Dates array, to see if the day is the first of a month. If this is the first of a month, the code draws its tick mark as it did in the previous version of the example.

The code then saves the current graphics state and resets the Graphics object’s transformations. Now any drawing will be done in pixels and not in the transformed coordinate system that the program uses to draw the tick marks and the data curves.

Next the program converts the date into a string with a format similar to SEP20.

The code then creates a point with X coordinate equal to the day number and Y coordinate equal to 0. That point represents the day’s location on the X axis. The code transforms the point using the drawing transformation to see where that point lies on the graph. It then calls the Graphics object’s DrawString method to draw the month string at the transformed location. The StringFormat object makes the text appear above and to the right of the point as shown in the picture at the top of the post.

Conclusion

The rest of the example is similar to the original version. See the original post to read about additional details. Click the Download button below to see how the rest of this program works.

Data

The following picture shows the data for the US, Spain, Italy, and the whole world when I posted the original version of this example.


[COVID-19]

If you compare this picture to the one at the top of this post, you’ll see that all of the numbers have continued to increase. That makes sense. The longer the pandemic continues, more people will be infected and therefore the more people per million will be infected.

The US now leads the pack in infections per million, but Spain is currently having a large increase and may eventually catch up to the US soon if nothing changes.

The US data is downward curving, which is good, but the numbers are still increasing quickly. Spain’s curve has taken an alarming upward turn.

It’s hard to tell what the world data is doing. It may also curve upward as Fall in the northern hemisphere begins.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics | Tagged , , , , , , , , , , , , , , , , | 1 Comment

Create button images in WPF and C#

[button images]

It seems like I’m constantly building programs like this one to create simple button images that I can use in other programs. This example uses XAML code to create some simple button images. The program’s code then saves those images in PNG files with transparent backgrounds.

XAML Code

The program’s XAML code starts with a Grid control that has two rows, one holding the Capture button and one holding a WrapPanel. The following code shows the WrapPanel.

<WrapPanel Grid.Row="1">
    <WrapPanel.Resources>
        <Style TargetType="Canvas">
            <Setter Property="Background" Value="Transparent" />
            <Setter Property="Width" Value="20" />
            <Setter Property="Height" Value="20" />
        </Style>
        <Style TargetType="Line">
            <Setter Property="Stroke" Value="Blue" />
            <Setter Property="StrokeThickness" Value="4" />
            <Setter Property="StrokeEndLineCap" Value="Round" />
            <Setter Property="StrokeStartLineCap" Value="Round" />
        </Style>
        <Style TargetType="Polyline">
            <Setter Property="Stroke" Value="Blue" />
            <Setter Property="StrokeThickness" Value="4" />
            <Setter Property="StrokeEndLineCap" Value="Round" />
            <Setter Property="StrokeStartLineCap" Value="Round" />
        </Style>
    </WrapPanel.Resources>
    <Canvas Name="canX">
        <Line X1="2" Y1="2" X2="16" Y2="16" StrokeThickness="3.5" />
        <Line X1="2" Y1="16" X2="16" Y2="2" StrokeThickness="3.5" />
    </Canvas>
    <Canvas Name="canPlus">
        <Line X1="2" Y1="10" X2="18" Y2="10" />
        <Line X1="10" Y1="18" X2="10" Y2="2" />
    </Canvas>
    <Canvas Name="canUp">
        <Polyline Points="2,14 10,6 18,14" />
    </Canvas>
    <Canvas Name="canDown">
        <Polyline Points="2,6 10,14 18,6" />
    </Canvas>
    <Canvas Name="canPencil">
        <Line X1="2" Y1="18" X2="13" Y2="7"
            StrokeThickness="6"
            StrokeStartLineCap="Triangle"
            StrokeEndLineCap="Flat"/>
        <Line X1="14" Y1="6" X2="17" Y2="3"
            StrokeThickness="6"
            StrokeStartLineCap="Flat"
            StrokeEndLineCap="Flat"/>
    </Canvas>
</WrapPanel>

This code begins with a resource dictionary that defines styles used by the Canvas, Line, and Polyline objects that are contained in the WrapPanel. Defining those values at this higher level makes the button images use similar sizes and styles. In this example, the images are 20 x 20 pixels and use thick, blue lines with rounded end caps.

After the resources, the WrapPanel contains a group of Canvas objects that hold the button images. These are relatively simple. For example, the following code shows how the program defines the first image, which contains an X.

<Canvas Name="canX">
    <Line X1="2" Y1="2" X2="16" Y2="16" StrokeThickness="3.5" />
    <Line X1="2" Y1="16" X2="16" Y2="2" StrokeThickness="3.5" />
</Canvas>

This code simply uses two Line objects with defined end points (X1, Y1) and (X2, Y2).

When I ran an earlier version of the program, these diagonal lines appeared slightly thicker than the horizontal lines used by the other button images. They really weren’t any thicker, but I think aliasing made them appear that way. I changed the thickness of the lines in this image to produce a more consistent-looking result.

If I were building these images in C# code rather than XAML, I would calculate the end points for the lines so they would be appropriate if you were to resize the Canvas objects. Unfortunately XAML code isn’t designed to perform calculations.

You could define key values such as 2 and 16 in the resource dictionary and then use those values when creating the images. That would work, but it would make the code more complicated and wouldn’t make this example all that much more flexible, so I decided to stick with this simpler approach.

C# Code

When you click the Capture button, the following code executes.

// Save the button images.
private void btnCapture_Click(object sender, RoutedEventArgs e)
{
    try
    {
        // Save the files.
        SaveControlImage(canX, "x.png");
        SaveControlImage(canPlus, "plus.png");
        SaveControlImage(canUp, "up.png");
        SaveControlImage(canDown, "down.png");
        SaveControlImage(canPencil, "pencil.png");

        MessageBox.Show("Done");
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

This code simply calls the SaveControlImage method for each of the program’s Canvas controls to save their images into PNG files.

For a description of the SaveControlImage method, see the post Draw a smiley face with WPF in C#.

Conclusion

This program lets you draw simple button images and save them into PNG files. You can then use them on buttons in other programs. To make different images, you’ll need to modify the XAML code. You may also want to create new images, in which case you may need to add more Canvas controls to the program.

Still, this program should give you a pretty good head start on creating similar button images. I expect it to save me a lot of time in the future.


Download Example   Follow me on Twitter   RSS feed   Donate




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

Overlay parts of images in C#

[overlay]

My previous post Brighten pixels in an image in C# lets you brighten certain pixels in an image. Unfortunately is often brightens pixels outside of the area that you want to modify.

For example, in the left side of the picture the raspberries are bright red, but so are parts of the box containing the cake. It would be nice if the previous example only brightened the raspberries and not the parts of the box. (I greatly exaggerated the redness to make the idea more obvious. In practice you would not want the raspberries so extremely red.)

One solution would be to let you select the parts of the image that you wanted to brighten. That would work, but it might be hard. You would need to be able to determine whether a pixel was inside the selected area, which could be hard of the area doesn’t have a simple shape like a rectangle. You might also want to be able to select multiple parts of the image to affect. Even if you did get that all to work the way you wanted it, you would need to apply the same technique to any other programs that you made that modify an image’s pixels.

Rather than taking that approach, I decided to make this example, which lets you overlay parts of one image on another. You load the modified overlay picture (in this case, the one with the reddened raspberries) on the left and the original image on the right. You select the area on the overlay image that you want to use and then you click the button to copy that area onto the picture on the right. You can repeat those steps several times to copy different pieces of the overlay image onto the original image.

This example was designed for exactly the purpose shown here: to copy an area in an overlay picture onto the corresponding area on the original picture. You can’t use it to move the overlay area somewhere else.

The following sections describe the most interesting pieces of this example.

Selecting the Area

The program lets you select an irregular area much as other programs do using MouseDown, MouseMove, and MouseUp event handlers. The big difference is that this example assumes that the image you are viewing may have been scaled. To keep track of the selected area, the program uses two lists of points, one at 1:1 scale and one at the currently selected scale.

The following code shows the variables that the program uses to select an area.

private bool Drawing = false;
private List<PointF> SelectedArea = new List<PointF>();
private List<PointF> ScaledSelectedArea = new List<PointF>();

The Drawing variable is true while you have the mouse down and are drawing to select an area. The SelectedArea list holds the area’s points in 1:1 coordinates. The ScaledSelectedArea list holds the area’s points at the current scale.

When you press the mouse down over the overlay picture, the following event handler executes.

// Start selecting an area.
private void picOverlay_MouseDown(object sender, MouseEventArgs e)
{
    if (picOverlay.Image == null) return;

    Drawing = true;

    ScaledSelectedArea = new List<PointF>();
    ScaledSelectedArea.Add(e.Location);

    SelectedArea = new List<PointF>();
    SelectedArea.Add(new PointF(e.X / ImageScale, e.Y / ImageScale));

    picOverlay.Refresh();
}

This code simply returns if you have not loaded a picture. If you have loaded a picture, it sets Drawing to true so the MouseMove event handler knows that you are drawing a selection area.

Next the code creates a new ScaledSelectionArea list and adds the current point to it. It then creates a new SelectionArea list, unscales the current point, and adds the result to the list.

For example, suppose the current scale is 0.5 so then the image is being displayed at half its true size. Then a pixel on the PictureBox represents two pixels on the full-scale image. If the point is at (10, 30) on the scaled image, then the corresponding point on the full-scale image is at (10 / 0.5, 30 / 0.5) = (20, 60).

After it saves the new point, the code refreshes the PictureBox so it can redraw itself. In particular, that removes any previous selection area from the picture.

When you move the mouse over the overlay picture, the following event handler executes.

// Continue selecting an area.
private void picOverlay_MouseMove(object sender, MouseEventArgs e)
{
    if (!Drawing) return;
    ScaledSelectedArea.Add(e.Location);
    SelectedArea.Add(new PointF(e.X / ImageScale, e.Y / ImageScale));
    picOverlay.Refresh();
}

If you are not correctly drawing a selection area, the event handler simply returns. If you are drawing a selection area, then the code adds the current point to the ScaledSelectionArea list and adds the unscaled point to the SelectedArea list.

The code finishes by refreshing the PictureBox to make it display the current selection area.

When you move the mouse over the overlay picture, the following event handler executes.

// Finish selecting an area.
private void picOverlay_MouseUp(object sender, MouseEventArgs e)
{
    Drawing = false;
    picOverlay.Refresh();
    btnCopy.Enabled = (SelectedArea.Count > 2);
}

This code sets Drawing to false so future MouseMove events know that you are not drawing a selection area. It then refreshes the PictureBox and, if the selection area is defined by at least three points (so it is (probably) not empty), the code enables the Copy button.

Those event handlers let you define the selection area. The following section explains how the program draws that area.

Drawing the Selection Area

The following code shows how the picOverlay control draws the selection area.

// Draw the selection area.
private void picOverlay_Paint(object sender, PaintEventArgs e)
{
    if (SelectedArea.Count < 3) return;

    // Draw the selection area.
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    PointF[] points = ScaledSelectedArea.ToArray();
    using (Pen pen = new Pen(Color.Red, 2))
    {
        if (Drawing) e.Graphics.DrawLines(pen, points);
        else e.Graphics.DrawPolygon(pen, points);

        pen.Color = Color.Yellow;
        pen.DashPattern = new float[] { 5, 5 };

        if (Drawing) e.Graphics.DrawLines(pen, points);
        else e.Graphics.DrawPolygon(pen, points);
    }
}

If the SelectedArea list contains fewer than three points, then it cannot define an area so the code simply exits. Otherwise the program sets the Graphics object’s SmoothingMode.

Next the code converts the ScaledSelectedArea list into an array. It uses the scaled list because it is about to draw on the scaled picture.

The code then creates a thick red pen. If you are currently drawing the selection area, the program draws it as a sequence of lines. If you are not drawing a selection area, then the program draws the area as a polygon.

The code then changes the pen to a dashed yellow pen and draws the area again. The result is a thick red and yellow dashed outline of the area.

Notice that the Paint event handler does not clear the Graphics object and it does not draw the overlay image. The program sets the picOverlay object’s Image property to the overlay image, so it is automatically drawn before the Paint event handler executes. If the event handler called the Graphics object’s Clear method, it would erase the image.

Rescaling the Images

The code shown so far works as long as you don’t change the image’s scale. If you rescale the image, the SelectedArea list’s points are still valid because they are stored at full scale. However, the points in the ScaledSelectedArea list are no longer correct because they were saved at a different scale.

When you select one of the Scale menu’s commands, the following event handler executes to handle this problem.

private void mnuScale_Click(object sender, EventArgs e)
{
    // Get the scale factor.
    ToolStripMenuItem menu_item = sender as ToolStripMenuItem;
    string scale_text = menu_item.Text.Replace("&", "").Replace("%", "");
    ImageScale = float.Parse(scale_text) / 100f;
    ShowScaledImages();

    // Display the new scale.
    mnuScale.Text = "Scale (" + menu_item.Text.Replace("&", "") + ")";

    // Check the selected menu item.
    foreach (ToolStripMenuItem item in mnuScale.DropDownItems)
    {
        item.Checked = (item == menu_item);
    }

    // Scale the selected area.
    ScaleSelectionArea();
}

This code first gets the menu item that raised the Click event. It parses the menu item’s text to see learn the desired scale factor and saves it in variable ImageScale.

The code then calls the ShowScaledImages method to display the overlay and original images at the desired scale. That method is relatively straightforward, so I wont’ show it here. Download the example to see the details.

The Click event handler then loops through the Scale menu’s items to check the one that was selected and uncheck the others.

The event handler finishes by calling the following ScaleSelectionArea method to scale the points that define the selection area.

// Scale the selection area's points and
// save them in the ScaledSelectedArea list.
private void ScaleSelectionArea()
{
    ScaledSelectedArea = new List();
    foreach (PointF point in SelectedArea)
    {
        ScaledSelectedArea.Add(new PointF(
            point.X * ImageScale,
            point.Y * ImageScale));
    }
}

This method creates a new ScaledSelectionArea list. It then loops through the points in the SelectedArea list, scales them, and saves the scaled results into the new ScaledSelectionArea list.

Making the Overlay

The previous section explains how the example lets you select an overlay area. Now it’s time to use that area to perform the overlay.

You could loop through the pixels in the overlay picture, decide if a pixel is in the selected area, and then copy it onto the original picture. That would work, but it would be slow and difficult.

Fortunately there’s a much easier way. Simply use the overlay image to create a TextureBrush and then use that brush to fill the selected area on the original image.

The following code does that when you click the Copy button.

// Copy the selected area from the overlay
// image onto the background image.
private void btnCopy_Click(object sender, EventArgs e)
{
    if (SelectedArea.Count < 3) return;
    btnCopy.Enabled = false;
    mnuFileReset.Enabled = true;

    using (Graphics gr = Graphics.FromImage(MainImage))
    {
        using (TextureBrush brush = new TextureBrush(OverlayImage))
        {
            gr.FillPolygon(brush, SelectedArea.ToArray());
        }
    }

    ShowScaledImages();
}

If the SelectedArea list contains fewer than three points, then it cannot define an area so the method simply returns.

Next the code disables the Copy button because you don’t need to copy the same selection area onto the original image multiple times. That wouldn’t hurt anything, but disabling the button provides extra feedback that clicking it did something.

The code also enables the File menu’s Reset command. It doesn’t really make sense to let you reset the main image until after you have copied part of the overlay image onto it. Again, it wouldn’t hurt anything to reset the unmodified image, but this follows the rule of trying to prevent the user from doing things that don’t make sense.

The program then creates a Graphics object associated with the original image, uses the overlay image to make a TextureBrush, and fills the selection area with the brush. Notice that the code is using the unscaled selection area points to draw on the unscaled image MainImage.

Speaking of MainImage, I’ll briefly say a few words about image storage and let you download the example for the rest. The program stores the main image in three different variables.

private Bitmap OriginalMainImage = null;
private Bitmap MainImage = null;
private Bitmap ScaledMainImage = null;

The variable OriginalMainImage holds the original image as loaded from the file. When you select the File menu’s Reset command, the program uses this image to reset the MainImage value.

The value MainImage holds the current main image, which may have been modified by an overlay.

The variable ScaledMainImage holds the current possibly modified image but at the current scale factor.

The following code shows how the program initializes those variables when you select the File menu’s Open Main Image command.

private void mnuFileOpenMainImage_Click(object sender, EventArgs e)
{
    if (ofdImage.ShowDialog() == DialogResult.OK)
    {
        try
        {
            // Load the image.
            OriginalMainImage = LoadBitmapUnlocked(ofdImage.FileName);
            MainImage = new Bitmap(OriginalMainImage);
            mnuFileReset.Enabled = false;
            ShowScaledImages();
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }
}

This code displays an OpenFileDialog. If you select an image file and click Open, the code loads the file into the OriginalMainImage variable. It then saves a a copy of that image in MainImage.

The code disables the File menu’s Reset command because it doesn’t make sense to reset the image if it has just been loaded and therefore has not changed.

Finally the code calls the ShowScaledImages method. That method simply displays the overlay and original images at the current scale. It’s fairly simple so it’s not shown here.

Conclusion

This example lets you overlay pieces of a modified version of an image on top of the original image. Combined with other programs that modify images, this program lets you make changes to selected areas of the image.

As is often the case, the example is complicated enough that I can’t include every detail here. Download the example program to see additional details and to experiment with it.


Download Example   Follow me on Twitter   RSS feed   Donate




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

Brighten pixels in an image in C#


[brighten pixels]

[pixels]

Sometimes when I take a picture, the poor lighting makes some pixels come out less brightly than I would like. For example, in the picture on the right the red raspberries aren’t very red.

This example lets you brighten pixels. Use the radio buttons to select red, green, or blue. Then enter a threshold amount and a multiplier. If a pixel’s selected component is at least the threshold amount greater than its other two components, then the program multiples that pixel’s selected component value by the multiplier.

For example, in the picture shown at the top of the post, Red is selected, the threshold is 40, and the multiplier is 4. If a pixel’s red component is at least 40 more than its green and blue components, then the program multiplies that pixel’s red component by 4.

That is very large multiplier so the reddish pixels become very red. I chose that value to make the changes in the example obvious. A more reasonable multiplier might be 1.5.

The example does not brighten pixels that don’t pass the threshold test. In this example, the pixel must be mostly red or it is left unchanged.

Brighten Pixels

The example program demonstrates a few useful techniques that I’ve described in earlier posts. For example, it shows how to load a bitmap without locking it and how to save an image using different file extensions such as .png and .jpg. The most important of those techniques is using the Bitmap32 class to quickly manipulate the pixels in an image. For information about the Bitmap32 class, see the post Use the Bitmap32 class to manipulate image pixels very quickly in C#.

The key to this example is the following BrightenPixels method.

private Bitmap BrightenPixels(
    Bitmap bitmap, PixelTypes pixel_type,
    int threshold, float scale)
{
    Bitmap bm = new Bitmap(bitmap);
    Bitmap32 bm32 = new Bitmap32(bm);
    bm32.LockBitmap();

    int width = bm.Width;
    int height = bm.Height;
    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            byte r, g, b, a;
            bm32.GetPixel(x, y, out r, out g, out b, out a);

            bool over_threshold = false;
            switch (pixel_type)
            {
                case PixelTypes.Red:
                    if ((r >= g + threshold) && (r > b + threshold))
                    {
                        int new_r = (int)(r * scale);
                        if (new_r > 255) new_r = 255;
                        else if (new_r < 0) new_r = 0;
                        r = (byte)new_r;
                    }
                    break;
                case PixelTypes.Green:
                    if ((g >= r + threshold) && (g > b + threshold))
                    {
                        int new_g = (int)(g * scale);
                        if (new_g > 255) new_g = 255;
                        else if (new_g < 0) new_g = 0;
                        g = (byte)new_g;
                    }
                    break;
                case PixelTypes.Blue:
                    if ((b >= r + threshold) && (b > g + threshold))
                    {
                        int new_b = (int)(b * scale);
                        if (new_b > 255) new_b = 255;
                        else if (new_b < 0) new_b = 0;
                        b = (byte)new_b;
                    }
                    break;
            }

            bm32.SetPixel(x, y, r, g, b, a);
        }
    }

    bm32.UnlockBitmap();
    return bm;
}

This code creates a copy of the original bitmap and makes an associated Bitmap32 object. It then locks the Bitmap32 object and loops through the image’s pixels.

For each pixel, the code uses the Bitmap32 object’s GetPixel method to get the pixel’s color components. It then uses a switch statement to operate on the selected red, green, or blue pixel component. If the selected component’s value is greater than the other components’ values plus the threshold, then the code multiplies the selected component by the multiplier. The code then ensures that the result is between 0 and 255 because component values, which are bytes, must lie between 0 and 255.

After the code has updated the selected component’s value, it uses the Bitmap32 object’s SetPixel method to update the pixel in the bitmap.

Once it has finished processing all of the image’s pixels, the code unlocks the Bitmap32 object and returns the modified bitmap.

Unusual Uses

You can use the program to brighten pixels, but you can also use it to darken them. If you set the multiplier to a value less than 1, then the selected pixels lose some of the selected color component. For example, if you select the red component, then the program would remove some of the red from the selected pixels. If you set the multiplier to 0 (or a negative value), then the program completely removes the selected component from the pixels.

The program has a couple of possible weird side uses. For example, if you set the threshold to -255, then every pixel passes the threshold test. If you then set the multiplier to 0, the program removes all of the selected color component from the entire image. If you set the multiplier to 255, then the program sets the selected component to 255 for all pixels that begin with any of that component.

Conclusion

This example has some limitations. For example, it only processes pixels where one component is larger than the others. It won’t, for example, process yellow pixels because those pixels have relatively large red and green components. You could make the pixel selection criteria more complicated to try to address this issue.

This example also processes all of the pixels that meet its criteria even if some of those pixels should not be processed. If you look at the picture at the top of the post, you’ll see that the program made parts of the box red even though I really just wanted to brighten the raspberries. In my next post, I’ll show one method that you can use to address this issue.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, graphics, image processing | Tagged , , , , , , , , | 1 Comment

Perform binary contrast enhancement more quickly in C#

[binary contrast enhancement]

The example Perform binary contrast enhancement interactively in C# lets you convert an image into completely black and white pixels depending on their brightness. Pixels that are brighter than a given cutoff value are converted into white. Those that are darker than the cutoff value are converted into black.

That example works well but is annoyingly slow for large images because it uses the Bitmap class’s GetPixel and SetPixel methods to get and set pixel values. This example improves that one by using the Bitmap32 class described in the post Use the Bitmap32 class to manipulate image pixels very quickly in C#.

The following code shows how the new example uses the Bitmap32 class to perform binary contrast enhancement on an image.

// Perform binary contrast enhancement on the bitmap.
private void BinaryContrast(Bitmap bm, int cutoff)
{
    Bitmap32 bm32 = new Bitmap32(bm);
    bm32.LockBitmap();

    for (int y = 0; y < bm.Height; y++)
    {
        for (int x = 0; x < bm.Width; x++)
        {
            byte r, g, b, a;
            bm32.GetPixel(x, y, out r, out g, out b, out a);
            if (r + g + b > cutoff)
                bm32.SetPixel(x, y, 255, 255, 255, 255);
            else
                bm32.SetPixel(x, y, 0, 0, 0, 255);
        }
    }
    bm32.UnlockBitmap();
}

The method creates a Bitmap32 object associated with the bitmap that it should process. It locks the Bitmap32 object and then loops through the image’s pixels.

For each pixel, the code uses the Bitmap32 object’s GetPixel method to get the pixel’s red, green, blue, and alpha color components. If the sum of the red, green, and blue components is greater than the cutoff value, the code uses the Bitmap32 object’s SetPixel method to make that pixel white. If the sum is less than or equal to the cuttoff value, the code uses SetPixel to make the pixel black.

After it finishes processing all of the bitmap’s pixels, the method unlocks the Bitmap32 and returns the modified bitmap.

Download the example and see the following posts to learn about additional details.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, image processing | Tagged , , , , , , , | Leave a comment

Save a RichTextBox image in C#

[example]

My post Get the image of a control or form, or a form’s client area in C# uses a control’s DrawToBitmap method to make it draw itself onto a bitmap. Unfortunately the RichTextBox control does not support that method.

Another approach you can take is to capture the part of the screen image above where the RichTextBox control lies. That’s the approach taken by the MSDN post how to save the content of richTextBox as jpg file?

In the following method I’ve modified the technique described by that post slightly so it captures the control’s client area.

public Bitmap RtbToBitmap(RichTextBox rtb)
{
    // Make sure the RichTextBox is fully painted.
    rtb.Update();

    // Get the image.
    int width = rtb.ClientSize.Width;
    int height = rtb.ClientSize.Height;
    Bitmap bmp = new Bitmap(width, height);
    using (Graphics gr = Graphics.FromImage(bmp))
    {
        gr.CopyFromScreen(
            rtb.PointToScreen(new Point(0, 0)),
            new Point(0, 0), rtb.ClientSize);
    }
    return bmp;
}

This method updates the RichTextBox. It then gets the control’s client width and height. It then makes a bitmap of that size and creates an associated Graphics object. It uses the object’s CopyFromScreen method to copy the part of the screen image above the RichTextBox onto the bitmap.

Notice how the program uses the RichTextBox control’s PointToScreen method to convert the control’s upper left corner at (0, 0) to screen coordinates.

After copying the image onto the bitmap, the method returns the bitmap.

Note that this only works if the RichTextBox is not covered by some other control. For example, if another form lies on top of the control when the method runs, then it will grab an image of the other form.

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


Download Example   Follow me on Twitter   RSS feed   Donate




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

Expand a ComboBox when the user presses Enter in C#

[example]

A ComboBox control’s DropDownStyle property determines how it lets the user select values. The three possible DropDownStyle values are:

  • Simple – The ComboBox displays a text box (where the user can type a value) and a list (where the user can select a value)
  • DropDown – The ComboBox displays a text and a list that the user can expand by clicking on the control’s dropdown arrow
  • DropDownList – The ComboBox displays a list that the user can expand by clicking on the control’s dropdown arrow

Because the third option, DropDownList, only lets the user select from the list, that setting is particularly useful for preventing the usrer from entering gibberish.

One drawback to the DropDown and DropDownList styles is that the user cannot see the items in the list without using the mouse to make it drop down. That means users must take their hands off of the keyboard to position and click the mouse, and that makes the users slower.

This example shows one way you can change that.

Enter the Enter Key

This example lets you press Enter to expand a ComboBox control’s list. You can then use the arrow keys to move up and down through the list. If you press Enter again, the list closes. (You can also close the list by pressing Escape.)

The obvious thing to do is to catch the control’s KeyPress event and expand the list when you see the Enter key. That almost works.

The KeyPress event handler can detect the Enter key and open the control’s drop down list. Unfortunately when you press Enter again, the list closes and then immediately reopens. The problem is that the control normally closes the list when it sees the Enter key. When you press Enter the second time, the control closes its list and then the KeyPres event handler reopens it.

You can make the event handler check the control’s DroppedDown property to see if the list is currently dropped down and then do nothing if the list is visible. Unfortunately the control closes the list before the KeyPress event handler executes, so the event handler always sees the list as hidden so it always reopens it.

To make matters stranger, the control behaves slightly differently when using the DropDown and DropDownList styles. Checking the DroppedDown property works for the DropDown style but not for the DropDownList style.

The workaround I found is to keep track of the time when a ComboBox list was last closed. If less than 0.1 seconds have passed since the list was last closed, then the KeyPress event handler should not reopen it.

Here’s the code that tracks when a ComboBox list was closed.

private DateTime LastCloseTime = DateTime.Now;

// Record the time when we closed a ComboBox.
private void cbo_DropDownClosed(object sender, EventArgs e)
{
    LastCloseTime = DateTime.Now;
}

The variable LastCloseTime stores the last time that a ComboBox list was closed.

The cbo_DropDownClosed event handler simply records the current time.

The following code shows the KeyPress event handler that opens ComboBox lists.

private void cbo_KeyPress(object sender, KeyPressEventArgs e)
{
    // See if the user pressed Enter.
    if (e.KeyChar == (char)Keys.Enter)
    {
        // See if the list is already dropped.
        ComboBox cbo = sender as ComboBox;
        if (cbo.DroppedDown) return;

        // See if it has been at least 0.1 seconds since
        // the last time we closed a ComboBox.
        TimeSpan time_since_close = DateTime.Now - LastCloseTime;
        if (time_since_close.TotalSeconds < 0.1) return;

        // Drop the list.
        cbo.DroppedDown = true;
        e.Handled = true;
    }
}

If the key that was pressed is Enter, then the code first checks whether the ComboBox list is open. If the list is open, then the event handler exits and lets the control’s normal behavior collapse the list.

If the list is not already open, the code calculates the time that has elapsed since the last time that a ComboBox list was closed. If that time is less than 0.1 seconds, then the list has just closed so the program should not reopen it. In that case the event handler simply exits.

If more than 0.1 second has passed since a list was closed, the code sets the control’s DroppedDown property to true to make the list drop down. The code finishes by setting e.Handled to true so the control does not try to process the key press further.

Strangely all of the pieces of the event handler seem to be necessary even though some seem redundant.

  • If you comment out the if (cbo.DroppedDown) return statement, then a ComboBox using the DropDown style will not let you close the list by pressing Enter.
  • If you comment out the if (time_since_close.TotalSeconds < 0.1) return; statement, then a ComboBox using the DropDownList style immediately reopens the list after you use Enter to close it.
  • If you comment out the e.Handled = true statement, then a ComboBox using the DropDown style immediately closes its list after you use Enter to open it.

To allow the user to press Enter to open and close ComboBox lists, simply attach the cbo_DropDownClosed and cbo_KeyPress event handlers to any ComboBox controls that you want to have this behavior. Note that the event handlers have no effect if the ComboBox has the Simple style, so it doesn’t hurt much to attach the event handlers to all ComboBox controls.

Conclusion

The event handlers shown here let the user quickly expand and collapse ComboBox lists by using only the keyboard. You can certainly do without this feature, but it can make data entry significantly faster if you need to fill in many ComboBox fields.

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


Download Example   Follow me on Twitter   RSS feed   Donate




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