結論から言うと、別のイベントで処理するようにしたら解決した。
どういう画面で起きたのか
みんな大好きDataGridViewを使って、List内に格納されているデータを表示する画面。
DataGridViewに表示する際に、値によって罫線を描画したりしなかったりがあってちょっとややこしい表。
俗に言うセルを結合というやつ。見かけだけ。
この罫線描画が今回の話の中心。
なぜ無限ループが起きた
- 原因
- CellPaintingのメソッド内で、罫線を描画しようとして無限ループが発生していた
やらかしたのはこういう感じのソース(※記憶とコピペ頼り)
private void DgvCellPainting(object sender,
DataGridViewCellPaintingEventArgs e)
{
//セルの値が0ではないか判定
if (dgv.Rows[e.RowsIndex,e.ColumnIndex].Value != 0)
{
// 罫線を描画する
int startX = dgv.RowHeadersVisible ? dgv.RowHeadersWidth : 0;
int startY = e.RowBounds.Top + e.RowBounds.Height - 1;
int endX = startX + dgv.Columns.GetColumnsWidth(
DataGridViewElementStates.Visible) -
dgv.HorizontalScrollingOffset;
//線を引く
e.Graphics.DrawLine(linePen,
startX, startY, endX, startY);
}
else
{
// ~罫線は描画しない、代わりに別の処理略~
}
// キーイベントを完了する
e.Handled = true;
}
これだと無限ループが発生して画面の動きがもっさりしだした。
コンソール出力したら延々垂れ流しで焦った。
調べてみた
マイクロソフトさん家では下記のように解説している
セルが描画されなければならないときに発生します。
参照元:https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.datagridview.cellpainting?view=netframework-4.8
簡素。
11時間労働が続く日の中で調べ上げたのがこちら。
CellPaintingイベントは、DataGridViewに1mmでも触れれば発生して描画しにかかるとっても繊細なイベント(イベントメソッド内にコンソール仕込んで、触ってみるとわかると思う)
描画イベントが発生するタイミングは大体こんな感じ。
- 画面表示時
- 表示する値が1コでも変更(DataGridViewが保持している内部の値が書き換わったのを検知したとき、バインド未完了の状態)
- DataGridViewの見た目が変わる(セルの幅や高さが変わったときなど)
- ヘッダーにマウスカーソルが触れる(ちょっと曖昧、発火した気がする)
- セルをクリックしたり方向キーで選択する(見た目が変わるとかぶる)
- 再描画メソッド呼び出したあと
さらにCellPaintingメソッドは、引数のeからセルの行列数が取得できる。
これは左上(-1,0)から右下へ(XX,XX)とセル一つずつ描画していくため。
ちなみに-1行目はヘッダーで、0行目からデータ表示部になる。
上記の内容から見ると・・・
- CellPaintingイベント発生
- 罫線の描画を行う
- 描画発生を検知する
- 1に戻る
こうなってしまうっぽい。
コンソール出力するとこういう感じで無限ループになる。
// 描画開始
(-1,0)
(-1,1)
~~~
(99,99)
// ここですべてのセルを描画し終わった・・・
// と思いきや描画開始
(-1,0)
(-1,1)
~~~
(99,99)
// ここですべてのセルを描画し終わった・・・
// と思いきや無限ループ
すべてのセルに対して描画処理を行ったあとにまた1から描画し直すということになる。
どう修正したか
RowPrePaintイベントを使用して、CellPaintingイベントから切り離した。
例によってマイクロソフトさん家の解説が簡素なので省略、DOBON.NETさん家の解説を引用
RowPrePaintイベントは行が描画される前に発生
参照元:https://dobon.net/vb/dotnet/datagridview/rowpostpaint.html
CellPaintingイベントはセル単位なのに対して、RowPostPaintイベントは行単位になるため引数のeでは行列は取得できない。
下記コードは特定の列まで行罫線なし、特定列以降最後まで行罫線を描画する
private void DgvRowPrePaint(object sender, DataGridViewRowPrePaintEventArgs e)
{
// 線描画位置を計算
Pen linePen = Pens.DarkGray;
int lineAllStartX = dgv.RowHeadersVisible ? dgv.RowHeadersWidth : 0;
int lineStartX = 1;
int lineStartY = e.RowBounds.Top - 1;
int lineEndX = lineAllStartX + dgv.Columns.GetColumnsWidth(DataGridViewElementStates.Visible) - dgv.HorizontalScrollingOffset;
foreach (DataGridViewColumn col in dgv.Columns)
{
// セル合計幅を取得
if (col.Index < 6)
{
lineStartX += col.Width;
}
}
// 罫線を描画する
if (dgv.Rows[e.RowIndex].Cells["XXX"].Value.ToString() == "XXX")
{
e.Graphics.DrawLine(linePen, lineAllStartX, lineStartY, lineEndX, lineStartY);
}
}
これで無限ループはしなくなった。
余談
完全な解決ではなかったことを実装後すぐに気づいて泣きそうになった。
クリックバチバチしまくると罫線が消えたり出たりする。
CellPaintingイベント内でデフォルトで罫線を一旦消す処理をしていて、そっちが勝っちゃうことがあった。
クリックイベントとダブルクリックイベントに再描画メソッド付けて無理くり直した。
おわり。