拖放 DataGrid 列

拖放 DataGrid 列

發佈日期: 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 聯繫,則可以通過 [email protected] 與他聯繫。

轉到原英文頁面


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章