Click here to Skip to main content
15,893,814 members
Articles / Programming Languages / C#

C# List View v1.3

Rate me:
Please Sign up or sign in to vote.
4.90/5 (168 votes)
2 Mar 2004CPOL13 min read 2.1M   55.8K   434  
A fully featured completely managed C# ListView.
using System;
using System.Text;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.IO;
using System.Windows.Forms;
using System.Diagnostics;
using System.Drawing.Design;
using System.Windows.Forms.Design;
using System.ComponentModel.Design;
using System.ComponentModel.Design.Serialization;
using System.Collections.Specialized;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.Reflection;






namespace GlacialComponents.Controls
{
	#region Helper Classes

	public enum ChangedTypes { GeneralInvalidate, SubItemChanged, ItemChanged, ItemCollectionChanged, ColumnChanged, ColumnCollectionChanged };
	public enum Alignments { alignLeft, alignCenter, alignRight }

	public class ChangedEventArgs : EventArgs
	{
		private int				m_nItem = -1;
		private int				m_nColumn = -1;
		private ChangedTypes	m_ctType = ChangedTypes.GeneralInvalidate;

		public ChangedEventArgs( ChangedTypes ctType, int nColumn, int nItem )
		{
			m_nItem = nItem;
			m_nColumn = nColumn;
			m_ctType = ctType;
		}

		public int Item
		{
			get	{ return m_nItem; }
		}

		public int Column
		{
			get	{ return 0; }
		}

		public ChangedTypes ChangedType
		{
			get { return m_ctType; 	}
		}
	}

	public class ClickEventArgs : EventArgs
	{
		private int m_nItemIndex;
		private int m_nColumnIndex;

		public ClickEventArgs( int itemindex, int columnindex )
		{
			m_nItemIndex = itemindex;
			m_nColumnIndex = columnindex;
		}

		public int ItemIndex
		{
			get
			{
				return m_nItemIndex;
			}
		}
		public int ColumnIndex
		{
			get
			{
				return m_nColumnIndex;
			}
		}
	}

	public class ManagedHScrollBar : System.Windows.Forms.HScrollBar
	{
		public int mTop
		{
			set
			{
				if ( Top!=value)
					Top = value;
			}
		}
		public int mLeft
		{
			set
			{
				if ( value != Left )
					Left = value;
			}
		}
		public int mWidth
		{
			get
			{
				if ( Visible != true )
					return 0;
				else
					return Width;
			}
			set
			{
				if ( Width != value )
					Width = value;
			}
		}
		public int mHeight
		{
			get
			{
				if ( Visible != true )
					return 0;
				else
					return Height;
			}
			set
			{
				if ( Height != value )
					Height = value;
			}
		}
		public bool mVisible
		{
			set
			{
				if ( Visible != value )
					Visible = value;
			}
		}
		public int mSmallChange
		{
			set
			{
				if ( SmallChange != value )
					SmallChange = value;
			}
		}
		public int mLargeChange
		{
			set
			{
				if ( LargeChange != value )
					LargeChange = value;
			}
		}
		public int mMaximum
		{
			set
			{
				if ( Maximum != value )
					Maximum = value;
			}
		}
	}

	public class ManagedVScrollBar : System.Windows.Forms.VScrollBar
	{
		public int mTop
		{
			set
			{
				if ( Top!=value)
					Top = value;
			}
		}
		public int mLeft
		{
			set
			{
				if ( value != Left )
					Left = value;
			}
		}
		public int mWidth
		{
			get
			{
				if ( Visible != true )
					return 0;
				else
					return Width;
			}
			set
			{
				if ( Width != value )
					Width = value;
			}
		}
		public int mHeight
		{
			get
			{
				if ( Visible != true )
					return 0;
				else
					return Height;
			}
			set
			{
				if ( Height != value )
					Height = value;
			}
		}
		public bool mVisible
		{
			set
			{
				if ( Visible != value )
					Visible = value;
			}
		}
		public int mSmallChange
		{
			set
			{
				if ( SmallChange != value )
					SmallChange = value;
			}
		}
		public int mLargeChange
		{
			set
			{
				if ( LargeChange != value )
					LargeChange = value;
			}
		}
		public int mMaximum
		{
			set
			{
				if ( Maximum != value )
					Maximum = value;
			}
		}
	}


	#endregion

	/// <summary>
	/// Summary description for GlacialList.
	/// </summary>
	public class GlacialList : System.Windows.Forms.UserControl
	{

		#region Debugging

		public static void DW( string strout )			// debug write
		{
#if false
			//System.IO.StreamWriter sw = new System.IO.StreamWriter( "e:\\debug.txt", true );
			//sw.WriteLine( strout );
			//sw.Close();
#else
			Debug.WriteLine( strout );
#endif
		}

		#endregion

		#region Header

		#region Events and Delegates

		#region Clicked Events

		public delegate void ClickedEventHandler( object source, ClickEventArgs e );//int nItem, int nSubItem );

		public event ClickedEventHandler ItemClickedEvent;
		public event ClickedEventHandler ColumnClickedEvent;
		public event ClickedEventHandler RightClickedEvent;

		#endregion

		#region Changed Events

		public delegate void ChangedEventHandler( object source, ChangedEventArgs e );				//int nItem, int nSubItem );

		public event ChangedEventHandler ItemChangedEvent;
		public event ChangedEventHandler ColumnChangedEvent;

		#endregion

		#endregion

		#region VarsDefsProps

		#region TestCode
		void OnItemClicked( object source, ClickEventArgs e )// int nItem, int nSubItem )
		{
			DW( "OnItemClicked" );

		}
		#endregion

		#region ClassVariablesAndEnums

		//public enum CoordType { none, ColumnSelect, ItemSelect, ColumnResize };
		public enum GridStyles { gridNone, gridDashed, gridSolid }
		public enum ListStates { stateNone, stateSelecting, stateColumnSelect, stateColumnResizing }
		public enum RedrawStates { RedrawNormal, RedrawSuspended, RedrawSuspendedPending }


		//static int					BorderSize = 2;
		static int					RESIZE_ARROW_PADDING = 2;
		static int					MINIMUM_COLUMN_SIZE = 10;



		private int					m_nLastSelectionIndex = 0;
		private int					m_nLastSubSelectionIndex = 0;



		private ListStates			m_nState = ListStates.stateNone;
		private Point				m_pointColumnResizeAnchor;
		private int					m_nResizeColumnNumber;			// the column number thats being resized


		private System.ComponentModel.Container components = null;

		private GlacialComponents.Controls.ManagedVScrollBar vPanelScrollBar;
		private GlacialComponents.Controls.ManagedHScrollBar hPanelScrollBar;

		#endregion

		#region ClassProperties

		private GridStyles						m_nGridLineStyle = GridStyles.gridSolid;
		private int								m_nItemHeight = 18;
		private int								m_nHeaderHeight = 18;
		private int								m_nBorderWidth = 2;
		private Color							m_colorGridColor = Color.Gray;
		private bool							m_bMultiSelect = false;
		private Color							m_colorSelectionColor = Color.SeaShell;
		private RedrawStates					m_RedrawState = RedrawStates.RedrawNormal;		// true if we can redraw, false if we are holding redraw up
		private bool							m_bHeaderVisible = true;

		private GLColumnCollection				m_Columns;// = new GLColumnCollection( this );
		private GLItemCollection				m_Items;// = new GLItemCollection();

		private int								m_nMaxHeight = 0;
		private bool							m_bAutoHeight = true;
		private bool							m_bAllowColumnResize = true;
		private bool							m_bFullRowSelect = true;


		#region Control Properties

		[
		Description("Allow resizing of columns"),
		Category("Behavior"),
		Browsable(true)
		]
		public bool AllowColumnResize
		{
			get { return m_bAllowColumnResize; }
			set { m_bAllowColumnResize = value; }
		}


		[
		Description("Do we want rows to automatically adjust height"),
		Category("Behavior"),
		Browsable(true)
		]
		public bool AutoHeight
		{
			get { return m_bAutoHeight; }
			set { m_bAutoHeight = value; }
		}


		/// <summary>
		/// you want the header to be visible or not
		/// </summary>
		[
		Description("Column Headers Visible"),
		Category("Behavior"),
		Browsable(true)
		]
		public bool HeaderVisible
		{
			get { return m_bHeaderVisible; }
			set { m_bHeaderVisible = value; }
		}


		[
		Category("Behavior"),
		Description("Column Collection"),
		DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
		Editor(typeof(CustomCollectionEditor), typeof(UITypeEditor)),
		Browsable(true)
		]
		public GLColumnCollection Columns
		{
			get	{ return m_Columns; }
		}

					
		[
		Category("Behavior"),
		Description("Items collection"),
		Browsable(false)
		]
		public GLItemCollection Items
		{
			get	{ return m_Items; }
		}


		[
		Description("Background color to mark selection."),
		Category("Appearance"),
		Browsable(true),
		]
		public Color SelectionColor
		{
			get	{ return m_colorSelectionColor; }
			set { m_colorSelectionColor = value; }
		}


		[
		Description("Allow full row select."),
		Category("Behavior"),
		Browsable(true)
		]
		public bool FullRowSelect
		{
			get	{ return m_bFullRowSelect; }
			set	{ m_bFullRowSelect = value; }
		}


		[
		Description("Allow multiple selections."),
		Category("Behavior"),
		Browsable(true)
		]
		public bool AllowMultiselect
		{
			get	{ return m_bMultiSelect; }
			set	{ m_bMultiSelect = value; }
		}


		[
		Description("Border Padding"),
		Category("Appearance"),
		Browsable(true),
		DefaultValue(2)
		]
		public int BorderPadding
		{
			get	{ return m_nBorderWidth; }
			set	{ m_nBorderWidth = value; }
		}


		[
		Description("Whether or not to draw gridlines"),
		Category("Grid"),
		Browsable(true),
		DefaultValue(0)
		]
		public GridStyles GridLines
		{
			get	{ return m_nGridLineStyle; }
			set
			{
				m_nGridLineStyle = (GridStyles)value;
				ManagedInvalidate();
			}
		}


		[
		Description("Color of the grid if we draw it."),
		Category("Grid"),
		Browsable(true)
		]
		public Color GridColor
		{
			get	{ return m_colorGridColor; }
			set
			{
				m_colorGridColor = (Color)value;
				ManagedInvalidate();
			}
		}


		/// <summary>
		/// how big do we want the individual items to be
		/// </summary>
		[
		Description("How high each row is."),
		Category("Appearance"),
		Browsable(true)
		]
		public int ItemHeight
		{
			get { return m_nItemHeight;	}
			set
			{
				m_nItemHeight = value;
				ManagedInvalidate();
			}
		}


		[
		Description("How high the columns are."),
		Category("Appearance"),
		Browsable(true)
		]
		public int HeaderHeight
		{
			get
			{
				if ( HeaderVisible == true )
					return m_nHeaderHeight;
				else
					return 0;
			}
			set
			{
				m_nHeaderHeight = value;
				ManagedInvalidate();
			}
		}


		/// <summary>
		/// amount of space inside any given cell to borders
		/// </summary>
		[
		Description("Cell padding area"),
		Browsable(false)
		]
		public int CellPaddingSize
		{
			get	{ return 4; }			// default I set to 4
		}


		#endregion

		#region Working Properties

		/// <summary>
		/// are we currently allowing redraw?
		/// </summary>
		[
		Description("To redraw when invalidate is called or not."),
		Browsable(false)
		]
		public RedrawStates RedrawState
		{
			get { return m_RedrawState; }
			set 
			{ 
				if ( (m_RedrawState==RedrawStates.RedrawSuspendedPending) && (value==RedrawStates.RedrawNormal) )
					Invalidate( true );		// changing back to normal, flush the invlidate we been holding onto

				if ( (m_RedrawState==RedrawStates.RedrawSuspendedPending) && (value==RedrawStates.RedrawSuspended) )
					return;

				m_RedrawState = value; 
			}
		}


		[
		Description("Number of items/rows in the list."),
		Category("Behavior"),
		Browsable(false),
		DefaultValue(0)
		]
		public int Count
		{
			get { return Items.Count; }
		}


		[
		Description("All items together height."),
		Browsable(false)
		]
		public int TotalRowHeight
		{
			get
			{
				return ItemHeight * Items.Count;
			}
		}


		[
		Description("Number of rows currently visible in inner rect."),
		Browsable(false)
		]
		public int VisibleRows
		{
			get	{ return RowsInnerClientRect.Height / ItemHeight; }
		}


		[
		Description("this will always reflect the most height any item line has needed"),
		Browsable(false)
		]
		public int MaxHeight
		{
			get	{ return m_nMaxHeight; }
			set
			{
				if ( value > m_nMaxHeight )
				{
					m_nMaxHeight = value;
					if ( AutoHeight == true )
					{
						ItemHeight = MaxHeight;
						ManagedInvalidate();
						DW("Item height set bigger");
					}
				}
			}
		}


		[
		Description("The rectangle of the header inside parent control"),
		Browsable(false)
		]
		public Rectangle HeaderRect
		{
			get	{ return new Rectangle( this.BorderPadding, this.BorderPadding, Width-(this.BorderPadding*2), HeaderHeight ); }
		}


		[
		Description("The rectangle of the client inside parent control"),
		Browsable(false)
		]
		public Rectangle RowsClientRect
		{
			get
			{
				int tmpY = HeaderHeight + BorderPadding;							// size of the header and the top border

				int tmpHeight = Height - HeaderHeight - (BorderPadding*2);

				return new Rectangle( BorderPadding, tmpY, Width-(this.BorderPadding*2), tmpHeight );
			}
		}


		[
		Description("The inner rectangle of the client inside parent control taking scroll bars into account."),
		Browsable(false)
		]
		public Rectangle RowsInnerClientRect
		{
			get
			{
				Rectangle innerRect = RowsClientRect;

				innerRect.Width -= vPanelScrollBar.mWidth;				// horizontal bar crosses vertical plane and vice versa
				innerRect.Height -= hPanelScrollBar.mHeight;

				if ( innerRect.Width < 0 )
					innerRect.Width = 0;
				if ( innerRect.Height < 0 )
					innerRect.Height= 0;

				return innerRect;
			}
		}


		#endregion

		#endregion

		#endregion

		#endregion

		#region Implementation

		#region Initialization

		public GlacialList()
		{
			DW("Constructor");
			// This call is required by the Windows.Forms Form Designer.
			InitializeComponent();

			m_Columns = new GLColumnCollection();
			m_Columns.ChangedEvent += new GLColumnCollection.ChangedEventHandler( Columns_Changed );				// listen to event changes inside the item

			m_Items = new GLItemCollection( this );
			m_Items.ChangedEvent += new GLItemCollection.ChangedEventHandler( Items_Changed );

		}


		/// <summary>
		/// Clean up any resources being used.
		/// </summary>
		protected override void Dispose( bool disposing )
		{
			if( disposing )
			{
				if( components != null )
					components.Dispose();
			}
			base.Dispose( disposing );
		}

		#region Component Designer generated code
		/// <summary>
		/// Required method for Designer support - do not modify 
		/// the contents of this method with the code editor.
		/// </summary>
		private void InitializeComponent()
		{
			this.hPanelScrollBar = new GlacialComponents.Controls.ManagedHScrollBar();
			this.vPanelScrollBar = new GlacialComponents.Controls.ManagedVScrollBar();
			this.SuspendLayout();
			// 
			// hPanelScrollBar
			// 
			this.hPanelScrollBar.Anchor = System.Windows.Forms.AnchorStyles.None;
			this.hPanelScrollBar.CausesValidation = false;
			this.hPanelScrollBar.Location = new System.Drawing.Point(12, 128);
			this.hPanelScrollBar.mHeight = 16;
			this.hPanelScrollBar.mWidth = 120;
			this.hPanelScrollBar.Name = "hPanelScrollBar";
			this.hPanelScrollBar.Size = new System.Drawing.Size(120, 16);
			this.hPanelScrollBar.TabIndex = 3;
			this.hPanelScrollBar.Scroll += new System.Windows.Forms.ScrollEventHandler(this.hPanelScrollBar_Scroll);
			// 
			// vPanelScrollBar
			// 
			this.vPanelScrollBar.Anchor = System.Windows.Forms.AnchorStyles.None;
			this.vPanelScrollBar.CausesValidation = false;
			this.vPanelScrollBar.Location = new System.Drawing.Point(132, 8);
			this.vPanelScrollBar.mHeight = 120;
			this.vPanelScrollBar.mWidth = 16;
			this.vPanelScrollBar.Name = "vPanelScrollBar";
			this.vPanelScrollBar.Size = new System.Drawing.Size(16, 120);
			this.vPanelScrollBar.TabIndex = 4;
			this.vPanelScrollBar.Scroll += new System.Windows.Forms.ScrollEventHandler(this.vPanelScrollBar_Scroll);
			// 
			// GlacialList
			// 
			this.Controls.AddRange(new System.Windows.Forms.Control[] {
																		  this.vPanelScrollBar,
																		  this.hPanelScrollBar});
			this.Name = "GlacialList";
			this.Size = new System.Drawing.Size(160, 152);
			this.Resize += new System.EventHandler(this.GlacialList_Resize);
			this.Load += new System.EventHandler(this.GlacialList_Load);
			this.MouseUp += new System.Windows.Forms.MouseEventHandler(this.GlacialList_MouseUp);
			this.Paint += new System.Windows.Forms.PaintEventHandler(this.GlacialList_Paint);
			this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.GlacialList_MouseMove);
			this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.GlacialList_MouseDown);
			this.ResumeLayout(false);

		}
		#endregion

		private void GlacialList_Load(object sender, System.EventArgs e)
		{
			DW("GlacialList_Load");

		}

		#endregion

		#region Event Handlers

		protected void Items_Changed( object source, ChangedEventArgs e )
		{
			DW("GlacialList::Items_Changed");

			if ( ItemChangedEvent != null )
				ItemChangedEvent( this, e );				// fire the column clicked event

			ManagedInvalidate();
		}

		public void Columns_Changed( object source, ChangedEventArgs e )
		{
			DW("Columns_Changed");

			if ( ColumnChangedEvent != null )
				ColumnChangedEvent( this, e );				// fire the column clicked event

			ManagedInvalidate();
		}

		#endregion

		#region HelperFunctions

		public void ManagedInvalidate()
		{
			DW("Managed refresh (invalidate)");

			if ( ( RedrawState == RedrawStates.RedrawSuspended ) || (RedrawState == RedrawStates.RedrawSuspendedPending) )
				RedrawState = RedrawStates.RedrawSuspendedPending;
			else
				Invalidate( true );
		}


		protected void InterpretCoords( int nScreenX, int nScreenY, out int nItem, out int nColumn, out ListStates nState )
		{
			DW("Interpret Coords");

			nState = ListStates.stateNone;
			nColumn = 0;		// compiler forces me to set this since it sometimes wont get set if routine falls through early
			nItem = 0;

			/*
			 * Calculate horizontal subitem
			 */
			int nCurrentX = -hPanelScrollBar.Value;		//GetHScrollPoint();			// offset the starting point by the current scroll point
			//int nColIndex = 0;

			foreach ( GLColumn col in Columns )
			{
				if ( (nScreenX > nCurrentX) && (nScreenX < (nCurrentX+col.Width-RESIZE_ARROW_PADDING)) )
				{
					//nColumn = nColIndex;
					nState = ListStates.stateColumnSelect;
					break;
				}
				if ( (nScreenX > (nCurrentX+col.Width-RESIZE_ARROW_PADDING)) && (nScreenX < (nCurrentX+col.Width+RESIZE_ARROW_PADDING)) )
				{
					//nColumn = nColIndex;
					if ( AllowColumnResize == true )
						nState = ListStates.stateColumnResizing;
					return;				// no need for this to fall through
				}

				nColumn++;
				nCurrentX += col.Width;
			}

			if ( ( nScreenY >= RowsInnerClientRect.Y ) && ( nScreenY < RowsInnerClientRect.Bottom ) )
			{	// we are in the client area
				nItem = ((nScreenY - RowsInnerClientRect.Y) / ItemHeight) + vPanelScrollBar.Value;

				if ( nItem >= Items.Count )
					nState = ListStates.stateNone;
				else
				{
					nState = ListStates.stateSelecting;

					// handle case of where FullRowSelect is OFF and we click on the second part of a spanned column
					for ( int nSubIndex = 0; nSubIndex < Columns.Count; nSubIndex++ )
					{
						if ( ( nSubIndex + (Items[nItem].SubItems[nSubIndex].Span-1) ) >= nColumn )
						{
							nColumn = nSubIndex;
							return;
						}
					}

				}

				return;
			}

			return;
		}


		public int GetColumnScreenX( int nColumn )
		{
			DW("Get Column Screen X");

			if ( nColumn >= Columns.Count )
				return 0;

			int nCurrentX = -hPanelScrollBar.Value;//GetHScrollPoint();			// offset the starting point by the current scroll point
			int nColIndex = 0;
			foreach ( GLColumn col in Columns )
			{
				if ( nColIndex >= nColumn )
					return nCurrentX;

				nColIndex++;
				nCurrentX += col.Width;
			}

			return 0;		// this should never happen;
		}


		public static string TruncateString( string strText, int nWidth, Graphics subDC, Font font )
		{
			// THIS FUNCTION ALSO HANDLES TRUNCATION OF MULTILINE STRINGS

			//DW("TuncateString");
			string strTruncated = "";


			Size sizeString = MeasureMultiLineString( strText, subDC, font );
			if ( sizeString.Width < nWidth )
				return strText;				// this doesnt need any work, bail out

			int strTDotSize;
			strTDotSize = (int)subDC.MeasureString( "...", font ).Width;
			if ( strTDotSize > nWidth )
				return "";					// Cant even fit the triple dots here


			StringReader r = new StringReader(strText); 
			string line; 
			while ((line = r.ReadLine()) != null) 
			{
				if ( (int)subDC.MeasureString( line, font ).Width < nWidth )
				{	// original sub line is fine, doesn't need truncation
					strTruncated += line + "\n";
				}
				else
				{	// sub line needs to be truncated
					for ( int index=line.Length; index!=0; index-- )
					{
						string tmpString;
						tmpString = line.Substring( 0, index ) + "...";

						//DW("Truncating string to " + strText );

						if ( (int)subDC.MeasureString( tmpString, font ).Width < nWidth )
						{
							strTruncated += tmpString + "\n";
							break;			// stop the for loop so we can test more strings
						}
					}
				}
			}

			// remove the trailing linefeed for the last line in a sequence (because its not needed and woudl possibly mess things up
			if ( strTruncated.Length > 1 )
				strTruncated.Remove( strTruncated.Length-1, 1 );

			return strTruncated;

		}


		public static Size MeasureMultiLineString( string strText, Graphics mDC, Font font )
		{
			StringReader r = new StringReader(strText); 
			Size strSize = new Size(0,0);

			string line; 
			while ((line = r.ReadLine()) != null) 
			{
				SizeF tsize = mDC.MeasureString( line, font );

				strSize.Height += (int)tsize.Height;
				if ( strSize.Width < (int)tsize.Width )
					strSize.Width = (int)tsize.Width;
			}

			return strSize;
		}

		#endregion

		#region Dimensions

		private void GlacialList_Resize(object sender, System.EventArgs e)
		{
			DW("GlacialList_Resize");

			RecalcScroll();
			ManagedInvalidate();
		}


		#endregion

		#region Drawing

		protected override void OnPaintBackground(PaintEventArgs pevent)
		{
			DW("OnPaintBackground");
			//Debug.WriteLine( "paint background" );

		}

		
		private void GlacialList_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
		{
			DW("GlacialList_Paint");

			RecalcScroll();


			Bitmap offScreenBmp = new Bitmap( this.Width, this.Height); 
			Graphics offScreenDC = Graphics.FromImage(offScreenBmp);
			offScreenDC.FillRectangle( SystemBrushes.Control, this.ClientRectangle );

			if ( Columns.Count > 0 )
			{
				int nInsideWidth;
				if ( Columns.Width > HeaderRect.Width )
					nInsideWidth = Columns.Width;
				else
					nInsideWidth = HeaderRect.Width;


				/*
				 * draw header
				 */
				if ( HeaderVisible == true )
				{
					Bitmap bmpHeader = new Bitmap( Columns.Width, HeaderRect.Height); 
					DrawHeader( bmpHeader, new Size( HeaderRect.Width, HeaderRect.Height ) );

					//compute the source rect to copy from the full header thats been drawn
					Rectangle sourceRect = new Rectangle( hPanelScrollBar.Value, 0, nInsideWidth, HeaderRect.Height);

					offScreenDC.DrawImage( bmpHeader, HeaderRect.X, HeaderRect.Y, sourceRect, GraphicsUnit.Pixel );		// Copy the the correct portion of the pad to the actual viewable screen offscreendc
					bmpHeader.Dispose();
				}

				/*
				 * draw client area
				 */
				Bitmap bmpRows = new Bitmap( nInsideWidth, RowsInnerClientRect.Height ); 
				DrawRows( bmpRows, new Size( nInsideWidth, RowsInnerClientRect.Height ) );

				//compute the source rect to copy from the full rows thats been drawn
				Rectangle sourceRowsRect = new Rectangle( hPanelScrollBar.Value, 0, nInsideWidth, RowsInnerClientRect.Height );

				offScreenDC.DrawImage( bmpRows, RowsInnerClientRect.X, RowsInnerClientRect.Y, sourceRowsRect, GraphicsUnit.Pixel );
				bmpRows.Dispose();
			}

			ControlPaint.DrawBorder3D( offScreenDC, this.ClientRectangle, System.Windows.Forms.Border3DStyle.Sunken );			// draw control border

			e.Graphics.DrawImage(offScreenBmp, 0, 0);			// Now transfer it all to the visible screen


			offScreenBmp.Dispose();
			offScreenDC.Dispose();

			return;
		}


		public void DrawHeader( Bitmap bmpHeader, Size sizeHeader )
		{
			DW("DrawHeader");

			if ( Columns.Count <= 0 )
				return;

			Graphics HeaderDC = Graphics.FromImage( bmpHeader );
			HeaderDC.FillRectangle( SystemBrushes.Control, 0, 0, sizeHeader.Width, sizeHeader.Height );

			// draw vertical lines first, then horizontal lines
			int nCurrentX = 0;
			foreach ( GLColumn column in Columns )
			{
				Bitmap bmpColumn = new Bitmap( column.Width, HeaderHeight ); 
				DrawColumn( bmpColumn, new Size( column.Width, HeaderHeight ), column );
				HeaderDC.DrawImage( bmpColumn, nCurrentX, 0 );
				bmpColumn.Dispose();

				nCurrentX += column.Width;
			}

			HeaderDC.Dispose();
		}


		public void DrawColumn( Bitmap bmpColumn, Size colSize, GLColumn column )
		{
			DW("DrawColumn");

			Graphics ColumnDC = Graphics.FromImage( bmpColumn );

			string strItemText;
			strItemText = column.Name;

			if ( column.State == GLColumn.ColumnStates.csNone )
				ControlPaint.DrawBorder3D( ColumnDC, new Rectangle(0,0,colSize.Width,colSize.Height), System.Windows.Forms.Border3DStyle.Raised );

			if ( column.State == GLColumn.ColumnStates.csPressed )
				ControlPaint.DrawBorder3D( ColumnDC, new Rectangle(0,0,colSize.Width,colSize.Height), System.Windows.Forms.Border3DStyle.Sunken );


			// calc alignment
			int ty, tx, tw;
			int nInteriorWidth;
			ty = (colSize.Height-Font.Height)/2;
			tw = (int)ColumnDC.MeasureString( column.Name, Font ).Width;
			tx = 0;
			nInteriorWidth = colSize.Width - (CellPaddingSize*2);

			if ( nInteriorWidth < 4 )
				return;			// if the label is bigger than the space for it, then dont draw anything


			switch ( column.Alignment )
			{
				case Alignments.alignLeft:					// left justified
				{	
					tx = BorderPadding;
					break;
				}
				case Alignments.alignCenter:					// center justified
				{
					tx = (column.Width-tw)/2;
					break;
				}
				case Alignments.alignRight:					// right justified
				{
					tx = column.Width - tw - BorderPadding;
					break;
				}
			}


			if ( tw > nInteriorWidth )
				ColumnDC.DrawString( TruncateString( strItemText, nInteriorWidth, ColumnDC, Font ), Font, Brushes.Black, CellPaddingSize, ty );
			else
				ColumnDC.DrawString( strItemText, Font, Brushes.Black, tx + CellPaddingSize, ty );

		}


		public void DrawRows( Bitmap bmpRows, Size sizeRows )
		{
			DW("DrawRows");

			Graphics RowsDC = Graphics.FromImage( bmpRows );
			RowsDC.FillRectangle( Brushes.White, 0, 0, sizeRows.Width, sizeRows.Height );

			// -optimization- so we dont make new row bmp's every time, just reuse this one
			Bitmap bmpRow = new Bitmap( sizeRows.Width, ItemHeight );		// this bitmap is reuseable because all rows are same size always

			// determine start item based on whether or not we have a vertical scrollbar present
			int nStartItem;				// which item to start with in this visible pane
			if ( this.vPanelScrollBar.Visible == true )
				nStartItem = this.vPanelScrollBar.Value;
			else
				nStartItem = 0;


			/* Draw Rows */
			int nYCursor = 0;
			for (int nItem = 0; ((nItem < (VisibleRows+1) ) && ((nItem+nStartItem) < Items.Count )); nItem++ )
			{	//Debug.WriteLine( "ItemCount " + Items.Count.ToString() + " Item Number " + nItem.ToString() );

				DrawRow( bmpRow, new Size( sizeRows.Width, ItemHeight ), nItem+nStartItem ); //, ArrayList Item )
				RowsDC.DrawImage( bmpRow, 0, nYCursor );

				nYCursor += ItemHeight;
			}


			if ( GridLines == GridStyles.gridSolid )//offScreenDC.DrawRectangle( Pens.Purple, rect );
				DrawGridLines( RowsDC, new Rectangle( 0, 0, sizeRows.Width, sizeRows.Height ), true);


			bmpRow.Dispose();
			RowsDC.Dispose();
		}


		public void DrawRow( Bitmap bmpRow, Size sizeRow, int nItem)
		{
			DW("DrawRow");

			Graphics RowDC = Graphics.FromImage( bmpRow );

			SolidBrush brushBK;
			if ( ( Items[nItem].Selected == true ) && ( FullRowSelect == true ) )
				brushBK = new SolidBrush( SelectionColor );
			else
				brushBK = new SolidBrush( Color.White );

			RowDC.FillRectangle( brushBK, 0,0, sizeRow.Width, sizeRow.Height );
			brushBK.Dispose();


			int nXCursor = 0;
			for ( int nSubItem = 0; nSubItem < Columns.Count; /*nSubItem++*/ )
			{
				if ( ( ( Items[nItem].Selected == true ) && ( FullRowSelect == true ) ) || ( Items[nItem].SubItems[nSubItem].Selected == true ) )
					brushBK = new SolidBrush( SelectionColor );
				else
					brushBK = new SolidBrush( Items[nItem].SubItems[nSubItem].BackColor );


				// keep row spanning in mind
				int nColumnsSpanned = Items[nItem].SubItems[nSubItem].Span;
				int nItemWidth = Columns.GetSpanSize( nSubItem, nColumnsSpanned );


				Bitmap bmpSubItem = new Bitmap( nItemWidth, sizeRow.Height ); 

				DrawSubItem( bmpSubItem, new Size( nItemWidth, sizeRow.Height ), nItem, nSubItem, brushBK );
				RowDC.DrawImage( bmpSubItem, nXCursor, 0 );

				bmpSubItem.Dispose();
				nXCursor += nItemWidth;			//Columns[nSubItem].Width;


				nSubItem += nColumnsSpanned;

				brushBK.Dispose();
			}


			RowDC.Dispose();
		}


		public void DrawSubItem( Bitmap bmpSubItem, Size sizeSubItem, int nItem, int nColumn, SolidBrush brushBK ) //, ArrayList Item )
		{
			DW("DrawSubItem");
			//DW("DrawSubItem " + Items[nItem].SubItems[nColumn].Text + " " + nItem.ToString() + " " + nColumn.ToString() );
			int th, ty, tw, tx;
			int nInteriorWidth;

			Graphics SubItemDC = Graphics.FromImage( bmpSubItem );
			SubItemDC.FillRectangle( brushBK, 0,0, sizeSubItem.Width, sizeSubItem.Height );

			if ( Items[nItem].SubItems[nColumn].EmbeddedImage != null )
			{
				Size sizeImg = Items[nItem].SubItems[nColumn].EmbeddedImage.Size;
				th = sizeImg.Height;

				MaxHeight = th+(CellPaddingSize*2);										// this will only set if autosize is true

				if ( th > sizeSubItem.Height-(CellPaddingSize*2) )
					th = sizeSubItem.Height-(CellPaddingSize*2);

				ty = (sizeSubItem.Height-th)/2;
				tw = sizeImg.Width;											//(int)SubItemDC.MeasureString( strItemText, Font ).Width;
				tx = 0;
				nInteriorWidth = sizeSubItem.Width - (CellPaddingSize*2);

				switch ( Columns[nColumn].Alignment )
				{
					case Alignments.alignLeft:
					{	// left justified
						tx = BorderPadding;
						break;
					}
					case Alignments.alignCenter:
					{	// center justified
						tx = (Columns[nColumn].Width-tw)/2;
						break;
					}
					case Alignments.alignRight:
					{	// right justified
						tx = Columns[nColumn].Width - tw - BorderPadding;
						break;
					}
				}

				SubItemDC.DrawImage( Items[nItem].SubItems[nColumn].EmbeddedImage, tx, ty );

			}



			// draw text portion -------------------------------------------------------------------------------------
			string strItemText = Items[nItem].SubItems[nColumn].Text;
			// calc alignment
			//DW( strItemText + " " + SubItemDC.MeasureString( strItemText, Font ).Height.ToString() );
			//DW( strItemText + " " + MeasureMultiLineString( strItemText, SubItemDC, Font ).ToString() );

			Size measuredSize = MeasureMultiLineString( strItemText, SubItemDC, Font );

			th = measuredSize.Height;
			MaxHeight = th+(CellPaddingSize*2);								// this will only set if autosize is true

			if ( th > sizeSubItem.Height-(CellPaddingSize*2) )
				th = sizeSubItem.Height-(CellPaddingSize*2);

			ty = (sizeSubItem.Height-th)/2;
			tw = measuredSize.Width;					//(int)SubItemDC.MeasureString( strItemText, Font ).Width;
			tx = 0;
			nInteriorWidth = sizeSubItem.Width - (CellPaddingSize*2);

			switch ( Columns[nColumn].Alignment )
			{
				case Alignments.alignLeft:
				{	// left justified
					tx = BorderPadding;
					break;
				}
				case Alignments.alignCenter:
				{	// center justified
					tx = (Columns[nColumn].Width-tw)/2;
					break;
				}
				case Alignments.alignRight:
				{	// right justified
					tx = Columns[nColumn].Width - tw - BorderPadding;
					break;
				}
			}


			SolidBrush textBrush = new SolidBrush( Items[nItem].SubItems[nColumn].TextColor );

			if ( tw > nInteriorWidth )
				SubItemDC.DrawString( TruncateString( strItemText, nInteriorWidth, SubItemDC, Font ), Font, textBrush, CellPaddingSize, ty );
			else
				SubItemDC.DrawString( strItemText, Font, textBrush, tx + CellPaddingSize, ty );

			textBrush.Dispose();
			SubItemDC.Dispose();
		}


		public void DrawGridLines( Graphics RowsDC, Rectangle rect, bool bVertLines )
		{
			DW("DrawGridLines");

			int nStartItem = this.vPanelScrollBar.Value;
			/* Draw Rows */
			int nYCursor = 0;
			for (int nItem = 0; ((nItem < (VisibleRows+1) ) && ((nItem+nStartItem) < Items.Count )); nItem++ )
			{	//Debug.WriteLine( "ItemCount " + Items.Count.ToString() + " Item Number " + nItem.ToString() );

				int nXCursor = 0;
				for ( int nColumn = 0; nColumn < Columns.Count; /*nColumn++*/ )
				{
					int nColumnsSpanned = Items[nItem+nStartItem].SubItems[nColumn].Span;
					int nItemWidth = Columns.GetSpanSize( nColumn, nColumnsSpanned );

					nXCursor += nItemWidth;
					RowsDC.DrawLine( SystemPens.ControlLight, nXCursor, nYCursor, nXCursor, nYCursor+ItemHeight );
					nColumn += nColumnsSpanned;
				}

				nYCursor += ItemHeight;
				RowsDC.DrawLine( SystemPens.ControlLight, 0, nYCursor, rect.Width, nYCursor );			// bottom line will always be there
			}
		}

		#endregion // drawing

		#region Scrolling

		protected void RecalcScroll( )//Graphics g )
		{
			DW("RecalcScroll");

			RedrawState = RedrawStates.RedrawSuspended;

			bool bSBChanged;
			do					// this loop is to handle changes and rechanges that happen when oen or the other changes
			{
				DW("Begin scrolbar updates loop");
				bSBChanged = false;

				if ( (Columns.Width > RowsInnerClientRect.Width) && (hPanelScrollBar.Visible == false) )
				{	// total width of all the rows is less than the visible rect
					hPanelScrollBar.mVisible = true;
					hPanelScrollBar.Value = 0;
					bSBChanged = true;
					ManagedInvalidate();

					DW("showing hscrollbar");
				}

				if ( (Columns.Width <= RowsInnerClientRect.Width) && (hPanelScrollBar.Visible == true) )
				{	// total width of all the rows is less than the visible rect
					hPanelScrollBar.mVisible = false;
					hPanelScrollBar.Value = 0;
					bSBChanged = true;
					ManagedInvalidate();

					DW("hiding hscrollbar");
				}

				if ( (TotalRowHeight > RowsInnerClientRect.Height) && (vPanelScrollBar.Visible == false) )
				{  // total height of all the rows is greater than the visible rect
					vPanelScrollBar.mVisible = true;
					hPanelScrollBar.Value = 0;
					bSBChanged = true;
					ManagedInvalidate();

					DW("showing vscrollbar");
				}

				if ( (TotalRowHeight <= RowsInnerClientRect.Height) && (vPanelScrollBar.Visible == true) )
				{	// total height of all rows is less than the visible rect
					vPanelScrollBar.mVisible = false;
					vPanelScrollBar.Value = 0;
					bSBChanged = true;
					ManagedInvalidate();

					DW("hiding vscrollbar");
				}

				DW("End scrolbar updates loop");
			} while ( bSBChanged == true );		// this should never really run more than twice


			//Rectangle headerRect = HeaderRect;		// tihs is an optimization so header rect doesnt recalc every time we call it
			Rectangle rectClient = RowsInnerClientRect;

			/*
			 *  now that we know which scrollbars are showing and which aren't, resize the scrollbars to fit those windows
			 */
			if ( vPanelScrollBar.Visible == true )
			{
				vPanelScrollBar.mTop = rectClient.Y;
				vPanelScrollBar.mLeft = rectClient.Right;
				vPanelScrollBar.mHeight = rectClient.Height;
				vPanelScrollBar.mLargeChange = VisibleRows;
				vPanelScrollBar.mMaximum = Count-1;

				if ( ((vPanelScrollBar.Value + VisibleRows) > Count) )		// catch all to make sure the scrollbar isnt going farther than visible items
				{
					DW("Changing vpanel value");
					vPanelScrollBar.Value = Count - VisibleRows;				// an item got deleted underneath somehow and scroll value is larger than can be displayed
				}
			}

			if ( hPanelScrollBar.Visible == true )
			{
				hPanelScrollBar.mLeft = rectClient.Left;
				hPanelScrollBar.mTop = rectClient.Bottom;
				hPanelScrollBar.mWidth = rectClient.Width;

				hPanelScrollBar.mLargeChange = rectClient.Width;	// this reall is the size we want to move
				hPanelScrollBar.mMaximum = Columns.Width;

				if ( (hPanelScrollBar.Value + hPanelScrollBar.LargeChange) > hPanelScrollBar.Maximum )
				{
					DW("Changing vpanel value");
					hPanelScrollBar.Value = hPanelScrollBar.Maximum - hPanelScrollBar.LargeChange;
				}
			}

			//DW("Exit recalc scroll");

			RedrawState = RedrawStates.RedrawNormal;
		}


		private void vPanelScrollBar_Scroll(object sender, System.Windows.Forms.ScrollEventArgs e)
		{
			DW("vPanelScrollBar_Scroll");
			ManagedInvalidate();
		}

		private void hPanelScrollBar_Scroll(object sender, System.Windows.Forms.ScrollEventArgs e)
		{
			DW("hPanelScrollBar_Scroll");
			ManagedInvalidate();
		}



		#endregion

		#region Mouse

		private void GlacialList_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
		{
			DW("GlacialList_MouseDown");

			//DW("Button " + e.Button.ToString() );

			int nItem = 0, nColumn = 0;
			ListStates eState;
			InterpretCoords( e.X, e.Y, out nItem, out nColumn, out eState );


			if ( e.Button == MouseButtons.Right )			// if its the right button then we don't really care till its released
				return;


			//-----------------------------------------------------------------------------------------
			if ( eState == ListStates.stateColumnSelect )										// Column select
			{
				m_nState = ListStates.stateNone;

				Columns[ nColumn ].State = GLColumn.ColumnStates.csPressed;
				if ( ColumnClickedEvent != null )
					ColumnClickedEvent( this, new ClickEventArgs( nItem, nColumn ) );				// fire the column clicked event

				//ManagedInvalidate();
				return;
			}
			//---Resizing -----------------------------------------------------------------------------------
			if ( eState == ListStates.stateColumnResizing )										// resizing
			{
				Cursor.Current = Cursors.VSplit;
				m_nState = ListStates.stateColumnResizing;

				m_pointColumnResizeAnchor = new Point( GetColumnScreenX(nColumn), e.Y );		// deal with moving column sizes
				m_nResizeColumnNumber = nColumn;

				return;
			}
			//--Item check, if no items exist go no further--
			//if ( Items.Count == 0 )
			//return;

			//---Items --------------------------------------------------------------------------------------
			if ( eState == ListStates.stateSelecting )
			{	// ctrl based multi select ------------------------------------------------------------

				m_nState = ListStates.stateSelecting;


				if ( (( ModifierKeys & Keys.Control) == Keys.Control ) && ( AllowMultiselect == true ) )
				{
					m_nLastSelectionIndex = nItem;

					if ( Items[nItem].Selected == true )
						Items[nItem].Selected = false;
					else
						Items[nItem].Selected = true;

					return;
				}

				// shift based multi row select -------------------------------------------------------
				if ( (( ModifierKeys & Keys.Shift) == Keys.Shift ) && ( AllowMultiselect == true ) )
				{
					Items.ClearSelection();
					if ( m_nLastSelectionIndex >= 0 )			// ie, non negative so that we have a starting point
					{
						int index = m_nLastSelectionIndex;
						do
						{
							Items[index].Selected = true;
							if ( index > nItem )		index--;
							if ( index < nItem )		index++;
						} while ( index != nItem );

						Items[index].Selected = true;
					}

					return;
				}

				// the normal single select -----------------------------------------------------------
				Items.ClearSelection();

				// following two if statements deal ONLY with non multi=select where a singel sub item is being selected
				if ( ( m_nLastSelectionIndex < Count ) && ( m_nLastSubSelectionIndex < Columns.Count ) )
					Items[m_nLastSelectionIndex].SubItems[m_nLastSubSelectionIndex].Selected = false;
				if ( ( FullRowSelect == false ) && ( ( nItem < Count ) && ( nColumn < Columns.Count ) ) )
					Items[nItem].SubItems[nColumn].Selected = true;


				m_nLastSelectionIndex = nItem;
				m_nLastSubSelectionIndex = nColumn;
				Items[nItem].Selected = true;

			}
		}


		private void GlacialList_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
		{
			DW("GlacialList_MouseMove");
			//Debug.WriteLine( e.X.ToString() + " " + e.Y.ToString() );

			try
			{
				if ( m_nState == ListStates.stateColumnResizing )
				{
					Cursor.Current = Cursors.VSplit;


					int nWidth;
					nWidth = e.X - m_pointColumnResizeAnchor.X;

					if ( nWidth <= MINIMUM_COLUMN_SIZE )
					{
						nWidth = MINIMUM_COLUMN_SIZE;
					}

					GLColumn col;
					col = (GLColumn)Columns[m_nResizeColumnNumber];
					col.Width = nWidth;

					return;
				}


				int nItem = 0, nSubItem = 0;
				ListStates eState;
				InterpretCoords( e.X, e.Y, out nItem, out nSubItem, out eState );

				if ( eState == ListStates.stateColumnResizing )
				{
					Cursor.Current = Cursors.VSplit;
					return;
				}

				Cursor.Current = Cursors.Arrow;
			}
			catch( Exception ex )
			{
				Debug.WriteLine("Exception throw in GlobalList_MouseMove with text : " + ex.ToString() );

			}

		}


		private void GlacialList_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
		{
			DW("GlacialList_MouseUp");

			Cursor.Current = Cursors.Arrow;
			Columns.ClearStates();


			int nItem = 0, nColumn = 0;
			ListStates eState;
			InterpretCoords( e.X, e.Y, out nItem, out nColumn, out eState );


			if ( e.Button == MouseButtons.Right )
			{
				if ( RightClickedEvent != null )
					RightClickedEvent( this, new ClickEventArgs( nItem, nColumn ) );
			}
			else if ( ( e.Button == MouseButtons.Left ) && ( this.m_nState == ListStates.stateSelecting ) )
			{
				if ( ItemClickedEvent != null )
					ItemClickedEvent( this, new ClickEventArgs( nItem, nColumn ) );			// fire the event to all that subscribed
			}

			m_nState = ListStates.stateNone;

			// check to see if there was a column being held down, if so then this should generate the column clicked event
			//Invalidate( true );
		}


		#endregion
	
		#endregion  // functionality

	}



}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer Primary Architects, Inc.
United States United States
I started my programming career in the late 80's with video games and have since written games on the AppleIIgs, SNES, Saturn, Playstation, and PC. After leaving the games industry and joining the ranks of consultants I began doing a lot of work with client/server applications, data movement, and communications. I've also become a recent convert to the XP principles of software development. Despite my defection to the business world I am still an avid gamer and I can be found on the gamezone most weekends slugging it out with others in the various online games there.

I currently live in Utah where I run PA (enterprise architecture consulting firm) and occasionally guest speak at architect forums. I mountain bike (badly), golf (very badly), and have fun (often).

Comments and Discussions