Solution / Request for Suggestions: DataGrid Button Column using C#

A

Alex Morris

I've tried to find examples of adding a button column to a WinForms
DataGrid. Unfortunately, all of the examples I have found only allow
you to click on the button for the currently selected row. Below is a
class that I wrote that displays clickable buttons for each row along
with a sample form that uses the class. Hopefully this will help some
people out.

If you try this code, please post some feedback on suggestions for
improvements or bugs. This has been in use in a few applications for a
couple months and seems to be stable and reliable. However, I'm still
a little concerned about the mouse event handlers that are registered
with the underlying DataGrid.

Thanks!

To use this example, create a new Windows Forms project, add a new
class module to the project, and then do the following.

1. Paste this code into the class module:

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Diagnostics;

namespace TestApp {
/// <summary>
/// Adds a column to a DataGrid that displays
/// a button that can be responded to
/// via an event that fires when it is clicked.
/// </summary>
/// <remarks>
/// Major gotcha: adding this DataGridColumnStyle
/// to a DataGrid will register
/// handlers with the MouseDown, MouseUp, and
/// MouseMove events in the DataGrid.
/// Therefore, destroying a DataGrid's TableStyle and
/// adding a new one will cause
/// unexpected behavior if the mouse handlers are not
/// removed manually before hand.
/// </remarks>
public class DataGridButtonColumn : DataGridColumnStyle {
/*
* Fires when a button is clicked in this column.
*/
public delegate void ClickEventHandler(object sender,
DataGridButtonEventArgs e);
public event ClickEventHandler Click;

private int _xMargin = 2;
private int _yMargin = 1;
private string _buttonName = "";

private bool _editing = false;
private int _editingRowNum = 0;

private bool _isMouseDown = false;
private int _mouseRow = -1;
private int _mouseCol = -1;

private DataGrid.HitTestInfo _mousePos = null;

public DataGridButtonColumn(string buttonName) {
_buttonName = buttonName;
}

#region Overridden methods
protected override void Abort(int rowNum) {
Debug.WriteLine("Abort()");
endEdit();
}

protected override bool Commit(CurrencyManager dataSource, int
rowNum) {
if(_editing) {
endEdit();
}

return true;
}

protected override void Edit(CurrencyManager source, int rowNum,
Rectangle bounds, bool readOnly, string instantText, bool
cellIsVisible) {
_editingRowNum = rowNum;
_editing = true;
}

protected override void Paint(Graphics g, Rectangle bounds,
CurrencyManager source, int rowNum) {
/*
* Propogate to the next Paint() handler.
*/
Paint(g, bounds, source, rowNum, false);
}

protected override void Paint(Graphics g, Rectangle bounds,
CurrencyManager source, int rowNum, bool alignToRight) {
/*
* Create a buffer to draw to. (Double-buffering)
*/
Bitmap buffer = new Bitmap(bounds.Width, bounds.Height);
Graphics bg = Graphics.FromImage(buffer);
Rectangle bufferBounds = new Rectangle(0, 0, bounds.Width,
bounds.Height);

/*
* Default background for the cell.
*/
Brush backBrush;
if(rowNum % 2 == 0) {
backBrush = new SolidBrush(this.DataGridTableStyle.BackColor);
} else {
backBrush = new
SolidBrush(this.DataGridTableStyle.AlternatingBackColor);
}
Brush foreBrush = new SolidBrush(this.DataGridTableStyle.ForeColor);
bg.FillRectangle(backBrush, bufferBounds);

/*
* Draw the button.
*/
Rectangle buttonBounds = bufferBounds;
buttonBounds.Offset(_xMargin, _yMargin);
buttonBounds.Width -= _xMargin * 2;
buttonBounds.Height -= _yMargin * 2;
/*
* The "Down" state requires that the button on this row has had the
mouse
* down event fired on it and the mouse must still be over top of
this button.
*/
if(_isMouseDown && rowNum == _mouseRow && _mousePos != null &&
_mousePos.Row == _mouseRow && _mousePos.Column == _mouseCol) {
DrawDown(bg, buttonBounds);
bounds.Offset(1, 1);
PaintText(bg, bufferBounds, _buttonName, foreBrush, alignToRight);
} else {
DrawUp(bg, buttonBounds);
PaintText(bg, bufferBounds, _buttonName, foreBrush, alignToRight);
}

/*
* Render the secondary buffer to the real buffer.
*/
g.DrawImageUnscaled(buffer, bounds);
}

protected override void Paint(Graphics g, Rectangle bounds,
CurrencyManager source, int rowNum, Brush backBrush, Brush foreBrush,
bool alignToRight) {
/*
* Let the base class handle any painting needs.
*/
base.Paint(g, bounds, source, rowNum, backBrush, foreBrush,
alignToRight);
}

protected override int GetMinimumHeight() {
return 0;
}

protected override int GetPreferredHeight(Graphics g, object value) {
return 0;
}

protected override Size GetPreferredSize(Graphics g, object value) {
return new Size (0, 0);
}

protected override void SetDataGridInColumn(DataGrid value) {
base.SetDataGridInColumn (value);

/*
* Add the mouse handlers, but make certain that there is only
* one handler registered per event.
*/
RemoveMouseHandlers(value);
AddMouseHandlers(value);
}
#endregion

#region Helper functions
public void RemoveMouseHandlers(DataGrid dg) {
dg.MouseDown -= new MouseEventHandler(DataGridMouseDown);
dg.MouseUp -= new MouseEventHandler(DataGridMouseUp);
dg.MouseMove -= new MouseEventHandler(DataGridMouseMove);
}

private void AddMouseHandlers(DataGrid dg) {
dg.MouseDown += new MouseEventHandler(DataGridMouseDown);
dg.MouseUp += new MouseEventHandler(DataGridMouseUp);
dg.MouseMove += new MouseEventHandler(DataGridMouseMove);
}

private void endEdit() {
_editing = false;
Invalidate();
}

private void PaintText(Graphics g, Rectangle TextBounds, string Text,
Brush ForeBrush, bool AlignToRight) {
Rectangle Rect = TextBounds;
RectangleF RectF = Rect;
StringFormat Format = new StringFormat();
if(AlignToRight) {
Format.FormatFlags = StringFormatFlags.DirectionRightToLeft;
}
switch(this.Alignment) {
case HorizontalAlignment.Left:
Format.Alignment = StringAlignment.Near;
break;
case HorizontalAlignment.Right:
Format.Alignment = StringAlignment.Far;
break;
case HorizontalAlignment.Center:
Format.Alignment = StringAlignment.Center;
break;
}
Format.FormatFlags = Format.FormatFlags;
Format.FormatFlags = StringFormatFlags.NoWrap;
Rect.Offset(0, _yMargin);
Rect.Height -= _yMargin;
g.DrawString(Text, this.DataGridTableStyle.DataGrid.Font, ForeBrush,
RectF, Format);
Format.Dispose();
}

private void DrawUp(Graphics g, Rectangle bounds) {
// Face
g.FillRectangle(new SolidBrush(SystemColors.ControlLight), bounds);
// Light edges
Point[] pt = new Point[3];
pt[0] = new Point(bounds.Left, bounds.Bottom);
pt[1] = new Point(bounds.Left, bounds.Top);
pt[2] = new Point(bounds.Right, bounds.Top);
g.DrawLines(Pens.White, pt);
// Dark-dark shadow
pt[0] = new Point(bounds.Right, bounds.Top);
pt[1] = new Point(bounds.Right, bounds.Bottom);
pt[2] = new Point(bounds.Left, bounds.Bottom);
g.DrawLines(new Pen(SystemColors.ControlDarkDark), pt);
// Dark shadow
pt[0] = new Point(bounds.Right-1, bounds.Top+1);
pt[1] = new Point(bounds.Right-1, bounds.Bottom-1);
pt[2] = new Point(bounds.Left+1, bounds.Bottom-1);
g.DrawLines(new Pen(SystemColors.ControlDark), pt);
}

private void DrawDown(Graphics g, Rectangle bounds) {
// Face
g.FillRectangle(new SolidBrush(SystemColors.ControlLight), bounds);
// Light edges
Point[] pt = new Point[3];
pt[0] = new Point(bounds.Left, bounds.Bottom);
pt[1] = new Point(bounds.Left, bounds.Top);
pt[2] = new Point(bounds.Right, bounds.Top);
g.DrawLines(Pens.White, pt);
// Light-light edges
pt[0] = new Point(bounds.Left+1, bounds.Bottom-1);
pt[1] = new Point(bounds.Left+1, bounds.Top+1);
pt[2] = new Point(bounds.Right-1, bounds.Top+1);
g.DrawLines(new Pen(SystemColors.ControlLightLight), pt);
// Dark shadow
pt[0] = new Point(bounds.Right, bounds.Top);
pt[1] = new Point(bounds.Right, bounds.Bottom);
pt[2] = new Point(bounds.Left, bounds.Bottom);
g.DrawLines(new Pen(SystemColors.ControlDark), pt);
}

private int getColumnNumber() {
/*
* For some reason, there is no way to determine what column you
occupy
* without traversing the entire column collection and finding
yourself.
*/
for(int i = 0; i < this.DataGridTableStyle.GridColumnStyles.Count;
i++) {
if(this == this.DataGridTableStyle.GridColumnStyles) {
return i;
}
}

return -1;
}
#endregion

#region Event handlers
private void DataGridMouseDown(object sender, MouseEventArgs e) {
/*
* Ignore all clicks other than left clicks.
*/
if(e.Button != MouseButtons.Left) {
return;
}

/*
* Store the row / column pair that this mouse down event occurred
on.
*/
DataGrid dg = this.DataGridTableStyle.DataGrid;
DataGrid.HitTestInfo ht = dg.HitTest(new Point(e.X, e.Y));
_mouseRow = ht.Row;
_mouseCol = ht.Column;
Debug.WriteLine("Down: " + ht);

/*
* Only set the _isMouseDown state to true if this column was
clicked.
*/
if(_mouseCol == getColumnNumber()) {
_isMouseDown = true;
_mousePos = ht;
}
Invalidate();
}

private void DataGridMouseUp(object sender, MouseEventArgs e) {
/*
* Store the row / column pair that this mouse up event occurred on.
* Later, the Paint() event will test to see if it was a valid click
on
* a button and fire the ButtonClicked event if it was a valid
click.
*/
DataGrid dg = this.DataGridTableStyle.DataGrid;
DataGrid.HitTestInfo ht = dg.HitTest(new Point(e.X, e.Y));
Debug.WriteLine("Up: " + ht);

if(_isMouseDown && ht.Row == _mouseRow && ht.Column == _mouseCol) {
Debug.WriteLine("Clicked " + ht);
// Fire the Click event if someone has registered to receive it
if(Click != null) {
Click(this, new DataGridButtonEventArgs(_mouseRow, _mouseCol));
}
}

_isMouseDown = false;
_mousePos = null;
Invalidate();
}

private void DataGridMouseMove(object sender, MouseEventArgs e) {
/*
* Determine if the button needs switch state to an up or down
position.
*
* We want to emulate the behavior of a button springing up if the
mouse
* was clicked down on it and then moved off before the mouse button
is
* released. We also need the button to push itself back down if
the
* mouse moves back over the button.
*/
if(_isMouseDown) {
DataGrid dg = this.DataGridTableStyle.DataGrid;
DataGrid.HitTestInfo ht = dg.HitTest(new Point(e.X, e.Y));
if(ht != _mousePos) {
_mousePos = ht;
Invalidate();
}
}
}
#endregion
}

/// <summary>
/// EventArgs class that includes the row and column that was clicked.
/// </summary>
public class DataGridButtonEventArgs : EventArgs {
private int _column;
private int _row;

public DataGridButtonEventArgs(int row, int col) {
_row = row;
_column = col;
}

public int Column {
get {
return _column;
}
}

public int Row {
get {
return _row;
}
}
}
}

2. Overwrite the code in Form1.cs with this code:

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

namespace TestApp {
public class Form1 : System.Windows.Forms.Form {
private System.Windows.Forms.DataGrid dgTest;
private System.ComponentModel.Container components = null;

public Form1() {
InitializeComponent();

/*
* Create dummy data. This should be pulled from a database in
reality.
*/
DataTable dt = new DataTable("ButtonSample");
dt.Columns.Add("Sample1");
dt.Columns.Add("Sample2");
dt.Columns.Add("Unbound1"); /* Necessary for the button column to
bind to. Can this be avoided? */
for(int i = 0; i < 10; i++) {
DataRow row = dt.NewRow();
row["Sample1"] = i.ToString();
row["Sample2"] = (i * 2).ToString();
dt.Rows.Add(row);
}
dgTest.DataSource = dt;

/*
* Create a DataGridTableStyle with a button column.
*/
DataGridTableStyle dgts = new DataGridTableStyle();
dgts.MappingName = dt.TableName;
dgts.GridColumnStyles.Add(
CreateTextBoxColumn("Sample1", "Sample 1"));
dgts.GridColumnStyles.Add(
CreateTextBoxColumn("Sample2", "Sample 2"));
DataGridButtonColumn buttonCol = new DataGridButtonColumn("Test");
buttonCol.MappingName = "Unbound1";
buttonCol.HeaderText = "Button";
buttonCol.Click += new
TestApp.DataGridButtonColumn.ClickEventHandler(buttonCol_Click);
dgts.GridColumnStyles.Add(buttonCol);
dgTest.TableStyles.Add(dgts);
}

private DataGridTextBoxColumn CreateTextBoxColumn(string mappingName,
string headerText) {
DataGridTextBoxColumn col = new DataGridTextBoxColumn();
col.MappingName = mappingName;
col.HeaderText = headerText;
col.NullText = "";
return col;
}

protected override void Dispose( bool disposing ) {
if( disposing ) {
if (components != null) {
components.Dispose();
}
}
base.Dispose( disposing );
}

#region Windows Form 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.dgTest = new System.Windows.Forms.DataGrid();
((System.ComponentModel.ISupportInitialize)(this.dgTest)).BeginInit();
this.SuspendLayout();
//
// dgTest
//
this.dgTest.DataMember = "";
this.dgTest.HeaderForeColor =
System.Drawing.SystemColors.ControlText;
this.dgTest.Location = new System.Drawing.Point(8, 8);
this.dgTest.Name = "dgTest";
this.dgTest.Size = new System.Drawing.Size(456, 288);
this.dgTest.TabIndex = 0;
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(472, 302);
this.Controls.Add(this.dgTest);
this.Name = "Form1";
this.Text = "Form1";
((System.ComponentModel.ISupportInitialize)(this.dgTest)).EndInit();
this.ResumeLayout(false);

}
#endregion

[STAThread]
static void Main() {
Application.Run(new Form1());
}

private void buttonCol_Click(object sender, DataGridButtonEventArgs
e) {
DataGrid x =
((DataGridButtonColumn)sender).DataGridTableStyle.DataGrid;
CurrencyManager cm = (CurrencyManager)BindingContext[x.DataSource];
DataRow r = ((DataRowView)cm.List[e.Row]).Row;

MessageBox.Show(
"Sample 1: " + r["Sample1"].ToString() +
", Sample 2: " + r["Sample2"].ToString());
}
}
}
 
A

Alex Morris

I did everything I could to make that code copy and paste correctly.
Other than "Show quoted text" cutting off the very last line of code,
everything will copy and paste without any problems. Feedback is
greatly appreciated.

- Alex M
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top