質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.51%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

5回答

7613閲覧

C#WinFormでコントロールから値が連続して変化した場合の重い処理をなんとかしたいです

退会済みユーザー

退会済みユーザー

総合スコア0

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

1グッド

0クリップ

投稿2016/11/08 03:56

編集2022/01/12 10:55

C#のwinFormでpictureBoxに描画をするアプリケーションを作成しています。
例えばpictureBoxの中に四角形を描画するとして、コントロールのnumericUpDownを配置し、その値を四角形の辺の長さに指定するとします。

以下のコードは単純にpictureBoxの中央に赤い四角形を描画する例です。

C#

1namespace TestForm { 2 partial class Form1 : Form { 3 private PictureBox pictureBox1 = new PictureBox(); 4 private NumericUpDown numericUpDown1 = new NumericUpDown(); 5 6 // initialize 7 public Form1() { 8 // pictureBox1を配置 9 pictureBox1.Location = new Point(100, 0); 10 pictureBox1.Size = new Size(500, 500); 11 12 // numericUpDown1を配置 13 numericUpDown1.Location = new Point(10, 0); 14 15 InitializeComponent(); 16 } 17 18 // numericUpDown1の値を変更した場合 19 private void numericUpDown1_Changed(object sender, EventArgs e) { 20 // 辺の長さ 21 int length = Convert.ToInt32(numericUpDown1.Value); 22 23 // Bitmapを生成してpictureBox1に表示する 24 pictureBox1.Image = MakeBmp(length); 25 } 26 27 // Bitmapを生成する関数 28 private Bitmap MakeBmp(length){ 29 // pictureBox1の大きさのBitmapを作成 30 Bitmap bmp = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); 31 using (Graphics g_bmp = Graphics.FromImage(bmp)) { 32 // 中央に四角形を作成 33 g_fill.FillRectangle(Brushes.Red, new Rectangle(0, 0, length, length)); 34 } 35 return bmp; 36 } 37 } 38}

実際はもっと複雑な描画を行っていますが上記のような単純な例で説明させていただきたいと思います。

このとき、numericUpDown1の値をマウスホイールで連続して値を変化させるとnumericUpDown1_Changedイベントが連続で発生します。
そうすると当然ですがBitmapの描画生成も連続して実行されるのですがこれが非常に重い処理となってしまいます。


【質問】
この重い処理をどうにかしたいと思っていますが何かよい案はないでしょうか?
また、numericUpDown1のコントロールも重くなってしまいます(連続して値が変化せず飛び飛びに値が変わってしまいます)。

考えているのは、「マウスホイールのように速い間隔で連続して値が変化しているときは、一定間隔で値を拾うようにしてBitmap生成の回数を減らす」という感じです。

この処理をどのようにすればよいのかわかりません。
もしもっと良い方法があればそちらでも構いませんのでアドバイス頂ければと思います。


【やってみたこと】
ちなみにasync/awaitで非同期処理もやってみましたが上記処理と重さは変わりませんでした。
いちおうコードを載せてみますが間違っているでしょうか?
なんとなくBitmap生成を別スレッドにしているだけでpictureBox.ImageにBitmapが格納されるのはそのスレッドが終了しないと格納されないので非同期にしても意味がないような気もします。

あと、非同期処理をしてもnumericUpDown1のコントロールが重いままなのは変わりありませんでした(なぜでしょう…?)。

c#

1namespace TestForm { 2 partial class Form1 : Form { 3 private PictureBox pictureBox1 = new PictureBox(); 4 private NumericUpDown numericUpDown1 = new NumericUpDown(); 5 6 // initialize 7 public Form1() { 8 // pictureBox1を配置 9 pictureBox1.Location = new Point(100, 0); 10 pictureBox1.Size = new Size(500, 500); 11 12 // numericUpDown1を配置 13 numericUpDown1.Location = new Point(10, 0); 14 15 InitializeComponent(); 16 } 17 18 // numericUpDown1の値を変更した場合 19 private async void numericUpDown1_Changed(object sender, EventArgs e) { 20 // 辺の長さ 21 int length = Convert.ToInt32(numericUpDown1.Value); 22 23 // Bitmapを生成してpictureBox1に表示する 24 pictureBox1.Image = await MakeBmpAsync(length); 25 } 26 27 // Bitmapを生成する関数 28 private async Task<Bitmap> MakeBmpAsync(length){ 29 // pictureBox1の大きさのBitmapを作成 30 Bitmap bmp = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); 31 using (Graphics g_bmp = Graphics.FromImage(bmp)) { 32 // 中央に四角形を作成 33 g_fill.FillRectangle(Brushes.Red, new Rectangle(0, 0, length, length)); 34 } 35 return bmp; 36 } 37 } 38} 39コード

どうぞよろしくお願い致します。


【開発環境】

Windows10 + VisualStudio2015Community + .NetFramework4.5

Tak1wa👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答5

0

値を変化させる度に毎回「新規にビットマップを生成」していれば重くなるのは当然です。普通はそのようなことはしません。不必要なマルチスレッド化は問題をややこしくするだけなのでお勧めしません

通常はPictureBoxにあらかじめビットマップオブジェクトを設定しておき、変化が起こったらそのビットマップに対して描画処理を行うようにします。単純な図形の描画でしたらマイクロ秒の世界です。

PictureBoxへのビットマップオブジェクトの設定

C#

1pictureBox1.Image = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); 2```ご質問のコードだとコントロール類の設定をコンストラクタで行っていますが、それだと正しく設定されない場合があるので、`Load`イベントで行うべきです。 3 4ビットマップの描画はこんな感じになります。直接`numericUpDown1_Changed`メソッド内に書いてもかまいません。 5```C# 6using(var g = Graphics.FromImage(pictureBox1.Image)) 7{ 8 g.Clear(背景の色); 9 g.FillRectangle(Brushes.Red, new Rectangle(0, 0, length, length)); 10} 11pictureBox1.Invalidate(); // ←これを書かないと表示が更新されない

追記

連続する何らかのイベントに反応して重い処理をさせたい、ただし、間に合わない場合は端折ってもいい、というような処理をしたい場合は、私はだいたいこんなコードを書きます。

ビットマップを2枚用意するのは、いわゆるダブルバッファリングというやつです。ただ、描画処理がすごく重くてビットマップの生成やガベージコレクションにかかる時間など無視できるなら、毎回生成してPictureBoxにセットする方法でも問題ないかもしれません。

C#

1private void Form1_Load(object sender, EventArgs e) 2{ 3 : 4 : 5 6 doubleBuffer[0] = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); 7 doubleBuffer[1] = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); 8 showIndex = 0; 9 backgroundWorker1.RunWorkerAsync(); 10} 11 12 13private void numericUpDown1_Changed(object sender, EventArgs e) 14{ 15 lastLength = Convert.ToInt32(numericUpDown1.Value); 16 changeNotify.Set(); 17} 18 19private int lastLength; 20private AutoResetEvent changeNotify = new AutoResetEvent(false); 21private Bitmap[] doubleBuffer = new Bitmap[2]; 22private int showIndex; 23 24private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) 25{ 26 while(true) 27 { 28 changeNotify.WaitOne(); 29 if(backgroundWorker1.CancellationPending ) 30 break; 31 32 int length = lastLength; 33 34 // 表示中でない方のビットマップに対して描画処理を行う 35 int drawIndex = showIndex ^ 1; 36 var drawBuffer = doubleBuffer[drawIndex]; 37 38 // 重い描画処理 39 : 40 : 41 42 // 描画したビットマップをPictureBoxに設定して表示を更新 43 pictureBox1.Image = drawBuffer; 44 showIndex = drawIndex; 45 this.Invoke(new Action(pictureBox1.Invalidate)); 46 } 47}

投稿2016/11/08 07:26

編集2016/11/08 11:22
catsforepaw

総合スコア5938

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2016/11/08 08:45

ありがとうございます! 質問にも書かせていただいたのですが実際には複雑な合成処理を行っています。 レイヤーのように画像を重ねたり影をつけたりしています。 最終的なBitmapを生成する途中で新たなBitmapを生成したりもしています。 ただ、おっしゃる通りなるべく新たなBitmapを生成しないように改善をしてみようと思います。 全て新規で生成しないのは無理かもしれませんが…。
catsforepaw

2016/11/08 09:21

どれほど複雑な描画処理なのか見当も付きませんが、処理内容が判らないと適切なアドバイスができないかもしれません。 リアルタイムに絵を動かすようなケースでは、描画処理中にビットマップを生成するのは避けてください。ビットマップの生成自体も結構重い処理ですが、それによるガベージコレクションの頻発も気になります。 あらかじめ作業用のビットマップオブジェクトを作成しておき、それを使い回すようにしてください。それができないケースはちょっと思いつきません。
catsforepaw

2016/11/08 11:12

純粋に描画処理そのものが複雑すぎて重くなってこれ以上速くできない場合は、仕方がないので描画処理を別スレッドで行うことになりますね。 ちなみに、ご質問の下のコードは同期処理のままです。非同期にしたい場合は`Task.Run'等で別スレッドで実行させる必要があります。ただし、それを行った場合は、今度は変化させる度に非同期処理が実行されてしまい、同時にいくつも描画処理が走ることになります。 また、フォームやコントロールは別スレッドから直接いじれないので、Invokeやら排他制御やら何かと面倒ではあります。 とりあえず、このケースでのバックグラウンド処理なら、私は`AutoResetEvent`を使います。 回答の方にサンプルコードを書いておきます。
退会済みユーザー

退会済みユーザー

2016/11/09 09:11

ありがとうございます! 後半のコードは複雑になるのですね。 AutoResetEvent...これも初めてみるクラスです…。 いただいたコードは一見してまったく処理がわかりませんでした。 なんとなく同期する2つのスレッドでシグナルをやりとりして処理する感じなのですね。 ちょっとAutoResetEventについて調べながらコードを理解してみようと思います!
catsforepaw

2016/11/09 09:51

AutoResetEventはWin32 APIが提供するミューテックスやセマフォなどと並ぶ同期オブジェクトの一つで「イベント」と呼ばれるものをラップしたクラスです。あまりにも一般的な名前なので、検索するときは「同期オブジェクト」も一緒に付けると良いです。 「イベント」オブジェクトはWindowsのマルチスレッドプログラムにおいて、別スレッドの実行のタイミングを制御するのによく使われます。サンプルコードではそれを利用して、普段は待機させておいて(WaitOneのところ)、UI操作(値の変更)のタイミングでその下の処理を開始させる、というようにしています。そして、イベントオブジェクトは通知をキューイングしないので、処理が間に合わない場合は処理回数を端折る、ということをさせるのに都合が良いです。
guest

0

考えているのは、「マウスホイールのように速い間隔で連続して値が変化しているときは、一定間隔で値を拾うようにしてBitmap生成の回数を減らす」という感じです。

前回描画した時刻を保持しておいて、
numericUpDownのイベントが発生した時刻と比べて、
一定時間経っていたら描画する。

時刻はDateTime.Nowでとれます。

Timerで監視すればいいでしょう。

面倒なのでRxのThrottle使ったほうが簡潔ですが。


(NuGetからSystem.Reactiveを追加して)

C#

1using System; 2using System.Drawing; 3using System.Windows.Forms; 4using System.Reactive.Linq; 5 6namespace WindowsFormsApplication1 7{ 8 public partial class Form1 : Form 9 { 10 11 // initialize 12 public Form1() 13 { 14 InitializeComponent(); 15 Observable.FromEvent<EventHandler, EventArgs>( 16 h => (sender, e) => h(e), h => numericUpDown1.ValueChanged += h, h => numericUpDown1.ValueChanged -= h) 17 .Throttle(TimeSpan.FromMilliseconds(500)) // 動作がわかりやすいように、わざと大きめの値を入れている 18 .ObserveOn(System.Threading.SynchronizationContext.Current) // UIスレッドで監視 19 .Subscribe(_ => numericUpDown1_Changed()); 20 } 21 22 private void numericUpDown1_Changed() // イベントから外しておく 23 { 24 int length = Convert.ToInt32(numericUpDown1.Value); 25 26 pictureBox1.Image = MakeBmp(length); 27 label1.Text = length.ToString(); // 確認用に追加 28 } 29 30 private Bitmap MakeBmp(int length) 31 { 32 Bitmap bmp = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); 33 using (Graphics g_bmp = Graphics.FromImage(bmp)) 34 { 35 g_bmp.FillRectangle(Brushes.Red, new Rectangle(0, 0, length, length)); 36 } 37 return bmp; 38 } 39 } 40}

投稿2016/11/08 04:07

編集2016/11/08 04:35
ozwk

総合スコア13512

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2016/11/08 04:17

ありがとうございます。 ちょっと疑問なところがあって、「連続して値が変化したとき、最後に決定した値がその時間間隔以内になってしまって描画されない」ということにならないでしょうか? numericUpDownでは100という値なのに、描画した四角形の辺の長さが97とかになってしまうという感じです。 「連続した値の最後の値はかならず描画する」というふうにしたいのですがそうすると一定間隔だけだとダメですよね…。 なにかタイマーみたいなものを別スレッドで実行させておいてそれを比較する感じでしょうか?
ozwk

2016/11/08 04:24

まあそうなるんですが、 大変面倒なのでRx使った方法追記しますね。
退会済みユーザー

退会済みユーザー

2016/11/08 04:38

再度のアドバイス、ありがとうございます! まず、Rxというものを初めて知りました…! ちょっと検索して調べてみましたがC#のeventに対して「様々な条件を付加して発火できる」という感じでしょうか?(ちょっと見ただけなので曖昧ですが) このような便利な機能があるとも知らず・・・これは他にも応用できそうでこちらで聞いてよかったです! ありがとうございます!
guest

0

PictureBox.LoadCompleted
これで読み込みが完了したかわかります。
後はフラグ用のメンバ変数でも作っておいて、
PictureBox.LoadCompletedの時とnumericUpDown.Changedのときで上手く監視してやれば実現できると思います。

投稿2016/11/08 05:17

ishi9

総合スコア1294

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2016/11/08 08:37

ありがとうございます! PictureBox.LoadCompletedは使っていませんでした! たしかに組み合わせたらうまくできるかもしれませんね。 ちょっと試してみようと思います!
guest

0

値を拾うようにしてBitmap生成の回数を減らす」という感じです。

毎回ビットマップの作成を行うと時間がかかると思いますので、PictureBoxに登録したBitmapを再使用するのはどうでしょうか。
一応、ランダムで線分を1,000個程度描いてみましたがスムーズに動きました。

C#

1 public partial class TestForm : Form 2 { 3 private PictureBox pictureBox1 = new PictureBox(); 4 private NumericUpDown numericUpDown1 = new NumericUpDown(); 5 6 public TestForm() 7 { 8 InitializeComponent(); 9 10 // pictureBox1を配置 11 pictureBox1.Location = new Point(100, 0); 12 pictureBox1.Size = new Size(500, 500); 13 pictureBox1.Visible = true; 14 pictureBox1.Image = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height); 15 Controls.Add(pictureBox1); 16 17 // numericUpDown1を配置 18 numericUpDown1.Location = new Point(10, 0); 19 numericUpDown1.Visible = true; 20 numericUpDown1.ValueChanged += numericUpDown1_Changed; 21 numericUpDown1.Maximum = 500; 22 Controls.Add(numericUpDown1); 23 } 24 25 // numericUpDown1の値を変更した場合 26 private void numericUpDown1_Changed(object sender, EventArgs e) 27 { 28 // 辺の長さ 29 int length = Convert.ToInt32(numericUpDown1.Value); 30 31 // Bitmapを生成してpictureBox1に表示する 32 MakeBmp(length); 33 } 34 35 // Bitmapを生成する関数 36 private void MakeBmp(int length) 37 { 38 // pictureBox1の大きさのBitmapを作成 39 Bitmap bmp = (Bitmap)pictureBox1.Image; 40 using (Graphics g_bmp = Graphics.FromImage(bmp)) 41 { 42 // 背景クリア 43 g_bmp.Clear(this.BackColor); 44 45 // 中央に四角形を作成 46 g_bmp.FillRectangle(Brushes.Red, new Rectangle(0, 0, length, length)); 47 48 // ランダムに1,000のラインを描画 49 Random r = new Random(); 50 51 for (int i = 0; i < 1024; i++) 52 { 53 g_bmp.DrawLine(Pens.Green, r.Next() % pictureBox1.Width, r.Next() % pictureBox1.Height, r.Next() % pictureBox1.Width, r.Next() % pictureBox1.Height); 54 } 55 } 56 pictureBox1.Refresh(); 57 } 58 }

投稿2016/11/08 05:08

red_bb

総合スコア71

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2016/11/08 08:35

ありがとうございます! 質問の部分にも書かせていただいたのですが実際は複雑な合成を行っています。 最終的な画像を生成する上で何回もBitmapを生成しては合成後にDispose()で消して、また別の合成処理のところでもBitmapを生成し、合成後にDispose()で消しています。 元の画像のBitmapは一度生成したらそのまま利用しているのですが、レイヤーのように様々な画像を重ねたり影などを設定するときに別途Bitmapを生成しています。 そのため1つのBitmapだけというわけではないのです。
guest

0

numericUpDownのValueChangedイベントで毎回bmpを生成しているから重いんですよね。

であればValueChangedイベントではlengthの値の更新だけしておいて、別に100msなり500msなりの周期でタイマを走らせておいて、そのタイミングでbmp生成してPictureBoxに渡してやるようにすればタイマの周期より短い間隔でbmp生成しなくなるのでご希望の動作になりませんか?

投稿2016/11/08 04:46

tds721

総合スコア15

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2016/11/08 08:30

ありがとうございます! そうですね、いちおうタイマーを走らせて一定のタイミングでbitmapを生成を検討しています。 これが正当な方法かはわかりませんが自分が思いつく範囲ですとそんな感じのイメージしかできませんでした。 参考になり感謝します!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.51%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問