拖放 DataGrid 列

 

克里斯·萨诺
Microsoft 开发人员网络 (MSDN)

2004 年 8 月

更新时间:2004 年 12 月
可下载的代码已更新为包含本文中讨论的代码的 Visual Basic .NET 示例。 整篇文章中还进行了一些文本更改。

总结: 了解如何利用基本 GDI 功能通过 DataGrid 控件实现视觉效果。 通过跨托管边界进行调用,可以利用本机 GDI 功能执行屏幕捕获,并最终实现拖放体验。 ) (28 个打印页

下载 ColumnDragDataGrid.msi 文件

目录

简介
入门
ScreenImage 类
DraggedDataGridColumn 类
ColumnDragDataGrid 类
列跟踪
重写 DataGrid 的 OnPaint 方法
结论

简介

几个月前,当我在 Microsoft 入门时,我的经理走进我的办公室,详细描述了一个项目,我将在接下来的两周里完成该项目。 我想要创建一个应用程序,该应用程序将用于在 MSDN 中聚合内容策略师的指标。 其中一项功能请求是 类似于 DataGrid 的控件,它允许用户在将数据导出到 Microsoft Excel 电子表格之前按其首选顺序排列列。 他离开办公室前的最后一句话是,“让它成为一个有趣的用户体验。

我知道,为了能够重新排列 DataGrid 列,我必须创建 一个 DataGridTableStyle 并操作其列样式集合来反映新的列排序,但这并不完全引人入微。 我想要整个拖动操作的可视化表示形式。 我开始玩一些 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、位置和大小参数的颜色数据。 下一个代码块演示如何实现此方法。

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

使用 关键字 (keyword) 的 C# 定义一个作用域,该作用域将在其末尾释放 Graphics 对象。 由于 System.Drawing 命名空间中的所有类都是围绕本机 GDI+ API 的托管包装器,因此我们几乎总是处理非托管资源,因此我们需要确保弃用不再需要其服务的资源。 此过程称为 确定性终结 ,它允许对象使用的资源立即重新分配以用于其他目的,而不是等待垃圾回收器完成其工作。 每当处理实现 IDisposable 接口的对象(例如此处使用的 Graphics 对象)时,都应该遵循这种做法。

检索到源和目标设备上下文的句柄,以便我们可以继续传输位。 源是与参数化 windowHandle 句柄关联的设备上下文,目标是之前创建的位图的设备上下文。

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

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

现在,我们有了一个定义的 Bitmap 对象 (myImage) ,以及一个表示此对象的画布的设备上下文,该对象此时在执行时是透明的。 本机 BitBlt 方法需要要将位复制到的画布部分的坐标和大小,以及要从中开始复制位的源设备上下文上的坐标。 方法还需要光栅操作代码值来定义位块的传输方式。

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

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 类封装位对象,该对象包含非托管资源,因此我必须完成此步骤。

完成此操作后,我能够专注于最大的一块谜题,即操作 DataGrid 控件以获取所需的视觉体验。

ColumnDragDataGrid 类

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

首先,让我们看一下用于跟踪应绘制内容的位置和方式的所有成员字段。

成员字段 定义
m_initialRegion 一个 DraggedDataGridColumn 对象,该对象代表当前正在拖动的列的所有内容。 本文稍后将介绍 DraggedDataGridColumn 类的详细信息。
m_mouseOverColumnRect 矩形结构,用于标识表示鼠标光标当前悬停在其上方的列的矩形区域。
m_mouseOverColumnIndex 鼠标光标当前位于的列的索引。
m_columnImage 一个 Bitmap 对象,该对象包含启动拖放操作时列的位图表示形式。
m_showColumnWhileDragging 一个布尔值,表示是否应在拖动列时显示捕获的列图像。 这通过 ShowColumnWhileDragging 属性公开。
m_showColumnHeaderWhileDragging 一个布尔值,该值表示在拖动列时是否应显示列的标题部分。 这通过 ShowColumnHeaderWhileDragging 属性公开。

此类中唯一的构造函数是无参数构造函数,并且非常简单。 不过,我觉得有一行代码值得一提:

this.SetStyle( ControlStyles.DoubleBuffer, true );

Windows 中的绘制过程分为两步。 当应用程序发出画图请求时,绘制消息 (WM_ERASEBKGND后跟WM_PAINT) 由系统生成。 这些消息将发送到应用程序消息队列,然后由应用程序检查这些消息,并路由到适当的控件进行处理。 WM_ERASEBKGND消息的默认处理方式是使用当前窗口背景色填充区域。 随后处理WM_PAINT,执行所有前景绘制。 如果序列涉及清除背景并在前景中绘图,则会创建一种称为 闪烁的令人不快的效果。 幸运的是,使用 双重缓冲可以缓解这种情况。

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

根据.NET Framework文档,若要在应用程序中实现双重缓冲,需要将 AllPaintingInWmPaintDoubleBufferUserPaintControlStyle 位设置为 true。 在这里,我只需要担心 DoubleBuffer 位。 基本 DataGrid 类已将 AllPaintingInWmPaintUserPaint 位设置为 true。

注意 上面提到的其他两个 ControlStyle 位定义为:

UserPaint: 将此位设置为 true 会告知 Windows,应用程序将完全负责该特定窗口的所有绘制 (控件) 。 这意味着你将处理WM_ERASEBKGND和WM_PAINT消息。 如果此位设置为 false,则应用程序仍会将WM_PAINT消息路由到控件,但不会执行任何绘制操作,而是将消息发送回系统进行处理。 发生这种情况时,系统会尝试呈现窗口,但由于它不知道有关窗口的任何信息,因此它通常不会很好地完成工作。

AllPaintingInWmPaint: 如位名称所示,当位设置为 true 时,所有绘制都由 Control 的 WmPaint 方法处理。 即使已挂钩,也会忽略WM_ERASEBKGND消息,并且永远不会调用控件的 OnEraseBackground 方法。

在深入探讨课堂的其余部分之前,需要复习两个重要概念。

失效

当使控件的某个区域失效时,它会添加到控件的更新区域,这会告知系统在下一次绘制操作期间要重新绘制的区域。 如果未定义更新区域,则重新绘制整个控件。

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

如前所述,调用控件的失效方法时,系统会生成WM_PAINT消息并将其路由到控件。 收到消息后,控件将引发 Paint 事件,如果有注册的处理程序正在侦听该事件,则会将其添加到控件的事件处理队列的后面。

请务必注意,引发的 Paint 事件并不总是立即得到处理。 原因有很多,最重要的是 Paint 事件涉及绘制中开销较高的操作之一,通常是最后一个处理的事件。

网格样式

DataGridTableStyle 定义如何将 DataGrid 绘制到屏幕上。 尽管它包含的属性与 DataGrid 的属性相似,但它们是互斥的。 很多人错误地认为更改同义属性(如 DataGrid 的 RowHeadersVisible 属性)也会更改 DataGridTableStyle 的 RowHeadersVisible 属性的值。 因此,当事情未按预期工作时,不必要地花费时间进行调试, (不必担心我对此过于) 。

可以创建不同表样式的集合,并将其与不同的数据源和成员互换使用。

每个 DataGridTableStyle 都包含一个 GridColumnStylesCollection,它是 DataGridColumnStyle 对象的集合。 这些对象是 DataGridBoolColumnDataGridTextBoxColumn 或第三方实现的列的实例,它们都派生自 DataGridColumnStyle。 如果需要包含标签甚至图像的列,则必须通过对 DataGridColumnStyle 进行子类化来创建自定义类。

**提示**需要重写 OnDataSourceChanged 方法,该方法在 DataGrid 绑定到数据源时调用。 这样就可以使用多种样式,并将其映射名称与 DataGrid 的 DataMember 属性值相关联,该属性值在控件绑定到数据源时设置。

列跟踪

绝大多数列跟踪功能都发生在 MouseDownMouseMoveMouseUp 事件处理程序中。 在即将开始的段落中,我将重点介绍这三个事件处理程序,并为更重要的代码段提供说明。 不讨论这些处理程序使用的帮助程序方法。 但是,如果你查看代码,你将看到我已经为这些方法提供了摘要。

MouseDown

单击网格上方的鼠标时,首先需要确定单击的位置。 若要启动拖动,光标必须已单击列标题上方。 如果此条件证明为 true,则收集一些列信息。 我们需要知道列的原点、宽度和高度,以及鼠标光标相对于列原点的位置。 此信息用于建立要在拖动列时跟踪的两个不同的列区域。

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;

      ...

   }

   ...

}

图 3. 显示由 GetColumnHeaderHeight 方法计算的列原点、列标题高度、列高度、列宽和光标位置的关系图

此事件处理程序的其余部分非常简单。 执行条件评估以查看 ShowColumnsWhileDraggingShowColumnHeaderWhileDragging 属性是否已设置为 true。 如果是,则计算列大小并调用 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;
         
      ...               
      
   }

}

图 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

当用户将鼠标按钮释放到单元格上方时,将执行条件评估,以确保拖动的列已拖放到其发起方之外的列上方。 如果表达式的计算结果为 true(如列索引中与它源自的索引不同),则切换列。 否则,网格将重新绘制。

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

图 5。 不同的绘制步骤

通过使用 Graphics 对象的 FillRectangle 方法,我们在拖动源自的列上绘制一个矩形。 从 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 的软件设计工程师。 当他不疯狂地在代码上工作时,他喜欢打冰球,看纽约洋基队和费城飞人队。 如果想就本文联系 Chris,可以通过 联系他 csano@microsoft.com