Introduction
DataGrid
is one of the most powerful Windows® Forms control. By using default DataGrid
property settings you can easily bind a DataSet
to it to create an impressive Windows Forms user interface – one that display large amounts of data efficiently and intuitively without bewildering the user.
Thanks to the extensible architecture of DataGrid
control, it gives developers the flexibility in hand to extend its functionalities, where native support is not sufficed. In this article, I am going to show you how to enhance the DataGrid
control with the drag and drop support for column reordering. This is particular useful in situation when you need to compare columns side-by-side that may not reside together by default.
Background
In this implementation, I've made use of some unmanaged Win32 APIs to perform the graphics drawing process – one that draw the semi-transparent dragged column during the drag and drop operation. All these Win32 APIs are encapsulated in the WinGDI.cs file for the separation of managed and unmanaged code. Underpinnings the drawing of drag and drop support for column reordering is the function BitBlt
.
BitBlt
– Bit Block Transfer, one of the powerful drawing functions which transfer a block of bits (a bitmap) from one device context to another. . One point worth mentioning in this API is that currently there are 17 constants defined for the dwRop
(Raster Operation Code) variable and you can easily create different kind of effects on image by changing the raster operation code. SRCCOPY (0xcc0020)
is the easiest to use and performs a simple copy action.
[DllImport("gdi32.dll")]
public static extern bool
BitBlt
(
IntPtr hdcDest,
int nXDest,
int nYDest,
int nWidth,
int nHeight,
IntPtr hdcSrc,
int nXSrc,
int nYSrc,
System.Int32 dwRop
);
For more information on this API, please consult the Win32 documentation.
Using the code
To preserve the existing functionalities provided by DataGrid
control, I've created a new control named DataGridEx
, which inherits from the existing DataGrid
control. A property AllowUserToOrderColumns
is added, which enables or disables the control's ability to reorder the columns. In order to provide drag and drop for column reordering, the methods OnMouseDown
, OnMouseMove
and OnMouseUp
of the base control must be overridden.
OnMouseDown
is the method that will be called when column header is clicked. To initiate the drag operation, it entails the AllowUserToOrderColumns
property has been set to true and the mouse button that initiates the click event must be left mouse button. Just before starting the drag operation, ColumnStartDrag
event will be fired to determine whether this column is drag-able. Developers at the application level can react to this event by setting the Cancel
variable in the event arguments to true to stop the drag operation. It everything goes fine, the process of drag operation starts.
protected
override void OnMouseDown(System.Windows.Forms.MouseEventArgs
e)
{
try
{
DataGrid.HitTestInfo hti = HitTest(e.X,
e.Y);
if (
(hti.Type ==
DataGrid.HitTestType.ColumnHeader)
&& ((e.Button &
MouseButtons.Left) == MouseButtons.Left)
&&
(allowColumnsReorder))
{
ColumnStartDragEventArgs csde = new
ColumnStartDragEventArgs(
hti.Column,
GetColumnName(hti.Column), false);
OnColumnStartDrag(csde);
if (!csde.Cancel)
{
dragColumnIndex = hti.Column;
isColumnDragging = true;
mouseDelta = new Point
(
e.X - (e.X -
(GetColumnWidth(hti.Column) / 2)),
(CaptionVisible == true ?
GetCaptionHeight() : 0)
);
Point dragColumn = PointToScreen(new Point(e.X,
mouseDelta.Y));
Point screenPoint =
PointToScreen(new Point(e.X, e.Y));
screenPoint.X -=
mouseDelta.X;
screenPoint.Y =
dragColumn.Y;
dragColumnStyle = GetColumnStyle(hti.Column);
CreateDisplayDC();
dragImageSize = new
Size(dragColumnStyle.Width,
ColumnHeaderHeight -
2);
buffer = new
Bitmap(Math.Max(dragColumnStyle.Width,
NODROPIMAGE_WIDTH + 2),
ColumnHeaderHeight);
bufferGraphics =
Graphics.FromImage(buffer);
pBufferDC =
bufferGraphics.GetHdc();
WinGDI.BitBlt(pBufferDC, 0, 0,
buffer.Width, buffer.Height,
pDisplayDC, screenPoint.X, screenPoint.Y,
WinGDI.SRCCOPY);
bufferPos = new
Point(screenPoint.X, screenPoint.Y);
}
}
}
catch(Exception) {
}
finally
{
base.OnMouseDown(e);
}
}
}
OnMouseMove
method will be called to draw the semi-transparent dragged column as the mouse moving around in the DataGrid
. If the dragged column is drag over to another column, ColumnSequenceChanging
event will be fired to determine whether the dragged column can be dropped onto it. Developers at the application level can use this event to determine the droppable columns based on their condition. An X icon will be drawn on the dragged column to inform users that the particular column is not droppable.
protected
override void OnMouseMove(MouseEventArgs
e)
{
try
{
DataGrid.HitTestInfo hti = HitTest(e.X,
e.Y);
if (
(allowColumnsReorder)
&&
(isColumnDragging)
&& ((e.Button &
MouseButtons.Left) == MouseButtons.Left))
{
if (hti.Column >=
0)
{
Point screenPoint =
PointToScreen(new Point(e.X, e.Y));
screenPoint.X -=
mouseDelta.X;
screenPoint.Y =
bufferPos.Y;
WinGDI.BitBlt(pDisplayDC,
bufferPos.X, bufferPos.Y,
buffer.Width, buffer.Height,
pBufferDC, 0,
0,
WinGDI.SRCCOPY);
WinGDI.BitBlt(pBufferDC, 0, 0,
buffer.Width, buffer.Height,
pDisplayDC, screenPoint.X,
screenPoint.Y,
WinGDI.SRCCOPY);
bufferPos = new
Point(screenPoint.X, screenPoint.Y);
ColumnSequenceChangingEventArgs
csce = new ColumnSequenceChangingEventArgs(dragColumnIndex,
GetColumnName(dragColumnIndex),
hti.Column,
GetColumnName(hti.Column),
false);
OnColumnSequenceChanging(csce);
if (csce.Cancel)
DrawDragImage(displayGraphics,
screenPoint.X, screenPoint.Y, false);
else
DrawDragImage(displayGraphics,
screenPoint.X, screenPoint.Y, true);
}
}
}
catch(Exception) {
}
finally
{
base.OnMouseMove(e);
}
}
}
When the mouse button releases, OnMouseUp
method will be called to invoke the MoveColumn
function to reorder the columns if the dragged and dropped column is not the same. ColumnSequenceChanged
event will be fired once the column reordering is done. This marks the end of the drag operation and performs the necessary house keeping.
protected
override void OnMouseUp(System.Windows.Forms.MouseEventArgs
e)
{
try
{
DataGrid.HitTestInfo hti = HitTest(e.X,
e.Y);
if(
(allowColumnsReorder)
&&
(isColumnDragging)
&& ((e.Button &
MouseButtons.Left) == MouseButtons.Left))
{
WinGDI.BitBlt(pDisplayDC, bufferPos.X,
bufferPos.Y, buffer.Width,
buffer.Height, pBufferDC, 0, 0,
WinGDI.SRCCOPY);
if (hti.Column >= 0 &&
dragColumnIndex != hti.Column)
{
ColumnSequenceChangingEventArgs
columnChanging =
new
ColumnSequenceChangingEventArgs(dragColumnIndex,
GetColumnName(dragColumnIndex),
hti.Column,
GetColumnName(hti.Column),
false);
OnColumnSequenceChanging(columnChanging);
if (!columnChanging.Cancel)
{
MoveColumn(dragColumnIndex,hti.Column);
ColumnSequenceChangedEventArgs
columnChanged =
new
ColumnSequenceChangedEventArgs(columnChanging.SourceColumnIndex,
columnChanging.SourceColumnName,
columnChanging.DestinationColumnIndex,
columnChanging.DestinationColumnName);
OnColumnSequenceChanged(columnChanged);
}
}
isColumnDragging =
false;
dragColumnIndex =
-1;
dragColumnStyle =
null;
DisposeDisplayDC();
DisposeBufferDC();
}
}
catch(Exception) {
}
finally
{
base.OnMouseUp(e);
}
}
The cornerstone of drawing the dragged column is encapsulated in the function DrawDragImage
. A normal semi-transparent dragged column will be drawn when it drags over to those droppable columns. Otherwise, a semi-transparent dragged column with an X icon will be drawn.
private void
DrawDragImage(Graphics g, int x, int y, bool
bEnabled)
{
try
{
g.SmoothingMode =
SmoothingMode.AntiAlias;
Rectangle rect = new Rectangle(x, y,
dragImageSize.Width,
dragImageSize.Height);
using(Pen p = new
Pen(Color.Black))
{
using(Brush backBrush = new
SolidBrush(Color.FromArgb(127,
Color.LightSteelBlue)))
{
using(Brush foreBrush = new
SolidBrush(Color.Black))
{
g.FillRectangle(backBrush,
rect.Left + 1, rect.Top + 1,
rect.Width - 2, rect.Height -
2);
g.DrawRectangle(p, rect.Left,
rect.Top, rect.Width - 1,
rect.Height -
1);
using(StringFormat sf = new
StringFormat(StringFormatFlags.NoWrap))
{
sf.LineAlignment =
StringAlignment.Center;
if (dragColumnStyle.Alignment ==
HorizontalAlignment.Center)
sf.Alignment =
StringAlignment.Center;
else if
(dragColumnStyle.Alignment ==
HorizontalAlignment.Left)
sf.Alignment =
StringAlignment.Near;
else if
(dragColumnStyle.Alignment ==
HorizontalAlignment.Right)
sf.Alignment =
StringAlignment.Far;
g.DrawString(dragColumnStyle.HeaderText,
HeaderFont,
foreBrush, rect,
sf);
}
if
(!bEnabled)
{
using(Pen redPen = new Pen(new
SolidBrush(Color.Red), 3))
{
Point p1; Point p2;
Point p3; Point p4;
if (dragImageSize.Width >
NODROPIMAGE_WIDTH)
{
int left = rect.Left +
((dragImageSize.Width
- NODROPIMAGE_WIDTH) /
2);
p1 = new Point(left, rect.Top +
1);
p2 = new Point(left +
NODROPIMAGE_WIDTH, rect.Top
+ buffer.Height -
2);
p3 = new Point(left +
NODROPIMAGE_WIDTH, rect.Top + 1);
p4 = new Point(left,
rect.Top + buffer.Height - 2);
}
else
{
p1 = new Point(rect.Left +
1, rect.Top + 1);
p2 = new Point(rect.Left +
NODROPIMAGE_WIDTH - 1,
rect.Top + buffer.Height -
2);
p3 = new Point(rect.Left +
NODROPIMAGE_WIDTH - 1,
rect.Top +
1);
p4 = new Point(rect.Left +
1, rect.Top + buffer.Height - 2);
}
redPen.StartCap =
LineCap.Round;
redPen.EndCap =
LineCap.Round;
g.DrawLine(redPen, p1,
p2);
g.DrawLine(redPen, p3,
p4);
}
}
}
}
}
}
catch(Exception) {
}
}
MoveColumn
performs the intelligent column reordering where dragged column will be positioned before the dropped column if the dragged column was previously positioned after the dropped column and vice versa.
public void
MoveColumn(int fromColumn, int toColumn)
{
if(fromColumn == toColumn)
return;
DataGridTableStyle oldTS =
TableStyles[0];
DataGridTableStyle newTS = new
DataGridTableStyle();
newTS.MappingName =
oldTS.MappingName;
CopyTableStyle(oldTS, newTS);
for(int i = 0; i <
oldTS.GridColumnStyles.Count; i++)
{
if(i != fromColumn && fromColumn
< toColumn)
newTS.GridColumnStyles.Add(oldTS.GridColumnStyles[i]);
if(i == toColumn)
newTS.GridColumnStyles.Add(oldTS.GridColumnStyles[fromColumn]);
if(i != fromColumn && fromColumn
> toColumn)
newTS.GridColumnStyles.Add(oldTS.GridColumnStyles[i]);
}
TableStyles.Remove(oldTS);
TableStyles.Add(newTS);
}
Points of Interest
For those developers who are interested to convert the drawing functions from unmanaged code to managed code, feel free to try the DrawImage
managed API. However, the performance of DrawImage
function is slower than the Win32 counterpart, as it creates and destroys new device context for each call.
Conclusion
Column Reordering is just one enhancement to the DataGrid
control. If the native functionalities provided in the Windows Forms controls aren't exactly what you need for all your scenarios, you can take advantage of the extensibility features of Windows Forms architecture to override and extend these controls to make them fit your particular needs.
History
Elvin Cheng is currently living in Woodlands, Singapore. He has been developing applications with the .NET Framework, using C# and ASP.NET since October 2002. Elvin specializes in building Real-time monitoring and tracking information system for Semi-conductor manufacturing industry. During his spare time, he enjoys reading books, watching movie and gym.