简介
几个月以前,当我初到 Microsoft 工作时,我的经理走进我的办公室,并且向我详细说明了我将在随后两个星期内将要从事的一个项目。我需要设想出一个应用程序,用于为 MSDN 内容策划人员整合衡量标准。其中一个功能要求是需要一个类似于 DataGrid 的控件,该控件使用户可以在将数据导出到 Microsoft Excel 电子表格之前,按照他们喜欢的顺序排列所有列。他在离开我的办公室之前说的最后一句话是:“将它变为有趣的用户体验。”
我知道为了能够重新排列 DataGrid 列,我必须操纵 DataGrid 的 DataGridColumnStyle 属性以反映新的列排序,但这并没有什么吸引人之处。我想要的是对整个拖动操作实现可视化表示。我在开始时使用了一些 System.Drawing 功能,并且达到了能够在屏幕间拖动图形的程度。我断定我需要让它更进一步。我可以让它看起来更像是用户在拖动列,而不是仅仅在 DataGrid 绘图表面上拖动枯燥乏味的矩形进行绘制。我对本机 GDI 库进行了一番寻根究底,经过几个小时的试验后,我终于弄清楚为了实现这一技巧而需要完成的工作。
![dragdrop_datagrid-fig1](https://i-blog.csdnimg.cn/blog_migrate/0011112d15f81b162e37a7761159fb51.png)
图 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 文档资料,为了在应用程序中实现双缓冲,您需要将 AllPaintingInWmPaint、DoubleBuffer 和 UserPaintControlStyle 位设置为真。这里,我只需慎重考虑 DoubleBuffer 位。基类 DataGrid 已经将 AllPaintingInWmPaint 和 UserPaint 位设置为真。
注上面提到的另外两个 ControlStyle 位被定义为:
UserPaint:该位设置为真时,会告诉 Windows 应用程序将完全负责该特定窗口(控件)的所有绘图。这意味着您将处理 WM_ERASEBKGND 和 WM_PAINT 消息。如果该位被设置为假,则应用程序仍将挂钩 WM_PAINT 消息,但它会将该消息发送回系统以进行处理,而不会执行任何绘图操作。当发生这种情况时,系统将尝试呈现该窗口,但是因为它不了解有关该窗口的任何信息,所以它的工作通常不会令人感到满意。
AllPaintingInWmPaint:正如该位的名称所表明的那样,当该位被设置为真时,所有绘图都将由控件的 WmPaint 方法进行处理。即使挂钩了 WM_ERASEBKGND 消息,该消息也将被忽略,并且永远不会调用控件的 OnEraseBackground 方法。
在深入研究该类的其余部分之前,需要回顾两个重要的概念。
无效
当您使控件的特定区域无效时,该区域将被添加到控件的更新区域,以便告诉系统在下一个绘图操作过程中重新绘制哪个区域。如果更新区域未定义,则将重新绘制整个控件。
![dragdrop_datagrid-fig2](https://i-blog.csdnimg.cn/blog_migrate/944425df7bf0d8ada22d5ccb166a738c.png)
图 2. 触发绘图操作前后无效区域的可视表示形式。在左侧,带有虚线边框的半透明灰色方形表示已定义的无效区域。右侧的方形显示了在执行绘图操作之后的外观。
正如前面提到的那样,当调用控件的无效方法时,系统将生成 WM_PAINT 消息并将其发送给控件。在收到该消息以后,该控件将引发 Paint 事件;如果已经注册了侦听该事件的处理程序,则会将该事件添加到该控件的事件处理队列的后面。
需要注意的是,被引发的 Paint 事件并不总是能够立即得到处理。这有很多原因,其中最重要的一点是 Paint 事件涉及到绘图中开销比较大的操作之一,并且通常是最后得到处理的事件。
网格样式
DataGridTableStyle 定义了将 DataGrid 绘制 到屏幕上的方式。即使它包含的属性类似于 DataGrid 的属性,它们也是互相排斥的。许多人错误地认为更改同名属性(如 DataGrid 的 RowHeadersVisible 属性)也会更改 DataGridTableStyle 的 RowHeadersVisible 属性的值。结果,当情况没有按预期的那样发展时,需要花费始料未及的时间来进行调试(不要担心,我也会犯这样的错误)。
您可以创建一个包含不同表格样式的集合,并且将它们交替用于不同的数据实体和源。
每个 DataGridTableStyle 都包含一个 GridColumnStylesCollection,它是在将数据绑定到 DataGrid 控件时自动创建的 DataGridColumnStyles 对象的集合。这些对象是 DataGridBoolColumn、DataGridTextBoxColumn 或由第三方实现的列(它们都派生自 DataGridColumnStyle)的实例。如果您需要一个包含标签甚至图像的列,则您将必须通过创建 DataGridColumnStyle 的子类来创建一个自定义类。
提示您需要重写 OnDataSource 方法(该方法在 DataGrid 控件被绑定到数据源时调用)。这样,您就可以使用多个样式,并且将它们的映射名称与 DataGrid 的 DataMember 属性值(该值在控件被绑定到数据源时设置)相关联。
列跟踪
绝大部分的列跟踪功能都发生在 MouseDown、MouseMove 和 MouseUp 事件处理程序中。在下面的段落中,我将重点讨论这三个事件处理程序,并且对比较重要的代码段进行解释。这些处理程序所利用的 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](https://i-blog.csdnimg.cn/blog_migrate/a3ac917816728eb366588db4e4ec009e.png)
图 3. 列起点、列标头高度(通过 GetColumnHeaderHeight 方法计算)、列高度、列宽度和光标位置示意图
该事件处理程序的其余部分相当简单。执行了一个条件计算以了解是否已经将 ShowColumnsWhileDragging 或 ShowColumnHeaderWhileDragging 属性设置为真。如果是,则计算列大小并且调用 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](https://i-blog.csdnimg.cn/blog_migrate/137130cde557aeb09c38db4765cc98de.png)
图 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](https://i-blog.csdnimg.cn/blog_migrate/868b80d76fc4718d18cf30108aa87867.png)
图 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 ) ); ... } }