Title: Make an owner-drawn ListBox that justifies columns in C#
This example shows how you can use an owner-drawn ListBox to left and right align values in columns. The following examples show different ways to align values in columns.
Unfortunately, the ListBox control doesn't provide a way to line up multiple columns of left and right justified values. You can do that yourself by making an owner-drawn ListBox, but it requires some extra work.
This example uses the techniques described in Draw aligned columns of data in C# to make an owner-drawn ListBox draw values in justified columns.
When the program starts, it executes the following code.
// Make the ListBox owner-drawn and give it data.
private void Form1_Load(object sender, EventArgs e)
{
lstBooks.DrawMode = DrawMode.OwnerDrawVariable;
lstBooks.Items.AddRange(Values);
}
This code makes the ListBox owner-drawn and gives it some data. The variable Values is an array of arrays of strings. In other words, it contains values for each row and each row holds an array of strings.
When you use an owner-drawn ListBox, you must handle two key events: MeasureItem and DrawItem. The MeasureItem event handler shown in the following code is called to tell the owner-drawn ListBox how much room it should leave for a menu item.
// Row and column sizes.
private float RowHeight, RowWidth;
private float[] ColWidths = null;
private const float RowMargin = 10;
private const float ColumnMargin = 10;
// Return the desired size of an item.
private void lstBooks_MeasureItem(object sender, MeasureItemEventArgs e)
{
// Measure the data if we haven't already done so.
if (ColWidths == null)
{
// Get the row and column sizes.
GetRowColumnSizes(e.Graphics, lstBooks.Font, Values,
out RowHeight, out ColWidths);
// Add margins.
for (int i = 0; i < ColWidths.Length; i++)
ColWidths[i] += ColumnMargin;
RowHeight += RowMargin;
// Get the total row width.
RowWidth = ColWidths.Sum();
}
// Set the desired size.
e.ItemHeight = (int)RowHeight;
e.ItemWidth = (int)RowWidth;
}
If ColWidths is null, then the program has not yet measured the data. In that case, this code calls the GetRowColumnSizes method to calculate the maximum row height and the column widths for the data values. For more information on this method, see the previous example Draw aligned columns of data in C#.
The code then adds a margin to each column's width and to the maximum row height so the values aren't too crowded when they are drawn.
Finally, the event handler sets e.ItemHeight and e.ItemWidth to tell the owner-drawn ListBox how much room to allow for the menu item.
The following DrawItem event handler is called when the owner-drawn ListBox needs to draw a menu item.
// Draw an item.
private void lstBooks_DrawItem(object sender, DrawItemEventArgs e)
{
string[] values = (string[])lstBooks.Items[e.Index];
e.DrawBackground();
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
{
DrawRow(e.Graphics, lstBooks.Font, SystemBrushes.HighlightText,
null,e.Bounds.X, e.Bounds.Y, RowHeight, ColWidths,
VertAlignments, HorzAlignments, values, false);
}
else
{
DrawRow(e.Graphics, lstBooks.Font, Brushes.Black, null,
e.Bounds.X, e.Bounds.Y, RowHeight, ColWidths,
VertAlignments, HorzAlignments, values, false);
}
}
This code gets the data for the ListBox row that it needs to draw. It then calls e.DrawBackground to draw an appropriate background. (If the item is selected because the user clicked it, the background is blue. If the item is not selected, the background is white.)
The code then calls the DrawRow method to draw the row in the ListBox. If the ListBox item is selected because the user clicked it, then the code draws it with the system-defined text highlight color. If the item is not selected, the code calls DrawRow to draw the row with black text.
Notice that the code passes DrawBox the X and Y coordinates of the row's upper left corner as given by the event handler's e.Bounds parameter.
The following code shows the DrawRow method.
// Draw the items in columns.
private void DrawRow(Graphics gr, Font font, Brush brush, Pen box_pen,
float x0, float y0, float row_height, float[] col_widths,
StringAlignment[] vert_alignments, StringAlignment[] horz_alignments,
string[] values, bool draw_box)
{
// Create a rectangle in which to draw.
RectangleF rect = new RectangleF();
rect.Height = row_height;
using (StringFormat sf = new StringFormat())
{
float x = x0;
for (int col_num = 0; col_num < values.Length; col_num++)
{
// Prepare the StringFormat and drawing rectangle.
sf.Alignment = horz_alignments[col_num];
sf.LineAlignment = vert_alignments[col_num];
rect.X = x;
rect.Y = y0;
rect.Width = col_widths[col_num];
// Draw.
gr.DrawString(values[col_num], font, brush, rect, sf);
// Draw the box if desired.
if (draw_box) gr.DrawRectangle(box_pen,
rect.X, rect.Y, rect.Width, rect.Height);
// Move to the next column.
x += col_widths[col_num];
}
}
}
This method makes a RectangleF and a StringFormat that it will use to draw the row's items and then loops through those items. For each item, the code sets the StringFormat object's Alignment and LineAlignment properties and draws the item at the appropriate location. If the draw_box parameter is true, the code also draws the RectangleF. (This is mostly for debugging.)
After it draws each item, the code increases the X coordinate so it is ready to draw the next item.
This example is fairly complicated but still manageable. An alternative would be to use a ListView control instead of a ListBox control. The behavior isn't quite the same so it doesn't provide exactly the same look and feel, but the ListView lets you set up more of the details about column justification interactively at design time.
Download the example to experiment with it and to see additional details.
|