トップ回答者
描画した DataGridView の ColumnHeader をちらつかせずに安定して表示したい

質問
-
- Windows Forms Data Controls and Databinding FAQ - MSDN Forums
を参考に ColumnHeader が 2行の DataGridView を作ってみました。
参考画像 (1)
これをスクロールさせると、Paint で描画している 1行目のヘッダーが再描画
されずに消えたり、描画しているセルで見えなくしている縦境界線が見えたり
します。
参考画像 (2) スクロール後に "A" が消えていたり、ヘッダー1行目で消しているはずの縦境界線が見えている
これをどんなにスクロールしようが、安定して描画できるようにするにはどう
したらよいのでしょう?
ダウンロード : ソースp.s.
----
DataGridView の右上がちょっと上書きされているのも気になります。
回答
-
# custar さんのソースは見られていません。
# 下記のコードは、custar さんのリンク先 FAQ のコードに対する変更になります。GetCellDisplayRectangle メソッドは「見えている領域」を戻しますので、項目の一部が隠れている場合、リンク先のコードでは正しく動作しないですね。
例えば "A-a" の一部がスクロールアウトしている場合、「見えている狭い幅×2」の領域の中心に "A" が描画されるため、描画される位置が定まらず、右側には前に描画された文字列の残像が残ってしまいます。
また、もし "A-a" がすべて隠れていれば GetCellDisplayRectangle は Empty を戻すため、この場合は全く描画されません。これへの対処として、GetCellDisplayRectangle の第3引数 cutOverflow を false にすれば、セルの右側が隠れていてもそれを含めて戻してくれるようになります。
しかし false を指定した場合でも、隠れた左側の部分については含めてくれないため、今回の場合はこの指定だけでは不都合でした(これは引数名の通りなので仕様ですね)。
そのため下記コードでは少し変なコードで領域全体を取得しています(もっとよい方法がありそうに思います)。
# 見えている領域での中心に描画するという仕様にする手もありますね。それとリンク先のコードでは、マージする項目は等幅の2列と決めているため、幅を2倍する箇所がありますが、custar さんの例では3列をマージされているため、コード内の×2の部分を×3に変更する必要があります(見られていないので推測ですが。追記:参考画像1を見る限りこの点は問題なさそうですね)。
等幅とは限らない場合は、幅の合計を求めると良いと思います(下記コードはそうしています)。最後に、右上の枠線が消えている件ですが、DataGridView では枠線が描画された後に Paint イベントが発生します。
そのため、Paint では、枠線を消さないように描画する必要があります。
下記コードでは、枠線の内側の領域にクリッピングすることで対処しています。リンク先 FAQ のコードは「たたき台」だと思いますし、下記コードもそうです(私のは手抜き)。
マージする列の持ち方や、グリッドの各種プロパティの値(DividerWidth など)に対処した汎用的なものができたら、是非公開なさってください(^^;Code Snippetprivate void dataGridView1_Paint(object sender, PaintEventArgs e)
{
string[] monthes = { "January", "February", "March" };using (Region clip = e.Graphics.Clip)
{
// 外枠が消えないようにクリップ
// (本来は BorderStyle を考慮する必要がある)
Rectangle r = dataGridView1.ClientRectangle;
r.Inflate(-1, -1);
e.Graphics.Clip = new Region(r);for (int j = 0; j < 6; )
{
// [Win] と [Loss] の見えてる方の項目から描画する矩形を割り出す
Rectangle r1 = dataGridView1.GetCellDisplayRectangle(j, -1, false);
if (r1 != Rectangle.Empty)
{
r1.X = r1.Right - dataGridView1.Columns[j].Width;
}
else
{
r1 = dataGridView1.GetCellDisplayRectangle(j + 1, -1, false);
r1.X = r1.Right
- dataGridView1.Columns[j + 1].Width
- dataGridView1.Columns[j].Width;
}
r1.Width = dataGridView1.Columns[j].Width
+ dataGridView1.Columns[j + 1].Width - 2;r1.Y += 2; // ← ここも +2 に変更
r1.Height = r1.Height / 2 - 2;e.Graphics.FillRectangle(
new SolidBrush(dataGridView1.ColumnHeadersDefaultCellStyle.BackColor), r1);// ヘッダの1行目の文字列を描画
StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;e.Graphics.DrawString(
monthes[j / 2],
dataGridView1.ColumnHeadersDefaultCellStyle.Font,
new SolidBrush(dataGridView1.ColumnHeadersDefaultCellStyle.ForeColor),
r1,
format);j += 2;
}e.Graphics.Clip = clip;
}
}private void dataGridView1_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e)
{
// Form1_Load へ追加
//dataGridView1.ColumnWidthChanged +=
// new DataGridViewColumnEventHandler(dataGridView1_ColumnWidthChanged);// 偶数列の幅変更時には、奇数列のヘッダも再描画させる
if (e.Column.Index % 2 != 0)
{
Rectangle r = dataGridView1.GetCellDisplayRectangle(e.Column.Index - 1, -1, true);
dataGridView1.Invalidate(r);
}
}
すべての返信
-
# custar さんのソースは見られていません。
# 下記のコードは、custar さんのリンク先 FAQ のコードに対する変更になります。GetCellDisplayRectangle メソッドは「見えている領域」を戻しますので、項目の一部が隠れている場合、リンク先のコードでは正しく動作しないですね。
例えば "A-a" の一部がスクロールアウトしている場合、「見えている狭い幅×2」の領域の中心に "A" が描画されるため、描画される位置が定まらず、右側には前に描画された文字列の残像が残ってしまいます。
また、もし "A-a" がすべて隠れていれば GetCellDisplayRectangle は Empty を戻すため、この場合は全く描画されません。これへの対処として、GetCellDisplayRectangle の第3引数 cutOverflow を false にすれば、セルの右側が隠れていてもそれを含めて戻してくれるようになります。
しかし false を指定した場合でも、隠れた左側の部分については含めてくれないため、今回の場合はこの指定だけでは不都合でした(これは引数名の通りなので仕様ですね)。
そのため下記コードでは少し変なコードで領域全体を取得しています(もっとよい方法がありそうに思います)。
# 見えている領域での中心に描画するという仕様にする手もありますね。それとリンク先のコードでは、マージする項目は等幅の2列と決めているため、幅を2倍する箇所がありますが、custar さんの例では3列をマージされているため、コード内の×2の部分を×3に変更する必要があります(見られていないので推測ですが。追記:参考画像1を見る限りこの点は問題なさそうですね)。
等幅とは限らない場合は、幅の合計を求めると良いと思います(下記コードはそうしています)。最後に、右上の枠線が消えている件ですが、DataGridView では枠線が描画された後に Paint イベントが発生します。
そのため、Paint では、枠線を消さないように描画する必要があります。
下記コードでは、枠線の内側の領域にクリッピングすることで対処しています。リンク先 FAQ のコードは「たたき台」だと思いますし、下記コードもそうです(私のは手抜き)。
マージする列の持ち方や、グリッドの各種プロパティの値(DividerWidth など)に対処した汎用的なものができたら、是非公開なさってください(^^;Code Snippetprivate void dataGridView1_Paint(object sender, PaintEventArgs e)
{
string[] monthes = { "January", "February", "March" };using (Region clip = e.Graphics.Clip)
{
// 外枠が消えないようにクリップ
// (本来は BorderStyle を考慮する必要がある)
Rectangle r = dataGridView1.ClientRectangle;
r.Inflate(-1, -1);
e.Graphics.Clip = new Region(r);for (int j = 0; j < 6; )
{
// [Win] と [Loss] の見えてる方の項目から描画する矩形を割り出す
Rectangle r1 = dataGridView1.GetCellDisplayRectangle(j, -1, false);
if (r1 != Rectangle.Empty)
{
r1.X = r1.Right - dataGridView1.Columns[j].Width;
}
else
{
r1 = dataGridView1.GetCellDisplayRectangle(j + 1, -1, false);
r1.X = r1.Right
- dataGridView1.Columns[j + 1].Width
- dataGridView1.Columns[j].Width;
}
r1.Width = dataGridView1.Columns[j].Width
+ dataGridView1.Columns[j + 1].Width - 2;r1.Y += 2; // ← ここも +2 に変更
r1.Height = r1.Height / 2 - 2;e.Graphics.FillRectangle(
new SolidBrush(dataGridView1.ColumnHeadersDefaultCellStyle.BackColor), r1);// ヘッダの1行目の文字列を描画
StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;e.Graphics.DrawString(
monthes[j / 2],
dataGridView1.ColumnHeadersDefaultCellStyle.Font,
new SolidBrush(dataGridView1.ColumnHeadersDefaultCellStyle.ForeColor),
r1,
format);j += 2;
}e.Graphics.Clip = clip;
}
}private void dataGridView1_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e)
{
// Form1_Load へ追加
//dataGridView1.ColumnWidthChanged +=
// new DataGridViewColumnEventHandler(dataGridView1_ColumnWidthChanged);// 偶数列の幅変更時には、奇数列のヘッダも再描画させる
if (e.Column.Index % 2 != 0)
{
Rectangle r = dataGridView1.GetCellDisplayRectangle(e.Column.Index - 1, -1, true);
dataGridView1.Invalidate(r);
}
} -
確かに私のコードには問題がありますね。
仮想画面を使用しなくても、最小化してから元に戻したりしても表示はおかしくなります。「自力で」とは書かれていますが、おせっかいさせてください。
# これも汚いコードですが…Code Snippet//Rectangle r = dataGridView1.ClientRectangle;
//r.Inflate(-1, -1);
//e.Graphics.Clip = new Region(r);// ↓に変更
Rectangle r = Rectangle.Empty;
for (int j = 0; j < dataGridView1.Columns.Count; j++)
{
Rectangle rh = dataGridView1.GetCellDisplayRectangle(j, -1, true);
if (rh != Rectangle.Empty)
{
if (r == Rectangle.Empty)
r = rh;
else
r = Rectangle.Union(r, rh);
}
}
e.Graphics.Clip = new Region(r);それと、custar さんのスクリーンショットでは、Loss と Win の間の線が1ピクセル欠けてしまっているようですね。以下のコードを適切な位置に入れてみてください。
# そういえば元コードも +1 してますね。こちらでは大丈夫なのですが。
r1.X += 1; r1.Width -= 1;