Introduction

A few months ago, when I was getting started at Microsoft, my manager walked into my office, and detailed a project on which I would be spending the next two weeks. I was to conjure up an application that would be used to aggregate metrics for content strategists at MSDN. One of the feature requests was a DataGrid-like control that would allow users to arrange columns in their preferred order before exporting the data to a Microsoft Excel spreadsheet. His last words prior to departing my office were, "Make it an interesting user experience."

I knew that in order to be able to rearrange DataGrid columns, I had to manipulate the DataGrid's DataGridColumnStyle property to reflect the new column ordering, but that wasn't exactly enthralling. I wanted a visual representation of the whole dragging operation. I started out playing around with some System.Drawing functionality, and got to a point where I was able to drag shapes across the screen. I decided that I needed to kick it up a notch. Instead of just dragging a bland and uninspiring rectangle painted atop the DataGrid drawing surface, I could make it appear as if the user was dragging the column. I dug down to the roots of the native GDI library and after several hours of experimenting, figured out what I needed to do in order to achieve this trickery.

Figure 1. The dragging operation

Getting Started

The first thing I needed to do was figure out how I was going to take a screenshot of the column that was about to be dragged. I knew exactly what I needed and wanted to do, but didn't know how to do it. After discovering that the classes residing under the System.Drawing namespace didn't provide me with the functionality that I needed to perform screen captures, I looked into the native GDI library and found that the BitBlt function was exactly what I was looking for.

The ScreenImage Class

In order to make invocations across the interoperation boundary, we need to declare the unmanaged functions and indicate which libraries they come from so the JIT compiler knows where to find them during runtime. Once this has been done, all we have to do is invoke them just as we do with managed method invocations, as seen in the code block below.

public sealed class ScreenImage {

[DllImport("gdi32.dll")]
private static extern bool BitBlt( IntPtr
handlerToDestinationDeviceContext, int x, int y, int nWidth, int nHeight,
IntPtr handlerToSourceDeviceContext, int xSrc, int ySrc, int opCode);

[DllImport("user32.dll")]
private static extern IntPtr GetWindowDC( IntPtr windowHandle );

[DllImport("user32.dll")]
private static extern int ReleaseDC( IntPtr windowHandle, IntPtr dc );

private static int SRCCOPY = 0x00CC0020;

public static Image GetScreenshot( IntPtr windowHandle, Point location, Size size ) {
... }

}


This class only exposes one method, GetScreenshot, which is a static method that returns an image object containing color data corresponding to the windowHandle, location, and size parameters. The next code block shows how I implemented this method.

public static Image GetScreenshot( IntPtr windowHandle, Point location, Size size ) {

Image myImage = new Bitmap( size.Width, size.Height );

using ( Graphics g = Graphics.FromImage( myImage ) ) {

IntPtr destDeviceContext = g.GetHdc();
IntPtr srcDeviceContext = GetWindowDC( windowHandle );

// TODO: throw exception
BitBlt( destDeviceContext, 0, 0, size.Width, size.Height,
srcDeviceContext, location.X, location.Y, SRCCOPY );

ReleaseDC( windowHandle, srcDeviceContext );
g.ReleaseHdc( destDeviceContext );

} // dispose the Graphics object

return myImage;

}


Let's take a line-by-line look at the method implementation. The first thing I did was create a new bitmap with dimensions corresponding to the parameterized size.

Image myImage = new Bitmap( size.Width, size.Height );


The following line of code retrieves the drawing surface associated with the new bitmap that was just created.

using ( Graphics g = Graphics.FromImage( myImage ) ) { ... }


The C# using keyword defines a scope at the end of which the Graphics object will be disposed. Since all of the classes in the System.Drawing namespace are managed wrappers around the native GDI+ API, we are almost always dealing with unmanaged resources, so we need to ensure that we jettison resources whose services are no longer needed. This process is called deterministic finalization and allows for the resources that are used by the object to be reallocated for other purposes immediately, rather than waiting for the garbage collector to come around and do its job. This is a practice that should be adhered to whenever you're dealing with objects that implement the IDisposable interface, such as the Graphics object used here.

Handles to the source and destination device contexts are retrieved so we can proceed with the transfer of the color data. The source is the device context associated with the parameterized windowHandle handle and the destination is the device context from the bitmap created earlier.

IntPtr srcDeviceContext = GetWindowDC(windowHandle);
IntPtr destDeviceContext = g.GetHdc();

Tip   A device context is a GDI data structure that is maintained internally by Windows and defines a set of graphical objects, as well as the graphical modes that affect output relating to those objects. Think of it as a canvas that Windows gives you on which to paint. GDI+ provides three different kinds of drawing surfaces: forms (commonly referred to as the display), printers, and bitmaps. In this article, we use the form and bitmap drawing surfaces.

Now we have a defined Bitmap object (myImage) and a device context representing the canvas of this object, which at this point of time in execution is transparent. The native BitBlt method requires the coordinates and the size of the portion of the canvas that we want to copy the bits to and the coordinates on the source device context from where we want to start copying bits. The method also requires a raster operation code value to define how the bit blocks are to be transferred.

Here, I set the starting coordinates of the destination device context to the top left corner and the raster operation code value to SRCCOPY, which signifies that you want to copy the source directly to the destination. The hexadecimal value equivalent (00x00CC0020) was retrieved from the GDI header file.

BitBlt( destDeviceContext, 0, 0, size.Width, size.Height,
srcDeviceContext, location.X, location.Y, SRCCOPY );


Once we're done with the device contexts, we need to release them. Failure to do so will result in the device context not being available for subsequent requests and possibly cause runtime exceptions to be thrown.

ReleaseDeviceContext( windowHandle, destDeviceContext );
g.ReleaseHdc( srcDeviceContext );


I affirmed that the ScreenImage class was working as expected, then the next thing I needed to do was create a simple data structure that would help me keep track of all of the data related to the column that was being dragged.

The DraggedDataGridColumn Class

The DraggedDataGridColumn class is a data structure that monitors the various states of the dragged column, including the initial location, current location, image representation, and cursor location relative to the column's initial origin. For a detailed description of all of the parameters, please take a look at the code in DraggedDataGridColumn.cs.

Tip   If your class encapsulates an object that implements IDisposable, you might be indirectly holding onto unmanaged resources. In this case, your class should also implement the IDisposable interface and invoke the Dispose() method on each disposable object. The DraggedDataGridColumn class encapsulates a Bitmap object, which explicitly holds onto unmanaged resources, so it was imperative that I completed this step.

With this out of the way, I was able to focus on the biggest piece of the puzzle—manipulating the DataGrid control to obtain the visual experience that I wanted.

The ColumnDragDataGrid Class

The DataGrid control is a powerful heavyweight control, but it doesn't natively provide us with the ability to drag-and-drop columns, so I had to extend it and add that functionality myself. Three different mouse events were handled and the DataGrid's OnPaint method was overridden to fulfill all of my drawing needs.

First, let's take a look at all of the member fields that are used to keep track of where and how things should be drawn.

Member Fields Definitions
m_initialRegion A DraggedDataGridColumn object that represents everything about the column that is currently being dragged. I'll get into the specifics of the DraggedDataGridColumn class later in this article.
m_mouseOverColumnRect A Rectangle structure that is used to identify the rectangular region representing the column above which the mouse cursor is currently hovering.
m_mouseOverColumnIndex The index of the column that the mouse cursor is currently over.
m_columnImage A Bitmap object containing a bitmap representation of the column at the time that the drag-and-drop operation was initiated.
m_showColumnWhileDragging A Boolean value representing whether the captured column image should be shown when the column is being dragged. This is exposed publicly through the ShowColumnWhileDragging property.
m_showColumnHeaderWhileDragging A Boolean value representing whether the header portion of the column should be shown when the column is being dragged. This is exposed publicly through the ShowColumnHeaderWhileDragging property.

The only constructor in this class is a parameter-less one and is fairly straightforward. There is, however, one line of code that I feel is worth mentioning:

this.SetStyle( ControlStyles.DoubleBuffer, true );


Painting in Windows is a two-step process. When an application makes a paint request, paint messages (WM_ERASEBKGND followed by WM_PAINT) are generated by the system. Those messages are sent to the application message queue where they are then examined by the application and routed to the appropriate control for handling. The default handling for the WM_ERASEBKGND message is to fill the area with the current window background color. The handling of WM_PAINT follows, which does all of the foreground painting. When you have a sequence that involves clearing the background and drawing in the foreground, you're creating an unpleasant effect known as flickering. Fortunately, this can be alleviated by using double buffering.

With double buffering, you have two different buffers to which you can write. One is the visible, on-screen buffer that is stored in the video RAM, and the other is a non-visible, off-screen buffer, represented by an internal GraphicsBuffer object, and stored in the system RAM. When a drawing operation initiates, all of the graphics objects are rendered on the aforementioned GraphicsBuffer object. Once the system determines that the operation has completed, both buffers are quickly synchronized.

According to the .NET Framework documentation, in order to implement double buffering in your application, you need to set the AllPaintingInWmPaint, DoubleBuffer, and UserPaint ControlStyle bits to true. Here, I only needed to worry about the DoubleBuffer bit. The base DataGrid class has already set the AllPaintingInWmPaint and UserPaint bits to true.

Note   The other two ControlStyle bits mentioned above are defined as:
UserPaint: Setting this bit to true tells Windows that your application will take full accountability of all of the painting for that particular window (control). This means that you'll handle the WM_ERASEBKGND and WM_PAINT messages. If this bit is set to false, the application will still hook the WM_PAINT message, but instead of performing any painting operations, it will send the message back to the system for handling. When this happens, the system attempts to render the window, but because it doesn't know anything about the window, it usually doesn't do a very good job.
AllPaintingInWmPaint: As the bit name indicates, when it bit is set to true, all of the painting is handled by Control's WmPaint method. The WM_ERASEBKGND message, even though hooked, is ignored and the control's OnEraseBackground method is never invoked.

Before delving into the rest of the class, there are two important concepts that need to be reviewed.

Invalidation

When you invalidate a certain region of a control, it is added to the control's update region, which tells the system which area to repaint during the next painting operation. If the update region is not defined, the whole control is repainted.

Figure 2. Visual representation of an invalidated region before and after the painting operation has been triggered. On the left, the translucent grey square with the dotted border represents the defined invalidation region. The right square manifests the appearance after the painting operation has been performed.

When a control's invalidate method is invoked, as mentioned earlier, a WM_PAINT message is generated by the system and routed to the control. Upon receiving the message, the control raises the Paint event and if there is a registered handler listening for it, it is added to the back of the control's event handling queue.

It is important to note that a raised Paint event does not always get handled immediately. There are a number of reasons for this, the most important being that the Paint event involves one of the more expensive operations in drawing and is generally the last event that gets handled.

Grid Styles

A DataGridTableStyle defines how a DataGrid is drawn to the screen. Even though it contains properties that are similar to those of the DataGrid, they are mutually exclusive. A lot of people erroneously assume that changing a synonymous property, such as the DataGrid's RowHeadersVisible property, also changes the value of the DataGridTableStyle's' RowHeadersVisible property. As a result, time is unnecessarily spent debugging when things don't work as expected (don't worry I'm guilty of this too).

You can create a collection of different table styles and use them interchangeably with different data entities and sources.

Every DataGridTableStyle contains a GridColumnStylesCollection, which is a collection of DataGridColumnStyles objects that are automatically created when data is bound to the DataGrid control. Those objects are instances of DataGridBoolColumn, DataGridTextBoxColumn, or a third-party implemented column, which are all derived from DataGridColumnStyle. If you want a column that contains labels, or even images, then you will have to create a custom class by subclassing DataGridColumnStyle.

Tip   You'll want to override the OnDataSource method, which is invoked when the DataGrid control is bound to a data source. This allows you to use multiple styles and correlate their mapping names with the DataGrid's DataMember property value, which is set when the control is bound to the data source.

Column Tracking

A vast majority of the column tracking functionality takes place in the MouseDown, MouseMove, and MouseUp event handlers. In the forthcoming paragraphs, I will focus on the three event handlers and provide explanations for the more important pieces of code. The helper methods that are utilized by those handlers are not discussed. However, if you take a look at the code, you'll see that I've provided summaries for those methods.

MouseDown

When the mouse is clicked above the grid, the first thing we need to do is determine where it was clicked. In order to initiate a drag, the cursor must have been clicked above a column header. If this condition proves to be true, some column information is gathered. We need to know the origin, width, and height of the column, as well as the mouse cursor's position relative to the column's origin. This information is used to establish the two different column regions that we are going to be keeping track of while the column is being dragged.

Private void ColumnDragDataGrid_MouseDown(object sender, MouseEventArgs e) {

DataGrid.HitTestInfo hti = this.HitTest( e.X, e.Y );

if ( ( hti.Type & DataGrid.HitTestType.ColumnHeader ) != 0 &&
this.m_draggedColumn == null ) {

int xCoordinate = this.GetLeftmostColumnHeaderXCoordinate( hti.Column );
int yCoordinate = this.GetTopmostColumnHeaderYCoordinate( e.X, e.Y );
int columnWidth = this.TableStyles[0].GridColumnStyles[hti.Column].Width;
int columnHeight = this.GetColumnHeight( yCoordinate );

Rectangle columnRegion = new Rectangle( xCoordinate, yCoordinate, columnWidth, columnHeight );
Point startingLocation = new Point( xCoordinate, yCoordinate );
Point cursorLocation = new Point( e.X - xCoordinate, e.Y - yCoordinate );

Size columnSize = Size.Empty;

...

}

...

}


Figure 3. Diagram showing column origin, column header height as calculated by the GetColumnHeaderHeight method, column height, column width, and cursor position

The rest of this event handler is fairly straightforward. A conditional evaluation is performed to see if the ShowColumnsWhileDragging or ShowColumnHeaderWhileDragging properties have been set to true. If so, the column sizes are calculated and the ScreenImage's GetScreenshot method is invoked. I pass the DataGrid control's handle (remember that a control is a child window), the starting coordinates and the column size and the method returns an image object containing the desired captured region. Everything is then stored in a DraggedDataGridColumn object.

Private void ColumnDragDataGrid_MouseDown(object sender, MouseEventArgs e) {

...

if ( ( hti.Type & DataGrid.HitTestType.ColumnHeader ) != 0 &&
this.m_draggedColumn == null ) {

...

if ( ShowColumnWhileDragging || ShowColumnHeaderWhileDragging ) {

if ( ShowColumnWhileDragging ) {
columnSize = new Size( columnWidth, columnHeight );
} else {
columnSize = new Size( columnWidth, this.GetColumnHeaderHeight( e.X, yCoordinate ) );
}

Bitmap columnImage = ( Bitmap ) ScreenImage.GetScreenshot(
this.Handle, startingLocation, columnSize );
m_draggedColumn = new DraggedDataGridColumn( hti.Column,
columnRegion, cursorLocation, columnImage );

} else {
m_draggedColumn = new DraggedDataGridColumn( hti.Column,
columnRegion, cursorLocation );
}

m_draggedColumn.CurrentRegion = columnRegion;

}

...

}


MouseMove

Every time the mouse cursor moves above the DataGrid, the MouseMove event is raised. In handling it, firstly I keep track of the column that the dragged column is currently hovering above so I can provide some visual feedback to the user. Secondly, I track down the new location of the column and deliver invalidation instructions.

Let's take a closer look at the code. The first thing that I need to do is ensure that a column is being dragged, then I get the x-coordinate of the column by subtracting the mouse coordinates relative to the column's origin from the mouse coordinates relative to the control (Figure 4, marker #1). This gives me the x-coordinate of the column. Because the y-coordinate never changes, I don't bother checking it.

private void ColumnDragDataGrid_MouseMove(object sender, MouseEventArgs e) {

DataGrid.HitTestInfo hti = this.HitTest( e.X, e.Y );

if ( m_draggedColumn != null ) {

int x = e.X - m_draggedColumn.CursorLocation.X;

...

}

}


Figure 4. Marker #1 shows the value that is stored in m_draggedColumn.CursorLocation.X. This value is subtracted from the current cursor location whose coordinates are relative to the control.

I then check to see if the cursor is hovering above a cell (the column headers are also considered cells). If it isn't, I assume that the user wants to abort the drag operation.

private void ColumnDragDataGrid_MouseMove(object sender, MouseEventArgs e) {

...

if ( m_draggedColumn != null ) {

if ( hti.Column >= 0 ) {
...
} else {

InvalidateColumnArea();
ResetMembersToDefault();

}

}

}


Next, I want to provide some kind of feedback to the user so they know where their dragged column will be dropped if they release the mouse button.

This is tracked through the m_mouseOverColumnIndex member field, which stores the index of the column whose boundaries contained the current location of the cursor after the last MouseMove event was handled. If this value is not the same as the column index that the hit test provides us with, then the user is hovering above a different column. If this is the case, the region indicated by the m_mouseOverColumnRect member field is invalidated and coordinates for the new region are recorded. The new region is then invalidated so Windows will know that new painting instructions for this area await its attention.

private void ColumnDragDataGrid_MouseMove(object sender, MouseEventArgs e) {

...

if ( m_draggedColumn != null ) {

...

if ( hti.Column >= 0 ) {

if ( hti.Column != m_mouseOverColumnIndex ) {

// NOTE: moc = mouse over column
int mocX = this.GetLeftmostColumnHeaderXCoordinate( hti.Column );
int mocWidth =
this.TableStyles[0].GridColumnStyles[hti.Column].Width;

// indicate that we want to invalidate the old rectangle area
if ( m_mouseOverColumnRect != Rectangle.Empty ) {
this.Invalidate( m_mouseOverColumnRect );
}

// if the mouse is hovering over the original column, we do not want to
// paint anything, so we negate the index.
if ( hti.Column == m_draggedColumn.Index ) {
m_mouseOverColumnIndex = -1;
} else {
m_mouseOverColumnIndex = hti.Column;
}

m_mouseOverColumnRect = new Rectangle( mocX,
m_draggedColumn.InitialRegion.Y, mocWidth,
m_draggedColumn.InitialRegion.Height );

// invalidate this area so it gets painted when OnPaint is called.
this.Invalidate( m_mouseOverColumnRect );

}

...

} else { ... }

}

}


Focus is then shifted towards tracking the location of the dragged column. I need to figure out if it's being dragged to the left or the right, so I can get the leftmost x-coordinate. Once this number has been obtained, the old and new regions of the columns are invalidated and the data pertaining to the new location is stored in m_draggedColumn.

private void ColumnDragDataGrid_MouseMove(object sender, MouseEventArgs e) {

...

if ( m_draggedColumn != null ) {

...

if ( hti.Column >= 0 ) {

...

int oldX = m_draggedColumn.CurrentRegion.X;
Point oldPoint = Point.Empty;

// column is being dragged to the right
if ( oldX < x ) {
oldPoint = new Point(  oldX - 5, m_draggedColumn.InitialRegion.Y );

// to the left
} else {
oldPoint = new Point( x - 5, m_draggedColumn.InitialRegion.Y );
}

Size sizeOfRectangleToInvalidate = new Size( Math.Abs( x - oldX )
+ m_draggedColumn.InitialRegion.Width +
( oldPoint.X * 2 ), m_draggedColumn.InitialRegion.Height );

this.Invalidate( new Rectangle( oldPoint, sizeOfRectangleToInvalidate ) );

m_draggedColumn.CurrentRegion = new Rectangle( x,
m_draggedColumn.InitialRegion.Y,
m_draggedColumn.InitialRegion.Width, m_draggedColumn.InitialRegion.Height );

} else { ... }

}

}


MouseUp

When the user releases the mouse button above a cell, a conditional evaluation is performed to ensure that the dragged column has been dropped above a column other than its originator. If the expression evaluates to true, as in the column index being different than the one where it originated from, the columns are switched. Otherwise, the grid is repainted.

private void ColumnDragDataGrid_MouseUp(object sender, MouseEventArgs e) {

DataGrid.HitTestInfo hti = this.HitTest( e.X, e.Y );

// is column being dropped above itself? if so, we don't want
// to do anything
if ( m_draggedColumn != null && hti.Column != m_draggedColumn.Index ) {

DataGridTableStyle dgts = this.TableStyles[this.DataMember];
DataGridColumnStyle[] columns = new DataGridColumnStyle[dgts.GridColumnStyles.Count];

// NOTE: csi = columnStyleIndex
for ( int csi = 0; csi < dgts.GridColumnStyles.Count; csi++ ) {

if ( csi != hti.Column && csi != m_draggedColumn.Index ) {
columns[csi] = dgts.GridColumnStyles[csi];
} else if ( csi == hti.Column ) {
columns[csi] = dgts.GridColumnStyles[m_draggedColumn.Index];
} else {
columns[csi] = dgts.GridColumnStyles[hti.Column];
}

}

// update TableStyle
this.SuspendLayout();
this.TableStyles[this.DataMember].GridColumnStyles.Clear();
this.ResumeLayout();

} else {
InvalidateColumnArea();
}

ResetMembersToDefault();

}


With the hard part of this feature out of the way, triggering the necessary paint operations was easy.

Overriding the DataGrid's OnPaint Method

You might have noticed by now that none of the painting logic was performed in any of the mouse event handlers. It all boils down to a matter of preference. I've seen other developers entwine their paint logic with the rest of their logic, but I find it much simpler and more orderly to keep all of the paint logic within the OnPaint method or the Paint event handler.

The DataGrid's OnPaint method needed to be overridden in order to accommodate the additional paint operations. First in line is ensuring that the base OnPaint method is invoked so the underlying DataGrid is drawn. This gives me the canvas that I will be drawing on.

Remember that when you draw objects on a canvas, the z-ordering is contingent on the order that the objects are drawn in. With this in mind, we need to draw the bottommost shapes first.

The first shape that gets drawn is the rectangle that is used to indicate which column is being dragged (Figure 5, marker #1).

Figure 5. The different drawing steps

By using the FillRectangle method of the Graphics object, we draw a rectangle over the column from which the drag originated. The region information is retrieved from the DraggedDataGridColumn object. A translucent brush is used so the underlying column is still visible. A black rectangle is then drawn around the border of the aforementioned rectangle to give it a more complete touch.

protected override void OnPaint( PaintEventArgs e ) {

...

if ( m_draggedColumn != null ) {

SolidBrush blackBrush = new SolidBrush( Color.FromArgb( 255, 0, 0, 0 ) );
SolidBrush darkGreyBrush = new SolidBrush( Color.FromArgb( 150, 50, 50, 50 ) );
Pen blackPen = new Pen( blackBrush, 2F );

g.FillRectangle( darkGreyBrush, m_draggedColumn.InitialRegion );
g.DrawRectangle( blackPen, region );

...

}

}


Colors in GDI are broken down into four 8-bit components, three of which represent the primary colors: red, green, and blue. The Alpha component, also 8-bits, determines the transparency of the color, which influences how the colors blend with the background. The Color.FromArgb method allows us to create a color with specific values.

Color.FromArgb( 150, 50, 50, 50 ) // dark grey with alpha translucency level set to 150


The column feedback that I mentioned earlier in the article is done in the form of a translucent light grey rectangle (Figure 5, marker #2). First, I check the column index to ensure that it is not -1, and then fill a rectangle over the column using the rectangular region data stored in m_mouseOverColumnRect.

protected override void OnPaint( PaintEventArgs e ) {

...

if ( m_draggedColumn != null ) {

// user feedback indicating which column the dragged column is over
if ( this.m_mouseOverColumnIndex != -1 ) {

using ( SolidBrush b = new SolidBrush( Color.FromArgb( 100, 100, 100, 100 ) ) ) {
g.FillRectangle( b, m_mouseOverColumnRect );
}

}

}

}


The next area of focus is the column that is being dragged. If the user has chosen to show the column or column header while the dragging operation is taking place, then the image is drawn. The captured image is stored in m_draggedColumn and is accessible through the ColumnImage property.

protected override void OnPaint( PaintEventArgs e ) {

...

if ( m_draggedColumn != null ) {

...

// draw bitmap image
if ( ShowColumnWhileDragging || ShowColumnHeaderWhileDragging ) {
g.DrawImage( m_draggedColumn.ColumnImage,
m_draggedColumn.CurrentRegion.X, m_draggedColumn.CurrentRegion.Y );
}

...

}

}


Finally, a translucent rectangle is filled to represent the dragging operation. This is done in a similar manner as the first shape. The column region information is read from m_draggedColumn. Another rectangle is then drawn to further enhance the previous rectangle (Figure 5, marker #3).

protected override void OnPaint( PaintEventArgs e ) {

...

if ( m_draggedColumn != null ) {

...

g.FillRectangle(  filmFill, m_draggedColumn.CurrentRegion.X,
m_draggedColumn.CurrentRegion.Y, m_draggedColumn.CurrentRegion.Width,
m_draggedColumn.CurrentRegion.Height );
g.DrawRectangle( filmBorder, new Rectangle(
m_draggedColumn.CurrentRegion.X, m_draggedColumn.CurrentRegion.Y +
Convert.ToInt16( filmBorder.Width ), width, height ) );

...

}

}


Conclusion

In this article, I showed you how I was able to utilize some basic GDI functionality to achieve visual effects with the DataGrid control. By making calls across managed boundaries, I leveraged native GDI functionality to perform screen captures and use that in conjunction with the drawing features in System.Drawing to create an appealing drag-and-drop experience.

• 本文已收录于以下专栏：

举报原因： 您举报文章：Dragging and Dropping DataGrid Columns 色情 政治 抄袭 广告 招聘 骂人 其他 (最多只允许输入30个字)