日期:2012-05-12  浏览次数:20469 次

摘要:了解如何利用基本的 GDI 功能,从而通过 DataGrid 控件获得可视化效果。通过跨越托管边界进行调用,可以利用本机 GDI 功能来执行屏幕捕获,并最终获得拖放体验。



下载 ColumnDragDataGrid.msi 文件。

本页内容
简介
入门
ScreenImage 类
DraggedDataGridColumn 类
ColumnDragDataGrid 类
列跟踪
重写 DataGrid 的 OnPaint 方法
小结

简介
几个月以前,当我初到 Microsoft 工作时,我的经理走进我的办公室,并且向我详细说明了我将在随后两个星期内将要从事的一个项目。我需要设想出一个应用程序,用于为 MSDN 内容策划人员整合衡量标准。其中一个功能要求是需要一个类似于 DataGrid 的控件,该控件使用户可以在将数据导出到 Microsoft Excel 电子表格之前,按照他们喜欢的顺序排列所有列。他在离开我的办公室之前说的最后一句话是:“将它变为有趣的用户体验。”

我知道为了能够重新排列 DataGrid 列,我必须操纵 DataGrid 的 DataGridColumnStyle 属性以反映新的列排序,但这并没有什么吸引人之处。我想要的是对整个拖动操作实现可视化表示。我在开始时使用了一些 System.Drawing 功能,并且达到了能够在屏幕间拖动图形的程度。我断定我需要让它更进一步。我可以让它看起来更像是用户在拖动列,而不是仅仅在 DataGrid 绘图表面上拖动枯燥乏味的矩形进行绘制。我对本机 GDI 库进行了一番寻根究底,经过几个小时的试验后,我终于弄清楚为了实现这一技巧而需要完成的工作。



图 1. 拖动操作

返回页首
入门
我需要做的第一件事是弄清如何获得将要拖动列的屏幕快照。我完全清楚自己需要什么以及希望做什么,但是我不知道如何 去做。在发现 System.Drawing 命名空间下的类没有为我提供执行屏幕捕获所需的功能之后,我浏览了本机 GDI 库并且发现 BitBlt 函数正是我在寻找的东西。

下一步是编写该函数的托管包装。我将在本文中讨论的第一点是,我该如何实现 ScreenImage 类。

返回页首
ScreenImage 类
为了跨越互操作边界进行调用,我们需要声明非托管函数并且指明它们都来自哪些库,以便 JIT 编译器在运行时知道它们的位置。在完成这一工作后,我们只需像调用托管方法一样调用它们,就象下面的代码块所示。

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 ) {
... }

}

该类只公开一个方法 — GetScreenshot,它是一个静态方法,返回一个含有与 windowHandle、location 和 size 参数相对应的颜色数据的图形对象。下一个代码块显示如何实现该方法。

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;

}

让我们逐行地考察一下方法实现。我做的第一件事是创建一个尺寸与参数设置的大小相对应的新位图。

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

下面的代码行检索与刚刚创建的新位图相关联的绘图表面。

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

C# using 关键字定义了一个范围,在该范围的末尾将处理 Graphics 对象。因为 System.Drawing 命名空间中的所有类都是本机 GDI+ API 的托管包装,所以我们几乎总是在处理非托管资源,并且因此需要确保丢弃不再需要其服务的资源。该过程称为确定性终止,通过该过程将对象使用的资源以其他目的立即进行重新分配,而不是等待垃圾回收器到访来完成它该做的工作。每当您在处理实现了 IDisposable 接口的对象(如,这里使用的 Graphics 对象)时,都应该遵守这种习惯。

我检索了源和目标设备上下文的句柄,以便可以继续转换颜色数据。源是与参数设置的 windowHandle 句柄相关联的设备上下文,而目标是先前创建的位图中的设备上下文。

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

提示设备上下文是由 Windows 在内部维护的 GDI 数据结构,它定义了一组图形对象以及影响与这些对象相关的输出的图形模式。可以将其视为 Windows 提供的、可在上面绘图的画布。GDI+ 提供了三种不同的绘图表面:窗体(通常称为显示、打印机和位图)。在本文中,我们使用窗体和位图绘图表面。

现在,我们具有一个已定义的 Bitmap 对象 (myImage) 和一个表示该对象的画布(它在这一执行时刻是透明的)的设备上下文。本机 BitBlt 方法需要我们要向其复制位的画布部分的坐标和大小,以及我们要从源设备上下文上开始复制位的坐标。该方法还需要一个光栅操作代码值,以便定义位块的转换方式。

这里,我将目标设备上下文的起始坐标设置为左上角,并且将光栅操作代码值设置为 SRCCOPY(它表示要将源直接复制到目标)。十六进制等效值 (00x00CC0020) 可从 GDI 头文件中检索获得。

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