2013年7月22日月曜日

【VB.NET】DataGridViewの列ヘッダーの2行表示

フォームにDataGridViewを配置しDataGridViewのPaintイベントで列ヘッダーを2段表示にする。

フォームにDataGrisViewを配置し列を追加します。

次にDataGridViewのPaintイベントで以下の手順で列ヘッダーに指定された行数を描画します。
  • 列ヘッダーの行数(段数)、行の高さの変数を宣言。
  • 列ヘッダーセル定義構造を以下の様に宣言。
      行
      列
      結合する列数
      結合する行数
      セルに関連付けられたテキスト
  • 列ヘッダーセル定義構造体配列を宣言。
  • フォームのロード時
      ちらつき防止のためのダブルバッファリング処理。
      ColumnHeadersHeightSizeModeを EnableResizingに設定。
      列ヘッダーの行数に合わせてColumnHeadersHeightを設定。 
  • DataGridViewのPaint
      行数と高さに基づいて各列ヘッダーに行数分の四角形を描画します。
      最下行にその列のヘッダーテキストを描画します。
      ヘッダーセル定義に基づいてセルの結合を描画します。
  • 列の幅が変更時、スクロール時、コントロールのサイズ変更時
      直前の描画が残らないように列ヘッダーの描画領域を無効化します。
Public Class Form1

    ''' <summary>
    ''' 列ヘッダーの行数
    ''' </summary>
    ''' <remarks></remarks>
    Private ColumnHeaderRowCount As Integer = 2

    ''' <summary>
    ''' 列ヘッダーの行の高さ
    ''' </summary>
    ''' <remarks></remarks>
    Private columnHeaderrRowHeight As Integer = 17

    ''' <summary>
    ''' 列ヘッダーセル定義構造体
    ''' </summary>
    ''' <remarks></remarks>
    Public Structure HeaderCell
        Public Row As Integer
        Public Column As Integer
        Public RowSpan As Integer
        Public ColumnSpan As Integer
        Public Text As String

        ''' <summary>
        ''' 列ヘッダーセル定義
        ''' </summary>
        ''' <param name="paramRow">行</param>
        ''' <param name="paramColumn">列</param>
        ''' <param name="paramRowSpan">結合する行数</param>
        ''' <param name="paramColumnSpan">結合する列数</param>
        ''' <param name="paramText">セルに関連付けられたテキスト</param>
        ''' <remarks></remarks>
        Sub New(ByVal paramRow As Integer, ByVal paramColumn As Integer, ByVal paramRowSpan As Integer, ByVal paramColumnSpan As Integer, ByVal paramText As String)
            ' TODO: Complete member initialization 
            Row = paramRow
            Column = paramColumn
            RowSpan = paramRowSpan
            ColumnSpan = paramColumnSpan
            Text = paramText
        End Sub

    End Structure

    ''' <summary>
    ''' 列ヘッダーセル定義
    ''' </summary>
    ''' <remarks></remarks>
    Public HeaderCells As HeaderCell() = {New HeaderCell(0, 0, 1, 2, "Category1"), _
New HeaderCell(0, 2, 1, 2, "Category2"), _
New HeaderCell(0, 4, 2, 1, "Column5")}

    ''' <summary>
    ''' フォームを読み込む時
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        'ちらつき防止
        Dim myType As Type = GetType(DataGridView)
        Dim myPropInfo As System.Reflection.PropertyInfo = myType.GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance Or System.Reflection.BindingFlags.NonPublic)
        myPropInfo.SetValue(Me.DataGridView1, True, Nothing)


        '列ヘッダーの高さの調整モード
        Me.DataGridView1.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.EnableResizing

        '列ヘッダーの高さを行数に合わせる
        Me.DataGridView1.ColumnHeadersHeight = columnHeaderrRowHeight * ColumnHeaderRowCount

    End Sub


    ''' <summary>
    ''' コントロールを再描画する時
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub DataGridView1_Paint(ByVal sender As System.Object, _
 ByVal e As System.Windows.Forms.PaintEventArgs) Handles DataGridView1.Paint

        '列が無い場合
        If Me.DataGridView1.ColumnCount = 0 Then
            Exit Sub
        End If

        '行が無い場合
        If Me.DataGridView1.RowCount = 0 Then
            Exit Sub
        End If

        '列ヘッダーの行の高さの取得
        Dim rowHeight As Integer = Me.DataGridView1.ColumnHeadersHeight / ColumnHeaderRowCount

        Dim lineWidth As Integer = 1

        '列ヘッダーを指定された行数にセル表示する
        For columuns = 0 To Me.DataGridView1.ColumnCount - 1

            For rows = 0 To ColumnHeaderRowCount - 1

                '列ヘッダーの表示領域の取得
                Dim rect As Rectangle = Me.DataGridView1.GetCellDisplayRectangle(columuns, -1, True)

                ''列ヘッダーの描画領域の底部の座標を保存
                Dim btm As Integer = rect.Bottom

                'セルの描画領域のY座標
                Select Case Me.DataGridView1.BorderStyle
                    Case Windows.Forms.BorderStyle.None
                        rect.Y = rowHeight * rows
                    Case Windows.Forms.BorderStyle.FixedSingle
                        rect.Y = rowHeight * rows + lineWidth
                    Case Windows.Forms.BorderStyle.Fixed3D
                        rect.Y = rowHeight * rows + (lineWidth * 2)
                End Select

                'セルの描画領域のX座標
                rect.X -= lineWidth

                'セルの描画領域の高さ
                rect.Height = rowHeight

                '最下行の場合高さを調整
                If rows = Me.ColumnHeaderRowCount - 1 Then
                    rect.Height = btm - rect.Y - lineWidth
                End If

                Dim gridPen As New Pen(Me.DataGridView1.GridColor)
                e.Graphics.DrawRectangle(gridPen, rect)


                'セルの背景色の領域
                rect.Y += lineWidth
                rect.X += lineWidth
                rect.Height -= lineWidth
                rect.Width -= lineWidth

                '背景色
                Dim backBrash As New SolidBrush(Me.DataGridView1.BackColor)

                e.Graphics.FillRectangle(backBrash, rect)

                '見出しを最下列に表示
                If rows = Me.ColumnHeaderRowCount - 1 Then


                    Dim text As String = Me.DataGridView1.Columns(columuns).HeaderText
                    Dim formatFlg As TextFormatFlags = GetTextFormatFlags(Me.DataGridView1.ColumnHeadersDefaultCellStyle.Alignment)

                    TextRenderer.DrawText(e.Graphics, text, Me.DataGridView1.ColumnHeadersDefaultCellStyle.Font, _
rect, Me.DataGridView1.ColumnHeadersDefaultCellStyle.ForeColor, _
formatFlg)
                End If

                'リソースの解放
                gridPen.Dispose()
                backBrash.Dispose()
            Next
        Next



        '列ヘッダーセル定義の処理
        For i = 0 To Me.HeaderCells.Count - 1

            'セルの結合の開始行がヘッダーの行数より大きい場合は除外
            If HeaderCells(i).Row > Me.ColumnHeaderRowCount - 1 Then
                Continue For
            End If

            'セルの結合の開始列の列インデックスが列数より大きい場合は除外
            If HeaderCells(i).Column > Me.DataGridView1.ColumnCount - 1 Then
                Continue For
            End If

            '描画領域の設定
            Dim rect As Rectangle = Nothing

            '結合する列中のソート状態
            Dim sortText As String = String.Empty

            '結合するセルの幅の取得
            For j = Me.HeaderCells(i).Column To Me.HeaderCells(i).Column + Me.HeaderCells(i).ColumnSpan - 1

                If Me.DataGridView1.Columns(j).Displayed = False Then
                    Continue For
                End If


                If rect = Nothing Then
                    rect = Me.DataGridView1.GetCellDisplayRectangle(j, -1, True)
                Else
                    rect.Width += Me.DataGridView1.GetCellDisplayRectangle(j, -1, True).Width
                End If
            Next

            '結合するセルが画面中に無い場合
            If rect = Nothing Then
                Continue For
            End If

            '結合する行がヘッダー行数より大きい場合
            Dim rowSapn As Integer = Me.HeaderCells(i).RowSpan
            If rowSapn > ColumnHeaderRowCount Then
                rowSapn = ColumnHeaderRowCount
            End If

            '列ヘッダーの描画領域の底部の座標を保存
            Dim btm As Integer = rect.Bottom

            '結合するセルの描画領域のY座標
            Select Case Me.DataGridView1.BorderStyle
                Case Windows.Forms.BorderStyle.None
                    rect.Y = rowHeight * (Me.HeaderCells(i).Row)
                Case Windows.Forms.BorderStyle.FixedSingle
                    rect.Y = rowHeight * (Me.HeaderCells(i).Row) + lineWidth
                Case Windows.Forms.BorderStyle.Fixed3D
                    rect.Y = rowHeight * (Me.HeaderCells(i).Row) + (lineWidth * 2)
            End Select

            '結合するセルの描画領域のX座標
            rect.X -= lineWidth

            '結合するセルの描画領域の高さ
            rect.Height = rowHeight * rowSapn

            '最下行の場合は描画領域の高さを調整する
            If Me.HeaderCells(i).Row + rowSapn = Me.ColumnHeaderRowCount Then
                rect.Height = btm - rect.Y - lineWidth
            End If

            'グッリドの線
            Dim gridPen As New Pen(Me.DataGridView1.GridColor)

            '背景色の取得
            Dim backgroundColor As System.Drawing.Color = Me.DataGridView1.ColumnHeadersDefaultCellStyle.BackColor

            '背景色
            Dim backBrash As New SolidBrush(backgroundColor)

            'くぼみ線
            Dim whiteBrash As New SolidBrush(Color.White)



            '枠線の描画
            e.Graphics.DrawRectangle(gridPen, rect)


            '結合セルの背景色の描画領域の設定
            rect.Y += lineWidth
            rect.X += lineWidth
            rect.Height -= lineWidth
            rect.Width -= lineWidth


            '背景色の描画
            e.Graphics.FillRectangle(backBrash, rect)


            'テキストの描画
            Dim foreColor As System.Drawing.Color = Me.DataGridView1.ColumnHeadersDefaultCellStyle.ForeColor
            Dim formatFlg As TextFormatFlags = GetTextFormatFlags(Me.DataGridView1.ColumnHeadersDefaultCellStyle.Alignment)

            TextRenderer.DrawText(e.Graphics, Me.HeaderCells(i).Text & sortText, Me.DataGridView1.ColumnHeadersDefaultCellStyle.Font, _
rect, foreColor, formatFlg)


            'リソースの解放
            gridPen.Dispose()
            backBrash.Dispose()
            whiteBrash.Dispose()

        Next

    End Sub


    ''' <summary>
    ''' 結合元のセルの文字位置から結合後の文字位置を取得する
    ''' </summary>
    ''' <param name="alignment">テキストの配置</param>
    ''' <remarks></remarks>
    Private Function GetTextFormatFlags(ByVal alignment As DataGridViewContentAlignment) As TextFormatFlags
        Try
            ''文字の描画
            Dim formatFlg As TextFormatFlags = TextFormatFlags.Right Or TextFormatFlags.VerticalCenter Or TextFormatFlags.EndEllipsis

            '表示位置
            Select Case alignment
                Case DataGridViewContentAlignment.BottomCenter
                    formatFlg = TextFormatFlags.Bottom Or TextFormatFlags.HorizontalCenter Or TextFormatFlags.EndEllipsis
                Case DataGridViewContentAlignment.BottomLeft
                    formatFlg = TextFormatFlags.Bottom Or TextFormatFlags.Left Or TextFormatFlags.EndEllipsis
                Case DataGridViewContentAlignment.BottomRight
                    formatFlg = TextFormatFlags.Bottom Or TextFormatFlags.Right Or TextFormatFlags.EndEllipsis
                Case DataGridViewContentAlignment.MiddleCenter
                    formatFlg = TextFormatFlags.VerticalCenter Or TextFormatFlags.HorizontalCenter Or TextFormatFlags.EndEllipsis
                Case DataGridViewContentAlignment.MiddleLeft
                    formatFlg = TextFormatFlags.VerticalCenter Or TextFormatFlags.Left Or TextFormatFlags.EndEllipsis
                Case DataGridViewContentAlignment.MiddleRight
                    formatFlg = TextFormatFlags.VerticalCenter Or TextFormatFlags.Right Or TextFormatFlags.EndEllipsis
                Case DataGridViewContentAlignment.TopCenter
                    formatFlg = TextFormatFlags.Top Or TextFormatFlags.HorizontalCenter Or TextFormatFlags.EndEllipsis
                Case DataGridViewContentAlignment.TopLeft
                    formatFlg = TextFormatFlags.Top Or TextFormatFlags.Left Or TextFormatFlags.EndEllipsis
                Case DataGridViewContentAlignment.TopRight
                    formatFlg = TextFormatFlags.Top Or TextFormatFlags.Right Or TextFormatFlags.EndEllipsis
            End Select

            Return formatFlg

        Catch ex As Exception
            Throw
        End Try
    End Function

    ''' <summary>
    ''' 列ヘッダーの描画領域の無効化
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub InvalidateUnitColumns()
        Try

            If Me.DataGridView1.RowCount > 0 Then
                Dim hRect As Rectangle = Me.DataGridView1.DisplayRectangle
                'hRect.Height = Me.DataGridView1.ColumnHeadersHeight
                Me.DataGridView1.Invalidate(hRect)
            End If


        Catch ex As Exception
            Throw
        End Try
    End Sub

    ''' <summary>
    ''' 列の幅が変更された時
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub DataGridView1_ColumnWidthChanged(ByVal sender As System.Object, ByVal e As System.Windows.Forms.DataGridViewColumnEventArgs) Handles DataGridView1.ColumnWidthChanged
        InvalidateUnitColumns()
    End Sub

    ''' <summary>
    ''' スクロールする時
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub DataGridView1_Scroll(ByVal sender As System.Object, ByVal e As System.Windows.Forms.ScrollEventArgs) Handles DataGridView1.Scroll
        InvalidateUnitColumns()
    End Sub

    ''' <summary>
    ''' コントロールのサイズが変更された時
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub DataGridView1_SizeChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles DataGridView1.SizeChanged
        InvalidateUnitColumns()
    End Sub

    ''' <summary>
    ''' マウスのボタンが押された時
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub DataGridView1_MouseDown(sender As System.Object, e As System.Windows.Forms.MouseEventArgs) Handles DataGridView1.MouseDown

        Try
            ''列幅、行高を調整するドラグ線を見えるようにするためにダブルバッファを解除する
            'ちらつき防止
            Dim myType As Type = GetType(DataGridView)
            Dim myPropInfo As System.Reflection.PropertyInfo = myType.GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance Or System.Reflection.BindingFlags.NonPublic)
            myPropInfo.SetValue(Me.DataGridView1, False, Nothing)

        Catch ex As Exception
            MessageBox.Show(ex.ToString)
        End Try
    End Sub

    ''' <summary>
    ''' マウスのボタンが離された時
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub DataGridView1_MouseUp(sender As System.Object, e As System.Windows.Forms.MouseEventArgs) Handles DataGridView1.MouseUp

        Try
            ''OnMouseDownイベントで解除されたダブルバッファを適用する
            Dim myType As Type = GetType(DataGridView)
            Dim myPropInfo As System.Reflection.PropertyInfo = myType.GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance Or System.Reflection.BindingFlags.NonPublic)
            myPropInfo.SetValue(Me.DataGridView1, True, Nothing)
        Catch ex As Exception
            MessageBox.Show(ex.ToString)
        End Try
    End Sub
End Class
実行結果


ただ四角形と文字を描画しているだけなので結合されたセル上でマウスクリックされた場合は
その直下の列がソートされます。またセルにマウスエンターされてもハイライトはされません。





11 件のコメント:

  1. 参考にしようと思ったけど、つづり間違い多いな・・・

    返信削除
    返信
    1. ご意見ありがとうございます。
      以下4件のつづり間違えを確認しましたので訂正しました。
        誤 → 正
      ・Categori → Category
      ・Hight → height
      ・rowHieght → rowHeight
      ・backGroundColor → backgroundColor

      削除
  2. すばらしいサンプルをありがとうございます。

    他のサイトにはないきめ細かな制御(ちらつき防止やBorderStyleの違いによる枠の処理)まで示していただき、助かりました。

    このサンプルをフルに活用させていただきます。

    本当に感謝です!

    返信削除
    返信
    1. コメントありがとうございます。
      こちらこそこのサンプルが活用されお役に立てれば大変嬉しいことです。

      削除
  3. このコメントはブログの管理者によって削除されました。

    返信削除
    返信
    1. 了解です。お役に立てると良いのですが。

      削除
  4. 素晴らしい!そのまま使えました。感謝しかありません。
    ありがとうございました。

    返信削除
    返信
    1. 返信遅くなりました。
      お役に立てて何よりです。

      削除
  5. 参考にさせて頂きました。

    DataGridView1.Columns(2).DividerWidth = 3
    の様に一部の区切り線を太くした場合にヘッダーの区切り線も太くしたいです。
    セルの背景色の領域設定時にrect.Widthを区切り線の太さ分狭くしてみましたが、
    思ったように描画されませんでした。
    区切り線の対応についてアドバイス頂けませんか。

    返信削除
    返信
    1. しばらくサボっていたため返信できずにすみません。
      もし自己解決していれば幸いなのですが…
      で区切り線の対処としまして当初区切り線に合わせてヘッダーにも区切り線を描画すれば良いのかな
      と考えたのですが、意外に横スクロール等の場面で線の位置がずれたりと上手く行きませんでした。
      そこで以下の方法で分割線への対応ができるのではないか考えます。
      1 セルの表示領域えお得るGetCellDisplayRectangleの3番目の引数をfalse(セル境界全体を取得)に変更。
      2 取得した領域にの区切り線の描画色で塗りつぶしの四角形を描画
      3 2の領域から区切り線の太さ分縮小したセルの背景色を描画(区切り線を太くする場合はその領域の幅を短く)
      4 3の領域のへ表示するテキストを描画する。

      近々コードを再アップするので参考にしていただければと思います。

      削除
  6. このコメントは投稿者によって削除されました。

    返信削除