前、ユーザーコントロールを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で空白文字を適当に何文字かいれ、背景色を塗りつぶして描画することで前の文字を消している。これもまた苦肉の策。
できたようでできてない。できてないことはないけど微妙。誰かもっといい方法を教えて欲しい。なんかもっといい方法があるような。しかし、こんな一部品のために一体どれほどの日数を費やしたことか。もっと業務的に作りこむものはあったのに。しかも、これも使わなくなる可能性大・・・。