1. 程式人生 > >VB.NET學習筆記:自定義控制元件之擴充套件DataGridViewColumnHeaderCell類增加CheckBox全選複選框

VB.NET學習筆記:自定義控制元件之擴充套件DataGridViewColumnHeaderCell類增加CheckBox全選複選框

測試環境:windows 7和Microsoft Visual Studio 2015
點選下載本文原始碼
VB.NET雖然提供了大量控制元件供我們使用,但很多控制元件僅提供最基礎的功能。比如用DataGridView控制元件可以非常方便顯示或操作資料庫資料,我們可以在首列新增DataGridViewCheckBoxColumn列進行全選或全不選操作,但Datagridview控制元件並沒有提供我們平時用的全選或取消全選的複選框,他的表頭就只有這一列的名稱,這樣會影響使用者的使用體驗。這就需要我們對DataGridViewCheckBoxColumn列的列頭進行擴充套件,建立符合使用要求的自定義控制元件。
一、自定義控制元件介紹


VB.NET中的窗體(WinFrom)自定義控制元件大致有三種形式:
1、組合控制元件(CompositeControls):繼承自UserControl類,將目前現有的控制元件根據需要組合到一起形成一個新的控制元件。
2、擴充套件控制元件(ExtendedControls):繼承自.NET類庫中已有的控制元件,新增一些新的屬性和方法來擴充套件原有控制元件。
3、自定義控制元件(CustomControls):繼承自Control類,不提供控制元件特定的功能或圖形介面,控制元件的繪製全部由使用者定義。
二、擴充套件DataGridViewColumnHeaderCell類
可以在DataGridViewCheckBoxColumn列的列頭拉一個CheckBox來實現全選全不選,但CheckBox的位置不好調控。最好的方法是擴充套件DataGridViewColumnHeaderCell類,在列頭繪製一個複選框checkbox控制元件,通過定義checkbox滑鼠單擊事件來實現行的全選或取消全選。
效果如下圖所示:
全選效果圖

單擊單元格事件效果圖
1、新建一個windows窗體應用程式專案。
新建專案-windows窗體應用程式
為專案新增新項,取名DataGridViewCheckBoxColumnHeaderCellEx,類庫。如下圖所示整個專案包含一個類庫和一個窗體。
專案包含2項
2、用下面程式碼替換類庫DataGridViewCheckBoxColumnHeaderCellEx.vb中自動生成的程式碼。

Option Strict On
Option Infer Off
Option Explicit On

'Imports System
'Imports System.Collections.Generic
'Imports System.Text
'Imports System.Windows.Forms
'Imports System.Drawing
Imports System.Windows.Forms.VisualStyles
Imports System.Runtime.InteropServices

#Region "擴充套件表頭單元格"
Public Class DataGridViewCheckBoxColumnHeaderCellEx
    Inherits DataGridViewColumnHeaderCell
    ' 委託處理DataGridViewCheckBoxClickedHandler事件
    Public Delegate Sub DataGridViewCheckBoxClickedHandler(ByVal columnIndex As Integer, ByVal isCheckedAll As Boolean)
    Public Event OnCheckBoxClicked As DataGridViewCheckBoxClickedHandler

#Region "屬性"
    ''' <summary>
    ''' 標識本列是否全選,方便在Cell中獲取
    ''' </summary>
    ''' <returns></returns>
    Public ReadOnly Property IsCheckedAll As Boolean
        Get
            Return CheckedAllState = CheckState.Checked
        End Get
    End Property

    Private m_checkedAllState As CheckState = CheckState.Unchecked
    ''' <summary>
    ''' 全選按鈕狀態
    ''' </summary>
    ''' <returns></returns>
    Public Property CheckedAllState As CheckState
        Get
            Return m_checkedAllState
        End Get
        Set(ByVal value As CheckState)
            m_checkedAllState = value
        End Set
    End Property

    ''' <summary>
    ''' 用於標識當前的checkbox狀態
    ''' </summary>
    Protected m_checkboxState As CheckBoxState = CheckBoxState.UncheckedNormal

    ''' <summary>
    ''' 是否是滑鼠經過的這種hot狀態,不是即為normal
    ''' </summary>
    Protected m_isHot As Boolean = False

    ''' <summary>
    ''' checkbox按鈕區域
    ''' </summary>
    Protected m_chkboxRegion As Rectangle

    ''' <summary>
    ''' 相對於本Headercell的位置
    ''' </summary>
    Protected m_absChkboxRegion As Rectangle
#End Region
#Region "重繪"
    ''' <summary>
    ''' 在本Headercell繪製CheckBox控制元件
    ''' </summary>
    ''' <param name="graphics"></param>
    ''' <param name="clipBounds"></param>
    ''' <param name="cellBounds"></param>
    ''' <param name="rowIndex"></param>
    ''' <param name="dataGridViewElementState"></param>
    ''' <param name="value"></param>
    ''' <param name="formattedValue"></param>
    ''' <param name="errorText"></param>
    ''' <param name="cellStyle"></param>
    ''' <param name="advancedBorderStyle"></param>
    ''' <param name="paintParts"></param>
    Protected Overloads Overrides Sub Paint(ByVal graphics As Graphics, ByVal clipBounds As Rectangle, ByVal cellBounds As Rectangle,
                                            ByVal rowIndex As Integer, ByVal dataGridViewElementState As DataGridViewElementStates, ByVal value As Object,
                                            ByVal formattedValue As Object, ByVal errorText As String, ByVal cellStyle As DataGridViewCellStyle,
                                            ByVal advancedBorderStyle As DataGridViewAdvancedBorderStyle, ByVal paintParts As DataGridViewPaintParts)
        MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, dataGridViewElementState, "", "", errorText, cellStyle, advancedBorderStyle, paintParts)
        Me.m_chkboxRegion = GetSmallRectOfRectangle(cellBounds, CheckBoxRenderer.GetGlyphSize(graphics, CheckBoxState.UncheckedNormal), m_absChkboxRegion)
        Me.RenderCheckBox(graphics)
    End Sub

    ''' <summary>
    ''' 按當前的checkbox狀態重繪checkbox
    ''' </summary>
    ''' <param name="graphics"></param>
    Protected Sub RenderCheckBox(ByVal graphics As Graphics)
        If m_isHot Then
            RenderCheckBoxHover(graphics)
        Else
            RenderCheckBoxNormal(graphics)
        End If

        CheckBoxRenderer.DrawCheckBox(graphics, m_chkboxRegion.Location, m_checkboxState)
    End Sub

    ''' <summary>
    ''' 標識checkbox狀態(normal)
    ''' </summary>
    ''' <param name="graphics"></param>
    Protected Sub RenderCheckBoxNormal(ByVal graphics As Graphics)
        Select Case m_checkedAllState
            Case CheckState.Unchecked
                Me.m_checkboxState = CheckBoxState.UncheckedNormal
            Case CheckState.Indeterminate
                Me.m_checkboxState = CheckBoxState.MixedNormal
            Case CheckState.Checked
                Me.m_checkboxState = CheckBoxState.CheckedNormal
        End Select
    End Sub

    ''' <summary>
    ''' 標識checkbox狀態(hot)
    ''' </summary>
    ''' <param name="graphics"></param>
    Protected Sub RenderCheckBoxHover(ByVal graphics As Graphics)
        Select Case m_checkedAllState
            Case CheckState.Unchecked
                Me.m_checkboxState = CheckBoxState.UncheckedHot
            Case CheckState.Indeterminate
                Me.m_checkboxState = CheckBoxState.MixedHot
            Case CheckState.Checked
                Me.m_checkboxState = CheckBoxState.CheckedHot
        End Select
    End Sub

    ''' <summary>
    ''' 獲取本Headercell的Rectangle及其中的checkbox控制元件的Rectangle
    ''' </summary>
    ''' <param name="rectangle"></param>
    ''' <param name="smallSize">checkbox控制元件的Size</param>
    ''' <param name="absRectangle">checkbox控制元件的Rectangle</param>
    ''' <returns></returns>
    Protected Shared Function GetSmallRectOfRectangle(ByVal rectangle As Rectangle, ByVal smallSize As Size, <Out> ByRef absRectangle As Rectangle) As Rectangle
        Dim rect As Rectangle = New Rectangle()
        absRectangle = New Rectangle()
        absRectangle.Size = smallSize
        absRectangle.X = CType(((rectangle.Width - smallSize.Width) / 2), Integer)
        absRectangle.Y = CType((rectangle.Height - smallSize.Height) / 2, Integer)
        rect.Size = smallSize
        rect.X = absRectangle.X + rectangle.X
        rect.Y = absRectangle.Y + rectangle.Y
        Return rect
    End Function
#End Region

#Region "事件"
    Protected Overrides Sub OnMouseMove(ByVal e As DataGridViewCellMouseEventArgs)
        MyBase.OnMouseMove(e)
        If IsInCheckRegion(e.Location) Then m_isHot = True
        Me.DataGridView.InvalidateCell(Me)
    End Sub

    Protected Overrides Sub OnMouseLeave(ByVal rowIndex As Integer)
        MyBase.OnMouseLeave(rowIndex)
        m_isHot = False
        Me.DataGridView.InvalidateCell(Me)
    End Sub

    Protected Overrides Sub OnMouseDown(ByVal e As DataGridViewCellMouseEventArgs)
        MyBase.OnMouseDown(e)
        m_isHot = IsInCheckRegion(e.Location)
        Me.DataGridView.InvalidateCell(Me)
    End Sub

    Protected Overloads Overrides Sub OnMouseClick(ByVal e As DataGridViewCellMouseEventArgs)
        Dim value As Boolean = False

        If IsInCheckRegion(e.Location) Then

            Select Case m_checkedAllState
                Case CheckState.Unchecked
                    m_checkedAllState = CheckState.Checked
                    value = True
                Case CheckState.Indeterminate
                    m_checkedAllState = CheckState.Checked
                    value = True
                Case CheckState.Checked
                    m_checkedAllState = CheckState.Unchecked
                    value = False
            End Select

            Me.Value = value

            '引發事件
            RaiseEvent OnCheckBoxClicked(e.ColumnIndex, value)

            Me.DataGridView.InvalidateCell(Me)
        End If

        MyBase.OnMouseClick(e)
    End Sub

    ''' <summary>
    ''' 是否在checkbox按鈕區域
    ''' </summary>
    ''' <param name="p"></param>
    ''' <returns></returns>
    Protected Function IsInCheckRegion(ByVal p As Point) As Boolean
        Return Me.m_absChkboxRegion.Contains(p)
    End Function
#End Region

    ''' <summary>
    ''' checkbox列的單元格改變事件
    ''' </summary>
    ''' <param name="columnIndex"></param>
    Friend Sub OnCheckBoxCellCheckedChange(ByVal columnIndex As Integer)
        If columnIndex <> Me.ColumnIndex Then Return

        Dim existsChecked As Boolean = False, existsNoChecked As Boolean = False

        For Each row As DataGridViewRow In Me.DataGridView.Rows
            existsChecked = existsChecked Or CType(row.Cells(columnIndex).EditedFormattedValue, Boolean)
            existsNoChecked = existsNoChecked Or Not CType(row.Cells(columnIndex).EditedFormattedValue, Boolean)
        Next

        Dim oldState As CheckState = Me.CheckedAllState

        If existsChecked Then
            If existsNoChecked Then
                Me.CheckedAllState = CheckState.Indeterminate
            Else
                Me.CheckedAllState = CheckState.Checked
            End If
        Else
            Me.CheckedAllState = CheckState.Unchecked
        End If

        If oldState <> Me.CheckedAllState Then Me.DataGridView.InvalidateCell(Me)
    End Sub
End Class
#End Region

程式碼重點解讀:
(1)、新DataGridViewCheckBoxColumnHeaderCellEx類,繼承自DataGridViewColumnHeaderCell。

Public Class DataGridViewCheckBoxColumnHeaderCellEx
    Inherits DataGridViewColumnHeaderCell

End Class

心得:通常組合控制元件用“使用者控制元件”;;自定義控制元件用“元件類”或“自定義控制元件”;;擴充套件沒有單獨介面控制元件用“類”,如本例,DataGridViewColumnHeaderCell類不能單獨顯示介面,只能在DataGridView控制元件顯示時一同顯示,所以擴充套件該類的DataGridViewCheckBoxColumnHeaderCellEx類只能用“類”;而擴充套件有單獨介面控制元件需用“元件類”或“自定義控制元件”。其中奧妙你只要試試就知道了。

自定義控制元件用到的庫
(2)、委託和事件

' 委託處理DataGridViewCheckBoxClickedHandler事件
    Public Delegate Sub DataGridViewCheckBoxClickedHandler(ByVal columnIndex As Integer, ByVal isCheckedAll As Boolean)
    Public Event OnCheckBoxClicked As DataGridViewCheckBoxClickedHandler

在滑鼠單擊時觸發該事件。

Protected Overloads Overrides Sub OnMouseClick(ByVal e As DataGridViewCellMouseEventArgs)
            '引發事件
            RaiseEvent OnCheckBoxClicked(e.ColumnIndex, value)
    End Sub

在窗體引用DataGridViewCheckBoxColumnHeaderCellEx類時訂閱事件。

'訂閱事件
        AddHandler checkbox1.OnCheckBoxClicked, AddressOf Checkbox_OnCheckboxClicked

(3)、一個函式過程返回2個值。如下程式碼Return rect返回本Headercell的Rectangle,absRectangle返回繪製的checkbox控制元件的Rectangle。

''' <summary>
    ''' 獲取本Headercell的Rectangle及其中的checkbox控制元件的Rectangle
    ''' </summary>
    ''' <param name="rectangle"></param>
    ''' <param name="smallSize">checkbox控制元件的Size</param>
    ''' <param name="absRectangle">checkbox控制元件的Rectangle</param>
    ''' <returns></returns>
    Protected Shared Function GetSmallRectOfRectangle(ByVal rectangle As Rectangle, ByVal smallSize As Size, <System.Runtime.InteropServices.Out> ByRef absRectangle As Rectangle) As Rectangle
        Dim rect As Rectangle = New Rectangle()
        absRectangle = New Rectangle()
        absRectangle.Size = smallSize
        absRectangle.X = CType(((rectangle.Width - smallSize.Width) / 2), Integer)
        absRectangle.Y = CType((rectangle.Height - smallSize.Height) / 2, Integer)
        rect.Size = smallSize
        rect.X = absRectangle.X + rectangle.X
        rect.Y = absRectangle.Y + rectangle.Y
        Return rect
    End Function

三、呼叫DataGridViewCheckBoxColumnHeaderCellEx類
用如下程式碼替換Form1.vb裡自動生成的程式碼。

Public Class Form1
    '定義一眉頭Checkbox1從類DataGridViewCheckBoxColumnHeaderCellEx構造而來
    Private checkbox1 As New DataGridViewCheckBoxColumnHeaderCellEx

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        '新列增加一個控制元件Checkbox1
        Me.DataGridView1.Columns(0).HeaderCell = checkbox1

        Me.DataGridView1.Rows.Add(5)
        Me.DataGridView1.AllowUserToAddRows = False

        '訂閱事件
        AddHandler checkbox1.OnCheckBoxClicked, AddressOf Checkbox_OnCheckboxClicked
    End Sub

    Private Sub Checkbox_OnCheckboxClicked(ByVal columnIndex As Integer, ByVal isCheckedAll As Boolean)
        Me.DataGridView1.EndEdit() '結束編輯操作

        For Each Row As DataGridViewRow In Me.DataGridView1.Rows
            Row.Cells(columnIndex).Value = isCheckedAll
        Next
    End Sub

    ''' <summary>
    ''' 單擊列單元格的內容事件
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub DataGridView1_CellContentClick(sender As Object, e As DataGridViewCellEventArgs) Handles DataGridView1.CellContentClick
        Me.DataGridView1.EndEdit() '結束編輯操作
        Me.checkbox1.OnCheckBoxCellCheckedChange(e.ColumnIndex)
    End Sub
End Class

程式碼重點解讀:
在處理單擊列頭實現全選或取消全選操作、單擊單元格操作時,必須結束單元格的編輯狀態,否則無法正確獲取修改後的單元格內容,造成處於編輯狀態的單元格無法改變勾選狀態或在單擊單元格時列頭的顯示與實際不相符。

Me.DataGridView1.EndEdit() '結束編輯操作

處在編輯狀態的單元格無法勾選,造成操作錯誤。
在參考的文章裡看到還要新建一個datagridviewCheckboxHeaderEventArgs類,繼承自EventArgs類,用在在checkbox單擊事件中提供類頭checkbox的選擇狀態。我的專案裡沒有用上,具體有什麼作用不詳,望能得到指點。

'定義包含列頭checkbox選擇狀態的引數類
Class datagridviewCheckboxHeaderEventArgs
    Inherits EventArgs

    Private checkedState As Boolean = False

    Public Property CheckedState As Boolean
        Get
            Return checkedState
        End Get
        Set(ByVal value As Boolean)
            checkedState = value
        End Set
    End Property
End Class

本文參考以下文章:
1、通過繪製在datagridview控制元件列頭新增一個checkbox控制元件
2、開源DataGridView擴充套件(1) 擴充套件支援全選的CheckBox列。
學習過程得到網友uruseibest的幫助,表示感謝!