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 , , , , , , , , , , , , , | 1 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

Print circles centered inside other closely packed circles in C#


[print circles]

This example shows how to print circles that are arranged densely in a hexagonal pattern. I needed that to make a cardboard sheet for arranging cupcakes to make a cupcake cake. The holes needed to be 1″ in radius and spaced to allowed the 1.5″ radius cupcake tops to pack tightly. You may never need to do this (in fact, we didn’t end up using it), but you may find the program interesting.

I spent quite a while googling around looking for some sort of tem plate that I could use. Then I realized that this is actually pretty easy. In the end I wrote the program in less time than I wasted in fruitless googling.

A Little Math

[print circles]

The key to arranging the circles is the little bit of high school geometry shown in the picture on the right. Consider the larger circles drawn with dashed lines in the picture at the top of the post. If the circles are packed tightly, then the dashed triangle shown on the right is a 30-60-90 triangle. In that case, it has relative side lengths 1, 2, √3. The shortest side length in the figure has length R where R is the radius of the outer (dashed) circles. That means the triangle’s other sides have lengths R * 2 and R * √3.

That means to get from the upper left circle to the circle to the right on the next lower row, you increase the circle’s X coordinate by R and its Y coordinate by R * √3.

That’s all the math you need. The following sections explain how the program’s code uses those values to arrange the circles.

Launching the Print Out

When you enter the radii and click Preview, the following code executes.

private float InnerRadius, OuterRadius;

// Display the PrintPreviewDialog.
private void btnPreview_Click(object sender, EventArgs e)
{
    InnerRadius = 100 * float.Parse(txtInnerRadius.Text);
    OuterRadius = 100 * float.Parse(txtOuterRadius.Text);
    Form frm = ppdCircles as Form;
    frm.WindowState = FormWindowState.Maximized;
    ppdCircles.PrintPreviewControl.Zoom = 1.0;
    ppdCircles.PrintPreviewControl.UseAntiAlias = true;
    ppdCircles.ShowDialog();
}

The variables InnerRadius and OuterRadius hold the radii of the inner (solid) and outer (dashed) circles.

The btnPreview_Click event handler gets the values that you entered in the text boxes, multiplies them by 100, and saves them in those variables. It multiples them by 100 because the printer’s units are 100ths of inches. For example, a radius of 1.00 inches corresponds to 100 printer units.

Next, the code uses the ppdCircles component. That component is a PrintPreviewDialog that I added to the program’s form at design time. The code creates a variable of type Form and casts the dialog into it. That is possible because a PrintPreviewDialog is a kind of form.

Now the code can use the dialog’s form properties, which are hidden when you work with the dialog as a PrintPreviewDialog. The program uses the Form variable to set the dialog’s WindowState property to Maximized so the dialog begins maximized.

Next, the code sets the dialog’s zoom level to 1.0 so the printout will be displayed at full scale. (In the picture at the top of the post, I reduced the zoom to 50% so you could see more circles.) The code also makes the dialog use anti-aliasing so the print preview is smooth.

Finally, the code displays the dialog.

Printing

When the PrintPreviewDialog is displayed, it uses a PrintDocument object to generate the image that it will display. At design time, I added a PrintDocument object named pdocCircles to the form. I also set the PrintDocument object’s PrintDocument property to ppdocCircles so the dialog could find that object when needed.

When the dialog is displayed, it invokes the PrintDocument object’s PrintPage event handler. The following code shows the event handler used by this example to print circles.

// Generate the printout.
private void pdocCircles_PrintPage(object sender,
    System.Drawing.Printing.PrintPageEventArgs e)
{
    float cx = e.MarginBounds.Left + OuterRadius;
    float cy = e.MarginBounds.Top + OuterRadius;

    float dx = OuterRadius;
    float dy = (float)(OuterRadius * Math.Sqrt(3));

    using (Pen dashed_pen = new Pen(Color.Black))
    {
        dashed_pen.DashPattern = new float[] { 10, 10 };

        while (cy - OuterRadius < e.MarginBounds.Bottom)
        {
            // Make rectangles for the first circle.
            RectangleF outer_rect = new RectangleF(
                cx - OuterRadius,
                cy - OuterRadius,
                2 * OuterRadius,
                2 * OuterRadius);
            RectangleF inner_rect = new RectangleF(
                cx - InnerRadius,
                cy - InnerRadius,
                2 * InnerRadius,
                2 * InnerRadius);

            // Draw a row.
            while (outer_rect.Left < e.MarginBounds.Right)
            {
                e.Graphics.DrawEllipse(dashed_pen, outer_rect);
                e.Graphics.DrawEllipse(Pens.Black, inner_rect);
                outer_rect.X += 2 * OuterRadius;
                inner_rect.X += 2 * OuterRadius;
            }

            // Prepare for the next row.
            cx -= dx;
            if (cx < e.MarginBounds.Left) cx += 2 * OuterRadius;
            cy += dy;
        }
    }
}

This code sets variables cx and cy to the coordinates for the center of the first circle. It starts at the upper left corner of the page inside its margins. It then adds the radius of the outer circle to that point’s X and Y coordinates. The result will place the first circle so it just touches the upper left corner of the page margins.

Next, the code sets variables dx and dy to the distances that it must move a circle’s center to get to a circle in the next lower row. (See the earlier section “A Little Math.”)

The code then creates a dashed pen and sets its DashPattern to produce a nice dashed line. It then enters a while loop that lasts as long as the value cy is small enough that subtracting the outer circle’s radius keeps part of the circle within the page’s bounds. (Alternatively you could change this so it only draws complete circles, but I decided to print any circle that lies at least partly inside the page’s margins.)

Now the code makes two rectangles to draw the inner and outer circles centered at the point (cx, cy) and with the desired radii.

The program then enters a loop to draw the row. The loop lasts as long as a circle centered at (cx, cy) will fit at least partly within the page margins horizontally. Within the loop, the program draws the inner and outer circles and then adds 2 * OuterRadius to the rectangles’ X coordinates to move to the next circle in the row.

After it has finished drawing a row, the code subtracts dx from cx. Then if cx is less than the page’s left margin, the code adds 2 * OuterRadius to it so the first circle on the next row fits within the page margins.

Finally, the code adds dy to cy to move down to the next row.

When the PrintPage event handler ends, it can indicate whether there are more pages to print by setting e.HasMorePages to true or false. This example prints only one page, so it should set e.HasMorePages to false. That is the initial value when the event handler executes, so the code doesn’t need to change it.

Conclusion

That’s all you need to do to print circles that are packed tightly in a hexagonal pattern. You can print directly by calling the PrintDocument object’s Print method if you like. I usually just display a preview. That way I can test the program as many times as necessary without wasting paper. When everything looks correct, I press the preview dialog’s Print button to actually print.

It turned out that natural variation in the side of the cupcakes made them not fit tightly together, so we threw away the cardboard pattern that I had painstakingly made and we simply glued each cupcake down on a pan with a little bit of frosting. That was much easier and allowed more flexibility in positioning. Just use this post as an example showing how arrange circles, print circles, and use the PrintDocument and PrintPreviewDialog components to create a printout in general.


Download Example   Follow me on Twitter   RSS feed   Donate




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

Change a DataGridView entry when the user double-clicks a cell in C#

[DataGridView]

This post is in response to the following question:

Respected Sir,

I need help on how can i change the datagridview cell value by double click on that cell.

Let say cell contain value ‘N’ when i double click it will change to ‘Y’ .

First, note that by default the DataGridView control allows the user to edit its data. If you click on a DataGridView cell and then click on the cell again, the control allows you to edit that cell’s value. For example, see my post Build a DataTable and bind it to a DataGridView in C#.

For the question to make sense, we must have disabled the control’s natural editing capabilities. This example does that in its Form_Load event handler shown shortly. It then detects double-clicks and takes action depending on the cell that was clicked.

To make it easier for the user to know that particular cells are double-clickable, the program changes the cursor for those cells.

The following sections describe the pieces of the program that accomplish those tasks.

Setup

The following code shows the form’s Load event handler.

private void Form1_Load(object sender, EventArgs e)
{
    // Make the DataTable object.
    DataTable dt = new DataTable("Books");

    // Add columns to the DataTable.
    dt.Columns.Add("Title",
        System.Type.GetType("System.String"));
    dt.Columns.Add("Has Download",
        System.Type.GetType("System.Boolean"));
    dt.Columns.Add("Year",
        System.Type.GetType("System.Int32"));
    dt.Columns.Add("URL",
        System.Type.GetType("System.String"));
    
    // Add items to the table.
    dt.Rows.Add(new object[] { "Essential Algorithms", ... );
    dt.Rows.Add(new object[] { "WPF 3d", true, "2018", ... );
    dt.Rows.Add(new object[] { "The C# Helper Top 100", ... );
    dt.Rows.Add(new object[] { "Interview Puzzles Dissected", ... );

    // Make the DataGridView use the DataTable as its data source.
    dgvBooks.DataSource = dt;

    // Draw URLs in a blue, underlined font.
    dgvBooks.Columns["URL"].DefaultCellStyle.Font=
        new Font(dgvBooks.Font, FontStyle.Underline);
    dgvBooks.Columns["URL"].DefaultCellStyle.ForeColor =
        Color.Blue;

    // Set column widths.
    dgvBooks.AutoResizeColumns();

    // Do not allow automatic editing.
    dgvBooks.EditMode = DataGridViewEditMode.EditProgrammatically;
    dgvBooks.AllowUserToAddRows = false;
    dgvBooks.AllowUserToDeleteRows = false;
}

This code creates a DataTable to hold the data. It adds columns of different data types and then inserts data into the table.

Next, the program sets the DataGridView control’s DataSource property to the DataTable. At this point, the control allows the user to view and edit the data.

Next, the program sets the font for the DefaultCellStyle property in the URL column. It sets that property to a new font that is based on the DataGridView control’s font but with the underline style. That makes the values in that column appear underlined.

Similarly, the code sets the URL column’s default cell style to draw blue text so the URL values are drawn with a blue, underlined font.

The code then calls the DataGridView control’s AutoResizeColumns method to make the columns size themselves to fit their data.

Finally, the Load event handler sets the control’s EditMode so the user cannot edit the data value. It also prevents the user from adding new records or deleting existing ones.

Changing Cursors

When the mouse enters a cell, the following event handler sets the appropriate cursor.

// Display an appropriate cursor in the Has Download and URL columns.
private void dgvBooks_CellMouseEnter(object sender,
    DataGridViewCellEventArgs e)
{
    int col = e.ColumnIndex;
    if (col == -1) return;

    if (dgvBooks.Columns[col].Name == "Has Download")
        dgvBooks.Cursor = Cursors.Hand;
    else if (dgvBooks.Columns[col].Name == "URL")
        dgvBooks.Cursor = Cursors.UpArrow;
}

This event handler checks the column number below the mouse. If the mouse is to the left of the first column of data, then the column number is -1. In that case, the method exits. If the mouse is below the DataGridView control’s last row or to the right of the rightmost column, then this event is not raised.

If the column number is at least 0, the code checks the name of the column. If this is the Has Download column, the code sets the control’s cursor to Hand. If this is the URL column, the code sets the control’s cursor to UpArrow.

When the mouse moves off of a column, the following event handler executes.

// Restore the default cursor..
private void dgvBooks_CellMouseLeave(object sender,
    DataGridViewCellEventArgs e)
{
    if (dgvBooks.Cursor != Cursors.Default)
        dgvBooks.Cursor = Cursors.Default;
}

This code simply checks the DataGridView control’s current cursor and resets it to the default if it is not already the default.

Modifying Values

When the user double-clicks on a cell, the following event handler executes.

// Toggle the Has Download field. Open the URL.
private void dgvBooks_CellDoubleClick(object sender,
    DataGridViewCellEventArgs e)
{
    int col = e.ColumnIndex;
    if (col == -1) return;

    int row = e.RowIndex;

    // See which column this is.
    if (dgvBooks.Columns[col].Name == "Has Download")
    {
        // Toggle the value.
        bool value = (bool)dgvBooks.Rows[row].Cells[col].Value;
        dgvBooks.Rows[row].Cells[col].Value = !value;
    }
    else if (dgvBooks.Columns[e.ColumnIndex].Name == "URL")
    {
        // Open the URL.
        string url = (string)dgvBooks.Rows[row].Cells[col].Value;
        System.Diagnostics.Process.Start(url);
    }
}

This code verifies that the column number is at least 0 as before. If user double-clicked the Has Download column, the code gets the current value in the clicked cell and then updates the cell to hold the opposite of that value.

If the user double-clicked the URL column, the program gets the value in the cell and then uses System.Diagnostics.Process.Start to “start” the URL. That launches the URL in the system’s default browser.

Conclusion

I generally don’t recommend that you write your own code to edit DataGridView data because that control already does a pretty good job. Still, it may sometimes be useful to provide custom editing of some sort such as allowing the user to toggle a value as in this example.

An alternative might be to prevent editing in columns that the user should not modify. For example, the following statement prevents the user from editing the values in the first column.

dt.Columns[0].ReadOnly = true;

Download the example to see the code and to experiment with the program.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in controls, database, user interface | Tagged , , , , , , , , | 5 Comments

Flow words around drop caps in C#


[drop caps]

The example Flow blocks around obstacles for document layout in C# implements a document layout algorithm that flows blocks around obstacles. Moving from there to flowing words around obstacles is relatively simple. The following sections describe the main places where I modified the previous algorithm to provide document layout for words instead of blocks.

Block

The following code shows the new example’s Block class.

public class Block
{
    public RectangleF Bounds;
    public float TopHeight;
    public float BottomHeight
    {
        get { return Bounds.Height - TopHeight; }
    }

    public Block()
    {
    }
    public Block(RectangleF bounds, float top_height)
    {
        Bounds = bounds;
        TopHeight = top_height;
    }

    // Draw the block.
    public virtual void Draw(Graphics gr, Brush fg_brush,
        Brush bg_brush, Pen pen)
    {
        if (Bounds.X < 0) return;

        gr.FillRectangle(bg_brush, Bounds);
        gr.DrawRectangle(pen, Bounds);
    }

    // Return true if the block intersects the rectangle.
    public bool IntersectsWith(RectangleF rect)
    {
        return Bounds.IntersectsWith(rect);
    }

    // Return true if the block intersects the other block.
    public bool IntersectsWith(Block other)
    {
        return Bounds.IntersectsWith(other.Bounds);
    }
}

The main differences are the addition of a parameterless constructor and the two versions of the IntersectsWith method. Those methods return true if block’s bounds intersect with a rectangle or with another Block object.

Also note that the Draw method is now virtual. That allows the TextBlock class to override it so it can draw text instead of just a box.

The TextBlock Class

The previous example uses the Block class to represent a rectangle that should be flowed around obstacles. To flow words, this example derives a new TextBlock class from the Block class. The following code shows the new class.

public class TextBlock : Block
{
    public Font Font;
    public String Text;

    public TextBlock(string text, Font font, Graphics gr)
    {
        Font = font;
        Text = text;

        SizeF size = gr.MeasureString(Text, font);
        Bounds = new RectangleF(new PointF(), size);

        FontInfo font_info = new FontInfo(gr, font);
        TopHeight = font_info.AscentPixels;
    }

    // Draw the text block.
    public override void Draw(Graphics gr, Brush fg_brush,
        Brush bg_brush, Pen pen)
    {
        if (Bounds.X < 0) return;

        // Draw the box (optional). 
        base.Draw(gr, fg_brush, bg_brush, pen);

        // Draw the text.
        using (StringFormat sf = new StringFormat())
        {
            sf.Alignment = StringAlignment.Center;
            sf.LineAlignment = StringAlignment.Center;
            gr.DrawString(Text, Font, fg_brush, Bounds, sf);
        }
    }

    public override string ToString()
    {
        return Text;
    }
}

This class inherits from the Block class. It provides two new fields: Font and Text. You can probably guess that those are the font that the object should use to draw its text and the text that it should draw.

The constructor takes as parameters the block’s text, its font, and a Graphics object that it can use to measure the text. The code saves the font and text, and then calls the Graphics object’s MeasureString method to measure the text as drawn with the given font. It uses the text’s size to define the block’s bounds.

Next, the program must figure out where the text’s baseline should be. Recall that the previous example’s document layout method aligned blocks on their baselines. For the blocks, that was unnecessary, but for text, that’s important so different words line up properly.

The distance between the top of a piece of text and its baseline is called its ascent. Unfortunately, neither the Font nor Graphics objects give you the ascent directly. Fortunately, you can figure it out. For an explanation of how to do that and to see a picture showing where a piece of text’s ascent is, see my earlier post Get font metrics in C#. That example defines a FontInfo class that gives the font’s ascent. The TextBlock class simply creates a FontInfo object for the font and saves its AscentPixels property in its TopHeight field.

The final piece of the TextBlock class is its Draw method, which overrides the same method defined by the Block class. The new method first calls the Draw class’s version of the method to draw a box where the text will be positioned as shown in the picture at the top of the post. In the final program, you can comment out that call so the program only draws the text as shown in the following picture.


[document layout]

The Draw method then draws the object’s text inside its bounds. Because the constructor made the bounds fit the text exactly, the text fills the bounds completely so the text is centered both vertically and horizontally within the bounds.

PictureBlock

I wanted the final program to be able to make text flow around pictures in fixed positions. The program uses the following PictureBlock class to represent and display those pictures.

public class PictureBlock : Block
{
    public Image Picture;

    public PictureBlock(Image picture, float x, float y)
    {
        Picture = picture;
        Bounds = new RectangleF(x, y,
            picture.Width, picture.Height);
    }

    // Draw the block.
    public override void Draw(Graphics gr, Brush fg_brush,
        Brush bg_brush, Pen pen)
    {
        if (Bounds.X < 0) return;

        // Draw the box (optional). 
        base.Draw(gr, fg_brush, bg_brush, pen);

        // Draw the image.
        gr.DrawImage(Picture, Bounds.Location);
    }
}

Like TextBlock the PictureBlock class inherits from Block. This class’s constructor takes as a parameter the picture that it should draw and the X and Y coordinates where it should be drawn. It saves the picture and uses the coordinates and the picture’s dimensions to set the object’s bounds.

The Draw method calls the base class’s Draw method. It then uses the Graphics object’s DrawImage method to draw the picture.

Form Load

The following code shows the variables that the program uses to hold the text and obstacles that it draws.

// Obstacles.
private List<Block> Obstacles;

// Blocks to flow around obstacles.
private List<List<Block>> ParagraphBlocks;

// The initials.
private List<Block> Initials;

// The text to draw.
private string[] Paragraphs =
{
    "Lorem ipsum dolor sit amet, consectetur...",
    "Etiam at nulla accumsan, fringilla erat...",
    "..."
};

// Extra space between paragraphs.
private const float ParagraphSpacing = 20;

The Obstacles list holds blocks that the text should flow around. These will be PictureBlock objects at fixed locations.

The ParagraphBlocks variable holds lists of blocks. Each list contains the words that make up a paragraph. (The words are Lorem Ipsum words. For more information on this interesting test string, see the site Lorem Ipsum.)

The first letter of the first word in each paragraph is removed and placed in the Initials list. Those values are the letters used for the drop caps. For example, Initials[2] is holds the first letter of paragraph number two.

Notice that the Obstacles, ParagraphBlocks, and Initials lists all contain Block objects. The TextBlock and PictureBlock classes are derived from Block, so TextBlock and PictureBlock objects are kinds of block. That means the program can use those objects as Block objects and can place them inside those lists.

The Paragraphs array holds all of the text. Finally, the value ParagraphSpacing indicates the amount of extra space to add between paragraphs.

The program initializes the data in the following Form_Load event handler.

// Define some blocks.
private void Form1_Load(object sender, EventArgs e)
{
    // Create a PictureBlock at a fixed position.
    PictureBlock picture_block = new PictureBlock(
        Properties.Resources.essential_algorithms_2e, 250, 70);

    // Add the PictureBlock as an obstacle.
    Obstacles = new List<Block>();
    Obstacles.Add(picture_block);

    // Build the lists of paragraphs and initials.
    ParagraphBlocks = new List<List<Block>>();
    using (Graphics gr = CreateGraphics())
    {
        // Give the initials the same size.
        RectangleF initial_bounds = new RectangleF(0, 0, 64, 64);

        // Break each paragraph into words.
        Initials = new List<Block>();
        Font initial_font =
            new Font("Times New Roman", 40, FontStyle.Bold);
        for (int i = 0; i < Paragraphs.Length; i++)
        {
            // Remove the first lettter of the first word.
            Initials.Add(new TextBlock(Paragraphs[i].Substring(0, 1),
                initial_font, gr));
            Paragraphs[i] = Paragraphs[i].Substring(1);
            Initials[i].Bounds = initial_bounds;

            // Break the rest of the paragraph into blocks.
            List<Block> new_blocks = new List<Block>();
            foreach (string word in Paragraphs[i].Split(' '))
            {
                // Make the word's block.
                new_blocks.Add(new TextBlock(word, RandomFont(), gr));
            }
            ParagraphBlocks.Add(new_blocks);
        }

        // Perform the initial flow.
        FlowParagraphBlocks(gr, picWriting.ClientRectangle,
            Obstacles, new SizeF(64, 64), ParagraphBlocks,
            ParagraphSpacing);
    }
}

This method first creates a PictureBlock at a specific position and adds that object to the Obstacles list.

Next, the code makes the ParagraphBlocks list and creates a Graphics object that it can use to measure text. It also creates a 64×64 pixel RectangleF to represent the size of the initials. It uses a single RetangleF so all of the drop caps will have the same size.

The code then loops through the paragraphs in the Paragraphs array. It removes the first letter from the first word in each paragraph and adds it to the Initials list. It then breaks the paragraph into words and places a TextBlock representing each word in the ParagraphBlocks list.

Notice that the code calls the RandomFont method to pick a different font for each word. That method gives each word a random font name, size, and style (bold, italic, regular, strikeout, or underline. If you comment out the compile-time symbol RANDOM_FONTS defined at the top of the file, then the RandomFont method returns a specific font instead of a random one. Download the example to see how it works.

The Load event handler finishes by calling the FlowParagraphBlocks method to flow the drop caps and text around the fixed obstacles.

FlowParagraphBlocks

The following code shows the FlowParagraphBlocks method that does the most interesting work.

// Flow blocks around obstacles.
private void FlowParagraphBlocks(Graphics gr,
    RectangleF bounds,
    List<Block> fixed_obstacles, SizeF min_initial_size, 
    List<List<Block>> paragraph_blocks,
    float paragraph_spacing)
{
    // Place objects to be hidden here.
    PointF hidden_point = new PointF(-1, -1);

    // Make a list to hold fixed obstacles and initials.
    List<Block> obstacles = new List<Block>(fixed_obstacles);

    // Start at the top.
    float y = bounds.Y;

    // Repeat until we place all blocks or run out of room.
    for (int paragraph_num = 0;
        paragraph_num < paragraph_blocks.Count;
        paragraph_num++)
    {
        // Position this paragraph's blocks.
        List<Block> this_paragraphs_blocks =
            paragraph_blocks[paragraph_num];

        // If this paragraph's initial won't fit,
        // move y down so we stop positioning blocks.
        if (y + Initials[paragraph_num].Bounds.Height > bounds.Bottom)
            y = bounds.Bottom + 1;

        // If we have run out of room, place everything at (-1, -1).
        if (y > bounds.Bottom)
        {
            // Position the initial and other blocks at (-1, -1).
            Initials[paragraph_num].Bounds.Location = hidden_point;

            // Position the text blocks.
            for (int i = 0; i < this_paragraphs_blocks.Count; i++)
                this_paragraphs_blocks[i].Bounds.Location = hidden_point;

            // Go on to the next paragraph.
            continue;
        }

        // Position the initial.
        Initials[paragraph_num].Bounds.Location =
            new PointF(bounds.Left, y);
        obstacles.Add(Initials[paragraph_num]);

        // Position the remaining text.
        int first_block = 0;
        while (first_block < this_paragraphs_blocks.Count)
        {
            // Position a row of blocks.
            int num_positioned = PositionOneRow(
                bounds, obstacles, this_paragraphs_blocks,
                ref first_block, ref y);

            // See if any fit.
            if (num_positioned == 0)
            {
                // None fit. Move down.
                MoveDown(bounds, obstacles,
                    this_paragraphs_blocks[first_block], ref y);
            }

            // See if we have run out of room.
            if (y > bounds.Bottom)
            {
                // Position the remaining blocks at (-1, -1).
                for (int i = first_block;
                    i < this_paragraphs_blocks.Count; i++)
                {
                    this_paragraphs_blocks[i].Bounds.Location =
                        hidden_point;
                }

                // Stop positioning the blocks in this
                // paragraph and go on to the next paragraph.
                break;
            }
        }

        // Don't start the next paragraph before the
        // bottom of the current paragraph's initial.
        if ((paragraph_num < paragraph_blocks.Count - 1) &&
            (y < bounds.Bottom))
        {
            if (y < Initials[paragraph_num].Bounds.Bottom)
                y = Initials[paragraph_num].Bounds.Bottom;
        }

        // Add some extra space between paragraphs.
        y += paragraph_spacing;
    }
}

The method makes a new obstacles list and copies the fixed obstacles (the PictureBlock) into it. It sets variable y equal to the top of the bounds where it is allowed to draw.

The code then loops through the paragraphs. It first checks whether the current paragraph’s initial will fit within the allowed bounds. If the initial won’t fit, the code moves y down beyond the bottom of the drawing area so the method won’t try to draw any more text.

Next, if y is below the bottom of the drawing area, the code loops through all of the paragraph’s words and positions them all at (-1, -1) to indicate that they did not fit.

If the initial does fit, then the code sets its position. That places the drop caps at the left edge of the drawing area. (You could modify that if you like. For example, you may want the drop caps indented.)

The code then adds the initial to the obstacles list so the paragraph’s text blocks can flow around it on subsequent lines.

Next, the code loops through the paragraph’s TextBlock objects. It calls the PositionOneRow method as in the previous example. If no word will fit on the current line, the code calls the MoveDown method to move down a bit, again as in the previous example.

After it has drawn text on the current line or moved down, the code checks the y position to see if we have fallen off of the drawing area. If we have left the drawing area, the code loops through the paragraph’s remaining words and places them at (-1, -1).

Before it continues looping through the paragraphs, the program checks that the y coordinate is at least beyond the bottom of the current paragraph’s initial. It then adds the extra spacing between paragraphs and continues looping through the paragraphs.

Download the example and take a look at the code for extra details about the PositionOneRow and MoveDown methods.

Paint

When the form loads, the program calls FlowParagraphBlocks to arrange the words. The program also calls that method when the form resizes.

When the form paints, it simply draws all of the blocks in their assigned positions. The following code shows how the Paint event handler does that.

// Draw the blocks.
private void picWriting_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

    // Draw obstacles.
    foreach (Block obstacle in Obstacles)
    {
        obstacle.Draw(e.Graphics, Brushes.Red, Brushes.Pink, Pens.Red);
    }

    // Draw initials.
    using (Brush bg_brush =
        new TextureBrush(Properties.Resources.Butterflies))
    {
        foreach (TextBlock initial in Initials)
        {
            initial.Draw(e.Graphics, Brushes.Black, bg_brush, Pens.Black);
        }
    }

    // Draw flowed blocks.
    foreach (List<Block> this_paragraphs_blocks in ParagraphBlocks)
    {
        foreach (Block block in this_paragraphs_blocks)
        {
#if DRAW_BOXES
            block.Draw(e.Graphics, Brushes.Black,
                Brushes.Transparent, Pens.Red);
#else
            block.Draw(e.Graphics, Brushes.Black,
                Brushes.Transparent, Pens.Transparent);
#endif
        }
    }
}

[Essential Algorithms, 2e]

This event handler sets the Graphics object’s SmoothingMode. It then loops through the fixed obstacles and draws them. In this example, that draws the single picture of my book Essential Algorithms, Second Edition.

The code then makes a TextureBrush that draws copies of an image. (In this example, it uses a colorful image of some butterflies tiled in the style of M. C. Escher.) The code then loops through the initials’ TextBlocks and calls their Draw methods to draw the drop caps. The parameters that the code uses makes that method fill the drop caps with the butterfly image, outline the rectangle in black, and draw the text in black.

After drawing the drop caps, the code loops through the blocks in the ParagraphBlocks lists and calls each block’s Draw method. The code outlines each block if the DRAW_BOXES symbol is defined.

That’s all the program needs to do to draw the picture, drop caps, and the other text. Flowing the text around the drop caps is fairly complicated, but after the text is positioned, drawing it is simple.

Conclusion

That’s the end of this example. I can’t say it was exactly simple, but the pieces are manageable if you take them slowly and one at a time.

Of course, all of this is just a taste of what a true document layout system must do. A program such as Microsoft Word or LaTeX must be able to flow text around obstacles such as pictures. Those pictures could hold drawings or text used for drop caps. Both pictures and inline text could include text with various colors and fonts, borders, fill colors, equations, tables, and a whole host of other features.

The problem is certainly daunting. Treating every object as a single Block base class helps. That lets you treat every object as something that occupies a rectangle and then you can shuffle the objects around. Exactly how you do that depends on your layout strategy.

As usual, download the example to see additional details and to experiment with the example.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, fonts, graphics, strings | Tagged , , , , , , , , , , , , | 2 Comments

Flow words around obstacles for document layout in C#


[document layout]

The example Flow blocks around obstacles for document layout in C# implements a document layout algorithm that flows blocks around obstacles. Moving from there to flowing words around obstacles is relatively simple. The following sections describe the two main places where I modified the previous algorithm to provide document layout for words instead of blocks.

The TextBlock Class

The previous example uses the Block class to represent a rectangle that should be flowed around obstacles. To flow words, this example derives a new TextBlock class from the Block class. The following code shows the new class.

public class TextBlock : Block
{
    public Font Font;
    public String Text;

    public TextBlock(string text, Font font, Graphics gr)
    {
        Font = font;
        Text = text;

        SizeF size = gr.MeasureString(Text, font);
        Bounds = new RectangleF(new PointF(), size);

        FontInfo font_info = new FontInfo(gr, font);
        TopHeight = font_info.AscentPixels;
    }

    // Draw the text block.
    public override void Draw(Graphics gr)
    {
        if (Bounds.X < 0) return;

        // Draw the box (optional). 
        base.Draw(gr);

        // Draw the text.
        gr.DrawString(Text, Font, Brushes.Black, Bounds);
    }
}

This class inherits from the Block class. It provides two new fields: Font and Text. You can probably guess that those are the font that the object should use to draw its text and the text that it should draw.

The constructor takes as parameters the block’s text, its font, and a Graphics object that it can use to measure the text. The code saves the font and text, and then calls the Graphics object’s MeasureString method to measure the text as drawn with the given font. It uses the text’s size to define the block’s bounds.

Next, the program must figure out where the text’s baseline should be. Recall that the previous example’s document layout method aligned blocks on their baselines. For the blocks, that was unnecessary. For text, that’s important so different words line up properly.

The distance between the top of a piece of text and its baseline is called its ascent. Unfortunately, neither the Font not Graphics objects give you the ascent directly. Fortunately, you can figure it out. For an explanation of how to do that and to see a picture showing how the ascent fits on the text, see my earlier post Get font metrics in C#. That method defines a FontInfo class that gives the font’s ascent. The program simply creates a FontInfo object for the font and saves its AscentPixels property in the TextBlock object’s TopHeight field.

The final piece of the TextBlock class is its Draw method, which overrides the same method in the Block class. The new method first calls the Draw class’s version of the method to draw a green box with a red baseline where the text will be positioned as shown in the picture at the top of the post. In the final program, you should comment out that call so the program only draws the text as in the following picture.


[document layout]

The method then draws the object’s text inside its bounds. Because the constructor defined the bounds so they would fit the text, the text fills the bounds completely. As a side effect, the text is centered both vertically and horizontally within the bounds.

I also made two minor changes to the Block class to make the derived TextBlock class work. First, I have the Block class a new parameterless constructor so the derived class didn’t need to pass the base constructor a bounds rectangle that it would not use.

Second, I changed the Draw method so it is virtual (so the TextBlock class can override it) and so it takes the parameters needed by the TextBlock class.

The Main Program

Instead of creating random blocks, the new example creates words that use random fonts. The following code shows how the program initializes its word data.

// Blocks to flow around obstacles.
private List<Block> Blocks;

// The text to draw.
private string Words = "Lorem ipsum dolor sit amet, ...";

// Define some blocks.
private void Form1_Load(object sender, EventArgs e)
{
    Obstacles = new List();
    Obstacles.Add(new RectangleF(0, 0, 64, 64));
    Obstacles.Add(new RectangleF(200, 50, 50, 100));
    Obstacles.Add(new RectangleF(50, 150, 100, 70));
    Obstacles.Add(new RectangleF(300, 200, 100, 64));

    Blocks = new List<Block>();
    Graphics gr = CreateGraphics();
    foreach (string word in Words.Split(' '))
        Blocks.Add(new TextBlock(word, RandomFont(), gr));
}

Notice that the variable Blocks is still a list of Block objects. The TextBlock class is derived from Block, so a TextBlock is a kind of block. That means the program can place TextBlock objects inside a List<Block>.

The string Words contains a long set of Lorem Ipsum words. For more information on this interesting test string, see the sire Lorem Ipsum.

The form’s Load event handler creates obstacles as before. It then breaks the Words string into words and uses each to create a TextBlock. The only new thing here is that the program calls the RandomFont method to generate each word’s font.

The RandomFont method simply picks a random font name, size, and style (bold, italic, regular, strikeout, or underline). Download the example to see the details.

Conclusion

The rest of the example is about the same as before. The Paint event handler calls the FlowBlocks method to arrange the blocks as before. The blocks are actually TextBlock objects instead of Block objects, but FlowBlocks doesn’t care.

The result is almost what we need to perform document layout with drop caps. The result shown here looks a bit strange because the obstacles are scattered around the drawing area rather than all places on the left edge. The words are also relatively large, so there are some fairly big gaps between words.

As usual, feel free to download the example and experiment with it. In my final post in this series, I’ll show how to handle paragraphs and pull the initial drop caps off of the first word in each paragraph. The result should look a lot more appealing than the one shown here.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, fonts, graphics, strings | Tagged , , , , , , , , , , , , | Leave a comment

Flow blocks around obstacles for document layout in C#


[document layout]

The following two document layout examples draw “illuminated” text by using an initial or drop cap.

Martin asked how we could do something similar when the text that follows the initial is drawn with different fonts. This is the first part of a three-part document layout example that does just that. This first post shows how you can perform layout for boxes, the second post will use these techniques to solve Martin’s problem, and the third will refine the technique to work more easily with drop caps.

This is a fairly long example, so I’ll describe it in pieces. Fortunately each of the methods is reasonably understandable if you focus only on its specific task and you don’t think about how the other pieces work until you get to them.

The Idea

Suppose we need to arrange some boxes inside a drawing area. The area contains some rectangular obstacles where we are not allowed to place our boxes.

The basic idea is to start in the upper left corner and arrange boxes in a row left to right. If we come to an obstacle, we skip over it. As we add boxes to the current row, we arrange the boxes so they all share a common baseline, which is somewhere within the boxes.

See the picture at the top of this post to see the idea. Note that all of the green boxes on the top row are aligned on their red baselines.

The first row skips past the first pink obstacle in the upper left corner. Block 0 is relatively large, so it is the only one that fits before the second obstacle interrupts the row. Blocks 1 through 4 fit on the first row, and then the document layout moves to a new row.

Only block 5, which is relatively small, fits on the second row before another obstacle gets in the way. The program places block 6, skips another obstacle, and then positions blocks 7 and 8 before starting the third row.

You can look at the remaining boxes and obstacles to get an idea about how the method works. You can probably devise other document layout methods, but this one seems to produce a reasonable result.

Also note that this would be a truly bizarre document layout for drop caps. Normally all of the drop caps would be at the left edge and the remaining text would flow to the right. This method handles that normal arrangement. It can also deal with one or two pictures scattered across the page.

The program uses the RectangleF structure to represent obstacles. It uses a Block class to represent the blocks that it should flow. The following section describes that class.

The following sections explain how the program arranges its blocks.

Obstacles and Blocks

The program uses the following Block class to represent the blocks that it should flow around obstacles.

public class Block
{
    public RectangleF Bounds;
    public float TopHeight;
    public float BottomHeight
    {
        get { return Bounds.Height - TopHeight; }
    }

    public Block(RectangleF bounds, float top_height)
    {
        Bounds = bounds;
        TopHeight = top_height;
    }

    // Draw the block.
    public void Draw(Graphics gr, int index, Font font)
    {
        if (Bounds.X < 0) return;

        gr.FillRectangle(Brushes.LightGreen, Bounds);
        gr.DrawRectangle(Pens.Green, Bounds);

        float x1 = Bounds.Left;
        float x2 = Bounds.Right;
        float y = Bounds.Top + TopHeight;
        gr.DrawLine(Pens.Red, x1, y, x2, y);

        gr.DrawString(index.ToString(),
            font, Brushes.Black,
            Bounds);
    }
}

This class defines two fields: Bounds and TopHeight. The Bounds value gives the block’s size and location. Initially only the size matters. [Insert your joke here.] After the block has been positioned, Bounds indicates where it should be drawn.

The TopHeight value is the distance from the block’s top to its baseline. The read-only BottomHeight property gives the distance from the block’s baseline to its bottom. That value is simply the block’s height minus its TopHeight value.

The class defines one simple initializing constructor.

The last part of the class defines a Draw method that draws the block on a Graphics object. That method fills the block’s area with light green, outlines it in green, draws a red line at the block’s baseline, and then draws the block’s index number inside the block’s area. (Look at the picture at the top of the post to see the result.)

The next section provides an overview of the methods that arrange the blocks. The sections after that describe each of those methods in greater detail.

Code Overview

The program uses the following methods to arrange blocks.

FlowBlocks
This is the high-level method that coordinates the document layout. It enters a loop that executes as long as any blocks are not positioned. Inside the loop, it calls PositionOneRow to position blocks on the next row. If no blocks fit on that row, it calls MoveDown to move down where a row might be drawn so it can hopefully position some blocks.
PositionOneRow
This method tries to position blocks in a row within a bounding area starting with a given Y coordinate. To do that, it tries to position one block, then two blocks, then three blocks, and so on until it finds a number of blocks that will not fit. For example, if it can position four blocks but not five blocks, then the method returns 4 and positions four blocks. To determine whether a given set of blocks fits on the row, the method calls BlocksFit.
BlocksFit
This method attempts to flow a given set of blocks around obstacles in a row with the given Y coordinate. If the blocks fit, the method leaves them positioned and returns true.
MoveDown
This method moves a Y coordinate downward to try to find a position for a new row. It is only called if the BlocksFit method was unable to fit even a single block on the next row. To make at least one block fit, the method moves the Y coordinate down so the next block will not overlap vertically with at least one obstacle. Note that the block may still overlap with other obstacles. In that case, the program will call MoveDown again until the block will fit or the document layout runs out of space.

The following sections show the methods’ code and describe them in greater detail. You may want to refer back to the overview as you read through the code.

FlowBlocks

The following code shows the top-level FlowBlocks method.

// Flow blocks around obstacles.
private void FlowBlocks(RectangleF bounds,
    List<RectangleF> obstacles, List<Block> blocks)
{
    float y = bounds.Y;

    // Repeat until we place all blocks or run out of room.
    int first_block = 0;
    while (first_block < blocks.Count)
    {
        // Position a row of blocks.
        int num_positioned = PositionOneRow(
            bounds, obstacles, blocks,
            ref first_block, ref y);

        // See if any fit.
        if (num_positioned == 0)
        {
            // None fit. Move down.
            MoveDown(bounds, obstacles, blocks[first_block], ref y);
        }

        // See if we have run out of room.
        if (y > bounds.Bottom)
        {
            // Position the remaining blocks at (-1, -1).
            for (int i = first_block; i < blocks.Count; i++)
            {
                blocks[i].Bounds.X = -1;
                blocks[i].Bounds.Y = -1;
            }
            break;
        }
    }
}

This method first sets variable y equal to the Y coordinate of the top of the drawing area. It uses variable first_block to keep track of the first block that has not yet been positioned. It sets that value to 0 and then enters a loop that continues until all of the blocks have been positioned.

Inside the loop, the method calls PositionOneRow to position blocks at the current Y coordinate. If no blocks will fit at that position, the method calls MoveDown to move the Y coordinate down so hopefully some blocks will fit.

After positioning some blocks or moving down, the method checks y to see if it has dropped off of the bottom of the document layout area. If we have run out of space, the method places the remaining unpositioned blocks at (-1, -1) and exits.

PositionOneRow

The following code shows the PositionOneRow method.

// Return the maximum number of blocks that will fit
// on one row starting at the indicated Y coordinate.
// If we position any blocks, update first_block and y.
private int PositionOneRow(RectangleF bounds,
    List<RectangleF> obstacles, List<Block> blocks,
    ref int first_block, ref float y)
{
    // Loop through the blocks.
    int last_that_fits = blocks.Count - 1;
    for (int i = first_block; i < blocks.Count; i++)
    {
        // See if we can place blocks[first_block]
        // through blocks[i].
        if (!BlocksFit(bounds, obstacles, blocks, first_block, i, y))
        {
            last_that_fits = i - 1;
            break;
        }
    }

    // If no blocks fit, return 0.
    if (last_that_fits < first_block) return 0;

    // Position the blocks that fit again.
    BlocksFit(bounds, obstacles, blocks,
        first_block, last_that_fits, y);

    // Find the maximum y coordinate for these blocks.
    for (int i = first_block; i <= last_that_fits; i++)
    {
        if (y < blocks[i].Bounds.Bottom)
            y = blocks[i].Bounds.Bottom;
    }

    // Update first_block.
    int num_blocks_positioned = last_that_fits - first_block + 1;
    first_block = last_that_fits + 1;

    // Return the number of blocks that fit.
    return num_blocks_positioned;
}

The parameter first_block gives the index of the first block that has not yet been positioned. This method enters a loop where i runs from first_block to the last block in the list. Inside the loop, it calls the BlocksFit method to see if the blocks with indices first_block through i will fit on the current row.

After it finds a set of blocks that will not fit on the current row, the method backtracks to the previous set. If that set is empty (no blocks will fit on the row), the method returns 0.

If some blocks fit on the row, the method calls BlocksFit again for the set that fit to reposition them. The code then loops through the positioned blocks and finds their maximum Y coordinate. It updates y so the next row is positioned below this one.

Finally, the method updates first_block and returns the number off blocks that it positioned.

BlocksFit

The following code shows the BlocksFit method.

// Return true if the indicated blocks will
// fit starting at the given Y coordinate.
private bool BlocksFit(RectangleF bounds,
    List<RectangleF> obstacles, List<Block> blocks,
    int first_block, int last_block, float y)
{
    // Find the maximum top height.
    float top_height = 0;
    for (int i = first_block; i <= last_block; i++)
    {
        if (top_height < blocks[i].TopHeight)
            top_height = blocks[i].TopHeight;
    }

    // Set the baseline.
    float baseline = y + top_height;

    // Position the blocks.
    float x = bounds.X;
    for (int i = first_block; i <= last_block; i++)
    {
        Block block = blocks[i];
        block.Bounds.X = x;
        x += block.Bounds.Width;
        if (x > bounds.Right) return false;

        block.Bounds.Y = baseline - block.TopHeight;

        // See if the block intersects with an obstacle.
        RectangleF rect = block.Bounds;
        bool had_hit = true;
        while (had_hit)
        {
            had_hit = false;
            foreach (RectangleF obstacle in Obstacles)
            {
                if (obstacle.IntersectsWith(rect))
                {
                    had_hit = true;

                    // Move the block to the right.
                    rect.X = obstacle.Right;

                    // See if we are out of bounds.
                    if (rect.Right > bounds.Right) return false;
                }
            }
        }

        // Update the block's bounds.
        block.Bounds = rect;
        x = rect.Right;

        // Loop to position the next block.
    }

    // If we get this far, then we have
    // positioned all of the blocks.
    return true;
}

This method first uses a loop to find the maximum top height of the blocks in question. It then positions the row’s baseline at that distance from the row’s upper Y coordinate.

The code then loops through the blocks and tries to position each block at the next available X coordinate. The code then loops through the obstacles to see if any intersect with the block at that position. If an obstacle intersects the block, the method moves the block to the right of that obstacle. It also restarts the loop that examines the obstacles in case one of the previous obstacles now intersects the moved block.

The inner loop ends when one of two things occurs. First, if block extends beyond the right edge of the document layout area, then the blocks will not fit on this row so the method returns false.

Second, if the block does not intersect any obstacle, then it is in a safe position. In that case, the method continues its outer loop to position the other blocks in the row.

If the method successfully positions all of the blocks, it returns true.

MoveDown

The following code shows the MoveDown method.

// Move down so the block will clear at least one new obstacle.
private void MoveDown(RectangleF bounds,
    List<RectangleF> obstacles, Block block, ref float y)
{
    float min_y = y + block.Bounds.Height;
    RectangleF rect = new RectangleF(
        bounds.X, y, bounds.Width, block.Bounds.Height);
    foreach (RectangleF obstacle in obstacles)
    {
        if (obstacle.IntersectsWith(rect))
        {
            if (min_y > obstacle.Bottom)
                min_y = obstacle.Bottom;
        }
    }

    y = min_y;
}

This method creates a rectangle as tall as the next block to position and as wide as the document layout area. It then loops through the obstacles to find the smallest Y coordinate of the bottom of any obstacle that intersects with the rectangle. The code updates the reference parameter y to move past that Y value and returns.

This moves the next row past the highest obstacle that intersects the rectangle. The higher-level methods then try to position the row again. If those methods fail again, they may call MoveDown again.

Main Program

That’s all of the document layout code. The following code shows how the program generates some random obstacles and blocks.

// Obstacles.
private List<RectangleF> Obstacles;

// Blocks to flow around obstacles.
private List<Block> Blocks;

// Define some blocks.
private void Form1_Load(object sender, EventArgs e)
{
    Obstacles = new List<RectangleF>();
    Obstacles.Add(new RectangleF(0, 0, 64, 64));
    Obstacles.Add(new RectangleF(200, 50, 50, 100));
    Obstacles.Add(new RectangleF(50, 150, 100, 70));
    Obstacles.Add(new RectangleF(300, 200, 100, 64));

    // Random rand = new Random(4);
    Random rand = new Random(4);
    Blocks = new List<Block>();
    for (int i = 0; i < 25; i++)
    {
        float width = rand.Next(10, 100);
        float height = rand.Next(10, 100);
        Blocks.Add(new Block(
            new RectangleF(0, 0, width, height), height * 0.6f));
    }
}

At the form level, the code defines lists to hold obstacles and blocks. The form’s Load event handler creates some specific obstacles and then uses a Random object to generate blocks with random sizes. It sets each block’s TopHeight value to 0.6 times its height.

The program arranges the blocks in the following Paint event handler.

// Flow and draw the blocks.
private void picWriting_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

    // Flow.
    FlowBlocks(picWriting.ClientRectangle, Obstacles, Blocks);

    // Draw obstacles.
    foreach (RectangleF obstacle in Obstacles)
    {
        e.Graphics.FillRectangle(Brushes.Pink, obstacle);
        e.Graphics.DrawRectangle(Pens.Red, obstacle);
    }

    // Draw flowed blocks.
    for (int i = 0; i < Blocks.Count; i++)
    {
        Blocks[i].Draw(e.Graphics, i, picWriting.Font); 
    }
}

This code calls the FlowBlocks method to position the blocks. It then loops through the obstacles and draws them as pink rectangles.

Next, the code creates a dashed pen and a StringFormat object to center text. It then loops through the blocks. It draws each block in green, draws its baseline with the dashed pen, and draws the block’s index inside it.

Conclusion

This method is rather long, but it seems to produce a reasonable result. The picture shown at the top of the post includes some fairly large blank areas. For example, there’s a pretty big break after block 0 and below block 0. Those should be smaller with a more realistic arrangement where drop caps are on the left and a relatively large amount of text flows to the right. After you see the next two posts, you can decide whether you want to modify the document layout algorithm.

Meanwhile, download the example and experiment with it. In my next post, I’ll show how you can modify the block flow method to flow text with various fonts around obstacles.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, fonts, graphics, strings | Tagged , , , , , , , , , , , , | 2 Comments