トップ回答者
DataGridViewの高速化

質問
-
現在、連続してデータを別のハードウェアから取り込んで随時DataGridViewに表示させているのですが
表示やスクロールが遅いのか目標のデータ取得件数(一定時間内)に達しません。
セルへの代入は
dataGridView1[*,*] = ****; //! セルへの代入
dataGridView1.CurrentCell = dataGridView1[*.*]; //! 次のセル選択
という方法で行っています。
試しにDataGridViewへの表示を抜いてみると達成できます。
DataGridView表示の高速化を行う方法はないのでしょうか。
まだ.NETを始めて半年も経たないので標準的?なやり方しかわかりません。
よろしくお願いします。
回答
-
> 非バインドモードなら dataGridView[*,*] = ****; と視覚的にわかりやすいのですが
DataGridVirew は、名前のとおり、本来RDBMS 等のデータを表示するものですからねぇ。
EXCEL みたいな使い方をするコントロールじゃありませんよ。
むしろ非バインドモードでセルに対して直接値を設定したいなら、先の記事にあったとおり、リストビューの方がいいかも知れません。
またデータ挿入ですが、
1.DataTable 作成
2.DataTable のインスタンスに列を追加
3.DataTable に行を追加
でいけると思いますが。どうしても行列指定して後からデータを設定したいなら
リンク先の記事にあるように空のテーブルを作っておいて、
dt.Rows[0][5] = 120.ToString();
というような感じで後から値を設定してもいいかと思います。- 回答としてマーク shimpo 2010年3月8日 0:05
-
> DataTableをバインドした後は上記のような形で値設定すればよいと解釈したのですが、
> なぜか表示が全くされませんでした。
DataSource の設定までに、ColumnCount や RowCount を設定されていることが原因です。
試してみると、DataTable の値は右の方に表示されてました。
この場合は、DataSource への代入の直前で、ColumnCount および RowCount をゼロにされると良さそうです。
バインドする方向で話は進んでいますが、
今回の遅いことの一番の原因は、次のコードが足を引っ張っているからではないでしょうか?
もしそうでしたら、バインドするように変更されたとしても、同様に最新データを CurrentCell にする必要があるのでしたら、結局遅くなってしまうかもしれません。
> dataGridView1.CurrentCell = dataGridView1[*.*]; //! 次のセル選択
要求される速度次第ですが、試しに現状の非バインドのまま、以下のように CurrentCell の設定を間引くことで、要求を満たすようにはならないでしょうか?
(DelaySetCurrentCell はもっとスマートにできればいいですが、とても汚いコードになりました。)
private void button1_Click(object sender, EventArgs e)
{
var grid = new DataGridView();
this.Controls.Add(grid);
grid.ColumnCount = 100;
grid.RowCount = 10000;
for (var colIndex = 0; colIndex < grid.ColumnCount; colIndex++)
{
for (var rowIndex = 0; rowIndex < grid.RowCount; rowIndex++)
{
grid[colIndex, rowIndex].Value = new string('x', 100);
//grid.CurrentCell = grid[colIndex, rowIndex];
DelaySetCurrentCell(grid[colIndex, rowIndex]);
}
}
}
private System.Timers.Timer _delayTimer;
private DataGridViewCell _delayCell;
private DataGridViewCell _delayCurrentOrigin;
private bool _delayDoEvents;
private void DelaySetCurrentCell(DataGridViewCell cell)
{
if (_delayTimer == null)
{
_delayTimer = new System.Timers.Timer();
_delayTimer.Interval = 200;
_delayTimer.AutoReset = false;
_delayTimer.Elapsed +=
(s, e) =>
{
_delayDoEvents = true;
this.BeginInvoke(new MethodInvoker(() =>
{
if (_delayCell == null) return;
if (_delayCurrentOrigin != _delayCell.DataGridView.CurrentCell) return;
try
{
// CurrentCell の変化が遅延されるため、
// CurrentCell に依存するコードがあれば問題が生じます。
_delayCell.DataGridView.CurrentCell = _delayCell;
}
catch { }
}));
};
}
_delayCell = cell;
if (cell != null) _delayCurrentOrigin = cell.DataGridView.CurrentCell;
_delayTimer.Enabled = true;
if (_delayDoEvents)
{
_delayDoEvents = false;
// 処理中に別のボタンが押せてしまうなど、弊害が生じるかもしれません。
Application.DoEvents();
}
}
ところで、非バインドが「極めて低速」というのは少し大げさかなーと思いました。パフォーマンスが良くないことには違いないですけど。
また、アプリケーション仕様でしょうから仕方がない話ですが、「行→列」という順番での値の設定は、逆の場合よりはパフォーマンスが落ちてしまいますね。 -
> 現在やっている内容は行列数の決まっているExcelのような空のワークシートを出しておいて
> 取得したデータを縦方向にセルに挿入していき、最後の行まで来たら次の列の最初の行に移ってデータ挿入を続けるという物です。
列ごとに設定するというのは仕様でしょうか?
もしそうでないなら、以下のように行挿入時に値を設定するという方法があります。この方がパフォーマンスは速そうです。
ちなみに当方の環境では、以下のテストコードで、10列・10万行を設定するのに平均タイム約1.5秒でした。
private void button1_Click(object sender, EventArgs e){ // パフォーマンステスト用に Stopwatch クラスを使っている。 System.Diagnostics.Stopwatch wacth = new Stopwatch(); wacth.Start(); DataTable dt = new DataTable(); // 10列追加 for ( int a = 0; a < 10; ++a ) { dt.Columns.Add(); } // 10万行追加 for (int a = 0; a < 100000; ++a) {
あと
// 行追加と同時に値を設定する dt.Rows.Add(new object[]{1,2,3,4,5,6,7,8,9,0}); } dataGridView1.DataSource = dt; // 処理時間をミリ秒で表示 wacth.Stop(); MessageBox.Show(wacth.ElapsedMilliseconds.ToString()); }
> DataTableをバインドした後は上記のような形で値設定すればよいと解釈したのですが、なぜか表示が全くされませんでした。
先に DataGridView で列の設定をしているからだと思います。何も設定していない DataGridView なら表示できました。
もっともそれは現実的ではないでしょうから、以下のように名前をつけて列を生成、
また DataGridView の「列の編集」で 「DataPropertyName」 にも列名を設定し、双方をバインドさせるといいかと思います。
private void button1_Click(object sender, EventArgs e){ System.Diagnostics.Stopwatch wacth = new Stopwatch(); wacth.Start(); DataTable dt = new DataTable(); // 10列追加 dt.Columns.Add(new DataColumn("1列目")); dt.Columns.Add(new DataColumn("2列目")); dt.Columns.Add(new DataColumn("3列目")); dt.Columns.Add(new DataColumn("4列目")); dt.Columns.Add(new DataColumn("5列目")); dt.Columns.Add(new DataColumn("6列目")); dt.Columns.Add(new DataColumn("7列目")); dt.Columns.Add(new DataColumn("8列目")); dt.Columns.Add(new DataColumn("9列目")); dt.Columns.Add(new DataColumn("10列目")); // 10万行追加 for (int a = 0; a < 100000; ++a) { dt.Rows.Add(new object[]{1,2,3,4,5,6,7,8,9,0}); } dataGridView1.DataSource = dt; wacth.Stop(); MessageBox.Show(wacth.ElapsedMilliseconds.ToString()); }
あと TH01さん
> 非バインドが「極めて低速」というのは少し大げさかなーと思いました。
そうですね。確かに言い方に御幣がありました。
誤解を招きかねないので 「バインドするのに比べて非常に低速」 という表現に改めます。失礼いたしました。<(_ _)>- 回答としてマーク shimpo 2010年2月5日 1:50
-
//! 各行の表題
mGridView.RowHeadersWidth = 60;
for ( int a = 0; a < mGridView.RowCount; ++a ) {
mGridView.Rows[a].HeaderCell.Value = ( a + 1 ).ToString();
}
つまり仕様としてはまんまExcelのように行ヘッダ、列ヘッダに番号が必要なんです。
ちなみにROW_MAXは10000、COLM_MAXは100となっています。
上記コードでボタンクリックイベント内で行番号設定するよりも、
DataGridView.CellPainting イベントで行う方が断然速いです。私の環境で約2秒も違いました。/// <summary> /// ボタンクリック時のイベントハンドラ /// </summary> private void button1_Click(object sender, EventArgs e) { // パフォーマンステスト用に Stopwatch クラスを使っている。 System.Diagnostics.Stopwatch wacth = new System.Diagnostics.Stopwatch(); wacth.Start(); DataTable dt = new DataTable(); // 10列追加 for ( int a = 0; a < 100; ++a ) { dt.Columns.Add(); } // 10万行追加 for (int a = 0; a < 10000; ++a) { // 行追加と同時に値を設定する dt.Rows.Add(new object[]{1,2,3,4,5,6,7,8,9,0}); } dataGridView1.DataSource = dt; //for (int i = 0; i < 10000; i++) { // dataGridView1.Rows[i].HeaderCell.Value = i + 1; //} // 処理時間をミリ秒で表示 wacth.Stop(); MessageBox.Show(wacth.ElapsedMilliseconds.ToString()); } /// <summary> /// セル描画時のイベントハンドラ /// </summary> private void dataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e) { //列ヘッダーかどうか調べる if (e.ColumnIndex < 0 && e.RowIndex >= 0) { //セルを描画する e.Paint(e.ClipBounds, DataGridViewPaintParts.All); //行番号を描画する範囲を決定する //e.AdvancedBorderStyleやe.CellStyle.Paddingは無視しています Rectangle indexRect = e.CellBounds; indexRect.Inflate(-2, -2); //行番号を描画する TextRenderer.DrawText(e.Graphics, (e.RowIndex + 1).ToString(), e.CellStyle.Font, indexRect, e.CellStyle.ForeColor, TextFormatFlags.Right | TextFormatFlags.VerticalCenter); //描画が完了したことを知らせる e.Handled = true; } }
メソッド内で for 文で行番号設定 → 約3秒
DataGridView.CellPainting イベント → 870ミリ秒
以下を参考にしてください。
http://dobon.net/vb/dotnet/datagridview/drawrownumber.html- 編集済み ひらぽんModerator 2010年2月5日 1:40 コードを追加した
- 回答としてマーク shimpo 2010年2月5日 1:52
-
> 試してみた所、1つ挿入が多くなるのが直っていました。
あれ。
変更内容は、Delay~ の呼び出し時点から CurrentCell が実際に行われるまでに、カレントが変化している場合を考慮することなのですが、その不具合には影響しないと考えていました。
多くならなかったのは、たまたまかもしれません。
> 今まで取り込み処理はバックグランドワーカーで行い、ひたすら取り込んでセルへの代入処理
> を促すメッセージをPostMessageしてました。
すでに BackgroundWorker を使用されていたのですね。
BackgroundWorker を使用されている上でさらに Delay~ を行うのは、回りくどい状態になります。
ただ、試しに BackgroundWorker では DataTable に格納するだけとし、表示はバインドに任せ、ある間隔で ReportProgress を行うことで選択セルを反映するようにしてみたのですが、よいパフォーマンスは得られませんでした。
バインドされた DataTable の全データを更新するのは結構遅く、これは別スレッドであっても同じですね。
理想的な方針としては次になると考えています。
・BackgroundWorker ではバインドしていない DataTable にデータを蓄えるだけにする。
・UIスレッドでは、自力で DataGridView に DataTable の内容を表示する。
こうすれば、データの取得と表示を分離できますので、表示が遅くて間に合わなくても、それにつられてデータ取得のレスポンスまで低下してしまうことは回避できると思われます。
DataGridView で自力でデータを表示する方法は、
・非バインドなので DataGridView では RowCount などを設定する。
・タイマー等で dataGridView1.Invalidate() を行う。
・CellFormatting イベントや VirtualMode を true にした場合の CellValueNeeded イベントハンドラで e.Value に表示する値を設定する。
のようなイメージになります。
以下にサンプルを作成してみました。
(長くなったので、ひらぽんさんをマネて、文字を小さくしてみました。)
ただ、現状の CurrentCell の設定目的が見栄えを良くすることでしたら、結局のところ、ひらぽんさんが書かれた通り、その仕様を見直すことが一番速いかもしれないとも思いました。// (上記のサンプルコード) const int ROW_MAX = 10000; const int COLM_MAX = 100; private DataGridView dataGridView1 = new DataGridView(); private System.Windows.Forms.Timer timer1 = new Timer(); private DataTable _table = new DataTable(); private BackgroundWorker backgroundWorker1 = new BackgroundWorker(); private object _lockObj = new object(); private int _lastColIndex; private int _lastRowIndex; private Stopwatch _stopwatch; private void Form1_Load(object sender, EventArgs e) { // 仮想モード(DataGridView.VirtualMode をヘルプで見てください) _lastRowIndex = -1; this.Controls.Add(dataGridView1); dataGridView1.Dock = DockStyle.Fill; dataGridView1.ReadOnly = true; dataGridView1.AllowUserToAddRows = false; dataGridView1.VirtualMode = true; dataGridView1.ColumnCount = COLM_MAX; dataGridView1.RowCount = ROW_MAX; dataGridView1.CellValueNeeded += new DataGridViewCellValueEventHandler(dataGridView1_CellValueNeeded); // DataGridView の表示更新間隔 timer1.Interval = 500; timer1.Tick += new EventHandler(_timer_Tick); timer1.Start(); // データの器の初期設定 for (var col = 1; col <= COLM_MAX; col++) _table.Columns.Add(col.ToString(), typeof(object)); for (var row = 0; row < ROW_MAX; row++) _table.Rows.Add(); _stopwatch = Stopwatch.StartNew(); // 取り込み開始 backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork); backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted); backgroundWorker1.RunWorkerAsync(); } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { for (var col = 0; col < COLM_MAX; col++) { for (var row = 0; row < ROW_MAX; row++) { _table.Rows[row][col] = col * ROW_MAX + row + 1; lock (_lockObj) { _lastRowIndex = row; _lastColIndex = col; } } } } private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { timer1.Stop(); UpdateGridValues(); _stopwatch.Stop(); MessageBox.Show(_stopwatch.ElapsedMilliseconds.ToString()); } private void _timer_Tick(object sender, EventArgs e) { if (!timer1.Enabled) return; UpdateGridValues(); } private void UpdateGridValues() { int lastRowIndex, lastColIndex; lock (_lockObj) { lastRowIndex = _lastRowIndex; lastColIndex = _lastColIndex; } if (lastRowIndex == -1) return; dataGridView1.CurrentCell = dataGridView1[lastColIndex, lastRowIndex]; dataGridView1.Invalidate(); } private void dataGridView1_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) { if (e.RowIndex == -1 || e.ColumnIndex == -1) return; e.Value = _table.Rows[e.RowIndex][e.ColumnIndex]; }
- 回答としてマーク shimpo 2010年2月9日 4:20
すべての返信
-
> DataGridView表示の高速化を行う方法はないのでしょうか。
> まだ.NETを始めて半年も経たないので標準的?なやり方しかわかりません。
それは標準ではない方法だと思います。
以前別の質問でも議題に上ったのですが、DataGridView に対し直接データを設定したり行追加を行うのは、極めて低速になります。
先に DataTable を作成して DataGridView にバインドした方が断然高速です。
DataGridView を使って単純な表を作りたい(後から値を設定したい)。- 編集済み ひらぽんModerator 2010年2月4日 6:19
-
> 非バインドモードなら dataGridView[*,*] = ****; と視覚的にわかりやすいのですが
DataGridVirew は、名前のとおり、本来RDBMS 等のデータを表示するものですからねぇ。
EXCEL みたいな使い方をするコントロールじゃありませんよ。
むしろ非バインドモードでセルに対して直接値を設定したいなら、先の記事にあったとおり、リストビューの方がいいかも知れません。
またデータ挿入ですが、
1.DataTable 作成
2.DataTable のインスタンスに列を追加
3.DataTable に行を追加
でいけると思いますが。どうしても行列指定して後からデータを設定したいなら
リンク先の記事にあるように空のテーブルを作っておいて、
dt.Rows[0][5] = 120.ToString();
というような感じで後から値を設定してもいいかと思います。- 回答としてマーク shimpo 2010年3月8日 0:05
-
現在やっている内容は行列数の決まっているExcelのような空のワークシートを出しておいて
取得したデータを縦方向にセルに挿入していき、最後の行まで来たら次の列の最初の行に移
ってデータ挿入を続けるという物です。
DataTableに以下のような感じで列、行を追加してます。
private DataTable Data = new DataTable(); //! データテーブル
for ( int a = 0; a < mGridView.ColumnCount; ++a ) {
Data.Columns.Add();
}
for ( int a = 0; a < mGridView.RowCount; ++a ) {
Data.Rows.Add();
}
mGridView.DataSource = Data; //! データソース割り当て
>dt.Rows[0][5] = 120.ToString();
DataTableをバインドした後は上記のような形で値設定すればよいと解釈したのですが、
なぜか表示が全くされませんでした。
何か根本的な事が理解できていないのだろうと思うのですが、全く初めてなので仕組みが
わからなくて困っています。 -
> DataTableをバインドした後は上記のような形で値設定すればよいと解釈したのですが、
> なぜか表示が全くされませんでした。
DataSource の設定までに、ColumnCount や RowCount を設定されていることが原因です。
試してみると、DataTable の値は右の方に表示されてました。
この場合は、DataSource への代入の直前で、ColumnCount および RowCount をゼロにされると良さそうです。
バインドする方向で話は進んでいますが、
今回の遅いことの一番の原因は、次のコードが足を引っ張っているからではないでしょうか?
もしそうでしたら、バインドするように変更されたとしても、同様に最新データを CurrentCell にする必要があるのでしたら、結局遅くなってしまうかもしれません。
> dataGridView1.CurrentCell = dataGridView1[*.*]; //! 次のセル選択
要求される速度次第ですが、試しに現状の非バインドのまま、以下のように CurrentCell の設定を間引くことで、要求を満たすようにはならないでしょうか?
(DelaySetCurrentCell はもっとスマートにできればいいですが、とても汚いコードになりました。)
private void button1_Click(object sender, EventArgs e)
{
var grid = new DataGridView();
this.Controls.Add(grid);
grid.ColumnCount = 100;
grid.RowCount = 10000;
for (var colIndex = 0; colIndex < grid.ColumnCount; colIndex++)
{
for (var rowIndex = 0; rowIndex < grid.RowCount; rowIndex++)
{
grid[colIndex, rowIndex].Value = new string('x', 100);
//grid.CurrentCell = grid[colIndex, rowIndex];
DelaySetCurrentCell(grid[colIndex, rowIndex]);
}
}
}
private System.Timers.Timer _delayTimer;
private DataGridViewCell _delayCell;
private DataGridViewCell _delayCurrentOrigin;
private bool _delayDoEvents;
private void DelaySetCurrentCell(DataGridViewCell cell)
{
if (_delayTimer == null)
{
_delayTimer = new System.Timers.Timer();
_delayTimer.Interval = 200;
_delayTimer.AutoReset = false;
_delayTimer.Elapsed +=
(s, e) =>
{
_delayDoEvents = true;
this.BeginInvoke(new MethodInvoker(() =>
{
if (_delayCell == null) return;
if (_delayCurrentOrigin != _delayCell.DataGridView.CurrentCell) return;
try
{
// CurrentCell の変化が遅延されるため、
// CurrentCell に依存するコードがあれば問題が生じます。
_delayCell.DataGridView.CurrentCell = _delayCell;
}
catch { }
}));
};
}
_delayCell = cell;
if (cell != null) _delayCurrentOrigin = cell.DataGridView.CurrentCell;
_delayTimer.Enabled = true;
if (_delayDoEvents)
{
_delayDoEvents = false;
// 処理中に別のボタンが押せてしまうなど、弊害が生じるかもしれません。
Application.DoEvents();
}
}
ところで、非バインドが「極めて低速」というのは少し大げさかなーと思いました。パフォーマンスが良くないことには違いないですけど。
また、アプリケーション仕様でしょうから仕方がない話ですが、「行→列」という順番での値の設定は、逆の場合よりはパフォーマンスが落ちてしまいますね。 -
> 現在やっている内容は行列数の決まっているExcelのような空のワークシートを出しておいて
> 取得したデータを縦方向にセルに挿入していき、最後の行まで来たら次の列の最初の行に移ってデータ挿入を続けるという物です。
列ごとに設定するというのは仕様でしょうか?
もしそうでないなら、以下のように行挿入時に値を設定するという方法があります。この方がパフォーマンスは速そうです。
ちなみに当方の環境では、以下のテストコードで、10列・10万行を設定するのに平均タイム約1.5秒でした。
private void button1_Click(object sender, EventArgs e){ // パフォーマンステスト用に Stopwatch クラスを使っている。 System.Diagnostics.Stopwatch wacth = new Stopwatch(); wacth.Start(); DataTable dt = new DataTable(); // 10列追加 for ( int a = 0; a < 10; ++a ) { dt.Columns.Add(); } // 10万行追加 for (int a = 0; a < 100000; ++a) {
あと
// 行追加と同時に値を設定する dt.Rows.Add(new object[]{1,2,3,4,5,6,7,8,9,0}); } dataGridView1.DataSource = dt; // 処理時間をミリ秒で表示 wacth.Stop(); MessageBox.Show(wacth.ElapsedMilliseconds.ToString()); }
> DataTableをバインドした後は上記のような形で値設定すればよいと解釈したのですが、なぜか表示が全くされませんでした。
先に DataGridView で列の設定をしているからだと思います。何も設定していない DataGridView なら表示できました。
もっともそれは現実的ではないでしょうから、以下のように名前をつけて列を生成、
また DataGridView の「列の編集」で 「DataPropertyName」 にも列名を設定し、双方をバインドさせるといいかと思います。
private void button1_Click(object sender, EventArgs e){ System.Diagnostics.Stopwatch wacth = new Stopwatch(); wacth.Start(); DataTable dt = new DataTable(); // 10列追加 dt.Columns.Add(new DataColumn("1列目")); dt.Columns.Add(new DataColumn("2列目")); dt.Columns.Add(new DataColumn("3列目")); dt.Columns.Add(new DataColumn("4列目")); dt.Columns.Add(new DataColumn("5列目")); dt.Columns.Add(new DataColumn("6列目")); dt.Columns.Add(new DataColumn("7列目")); dt.Columns.Add(new DataColumn("8列目")); dt.Columns.Add(new DataColumn("9列目")); dt.Columns.Add(new DataColumn("10列目")); // 10万行追加 for (int a = 0; a < 100000; ++a) { dt.Rows.Add(new object[]{1,2,3,4,5,6,7,8,9,0}); } dataGridView1.DataSource = dt; wacth.Stop(); MessageBox.Show(wacth.ElapsedMilliseconds.ToString()); }
あと TH01さん
> 非バインドが「極めて低速」というのは少し大げさかなーと思いました。
そうですね。確かに言い方に御幣がありました。
誤解を招きかねないので 「バインドするのに比べて非常に低速」 という表現に改めます。失礼いたしました。<(_ _)>- 回答としてマーク shimpo 2010年2月5日 1:50
-
おっしゃっているやり方(行の追加は少し違いますけど)で表示はできました。
現在の初期化コードは以下のようになっています。
//! データテーブルを作成する
for ( int a = 0; a < COLM_MAX; ++a ) {
Data.Columns.Add( new DataColumn( ( a + 1 ).ToString() ) );
}
for ( int a = 0; a < ROW_MAX; ++a ) {
Data.Rows.Add();
}
mGridView.DataSource = Data; //! データソース割り当て//! 各列の設定
for ( int a = 0; a < mGridView.ColumnCount; ++a ) {
mGridView.Columns[a].Width = 60;
}
//! 各行の表題
mGridView.RowHeadersWidth = 60;
for ( int a = 0; a < mGridView.RowCount; ++a ) {
mGridView.Rows[a].HeaderCell.Value = ( a + 1 ).ToString();
}
つまり仕様としてはまんまExcelのように行ヘッダ、列ヘッダに番号が必要なんです。
ちなみにROW_MAXは10000、COLM_MAXは100となっています。
ところが、もたもや目標速度には達成しませんでした。と、いうより見た目には
全く速度は変わっていません。
> dataGridView1.CurrentCell = dataGridView1[*.*]; //! 次のセル選択
TH01さんがおっしゃっているように、やはりこれが尾を引いているようです。
非バインドモードでも上記コードを抜くと目標速度に達成しました。
バインドモードの場合でもExcelのようにセルに値を設定したら次のセルを選択
しておかなければならないのは仕様なのですが、バインドモードとセル選択は
別なのでどうしようもないのでしょうか。 -
//! 各行の表題
mGridView.RowHeadersWidth = 60;
for ( int a = 0; a < mGridView.RowCount; ++a ) {
mGridView.Rows[a].HeaderCell.Value = ( a + 1 ).ToString();
}
つまり仕様としてはまんまExcelのように行ヘッダ、列ヘッダに番号が必要なんです。
ちなみにROW_MAXは10000、COLM_MAXは100となっています。
上記コードでボタンクリックイベント内で行番号設定するよりも、
DataGridView.CellPainting イベントで行う方が断然速いです。私の環境で約2秒も違いました。/// <summary> /// ボタンクリック時のイベントハンドラ /// </summary> private void button1_Click(object sender, EventArgs e) { // パフォーマンステスト用に Stopwatch クラスを使っている。 System.Diagnostics.Stopwatch wacth = new System.Diagnostics.Stopwatch(); wacth.Start(); DataTable dt = new DataTable(); // 10列追加 for ( int a = 0; a < 100; ++a ) { dt.Columns.Add(); } // 10万行追加 for (int a = 0; a < 10000; ++a) { // 行追加と同時に値を設定する dt.Rows.Add(new object[]{1,2,3,4,5,6,7,8,9,0}); } dataGridView1.DataSource = dt; //for (int i = 0; i < 10000; i++) { // dataGridView1.Rows[i].HeaderCell.Value = i + 1; //} // 処理時間をミリ秒で表示 wacth.Stop(); MessageBox.Show(wacth.ElapsedMilliseconds.ToString()); } /// <summary> /// セル描画時のイベントハンドラ /// </summary> private void dataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e) { //列ヘッダーかどうか調べる if (e.ColumnIndex < 0 && e.RowIndex >= 0) { //セルを描画する e.Paint(e.ClipBounds, DataGridViewPaintParts.All); //行番号を描画する範囲を決定する //e.AdvancedBorderStyleやe.CellStyle.Paddingは無視しています Rectangle indexRect = e.CellBounds; indexRect.Inflate(-2, -2); //行番号を描画する TextRenderer.DrawText(e.Graphics, (e.RowIndex + 1).ToString(), e.CellStyle.Font, indexRect, e.CellStyle.ForeColor, TextFormatFlags.Right | TextFormatFlags.VerticalCenter); //描画が完了したことを知らせる e.Handled = true; } }
メソッド内で for 文で行番号設定 → 約3秒
DataGridView.CellPainting イベント → 870ミリ秒
以下を参考にしてください。
http://dobon.net/vb/dotnet/datagridview/drawrownumber.html- 編集済み ひらぽんModerator 2010年2月5日 1:40 コードを追加した
- 回答としてマーク shimpo 2010年2月5日 1:52
-
shimpo さん
> この方法やってみたのですが、結果は同じでした。体感的にも変わりなく
そうですか。試していただいてありがとうございました。
体感的に変わらないということは、状況を把握できてないところが私にあるようです。
(上の私のコードの場合では、grid.CurrentCell に代入する場合と DelaySetCurrentCell を行う場合とでは、shimpo さんの環境でももちろん変わりますよね?)
あと、_delayFlag は転記ミスで、_delayDoEvents のことでした。失礼しました。
修正しました。
ひらぽん さん
> 10列・10万行を設定するのに平均タイム約1.5秒でした。
非バインドの場合は 10秒ほどですので、やっぱり遅いですね。
(非バインドの遅くなる理由は値のコピーと非共有ぐらいで、そんなに速度は変わらないと思ったのですが、それらは結構コスト高なんですね。)
CurrentCell で解決すれば、その後にバインドへの変更をお勧めしようと思っていました。
shimpo さん
> > dataGridView1.CurrentCell = dataGridView1[*.*]; //! 次のセル選択
> TH01さんがおっしゃっているように、やはりこれが尾を引いているようです。
> 非バインドモードでも上記コードを抜くと目標速度に達成しました。
すると CurrentCell を間引く方針は有効だと思うので DelaySetCurrentCell で改善されるはずですが、
_delayTimer.Interval = 200;
という行の 200(値はミリ秒です)という値をもう少し大きくすると変わらないでしょうか?
> バインドモードの場合でもExcelのようにセルに値を設定したら次のセルを選択
> しておかなければならないのは仕様なのですが、バインドモードとセル選択は
> 別なのでどうしようもないのでしょうか。
選択する目的次第ですが、毎回しないようにするしかないと思います。
DelaySetCurrentCell は連続データの読み込み中にも位置が反映されるようにしましたが、データの読み込みがある単位で発生するのでしたら、その最後に1回だけ CurrentCell の設定をされるといいのではと思います。 -
すると CurrentCell を間引く方針は有効だと思うので DelaySetCurrentCell で改善されるはずですが、
_delayTimer.Interval = 200;
という行の 200(値はミリ秒です)という値をもう少し大きくすると変わらないでしょうか?
> dataGridView1.CurrentCell = dataGridView1[*.*]; //! 次のセル選択
に戻すと正常なので何かDelaySetCurrentCellに問題があるような気がします。
C#に詳しくないのでどこがおかしいのかよくわかりませんが。。
ただ、DelaySetCurrentCellを使うとワークシートのスクロールが滑らかには
ならないので見栄えが悪いと営業に言われてしまいました(_ _) -
> この方法ですが、セルへの挿入が時々なぜか1つ多くなります。
副作用についての考察が足りませんでした。
私は CurrentCell の設定を単なるフォーカスされるセルの変更とだけ考えていましたが、もし CurrentCell に依存するコードが存在する場合には、そこに問題が生じます。
CurrentCell を参照する処理はありますでしょうか?
他に参考にされる方がいらっしゃるといけないので、念のために上の返信のコードに注釈を入れておきます。
(それと、1つ多くなる現象とは関係ありませんが、1点修正しました。)
私のコードに1つ多くなることに直接つながる原因は見当たりませんが、CurrentCell の遅延設定、もしくは DoEvents の影響である可能性はあります。
ただし速くするためには CurrentCell を変更しないことが必要な条件(それとバインドがベター)になりますので、その場合は CurrentCell に依存するコードも排除する必要があります。
要求を満たせないのでしたら、見栄えがよくても意味がないと思います。(^^;
(問題が発生するコードで間引くのでは、もっとダメですが・・・) -
試してみた所、1つ挿入が多くなるのが直っていました。
ディレイを100msにした所、見栄えもまだマシになりましたので、これで押そうかと思います。
ただ、それでも時々要求速度を達成できないので、試行錯誤しました。
今まで取り込み処理はバックグランドワーカーで行い、ひたすら取り込んでセルへの代入処理
を促すメッセージをPostMessageしてました。フォアグラウンド側ではメッセージを受けたらそ
のバッファのインデックスを参照して挿入というスレッド処理にしてました。
このPostMessageをやめてあまり正当ではないですが、取り込み開始処理内で下記のように
したら少しパフォーマンスが向上して大体は要求速度に到達しました。
while ( backGroundWorker.IsBusy ) {
Application.DoEvents();
if ( Index != pIndex ) { //! 取り込みバッファインデックスと処理インデックスが異なる
SetValue( pIndex ); //! セルへの代入処理
}
}
要求を満たせない方が問題なのは確かなので営業をなんとか説き伏せてみます(^^; -
> 試してみた所、1つ挿入が多くなるのが直っていました。
あれ。
変更内容は、Delay~ の呼び出し時点から CurrentCell が実際に行われるまでに、カレントが変化している場合を考慮することなのですが、その不具合には影響しないと考えていました。
多くならなかったのは、たまたまかもしれません。
> 今まで取り込み処理はバックグランドワーカーで行い、ひたすら取り込んでセルへの代入処理
> を促すメッセージをPostMessageしてました。
すでに BackgroundWorker を使用されていたのですね。
BackgroundWorker を使用されている上でさらに Delay~ を行うのは、回りくどい状態になります。
ただ、試しに BackgroundWorker では DataTable に格納するだけとし、表示はバインドに任せ、ある間隔で ReportProgress を行うことで選択セルを反映するようにしてみたのですが、よいパフォーマンスは得られませんでした。
バインドされた DataTable の全データを更新するのは結構遅く、これは別スレッドであっても同じですね。
理想的な方針としては次になると考えています。
・BackgroundWorker ではバインドしていない DataTable にデータを蓄えるだけにする。
・UIスレッドでは、自力で DataGridView に DataTable の内容を表示する。
こうすれば、データの取得と表示を分離できますので、表示が遅くて間に合わなくても、それにつられてデータ取得のレスポンスまで低下してしまうことは回避できると思われます。
DataGridView で自力でデータを表示する方法は、
・非バインドなので DataGridView では RowCount などを設定する。
・タイマー等で dataGridView1.Invalidate() を行う。
・CellFormatting イベントや VirtualMode を true にした場合の CellValueNeeded イベントハンドラで e.Value に表示する値を設定する。
のようなイメージになります。
以下にサンプルを作成してみました。
(長くなったので、ひらぽんさんをマネて、文字を小さくしてみました。)
ただ、現状の CurrentCell の設定目的が見栄えを良くすることでしたら、結局のところ、ひらぽんさんが書かれた通り、その仕様を見直すことが一番速いかもしれないとも思いました。// (上記のサンプルコード) const int ROW_MAX = 10000; const int COLM_MAX = 100; private DataGridView dataGridView1 = new DataGridView(); private System.Windows.Forms.Timer timer1 = new Timer(); private DataTable _table = new DataTable(); private BackgroundWorker backgroundWorker1 = new BackgroundWorker(); private object _lockObj = new object(); private int _lastColIndex; private int _lastRowIndex; private Stopwatch _stopwatch; private void Form1_Load(object sender, EventArgs e) { // 仮想モード(DataGridView.VirtualMode をヘルプで見てください) _lastRowIndex = -1; this.Controls.Add(dataGridView1); dataGridView1.Dock = DockStyle.Fill; dataGridView1.ReadOnly = true; dataGridView1.AllowUserToAddRows = false; dataGridView1.VirtualMode = true; dataGridView1.ColumnCount = COLM_MAX; dataGridView1.RowCount = ROW_MAX; dataGridView1.CellValueNeeded += new DataGridViewCellValueEventHandler(dataGridView1_CellValueNeeded); // DataGridView の表示更新間隔 timer1.Interval = 500; timer1.Tick += new EventHandler(_timer_Tick); timer1.Start(); // データの器の初期設定 for (var col = 1; col <= COLM_MAX; col++) _table.Columns.Add(col.ToString(), typeof(object)); for (var row = 0; row < ROW_MAX; row++) _table.Rows.Add(); _stopwatch = Stopwatch.StartNew(); // 取り込み開始 backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork); backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted); backgroundWorker1.RunWorkerAsync(); } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { for (var col = 0; col < COLM_MAX; col++) { for (var row = 0; row < ROW_MAX; row++) { _table.Rows[row][col] = col * ROW_MAX + row + 1; lock (_lockObj) { _lastRowIndex = row; _lastColIndex = col; } } } } private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { timer1.Stop(); UpdateGridValues(); _stopwatch.Stop(); MessageBox.Show(_stopwatch.ElapsedMilliseconds.ToString()); } private void _timer_Tick(object sender, EventArgs e) { if (!timer1.Enabled) return; UpdateGridValues(); } private void UpdateGridValues() { int lastRowIndex, lastColIndex; lock (_lockObj) { lastRowIndex = _lastRowIndex; lastColIndex = _lastColIndex; } if (lastRowIndex == -1) return; dataGridView1.CurrentCell = dataGridView1[lastColIndex, lastRowIndex]; dataGridView1.Invalidate(); } private void dataGridView1_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) { if (e.RowIndex == -1 || e.ColumnIndex == -1) return; e.Value = _table.Rows[e.RowIndex][e.ColumnIndex]; }
- 回答としてマーク shimpo 2010年2月9日 4:20
-
十分把握できていない部分もあるのですが、解っていることだけを書かせていただきます(結局わかっていないことばかりになりました)。
shimpo さんが思われている通り、DataGridView 側の処理が BackgroundWorker 側のスレッドを直接圧迫することはなさそうに思います(十分な CPU リソースがない場合を除きます)。
私の文章は、スレッドごとに処理を明確に分離すべきという点を強調するために、あのような表現になりました。
次に、ではなぜ取り込み側まで遅くなるのかですが、次の2点が考えられます。
・PostMessage 等を使ってキューに入れることはそれなりに負荷がある?
・書かれているコードでは、表示が遅いと取り込みが遅いように見える。
1点目は、書いていて何ですが、そんなことはないような気はしています。
今回のような PostMessage の目的の場合、.NET では Control.BeginInvoke が使え、BackgroundWorker ではそれを内包した ReportProgress の仕組みがあります。(BeginInvoke の中では PostMessage が行われます。)
ReportProgress を使ってテストした際に感じたことですが、BeginInvoke は結構負荷があるようでした。Invoke 先では何も実行しなくても、BeginInvoke をするだけでそれなりに負荷がありました。
PostMessage だけだと軽そうなイメージはありますが、メッセージキューに多くのメッセージを追加するとパフォーマンスが落ちる場合もあるのかなと、可能性として挙げました。
それよりも、現状は「表示 = データの蓄え」になっていると思いますので、BackgroundWorker が活かせていないように感じました。
それ以上のことは、他に書かれているコードを見せてもらわないと何とも言えません。
(見たいわけではありません(^^;。どちらかというと逃げセリフです・・・)
現在進行中のこちらのスレッドも参考になりそうで、見守ってます。
マルチメディアタイマーによるスレッドの呼び出しと他操作による影響
http://social.msdn.microsoft.com/Forums/ja-JP/vcgeneralja/thread/06117a99-a0b6-49ff-9f81-8765ee3b3421 -
C#もまだ満足にわかってない所があり人様に見せられるコードじゃないですが、余計な部分は
割愛したコードを載せて置きます。
private IntPtr MainWnd;
private int Colm = 0;
private int Row = 0;
private struct _Worker
{
public int Period; //! 取り込み周期
public int Index; //! 取り込みインデックス
public int pIndex; //! 処理インデックス
public int Error; //! エラーフラグ
public byte[][] Buff; //! リングバッファ
};
private _Worker Worker = new _Worker();
//! 取り込み開始処理
private void mStart_Click( object sender, EventArgs e )
{
~中略~
MainWnd = this.Handle; //! メインウィンドウハンドル
Worker.Buff = new byte[MAX_RINGBUF][]; //! リングバッファ確保
for ( int a = 0; a < MAX_RINGBUF; ++a ) {
Worker.Buff[a] = new byte[6]; //! ジャグ配列に割り当て
}
Worker.Index = Worker.pIndex = 0; //! 取り込みインデックス、処理インデックス
Worker.Period = 1000 / Interval[mInterval.SelectedIndex]; //! 取り込み周期
backgroundWorker1.RunWorkerAsync(); //! スレッド開始
}//! バックグラウンドスレッド処理
private void DoWork( object sender, DoWorkEventArgs e )
{
long before = 0;
Stopwatch watch = new System.Diagnostics.Stopwatch();
watch.Start();int end = int.Parse( mCount.Text ); //! トータル取り込み回数
while ( backgroundWorker1.CancellationPending == false ) { //! キャンセル無し
if ( watch.ElapsedMilliseconds - before >= Worker.Period ) {
before = watch.ElapsedMilliseconds;if ( OnSample( end ) == false ) {
backgroundWorker1.CancelAsync(); //! スレッド停止
Worker.Error = ERR_USBCOM; //! 通信エラー発生
return;
}
}
}
watch.Stop();
SampleTime = ( int )watch.ElapsedMilliseconds; //! 取り込み時間計算
}//! 取り込み処理
private bool OnSample( int end )
{
int cnt;
//! インタラプト転送で取り込み
if ( Usb.Interrupt( out cnt, Worker.Buff[Worker.Index] ) == false ) {
return false;
}Win32.PostMessage( MainWnd, WM_USER_SETVALUE, ( IntPtr )Worker.Index, ( IntPtr )0 );
++Worker.Index; //! 取り込み回数更新
if ( Worker.Index == MAX_RINGBUF ) {
Worker.Index = 0;
}++Count; //! 取り込み件数更新
if ( end != 0 ) { //! 取り込み回数が指定されている
if ( Count >= end ) {
backgroundWorker1.CancelAsync(); //! スレッド停止
}
}return true;
}//! セルに取り込み値を代入(WM_USER_SETVALUEメッセージハンドラ)
public bool SetValue( int index )
{
string str = Encoding.GetEncoding( ( int )Def.SJIS ).GetString( Worker.Buff[index] );
float value = float.Parse( str );
~~中略~~
Data.Rows[Row][Colm] = value.ToString(); //! セルに代入
SetCurSel( Colm, Row ); //! 選択セル移動Worker.pIndex++; //! 処理回数更新
if ( Worker.pIndex == MAX_RINGBUF ) {
Worker.pIndex = 0;
}return true;
}//! カレントセルの設定
public void SetCurSel( int colm, int row )
{
mGridView.CurrentCell = mGridView[colm, row]; //! 選択セル移動
//=== DelaySetCurrentCell( mGridView[colm, row] );
}
DoWork()の処理内にあるように指定周期毎に取り込んで行く訳ですが、取り込み終了した時に
指定周期と取り込んだ回数、取り込み時間から取り込み時間中に取りこぼしがあったかどうか計
算でわかりますが、これが取りこぼし発生となる訳です。それが目標速度に達成できないという
意味です。
処理速度の遅いPCだと取り込み周期を少し遅くしても取りこぼしが頻繁に発生してNGという結果
が出るので取りこぼしの無いようにしてくれというのが上からの要求です。- 編集済み shimpo 2010年2月9日 8:33
-
Usb.Interrupt の処理時間が指定周期間隔以上かかった場合に、計算上では取りこぼしが発生したことになってしまうのではないでしょうか?
計算値は理論値だと思いますが、理論値未満が取りこぼしとは言えないのではと思いました。
それと、周期はどれぐらいでしょうか?
キューに溜めることができるのは、既定では最大 10,000 個らしいので、周期が短くて UI スレッドが忙しいと、今回の処理では PostMessage が失敗する可能性がありそうに思いました。
現状ではやはり、データの蓄えを UI スレッド側で行われていますので、PostMessage の失敗により(計算上ではなく本当に)取りこぼしが発生する(というかデータが消失する)可能性がありそうです。
対策としては、私のサンプルが使えそうに思います。
具体的には、ワーカースレッド側ではリングバッファではなく直接2次元配列に格納するようにし、UI スレッド側ではタイマーで表示の更新を行い、その際の Interval を Worker.Period にすれば、良い感じになりそうに思います。(見栄えはわかりませんが、取りこぼしは無くなるとは思います。理論値がでるかは別です。)
# ソース貼られちゃった・・・と思ったのは内緒です。 -
確かにUSB通信処理に時間がかかるのは事実です。ファームウェア設計者に聞くと
要求が来てから8ms位たって送信すると言っておりました。
PostMessageが失敗する事があるというのは知りませんでした。
失敗があるなら確かに取りこぼし(データ抜け)が発生しますね。遅いPCだと取りこぼし
の頻度がかなり高いです。
取り込み周期はいろいろですが、100回/秒で起こっています。遅いPCだと20回/秒
でも発生しています。
実際には取り込んだデータをワークシートに表示するケースとリアルタイムにグラフ
表示する場合とがあるので、記載いただいた対策のソースはそのまま使えませんが
なるだけ参考にしてやってみます。 -
TH01さん
>・非バインドなので DataGridView では RowCount などを設定する。
>・タイマー等で dataGridView1.Invalidate() を行う。
>・CellFormatting イベントや VirtualMode を true にした場合の CellValueNeeded イベントハンドラで
>e.Value に表示する値を設定する。
上記の方法で実装した所、確かにかなり早くなりました。スクロールの見栄えはとりあえず速度を確保する為、
と営業を説き伏せました(^^;
データ抜け自体はしていないので、問題もなく速度的にはこれが限界かな・・と思います。
バインドモードより早くなったように感じます。
いろいろとありがとうございました。