BLOG.CSHARPHELPER.COM: Make an owner-drawn ListView control that draws server status information in C#
Make an owner-drawn ListView control that draws server status information in C#
Normally a ListView displays textual data but you can change that behavior to make it display anything you can draw. This example draws images and colored circles to show the status of its entries.
The approach is to set the ListView's OwnerDraw property to True and then make items and subitems as placeholders in the control. Then code then catches the ListView's DrawColumnHeader, DrawItem, and DrawSubItem event handlers and draws the items appropriately.
The only really non-obvious issue is that the DrawSubItem event handler that is called to draw the 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 it will be taken care of by the DrawSubItem event handler.
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 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 creates the ListView controls' data as shown in the following code.
// 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 late get information about each item.
The following code shows the DrawColumnHeader event handler.
// 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's column text set at design time.
The following code shows the DrawItem event handler.
// 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's details view, the code simply 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.
The code then calculates the scale at which it should draw the server's logo image to fit the available area nicely. It next 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. (The first item in the top left ListView has the focus in the picture shown above.)
That's all the code needed to draw the 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. (Change FullRowSelect to false at design time to see this.)
Comments