当前位置: 首页 > 编程日记 > 正文

拖放 DataGrid 列--来自MSDN

 
发布日期: 09/19/2004 | 更新日期: 09/19/2004

Chris Sano
Microsoft Corporation

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

*

下载 ColumnDragDataGrid.msi 文件

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

简介

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

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

dragdrop_datagrid-fig1

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 );

一旦用完设备上下文,我们就需要将其释放。如果不这样做,将导致无法将该设备上下文用于随后的请求,并且可能导致引发运行时异常。

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

我确认了 ScreenImage 类能够按预期工作,然后,我需要做的下一件事情是创建一个简单的数据结构,以便帮助我跟踪与被拖动的列相关的所有数据。

返回页首返回页首

DraggedDataGridColumn 类

DraggedDataGridColumn 类是一个数据结构,用于监视所拖动列的各种状态,包括初始位置、当前位置、图像表示形式以及相对于该列的初始起点的光标位置。有关所有参数的详细说明,请参阅 DraggedDataGridColumn.cs 中的代码。

提示如果类封装了实现 IDisposable 的对象,那么您就可能间接抓住非托管资源。在这种情况下,类还应该实现 IDisposable 接口并且对每个可处置的对象调用 Dispose() 方法。DraggedDataGridColumn 类封装了一个 Bitmap 对象,该对象显式抓住非托管资源,因此我必须完成这一步骤。

在处理好这一问题之后,我就能够集中精力来解决有关难题的最主要部分了,即操纵 DataGrid 控件以获得我需要的可视效果。

返回页首返回页首

ColumnDragDataGrid 类

DataGrid 控件是一个功能强大的重量级控件,但它本身不会向我们提供拖放列的能力,所以我必须扩展它并且自己来添加该功能。我处理了三个不同的鼠标事件,并且重写了 DataGrid 的 OnPaint 方法来满足我的所有绘图需要。

首先,让我们看看所有用于跟踪应该在何处以及如何进行绘制的成员字段。

成员字段定义

m_initialRegion

一个 DraggedDataGridColumn 对象,表示有关当前正在拖动的列的所有相关内容。我将在本文后面详细讨论 DraggedDataGridColumn 类的细节。

m_mouseOverColumnRect

一个 Rectangle 结构,用于标识一个矩形区域,该区域表示鼠标光标当前正在哪个列上方悬停。

m_mouseOverColumnIndex

鼠标光标当前正在其上方悬停的列的索引。

m_columnImage

在启动拖放操作时包含列的位图表示形式的 Bitmap 对象。

m_showColumnWhileDragging

一个 Boolean 值,表示拖动列时是否应该显示捕获到的列图像。通过 ShowColumnWhileDragging 属性公开。

m_showColumnHeaderWhileDragging

一个 Boolean 值,表示当列被拖动时是否应该显示该列的头部。这是通过 ShowColumnHeaderWhileDragging 属性公开的。

该类中的唯一构造函数是一个不带参数的构造函数,并且相当简单。但是,我觉得有一行代码值得一提:

this.SetStyle( ControlStyles.DoubleBuffer, true );

Windows 中的绘图过程分为两步。当应用程序进行绘图请求时,系统将生成绘图消息(先是 WM_ERASEBKGND,然后是 WM_PAINT)。这些消息被发送到应用程序消息队列中,然后,应用程序将在这里检查这些消息并将它们发送到适当的控件以便进行处理。WM_ERASEBKGND 消息的默认处理方式是用当前窗口背景色填充该区域。随后将处理 WM_PAINT,这会完成所有前景绘图。当您的操作序列涉及到清除背景以及在前景中绘图时,您将产生被称为闪烁 的令人不快的效果。值得庆幸的是,可以通过使用双缓冲 来减轻这一效果。

对于双缓冲,您有两种不同的可以写入的缓冲。一种是存储在视频 RAM 中的可见的屏幕缓冲;另一种是不可见的离屏缓冲,它由内部 GraphicsBuffer 对象表示,并且存储在系统 RAM 中。当绘图操作启动时,将在上述 GraphicsBuffer 对象上呈现所有图形对象。一旦系统确定该操作已完成,就会快速同步这两个缓冲区。

根据 .NET Framework 文档资料,为了在应用程序中实现双缓冲,您需要将 AllPaintingInWmPaintDoubleBufferUserPaintControlStyle 位设置为真。这里,我只需慎重考虑 DoubleBuffer 位。基类 DataGrid 已经将 AllPaintingInWmPaintUserPaint 位设置为真。

上面提到的另外两个 ControlStyle 位被定义为:

UserPaint该位设置为真时,会告诉 Windows 应用程序将完全负责该特定窗口(控件)的所有绘图。这意味着您将处理 WM_ERASEBKGND 和 WM_PAINT 消息。如果该位被设置为假,则应用程序仍将挂钩 WM_PAINT 消息,但它会将该消息发送回系统以进行处理,而不会执行任何绘图操作。当发生这种情况时,系统将尝试呈现该窗口,但是因为它不了解有关该窗口的任何信息,所以它的工作通常不会令人感到满意。

AllPaintingInWmPaint正如该位的名称所表明的那样,当该位被设置为真时,所有绘图都将由控件的 WmPaint 方法进行处理。即使挂钩了 WM_ERASEBKGND 消息,该消息也将被忽略,并且永远不会调用控件的 OnEraseBackground 方法。

在深入研究该类的其余部分之前,需要回顾两个重要的概念。

无效

当您使控件的特定区域无效时,该区域将被添加到控件的更新区域,以便告诉系统在下一个绘图操作过程中重新绘制哪个区域。如果更新区域未定义,则将重新绘制整个控件。

dragdrop_datagrid-fig2

2. 触发绘图操作前后无效区域的可视表示形式。在左侧,带有虚线边框的半透明灰色方形表示已定义的无效区域。右侧的方形显示了在执行绘图操作之后的外观。

正如前面提到的那样,当调用控件的无效方法时,系统将生成 WM_PAINT 消息并将其发送给控件。在收到该消息以后,该控件将引发 Paint 事件;如果已经注册了侦听该事件的处理程序,则会将该事件添加到该控件的事件处理队列的后面。

需要注意的是,被引发的 Paint 事件并不总是能够立即得到处理。这有很多原因,其中最重要的一点是 Paint 事件涉及到绘图中开销比较大的操作之一,并且通常是最后得到处理的事件。

网格样式

DataGridTableStyle 定义了将 DataGrid 绘制 到屏幕上的方式。即使它包含的属性类似于 DataGrid 的属性,它们也是互相排斥的。许多人错误地认为更改同名属性(如 DataGrid 的 RowHeadersVisible 属性)也会更改 DataGridTableStyle 的 RowHeadersVisible 属性的值。结果,当情况没有按预期的那样发展时,需要花费始料未及的时间来进行调试(不要担心,我也会犯这样的错误)。

您可以创建一个包含不同表格样式的集合,并且将它们交替用于不同的数据实体和源。

每个 DataGridTableStyle 都包含一个 GridColumnStylesCollection,它是在将数据绑定到 DataGrid 控件时自动创建的 DataGridColumnStyles 对象的集合。这些对象是 DataGridBoolColumnDataGridTextBoxColumn 或由第三方实现的列(它们都派生自 DataGridColumnStyle)的实例。如果您需要一个包含标签甚至图像的列,则您将必须通过创建 DataGridColumnStyle 的子类来创建一个自定义类。

提示您需要重写 OnDataSource 方法(该方法在 DataGrid 控件被绑定到数据源时调用)。这样,您就可以使用多个样式,并且将它们的映射名称与 DataGrid 的 DataMember 属性值(该值在控件被绑定到数据源时设置)相关联。

返回页首返回页首

列跟踪

绝大部分的列跟踪功能都发生在 MouseDownMouseMoveMouseUp 事件处理程序中。在下面的段落中,我将重点讨论这三个事件处理程序,并且对比较重要的代码段进行解释。这些处理程序所利用的 Helper 方法未予讨论。但是,如果看了代码,您就会发现我已经提供了这些方法的摘要。

MouseDown

当用户在网格上方单击鼠标时,我们需要做的第一件事就是确定单击鼠标的位置。为了启动拖动操作,必须在列标头的上方单击光标。如果证明该条件为真,则将收集一些列信息。我们需要知道该列的起点、宽度和高度,以及相对于列起点的鼠标光标位置。该信息用于建立在列被拖动时要跟踪的两个不同的列区域。

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;
...
}
...
}
dragdrop_datagrid-fig3

3. 列起点、列标头高度(通过 GetColumnHeaderHeight 方法计算)、列高度、列宽度和光标位置示意图

该事件处理程序的其余部分相当简单。执行了一个条件计算以了解是否已经将 ShowColumnsWhileDraggingShowColumnHeaderWhileDragging 属性设置为真。如果是,则计算列大小并且调用 ScreenImage 的 GetScreenshot 方法。我传递了 DataGrid 控件的句柄(记住,控件是子窗口)、起始坐标和列大小,而该方法返回一个包含所需捕获区域的图形对象。然后,所有信息都被存储在一个 DraggedDataGridColumn 对象中。

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

每当鼠标光标在 DataGrid 上方移动时,都会引发 MouseMove 事件。在处理该事件的过程中,我首先跟踪被拖动的列当前在其上方悬停的列,以便可以向用户提供一些可视反馈。其次,我跟踪该列的新位置并发出无效指令。

让我们进一步考察一下代码。我需要做的第一件事是确保列被拖动,然后我通过从相对于控件的鼠标坐标中减去相对于列起点的鼠标坐标来获得该列的 x 坐标(图 4,刻度线标志 #1)。这可以为我提供该列的 x 坐标。因为 y 坐标永远不会更改,所以我不必花费功夫来检查它。

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;
...               
}
}
dragdrop_datagrid-fig4

4. 刻度线标志 #1 显示了 m_draggedColumn.CursorLocation.X 中存储的值。该值从当前光标位置(其坐标相对于控件)中减去。

然后,我检查光标是否悬停在单元格的上方(列标头也被视为单元格)。如果不是,则我假设用户希望中止拖动操作。

private void ColumnDragDataGrid_MouseMove(object sender, MouseEventArgs e) {
...
if ( m_draggedColumn != null ) {
if ( hti.Column >= 0 ) {
...      
} else {
InvalidateColumnArea();
ResetMembersToDefault();
}
}
}

接下来,我希望向用户提供某种反馈,以便他们知道所拖动的列将在他们释放鼠标按键时放置到何处。

这是通过 m_mouseOverColumnIndex 成员字段进行跟踪的,该字段存储了以下列的索引:该列的边界包含光标在处理最后一个 MouseMove 事件之后的当前位置。如果该值不同于点击测试为我们提供的列索引,则用户正在将鼠标悬停在不同列的上方。如果是这样,则将使 m_mouseOverColumnRect 成员字段指示的区域无效,并且记录新区域的坐标。然后,使该新区域无效,以便 Windows 知道这一等待它关注区域的新绘图指令。

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

随后,将变换焦点以有助于跟踪被拖动列的位置。我需要弄清楚是向左还是向右拖动它,以便我可以获得最左边的 x 坐标。在获得该值后,将使列的旧区域和新区域无效,并且将与新位置相关的数据存储在 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

当用户在单元格上方松开鼠标按键时,将执行条件计算,以确保将拖动的列放置到除了其发送方之外的列的上方。如果列索引中表达式计算为真(该列索引不是产生它的列的索引),则切换列。否则,将重新绘制该网格。

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.TableStyles[this.DataMember].GridColumnStyles.AddRange( columns );
this.ResumeLayout();
} else {
InvalidateColumnArea();
}
ResetMembersToDefault();
}

在跨越了该功能的难点之后,触发必要的绘图操作将非常容易。

返回页首返回页首

重写 DataGrid 的 OnPaint 方法

迄今为止,您可能已经注意到没有在任何鼠标事件处理程序中执行任何绘图逻辑。这完全归结为个人喜好。我已经看到其他开发人员将他们的绘图逻辑与其余逻辑和并在一起使用,但我发现将所有绘图逻辑都放在 OnPaint 方法或 Paint 事件处理程序中会更为简单、更有条理。

需要重写 DataGrid 的 OnPaint 方法,以便容纳附加的绘图操作。首先要确保调用基本的 OnPaint 方法,以便绘制基础 DataGrid。这为我提供了可用来进行绘制的画布。

请记住,当您在画布上绘制对象时,z 排序要视对象的绘制顺序而定。了解这一点以后,我们需要首先绘制最底层的形状。

得到绘制的第一个图形是用于指示正在拖动哪个列的矩形( 5,刻度线标志 #1)。

dragdrop_datagrid-fig5

5. 不同的绘制步骤

通过使用 Graphics 对象的 m_draggedColumn 方法,我们在产生拖动操作的列的上方绘制了一个矩形。该区域信息是从 DraggedDataGridColumn 对象中检索到的。使用了半透明的画笔,以便使底层的列仍然可见。然后,在上述矩形的边框周围绘制了一个黑色矩形,以使其具有更为完整的修饰。

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

GDI 中的颜色被分解为四个 8 位的成分,其中的三个成分代表三原色:红色、绿色和蓝色。Alpha 成分(同样是 8 位)确定了颜色的透明度 — 它影响颜色与背景的融合方式。通过 Color.FromArgb 方法可以创建具有特定值的颜色。

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

我在本文前面提到的列反馈是以半透明的浅灰色矩形的形式完成的(图 5,刻度线标志 #2)。首先,我检查列索引以确保它不是 -1,然后使用 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 );            
}
}
}   
}

下一个焦点区域是正在拖动的列。如果用户已经选择在拖动操作发生时显示列或列标头,则绘制该图像。捕获的图像存储在 m_draggedColumn 中,并且可以通过 ColumnImage 属性访问。

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

最后,填充一个半透明矩形以代表拖动操作。这将使用与第一个图形类似的方式完成。从 m_draggedColumn 中读取列区域信息。然后,再绘制一个矩形以进一步增强前面的矩形( 5,刻度线标志 #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 ) );
...   
}   
}
返回页首返回页首

小结

在本文中,我向您说明了我如何能够利用一些基本 GDI 功能,通过 DataGrid 控件获得可视化效果。通过跨越托管边界进行调用,我利用本机 GDI 功能来执行屏幕捕获,并且将该功能与 System.Drawing 中的绘图功能结合使用以产生吸引人的拖放体验。

Chris Sano 是一位使用 MSDN 的软件设计工程师。在狂热地编写代码之余,他喜欢打冰球并观看纽约 Yankees 队和费城 Flyers 队的比赛。如果您希望就本文与 Chris 联系,则可以通过 csano@microsoft.com 与他联系。

相关文章:

最近很火的最新一代国际视频标准 VVC 到底是什么?阿里专家为你揭秘

作者 | 叶琰&#xff0c;阿里巴巴达摩院XG实验室视频标准团队负责人责编 | 夕颜头图 | CSDN付费下载自视觉中国2020年7月1日晚上&#xff08;日内瓦时间&#xff09;&#xff0c;第十九次JVET会议在线上落下帷幕&#xff0c;新一代国际视频编码标准VVC第一版&#xff08;Versat…

nesC编程入门

1.接口 NesC程序主要由各式组件&#xff08;component&#xff09;构成&#xff0c;组件和组件之间通过特定的接口&#xff08;interface&#xff09;互相沟通。一个接口内声明了提供相关服务的方法&#xff08;C语言函数&#xff09;。例如数据读取接口&#xff08;Read&#…

用asp.net实现的把本文推荐给好友功能

作者&#xff1a; 飞鹰 www.ASPCool.com 时间:2001-11-25 17:39:07 ///<summary> ///<author>飞鹰ASPCool.com</author> ///<description>本文用asp.net实现把此文推荐给好友的功能。</desciption> ///<copyright>ASP酷技术资讯…

Access sql语句创建表及字段类型

创建一张空表&#xff1a; Sql"Create TABLE [表名]" 创建一张有字段的表&#xff1a; Sql"Create TABLE [表名]([字段名1] MEMO NOT NULL, [字段名2] MEMO, [字段名3] COUNTER NOT NULL, [字段名4] DATETIME, [字段名5] TEXT(200), [字段名6] TEXT(200)) 字段类…

“刚毕业1年,做Python能挣多少?”网友:吹的不多..

01现状揭秘&#xff1a;Python的火持续燃烧程序员&#xff1a;心态崩了&#xff01;2020年转眼已经大半&#xff0c;在近几个月的榜单中&#xff0c;Python已经连续走上卫冕的道路&#xff0c;并且与Java的差距拉得更远了一些。以往与Java常呈现你追我赶之势&#xff0c;而这一…

Java编程的逻辑 (39) - 剖析LinkedList

本系列文章经补充和完善&#xff0c;已修订整理成书《Java编程的逻辑》&#xff0c;由机械工业出版社华章分社出版&#xff0c;于2018年1月上市热销&#xff0c;读者好评如潮&#xff01;各大网店和书店有售&#xff0c;欢迎购买&#xff0c;京东自营链接&#xff1a;http://it…

运用.NET读写Windows注册编辑表

作者&#xff1a; 冉林仓 www.ASPCool.com 时间:2001-11-9 如果你曾经使用过RegOpenKeyEx、RegCreateKeyEx、RegCloseKey等Win32 API函数读写过注册编辑表&#xff0c;你肯定非常熟悉这些复杂的Registry函数。相反&#xff0c;在.NET框架中&#xff0c;Registry和RegistryK…

使用正则表达式抽取新闻/BBS网页发表时间

package org.apache.nutch.parse.html; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 分析时间戳 * * author xum * */ public class Publish…

为什么Python没有main函数?

作者 | 豌豆花下猫来源 | Python猫&#xff08;ID:python_cat&#xff09;众所周知&#xff0c;Python中没有所谓的main函数&#xff0c;但是网上经常有文章提到“ Python的main函数”和“建议编写main函数”。其实&#xff0c;可能他们是想模仿真正的main函数&#xff0c;但是…

HTTP访问服务的相关解释

一、访问网站的基本流程第一步&#xff1a;客户端用户在浏览器输入www.51cto.com网站&#xff0c;回车后&#xff0c;系统首先会查找系统本地的DNS缓存及hosts文件信息&#xff0c;确定是否存在www.51cto.com余名对应的IP解析记录&#xff0c;如果有就直接获取IP地址&#xff0…

关于ASP.Net中的时间处理

作者&#xff1a; 飞刀 www.ASPCool.com 时间:2001-8-8 这里我想谈谈ASP.Net中对时间的处理 在ASP.Net中&#xff0c;M$为我们提供一种名为DateTime的对象&#xff0c;我们用这个对象来取得当前的时间。比如&#xff1a; DateTime dtDateTime.Now; 在上面…

还缺30万人!程序员2020年要过好日子了……

最近&#xff0c;程序员届有一个重大好消息&#xff0c;可能很多人还不知道&#xff0c;那就是&#xff1a;国内某些城市已经开始程序员人才补贴了&#xff01;对于人工智能公司的项目开发、人才引进、科技研发&#xff0c;最高按照国拨经费的30%给予配套支持&#xff0c;单个项…

淘宝海量数据库之二:一致性选择

众所周知&#xff0c;一致性是数据最关键的属性之一。2000年&#xff0c;Eric Brewer教授在ACM分布式计算年会上指出了著名的CAP理论&#xff1a; Brewer, E. A. 2000. Towards robust distributed systems. In Proceedings of the 19th Annual ACM Symposium on Principles of…

Linux 小记录!

rmdir与 rm -r 的不同处前者这能删除目录 后者目录和文件都可以删除cp 和echo 都会覆盖原有的内容ctrl &#xff0b; c 强制中断这条命令/前后是没有空格的快捷键&#xff1a;TAB 命令 路径补全符号&#xff1a;; 多个命令的分隔符/ 根或者路径的分隔符。> 标准输出重定向…

Session 详解

作者&#xff1a; heallven www.ASPCool.com 时间:2004-8-28 阅读本文章之前的准备 阅读本文章前&#xff0c;需要读者对以下知识有所了解。否则&#xff0c;阅读过程中会在相应的内容上遇到不同程度的问题。 懂得ASP/ASP.NET编程 了解ASP/ASP.NET的S…

实现一个模拟CMD.exe命令编辑模式执行与显示的Delphi控件

cmd.exe这个东西是Windows系统自带的执行Dos的一个灰常好的人机命令交互的执行方式&#xff0c;现在很多脚本语言也都带有这种即时解释的人机模式。当下由于工程的需要&#xff0c;也要做一个类似命令解释显示的编辑器&#xff0c;基本上完全模拟Cmd.exe的这种交互模式&#xf…

谷歌这波大动作,暴露了什么信号?

我们都知道谷歌爸爸收购了Cask Data一家公司。长期以来&#xff0c;谷歌致力于推动围绕 GoogleCloud 的企业业务&#xff0c;但在这方面一直被亚马逊和微软吊打&#xff0c;这次的收购正是为了弥补自身的短板。被收购的 Cask Data 是一家专门提供基于Hadoop的大型数据分析服务解…

OSChina 周一乱弹 ——喝不到放心奶

2019独角兽企业重金招聘Python工程师标准>>> 【今日歌曲推荐】 陈李雨声 : 梦想还是要有的 万一实现了呢。《secret base》 《secret base》- 茅野愛衣 / 戸松遥 / 早見沙織 手机党少年们想听歌&#xff0c;请使劲儿戳&#xff08;这里&#xff09;. 紫King : 这个大…

Assembly学习心得

http://blog.csdn.net/etmonitor/Assembly学习心得说明&#xff1a;最近开始准备把学到的.NET知识重新整理一遍&#xff0c;眼过千遍不如手过一遍&#xff0c;所以我准备记下我的学习心得&#xff0c;已备参考。J各位都是大虾了&#xff0c;如果有哪些错误或者不完整的地方&…

Oracle profile 用户资源限制 说明

一. 官网说明CREATE PROFILEhttp://download.oracle.com/docs/cd/E11882_01/server.112/e17118/statements_6010.htm#SQLRF01310Oracle recommends that you use the Database Resource Manager rather than this SQL statement to establish resource limits. The Database Re…

刚发布!2020年AI人才发展报告,这三个暗示程序员一定要知道!

最近&#xff0c;程序员届有一个重大好消息&#xff0c;可能很多人还不知道&#xff0c;那就是&#xff1a;国内某些城市已经开始程序员人才补贴了&#xff01;对于人工智能公司的项目开发、人才引进、科技研发&#xff0c;最高按照国拨经费的30%给予配套支持&#xff0c;单个项…

阿里巴巴开源技术汇总:115个软件(一)

阿里巴巴开源技术汇总&#xff1a;115个软件 摘要&#xff1a; 云栖社区近期策划了多期和开源产品相关的内容&#xff0c;如GitHub最流行的开源机器学习、大数据等项目&#xff0c;揭秘阿里Weex项目&#xff0c;Hilo开源分析等。深入挖掘&#xff0c;发现开源中国已经收集了数年…

Globalization Resources

http://blog.csdn.net/etmonitor/.NET系统学习----Globalization & Resources l 前言l 了解资源文件l 创建资源文件l 在程序中使用资源文件l 资源文件的命名和部署l 参考前言&#xff1a;在学习如何使用.NET资源文件…

用 Python 可以实现侧脸转正脸?我也要试一下!

作者 | 李秋键责编 | Carol封图 | CSDN 下载自视觉中国近几年来GAN图像生成应用越来越广泛&#xff0c;其中主要得益于GAN 在博弈下不断提高建模能力&#xff0c;最终实现以假乱真的图像生成。GAN 由两个神经网络组成&#xff0c;一个生成器和一个判别器组成&#xff0c;其中生…

Hive SQL 监控系统 - Hive Falcon

1.概述 在开发工作当中&#xff0c;提交 Hadoop 任务&#xff0c;任务的运行详情&#xff0c;这是我们所关心的&#xff0c;当业务并不复杂的时候&#xff0c;我们可以使用 Hadoop 提供的命令工具去管理 YARN 中的任务。在编写 Hive SQL 的时候&#xff0c;需要在 Hive 终端&am…

System commands can run from cmd

gpedit.msc-----组策略 sndrec32-------录音机 Nslookup-------IP地址侦测器 explorer-------打开资源管理器 logoff---------注销命令 tsshutdn-------60秒倒计时关机命令 lusrmgr.msc----本机用户和组 services.msc---本地服务设置 oobe/msoobe /a----检查XP是否激活 notepad…

做 Java 工程师,挺!好!

很多想要入行编程圈的人问到我该学哪一种语言&#xff0c;我都毫不犹豫的说Java。首先我们先看个排行榜&#xff0c;来自权威开发语言排行榜TIOBE的数据&#xff08;截止到2020年4月&#xff09;&#xff0c;可以看到Java语言依然在语言排行榜霸占第一的位置&#xff01;看到这…

使用tmpfs缓存文件提高性能

[ZT]使用tmpfs缓存文件提高性能 - 夜隼 - 博客园使用tmpfs缓存文件提高性能tmpfs是一种虚拟内存文件系统&#xff0c;它最大的特点就是它的存储空间在VM&#xff08;virtual memory&#xff09;里面。Linux系统中VM主要由RM(Real Memory)和swap组成&#xff0c;因此tmpfs最大的…

【转】RelativeLayout和LinearLayout及FrameLayout性能分析

原文&#xff1a;http://blog.csdn.net/hejjunlin/article/details/51159419 工作一段时间后&#xff0c;经常会被领导说&#xff0c;你这个进入速度太慢了&#xff0c;竞品的进入速度很快&#xff0c;你搞下优化吧&#xff1f;每当这时&#xff0c;你会怎么办&#xff1f;功能…

SQL Tips

出自&#xff1a;http://blog.csdn.net/etmonitor/一.怎样删除一个表中某个字段重复的列呀,举个例子表[table1]id name1 aa2 bb3 cc1 aa2 bb3 cc我想最后的表是这样的id name1 aa2 bb3 cc回答:将记录存到临时表#t中&#xff0c;重复的记录只存一条&#xff0c;然后将临时…