Title: Make an OwnerDraw ListView in C#
Normally a ListView displays textual data, but you can change that behavior to make it display anything that you can draw. This example draws images and colored circles to show the status of fictional servers.
The idea is to set the ListView control's OwnerDraw property to True and then make items and subitems as placeholders in the control. Then code then catches the ListView control's DrawColumnHeader, DrawItem, and DrawSubItem event handlers and draws the items appropriately.
The only really non-obvious issue is that the DrawSubItem event handler, which is called to draw the control's details view executes for every subitem in each row including the item itself. That means the DrawItem event handler should not draw the item because DrawSubItem event handler will take care of that.
The basic ideas aren't too complicated, but you do need to do a fair amount of drawing so this is a fairly long example.
The program uses the following ServerStatus class to hold information about each fictitious server.
class ServerStatus
{
public string ServerName;
public Image Logo;
public Color StatusColor;
public ServerStatus(string serverName,
Image logo, Color statusColor)
{
ServerName = serverName;
Logo = logo;
StatusColor = statusColor;
}
}
This class holds a server's name, a picture representing the server, and a color indicating its status.
When the program loads, it uses the following code to create data for its five ListView controls.
// Make some data.
private void Form1_Load(object sender, EventArgs e)
{
ListView[] listViews = new ListView[] { lvwList, lvwSmallIcon,
lvwLargeIcon, lvwTile, lvwDetails };
foreach (ListView lvw in listViews)
{
AddItem(lvw, "Butterfly", Properties.Resources.Butterfly,
Color.Green);
AddItem(lvw, "Guppy", Properties.Resources.Fish,
Color.Red);
AddItem(lvw, "Peggy", Properties.Resources.Peggy,
Color.Yellow);
}
}
// Make a server status item.
private void AddItem(ListView lvw, string server, Image logo,
Color status)
{
// Make the item.
ListViewItem item = new ListViewItem(server);
// Save the ServeStatus item in the Tag property.
ServerStatus server_status =
new ServerStatus(server, logo, status);
item.Tag = server_status;
item.SubItems[0].Name = "Server";
// Add subitems so they can draw.
item.SubItems.Add("Logo");
item.SubItems.Add("Status");
// Add the item to the ListView.
lvw.Items.Add(item);
}
The form's Load event handler calls AddItem to add items to the ListView controls. AddItem makes a ListViewItem object and adds it to a ListView. It sets the item's Tag property to a ServerStatus object so the program can later get information about each item.
The following code shows the DrawColumnHeader event handler that's called for OwnerDraw ListView controls.
// Just draw the column's text.
private void lvwServers_DrawColumnHeader(object sender,
DrawListViewColumnHeaderEventArgs e)
{
using (StringFormat string_format = new StringFormat())
{
string_format.Alignment = StringAlignment.Center;
string_format.LineAlignment = StringAlignment.Center;
string text = lvwList.Columns[e.ColumnIndex].Text;
switch (e.ColumnIndex)
{
case 0:
e.Graphics.DrawString(text, lvwList.Font,
Brushes.Black, e.Bounds);
break;
case 1:
e.Graphics.DrawString(text, lvwList.Font,
Brushes.Blue, e.Bounds);
break;
case 2:
e.Graphics.DrawString(text, lvwList.Font,
Brushes.Green, e.Bounds);
break;
}
}
}
In this example, the DrawColumnHeader event handler is fairly simple. It just draws each column's text within the indicated bounds. It gets the text from the ListView control's column text, which is set at design time.
The following code shows the DrawItem event handler that draws the items for an OwnerDraw ListView.
// Draw the item. In this case, the server's logo.
private void lvwServers_DrawItem(object sender,
DrawListViewItemEventArgs e)
{
// Draw Details view items in the DrawSubItem event handler.
ListView lvw = e.Item.ListView;
if (lvw.View == View.Details) return;
// Get the ListView item and the ServerStatus object.
ListViewItem item = e.Item;
ServerStatus server_status = item.Tag as ServerStatus;
// Clear.
e.DrawBackground();
// Draw a status indicator.
e.Graphics.SmoothingMode =
System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Rectangle rect = new Rectangle(
e.Bounds.Left + 1, e.Bounds.Top + 1,
e.Bounds.Height - 2, e.Bounds.Height - 2);
using (SolidBrush br =
new SolidBrush(server_status.StatusColor))
{
e.Graphics.FillEllipse(br, rect);
}
e.Graphics.DrawEllipse(Pens.Black, rect);
int left = rect.Right + 2;
// See how much we must scale it.
float scale;
scale = e.Bounds.Height / (float)server_status.Logo.Height;
// Scale and position the image.
e.Graphics.ScaleTransform(scale, scale);
e.Graphics.TranslateTransform(
left,
e.Bounds.Top + (e.Bounds.Height -
server_status.Logo.Height * scale) / 2,
System.Drawing.Drawing2D.MatrixOrder.Append);
// Draw the image.
e.Graphics.DrawImage(server_status.Logo, 0, 0);
// Draw the focus rectangle if appropriate.
e.Graphics.ResetTransform();
e.DrawFocusRectangle();
}
If this is the ListView control's Details view, the code exits because the details view is drawn by the DrawSubItem event handler.
If this is not the Details view, the code gets the item's corresponding ServerStatus object from the item's Tag property.
Next the code clears the item's background. It makes a Rectangle in the left of the area where the item should be drawn that is as wide as it is tall and draws a status indicator circle there.
Next the code calculates the scale at which it should draw the server's logo image to fit the available area nicely. It then draws the image to the right of the status circle.
The event handler finishes by calling DrawFocusRectangle to draw a focus rectangle around the item if it has the focus. In the picture, for example, the first item in the top left ListView has the focus.
That's all the code needed to draw an OwnerDraw item unless the ListView is displaying the Detail view. The following DrawSubItem event handler produces all of the graphics for the Detail view.
// Draw subitems for Detail view.
private void lvwServers_DrawSubItem(object sender,
DrawListViewSubItemEventArgs e)
{
// Get the ListView item and the ServerStatus object.
ListViewItem item = e.Item;
ServerStatus server_status = item.Tag as ServerStatus;
// Draw.
switch (e.ColumnIndex)
{
case 0:
// Draw the server's name.
e.Graphics.DrawString(server_status.ServerName,
lvwList.Font, Brushes.Black, e.Bounds);
break;
case 1:
// Draw the server's logo.
float scale = e.Bounds.Height /
(float)server_status.Logo.Height;
e.Graphics.ScaleTransform(scale, scale);
e.Graphics.TranslateTransform(
e.Bounds.Left,
e.Bounds.Top + (e.Bounds.Height -
server_status.Logo.Height * scale) / 2,
System.Drawing.Drawing2D.MatrixOrder.Append);
e.Graphics.DrawImage(server_status.Logo, 0, 0);
break;
case 2:
// Draw the server's status.
Rectangle rect = new Rectangle(
e.Bounds.Left + 1, e.Bounds.Top + 1,
e.Bounds.Width - 2, e.Bounds.Height - 2);
using (SolidBrush br =
new SolidBrush(server_status.StatusColor))
{
e.Graphics.FillRectangle(br, rect);
}
Color pen_color = Color.FromArgb(255,
255 - server_status.StatusColor.R,
255 - server_status.StatusColor.G,
255 - server_status.StatusColor.B);
using (SolidBrush br = new SolidBrush(pen_color))
{
using (StringFormat string_format =
new StringFormat())
{
string_format.Alignment =
StringAlignment.Center;
string_format.LineAlignment =
StringAlignment.Center;
using (Font font =
new Font(lvwList.Font, FontStyle.Bold))
{
e.Graphics.DrawString(
server_status.StatusColor.Name,
font, br, e.Bounds, string_format);
}
}
}
break;
}
// Draw the focus rectangle if appropriate.
e.Graphics.ResetTransform();
ListView lvw = e.Item.ListView;
if (lvw.FullRowSelect)
{
e.DrawFocusRectangle(e.Item.Bounds);
}
else if (e.SubItem.Name == "Server")
{
e.DrawFocusRectangle(e.Bounds);
}
}
This code starts by getting the item corresponding to the subitem that it should draw. It then gets the ServerStatus object corresponding to that item.
Next the code uses a switch statement to see which of the item's columns it should draw. Depending on which column this is, the code draws:
- The server's name
- The server's logo
- The server's status inside a colored box
The event handler finishes by calling DrawFocusRectangle to draw a focus rectangle around the item if it has the focus. If the ListView control's FullRowSelect property is true, the control passes DrawFocusRectangle the bounds of the item so it draws the rectangle around the whole row. If FullRowSelect is false, it only calls DrawFocusRectangle if the code is drawing the server's name so the box appears only around the name. You can change FullRowSelect to false at design time to see how this works.
This is a lot of code, but it's fairly general purpose so you should be able to modify it relatively easily to draw other OwnerDraw listView controls.
Download the example to experiment with it and to see additional details.
|