DataGridView カスタムボタンセル

前、ユーザーコントロールをDataGridViewのセルの中に埋め込もうとして四苦八苦した。

http://silent-diary.at.webry.info/201411/article_15.html

http://silent-diary.at.webry.info/201501/article_5.html

でも、挫折した。

http://silent-diary.at.webry.info/201501/article_20.html

UserControlにはラベルとボタンを配置。データの状況に応じてラベルやボタンの名称を変え、ボタンをクリックすることでDBのデータを更新する処理をさせたかった。普通はDataGridViewの行をクリックして選択し、別のところに貼り付けたコマンドボタンをクリックすればいいだけなんだけど、DataGridViewの1行毎にセルの中にボタンを埋め込んで、同じセルの中にラベルを置くというのがこんなに難しいと思わなかった。

一応できたと思ったんだけど、ボタンをクリックできるようにマウスカーソルをカスタムセルの上に持っていくだけで編集モードにした。それが、勝手に行を選択状態にしたり、勝手にスクロールする結果となり挫折した。

よくよく考えると、目的を達するためには、ユーザーコントロールにする必要は無かった。別にセルの中に文字を打ち込むわけじゃないので、編集モードにする必要が無かった。編集モードにすることなく、セルをクリックするだけでイベントを発生させられればいい。そう、標準のDataGridViewButtonCellのように。DataGridViewButtonCellのソースコードを見たところ、やはりInitializeEditingControlメソッドがない。編集モードになってない。動作はDataGridViewButtonCellのように。問題は、独自に描画するボタン領域の上にマウスカーソルが来た時に、DataGridViewButtonCellのように描画し動作させないといけない。DataGridViewButtonCellはセルいっぱいにボタンを表示しているけど、独自に作るボタンやテキストを描画する領域を計算し、マウスカーソルの座標を計算して、ボタンの領域に入った出たを判定しないといえけない。

で、一応作ってみた。動作的にちょっと微妙なんだけど、もう許してって感じ。

カスタムボタンカラム

Imports System.Windows.Forms

Public Class CustomButtonColumn

Inherits DataGridViewButtonColumn

'コンストラクタ

Public Sub New()

Me.CellTemplate = New CustomButtonCell()

End Sub

'CellTemplateの取得と設定

Public Overrides Property CellTemplate() As DataGridViewCell

Get

Return MyBase.CellTemplate

End Get

Set(ByVal value As DataGridViewCell)

'DataGridViewOrderStateCell以外はホストしない

If Not TypeOf value Is CustomButtonCell Then

Throw New InvalidCastException( _

"DataGridViewOrderStateCellオブジェクトを" + _

"指定してください。")

End If

MyBase.CellTemplate = value

End Set

End Property

End Class

カスタムボタンセル

Imports System.Windows.Forms.VisualStyles

Imports System.Windows.Forms

Imports System.Drawing

Class CustomButtonCell

Inherits DataGridViewButtonCell

Private Const ButtonWidth As Integer = 80

Private Const ButtonHeight As Integer = 40

Private Const TextHeight As Integer = 40

Private Const TextWidth As Integer = ButtonWidth

Private buttonArea As Rectangle

Private buttonState As PushButtonState

'コンストラクタ

Public Sub New()

End Sub

Private _cellEnabled As Boolean = True

'セル有効/無効

Public Property CellEnabled() As Boolean

Get

Return Me._cellEnabled

End Get

Set(ByVal value As Boolean)

Me._cellEnabled = value

End Set

End Property

Private _stateTextValue As String

'表示テキスト

Public Property StateText() As String

Get

Return Me._stateTextValue

End Get

Set(ByVal value As String)

Me._stateTextValue = value

End Set

End Property

Private _buttonTextValue As String

'ボタンテキスト

Public Property ButtonText() As String

Get

Return Me._buttonTextValue

End Get

Set(ByVal value As String)

Me._buttonTextValue = value

End Set

End Property

Private _buttonVisible As Boolean = True

'ボタン表示

Public Property ButtonVisible() As Boolean

Get

Return Me._buttonVisible

End Get

Set(ByVal value As Boolean)

Me._buttonVisible = value

End Set

End Property

Private _stateValue As String

'セルの値(Valueとは別)

Public Property StateValue() As String

Get

Return Me._stateValue

End Get

Set(ByVal value As String)

Me._stateValue = value

End Set

End Property

'ボタンクリック状態

Private _buttonClicked As Boolean = False

Public ReadOnly Property ButtonClicked As Boolean

Get

Return Me._buttonClicked

End Get

End Property

'セルの値のデータ型を指定する

Public Overrides ReadOnly Property ValueType() As Type

Get

Return GetType(String)

End Get

End Property

'新しいレコード行のセルの既定値を指定する

Public Overrides ReadOnly Property DefaultNewRowValue() As Object

Get

Return "0"

End Get

End Property

'新しいプロパティを追加しているため、

' Cloneメソッドをオーバーライドする必要がある

Public Overrides Function Clone() As Object

Dim cell As CustomButtonCell = _

CType(MyBase.Clone(), CustomButtonCell)

cell.StateText = Me.StateText

cell.ButtonText = Me.ButtonText

cell.StateValue = Me.StateValue

cell.ButtonVisible = Me.ButtonVisible

cell.CellEnabled = Me.CellEnabled

Return cell

End Function

Protected Overrides Sub Paint(ByVal g As Graphics, _

ByVal clipBounds As Rectangle, _

ByVal cellBounds As Rectangle, _

ByVal rowIndex As Integer, _

ByVal elementState 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)

If IsNothing(cellStyle) Then

Throw New ArgumentNullException("cellStyle")

End If

'セルの中にマウスカーソルが入ってるかの判定。

'DataGridViewButtonCellcsからVBに書き直してみたけど、結局使ってない。

Dim ptCurrentCell As Point = Me.DataGridView.CurrentCellAddress

Dim cellSelected As Boolean

If (elementState And DataGridViewElementStates.Selected) = DataGridViewElementStates.Selected Then

cellSelected = True

Else

cellSelected = False

End If

Dim cellCurrent As Boolean

If ptCurrentCell.X = Me.ColumnIndex And ptCurrentCell.Y = rowIndex Then

cellCurrent = True

Else

cellCurrent = False

End If

'セルの背景の描画

If (paintParts And DataGridViewPaintParts.Background) = _

DataGridViewPaintParts.Background Then

Dim cellBackground As New SolidBrush(cellStyle.BackColor)

g.FillRectangle(cellBackground, cellBounds)

cellBackground.Dispose()

End If

'セルの境界線の描画

If (paintParts And DataGridViewPaintParts.Border) = _

DataGridViewPaintParts.Border Then

PaintBorder(g, clipBounds, cellBounds, cellStyle, _

advancedBorderStyle)

End If

'ボタン表示領域の計算(セルの中心からちょっと下)

buttonArea.X = (cellBounds.Width / 2) - (ButtonWidth / 2) + cellBounds.X

buttonArea.Y = (cellBounds.Height / 2) - (ButtonHeight / 2) + cellBounds.Y + (TextHeight / 2)

buttonArea.Height = ButtonHeight

buttonArea.Width = ButtonWidth

'テキスト表示領域(セルの中心からちょっと上)

Dim TextArea As Rectangle

TextArea.X = (cellBounds.Width / 2) - (ButtonWidth / 2) + cellBounds.X

TextArea.Y = (cellBounds.Height / 2) - (ButtonHeight / 2) + cellBounds.Y - (TextHeight / 2)

TextArea.Height = TextHeight

TextArea.Width = TextWidth

'マウスカーソルの座標(DataGridView内の位置)

Dim cursorPosition As Point = _

Me.DataGridView.PointToClient(Cursor.Position)

'マウスボタンがクリックされている時にテキスト表示を塗りつぶす。

'Paintメソッドはちょっとした動きで頻繁に呼ばれる。

'このサンプルではセルの上をマウスカーソルが動くだけでPaintメソッドが呼ばれるので、その都度DrawTextするとちらつくので、この判断を入れている

If _buttonClicked = True Then

'塗りつぶす領域

Dim WhiteRect As Rectangle = New Rectangle(cellBounds.X, TextArea.Y, cellBounds.Width, TextArea.Height)

'全角空白文字を入れてるのは、""だと領域を指定しても背景が塗りつぶされず、前に描画している文字が見えてしまうため。(苦肉の策)

TextRenderer.DrawText(g, "         ", Me.DataGridView.Font, WhiteRect, cellStyle.ForeColor, cellStyle.BackColor, TextFormatFlags.LeftAndRightPadding)

_buttonClicked = False

End If

'新しい文字列を描画する。

TextRenderer.DrawText(g, Me.StateText, Me.DataGridView.Font, TextArea, cellStyle.ForeColor, cellStyle.BackColor, TextFormatFlags.HorizontalCenter + TextFormatFlags.PreserveGraphicsTranslateTransform)

If Me._buttonVisible Then

'ボタンを表示する場合

If Me._cellEnabled Then

'セルが有効の場合(独自プロパティ)

If buttonArea.Contains(cursorPosition) Then

'マウスカーソルがボタンの表示領域上の場合

If Control.MouseButtons And MouseButtons.Left Then

'マウスの左ボタンがクリックされている場合

'ボタンは押されている状態

buttonState = PushButtonState.Pressed

Else

'左ボタンではない場合

'ボタンの上にマウスカーソルが乗ってる状態

buttonState = PushButtonState.Hot

End If

Else

'マウスカーソルがセルの中にあっても、ボタン表示領域上ではない場合

'ボタンは普通の状態

buttonState = PushButtonState.Normal

End If

Else

'セルが無効の状態の場合(独自プロパティ)

buttonState = PushButtonState.Disabled

End If

'ボタンを描画

ButtonRenderer.DrawButton(g, buttonArea, Me.ButtonText, cellStyle.Font, False, buttonState)

End If

End Sub

Protected Overrides Sub OnMouseMove(e As DataGridViewCellMouseEventArgs)

'標準のボタンセルはOnMouseMoveとOnMouseLeaveでInvalidateCellを呼んでいる

Dim cursorPosition As Point = Me.DataGridView.PointToClient(Cursor.Position)

'ボタンの表示領域より少しだけ大きな領域を取得する

Dim ExButtonArea As New Rectangle(buttonArea.X - 8, buttonArea.Y - 8, buttonArea.Width + 16, buttonArea.Height + 16)

If ExButtonArea.Contains(cursorPosition) Then

'マウスカーソルがボタン表示領域(より少し大きな領域)に入ったらセルを再描画

'そうしないと、マウスカーソルがボタン領域を出ても、標準状態に戻らない(苦肉の策)

Me.DataGridView.InvalidateCell(Me)

End If

End Sub

Protected Overrides Sub OnContentClick(e As DataGridViewCellEventArgs)

MyBase.OnContentClick(e)

'カスタムボタンセルの何処をクリックしてもCellContentClickイベントが発生してしまう。

'ボタンの表示領域をクリックした時だけCellContentClickイベントが発生したように見せかけるための処理

Dim cursorPosition As Point = Me.DataGridView.PointToClient(Cursor.Position)

If buttonArea.Contains(cursorPosition) And Me._cellEnabled Then

'マウスカーソルがボタン表示領域に入った状態でこのイベントが発生したらボタンがクリックされたとみなす。

'Paintメソッドの中でこのフィールド変数を使う

_buttonClicked = True

Else

_buttonClicked = False

End If

End Sub

End Class

カスタムボタンセルを使うフォーム

Public Class Form1

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

'Dim col0 As New DataGridViewTextBoxColumn

'col0.Name = "colNo"

'Me.DataGridView1.Columns.Add(col0)

Dim ButtonColumnName As String = "CustomButton"

Dim col1 As New CustomButtonColumn()

col1.Name = ButtonColumnName

Me.DataGridView1.Columns.Add(col1)

Dim idx As Integer

Dim CstmBtnCell As CustomButtonCell

DataGridView1.RowTemplate.Height = 150

'新規行の追加は無しの前提。許すなら、新規行のボタンの表示とか考えないといけない

DataGridView1.AllowUserToAddRows = False

' DataGridViewの行追加(1行目)

DataGridView1.Rows.Add()

idx = DataGridView1.Rows.Count - 1

CstmBtnCell = DataGridView1.Rows(idx).Cells(ButtonColumnName)

CstmBtnCell.ButtonText = "更新"

CstmBtnCell.StateText = "1行目"

CstmBtnCell.StateValue = "0"

' DataGridViewの行追加(2行目)

DataGridView1.Rows.Add()

idx = DataGridView1.Rows.Count - 1

CstmBtnCell = DataGridView1.Rows(idx).Cells(ButtonColumnName)

CstmBtnCell.ButtonText = "更新"

CstmBtnCell.StateText = "2行目"

CstmBtnCell.StateValue = "1"

' DataGridViewの行追加(3行目)

DataGridView1.Rows.Add()

idx = DataGridView1.Rows.Count - 1

CstmBtnCell = DataGridView1.Rows(idx).Cells(ButtonColumnName)

CstmBtnCell.ButtonText = "押せない"

CstmBtnCell.StateText = "3行目(無効)"

CstmBtnCell.StateValue = "2"

CstmBtnCell.CellEnabled = False

' DataGridViewの行追加(4行目)

DataGridView1.Rows.Add()

idx = DataGridView1.Rows.Count - 1

CstmBtnCell = DataGridView1.Rows(idx).Cells(ButtonColumnName)

CstmBtnCell.ButtonText = "見えない"

CstmBtnCell.StateText = "見えない"

CstmBtnCell.StateValue = "3"

CstmBtnCell.ButtonVisible = False

End Sub

Private Sub DataGridView1_CellContentClick(sender As Object, e As DataGridViewCellEventArgs) Handles DataGridView1.CellContentClick

If e.RowIndex >= 0 And e.ColumnIndex >= 0 Then

Dim OSCell As CustomButtonCell

OSCell = DataGridView1.Rows(e.RowIndex).Cells("CustomButton")

If e.ColumnIndex = OSCell.ColumnIndex And OSCell.ButtonClicked And OSCell.CellEnabled And OSCell.ButtonVisible Then

OSCell.ButtonText = "クリック済み"

OSCell.StateText = "変更済み"

OSCell.StateValue = "0"

End If

End If

End Sub

End Class

画像
画像

 2枚目の画像の2行目は、ボタンクリック後にマウスカーソルがボタンの上に乗っかっている状態。

最初、DataGridViewButtonCellのC#ソースをVBに置き換える勢いでやったんだけど、すぐ挫折して、でもなんとか上記に辿り着いた。問題はやはり、ボタン領域にマウスカーソルが入ったかどうかの判定。DataGridViewButtonCellはセルいっぱいにボタンを表示しているので、CellMouseEnter、CellMouseLeaveのイベントでDataGridView.InvalidateCellメソッドでセルの再描画をしている。

しかし、今回の場合、セルのボタン領域が境なので、このイベントは使えず、CellMouseMoveで常に判定して描画することになる。ただ、そうするとマウスが動いている間常に描画してしまい、セルがちらついて見た目が悪い。しょうがないので、ボタンの領域に入った時だけ描画する判断を入れた。ただしその領域はボタンより大きめで判断している。ボタンからセルが出るとき、出た後は描画しないので、ボタンの上にマウスカーソルが乗った状態から変化しない事になる。なので、ボタン領域よりちょっとだけ大きめのRectangleを作って、その領域で判断させた。でも、これでもマウスの動き方次第で描画してくれないことはある。

あと、テキストの書き換えもよくわからない。TextRenderer.DrawTextは同じ領域に書いても、先に書いている文字を消してくれないので、文字が重なってしまう。でも、DrawTextで書いた文字を消す方法がわからない。しょうがないので、TextRenderer.DrawTextで空白文字を適当に何文字かいれ、背景色を塗りつぶして描画することで前の文字を消している。これもまた苦肉の策。

できたようでできてない。できてないことはないけど微妙。誰かもっといい方法を教えて欲しい。なんかもっといい方法があるような。しかし、こんな一部品のために一体どれほどの日数を費やしたことか。もっと業務的に作りこむものはあったのに。しかも、これも使わなくなる可能性大・・・。