using System;
using System.Data;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace IssueVision
{
// Displays the history list. Uses an owner draw listbox. Maintains
// an offscreen bitmap to draw each item to prevent flickering when
// the control is resized.
public class HistoryList : ListBox
{
// data that is attached to each row in the list
private struct RowItem
{
public bool IsHeader;
public string DisplayName;
public string Comment;
public string DateCreated;
// init the item
public RowItem(bool isHeader, string displayName, string comment, DateTime dateCreated)
{
IsHeader = isHeader;
DisplayName = displayName;
Comment = comment;
if (dateCreated != DateTime.MinValue)
DateCreated = string.Format("{0} {1}", dateCreated.ToShortDateString(), dateCreated.ToShortTimeString()).ToLower();
else
DateCreated = string.Empty;
}
}
private class Consts
{
// cell space in each row
public const int Buffer = 4;
// where to draw the second column of information (from right side)
public const int RightBuffer = 120;
// don't draw the second column if the control is only this wide
public const int DrawRightSideWidth = 210;
}
// internal members
private Font m_fontHeader;
private StringFormat m_format;
// maintain offscreen bitmap to prevent flickering
private Bitmap m_bmpItem;
private Graphics m_graphics;
// updating the contents of the list
public new DataRow[] DataSource
{
set
{
// delete any existing items
Clear();
// go through and add rows to the list
if (value != null)
{
foreach (DataRow row in value)
{
Items.Add(new RowItem(false, (string)row["DisplayName"], (string)row["Comment"], (DateTime)row["DateCreated"]));
}
}
}
}
public HistoryList()
{
// owner draw, variable height so can draw the header
// with a different height
DrawMode = DrawMode.OwnerDrawVariable;
// font used when drawing the header
m_fontHeader = new Font("tahoma", Font.Size, FontStyle.Bold);
// format used when drawing comments
m_format = new StringFormat();
m_format.Trimming = StringTrimming.EllipsisCharacter;
}
// remove all items from the list
public void Clear()
{
// delete all items
Items.Clear();
// always add the header as the first item
Items.Add(new RowItem(true, string.Empty, string.Empty, DateTime.MinValue));
Invalidate();
}
// calculate the height of the row
protected override void OnMeasureItem(MeasureItemEventArgs e)
{
base.OnMeasureItem(e);
if (e.Index >= 0)
{
// header is index 0, others are data items
if (e.Index == 0)
{
e.ItemHeight = Font.Height + (Consts.Buffer * 3);
}
else
{
e.ItemHeight = (Font.Height * 3) + (Consts.Buffer * 5);
}
}
}
// a row needs to be drawn
protected override void OnDrawItem(DrawItemEventArgs e)
{
base.OnDrawItem(e);
if (e.Index < 0 || e.Index >= Items.Count)
return;
// each section is drawn on an offscreen bitmap to prevent flickering
// the bitmap is only created when necessary (when the row size changes)
CreateOffscreenGraphics(e.Index);
m_graphics.Clear(SystemColors.Window);
// header is index 0, others are data items
if (e.Index == 0)
{
DrawHeader(m_graphics, e.Bounds.Width, e.Bounds.Height);
}
else
{
DrawRow(m_graphics, e.Bounds.Width, e.Bounds.Height, (RowItem)this.Items[e.Index]);
}
//blit the offscreen bitmap to the screen
e.Graphics.DrawImage(m_bmpItem, e.Bounds.Left, e.Bounds.Top, m_bmpItem.Width, m_bmpItem.Height);
}
// need to redraw rows when resize
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
Invalidate();
}
// draw the header
private void DrawHeader(Graphics g, int width, int height)
{
// left label
g.DrawString("User/Comment", m_fontHeader, SystemBrushes.ControlText, Consts.Buffer, (height - m_fontHeader.Height - Consts.Buffer));
if (width > Consts.DrawRightSideWidth)
{
g.DrawString("Task Date", m_fontHeader, SystemBrushes.ControlText, (width - Consts.RightBuffer), (height - m_fontHeader.Height - Consts.Buffer));
}
// separator
g.DrawLine(Pens.LightGray, 0, height - 1, width, height - 1);
}
// draw data item
private void DrawRow(Graphics g, int width, int height, RowItem item)
{
//display name
g.DrawString(item.DisplayName, this.Font, SystemBrushes.ControlText, Consts.Buffer, Consts.Buffer);
//date created
if (width > Consts.DrawRightSideWidth)
{
g.DrawString(item.DateCreated, this.Font, Brushes.DarkGray, width - Consts.RightBuffer, Consts.Buffer);
}
//calculate the comment area
RectangleF boundsComment = new RectangleF(Consts.Buffer, this.Font.Height + (Consts.Buffer * 3), width - (Consts.Buffer * 2), this.Font.Height * 2);
//comment
g.DrawString(item.Comment, this.Font, SystemBrushes.ControlText, boundsComment, m_format);
//separator
g.DrawLine(Pens.LightGray, 0, height - 1, width, height - 1);
}
// maintain an offscreen bitmap that is used to draw each
// item in the listbox, use the same bitmap if possible, only
// create a new one when the size changes
private void CreateOffscreenGraphics(int index)
{
if (index >= 0 && index < Items.Count)
{
// see if need to create the offscreen bitmap
Rectangle bounds = GetItemRectangle(index);
if (m_bmpItem == null || m_bmpItem.Width != bounds.Width || m_bmpItem.Height != bounds.Height)
{
// size changed, create the bitmap and graphics
if (m_bmpItem != null)
{
m_bmpItem.Dispose();
}
m_bmpItem = new Bitmap(bounds.Width, bounds.Height);
if (m_graphics != null)
{
m_graphics.Dispose();
}
m_graphics = Graphics.FromImage(m_bmpItem);
}
}
}
// paint the background part of the control that does not contain any items
protected override void WndProc(ref Message m)
{
const int WM_ERASEBKGND = 20;
// look for background erase message
if (m.Msg == WM_ERASEBKGND)
{
// let Windows handle the message if the list is empty
if (Items.Count > 0)
{
// calculate the area between the bottom of the last
// item and the bottom of the list box control
Rectangle rc = GetItemRectangle(Items.Count - 1);
// fill in the bottom area
if (rc.Bottom != 0 && rc.Bottom < DisplayRectangle.Bottom)
{
Graphics g = Graphics.FromHwnd(Handle);
g.FillRectangle(SystemBrushes.Window, DisplayRectangle.Left, rc.Bottom, DisplayRectangle.Width, DisplayRectangle.Bottom - rc.Bottom);
g.Dispose();
}
}
}
else
{
base.WndProc(ref m);
}
}
}
}