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

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

ただいまの
回答率

89.52%

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

受付中

回答 5

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 4,425
退会済みユーザー

退会済みユーザー

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

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

namespace TestForm {
    partial class Form1 : Form {
        private PictureBox pictureBox1 = new PictureBox();
        private NumericUpDown numericUpDown1 = new NumericUpDown();

        // initialize
        public Form1() {
            // pictureBox1を配置
            pictureBox1.Location = new Point(100, 0);
            pictureBox1.Size = new Size(500, 500);

            // numericUpDown1を配置
            numericUpDown1.Location = new Point(10, 0);

            InitializeComponent();
        }

        // numericUpDown1の値を変更した場合
        private void numericUpDown1_Changed(object sender, EventArgs e) {
            // 辺の長さ
            int length = Convert.ToInt32(numericUpDown1.Value);

            // Bitmapを生成してpictureBox1に表示する
            pictureBox1.Image = MakeBmp(length);
        }

        // Bitmapを生成する関数
        private Bitmap MakeBmp(length){
            // pictureBox1の大きさのBitmapを作成
            Bitmap bmp = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height);
            using (Graphics g_bmp = Graphics.FromImage(bmp)) {
                // 中央に四角形を作成
                g_fill.FillRectangle(Brushes.Red, new Rectangle(0, 0, length, length));
            }
            return bmp;
        }
    }
}

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

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


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

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

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


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

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

namespace TestForm {
    partial class Form1 : Form {
        private PictureBox pictureBox1 = new PictureBox();
        private NumericUpDown numericUpDown1 = new NumericUpDown();

        // initialize
        public Form1() {
            // pictureBox1を配置
            pictureBox1.Location = new Point(100, 0);
            pictureBox1.Size = new Size(500, 500);

            // numericUpDown1を配置
            numericUpDown1.Location = new Point(10, 0);

            InitializeComponent();
        }

        // numericUpDown1の値を変更した場合
        private async void numericUpDown1_Changed(object sender, EventArgs e) {
            // 辺の長さ
            int length = Convert.ToInt32(numericUpDown1.Value);

            // Bitmapを生成してpictureBox1に表示する
            pictureBox1.Image = await MakeBmpAsync(length);
        }

        // Bitmapを生成する関数
        private async Task<Bitmap> MakeBmpAsync(length){
            // pictureBox1の大きさのBitmapを作成
            Bitmap bmp = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height);
            using (Graphics g_bmp = Graphics.FromImage(bmp)) {
                // 中央に四角形を作成
                g_fill.FillRectangle(Brushes.Red, new Rectangle(0, 0, length, length));
            }
            return bmp;
        }
    }
}
コード

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


【開発環境】

Windows10 + VisualStudio2015Community + .NetFramework4.5

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 5

+2

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

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

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

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

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


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

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Reactive.Linq;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {

        // initialize
        public Form1()
        {
            InitializeComponent();
            Observable.FromEvent<EventHandler, EventArgs>(
                h => (sender, e) => h(e), h => numericUpDown1.ValueChanged += h, h => numericUpDown1.ValueChanged -= h)
                .Throttle(TimeSpan.FromMilliseconds(500)) // 動作がわかりやすいように、わざと大きめの値を入れている
                .ObserveOn(System.Threading.SynchronizationContext.Current) // UIスレッドで監視
                .Subscribe(_ => numericUpDown1_Changed());
        }

        private void numericUpDown1_Changed() // イベントから外しておく
        {
            int length = Convert.ToInt32(numericUpDown1.Value);

            pictureBox1.Image = MakeBmp(length);
            label1.Text = length.ToString(); // 確認用に追加
        }

        private Bitmap MakeBmp(int length)
        {
            Bitmap bmp = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height);
            using (Graphics g_bmp = Graphics.FromImage(bmp))
            {
                g_bmp.FillRectangle(Brushes.Red, new Rectangle(0, 0, length, length));
            }
            return bmp;
        }
    }
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/11/08 13:17

    ありがとうございます。
    ちょっと疑問なところがあって、「連続して値が変化したとき、最後に決定した値がその時間間隔以内になってしまって描画されない」ということにならないでしょうか?
    numericUpDownでは100という値なのに、描画した四角形の辺の長さが97とかになってしまうという感じです。

    「連続した値の最後の値はかならず描画する」というふうにしたいのですがそうすると一定間隔だけだとダメですよね…。
    なにかタイマーみたいなものを別スレッドで実行させておいてそれを比較する感じでしょうか?

    キャンセル

  • 2016/11/08 13:24

    まあそうなるんですが、
    大変面倒なのでRx使った方法追記しますね。

    キャンセル

  • 2016/11/08 13:38

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

    キャンセル

+2

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

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

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

pictureBox1.Image = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height);

ご質問のコードだとコントロール類の設定をコンストラクタで行っていますが、それだと正しく設定されない場合があるので、Loadイベントで行うべきです。

ビットマップの描画はこんな感じになります。直接numericUpDown1_Changedメソッド内に書いてもかまいません。

using(var g = Graphics.FromImage(pictureBox1.Image))
{
    g.Clear(背景の色);
    g.FillRectangle(Brushes.Red, new Rectangle(0, 0, length, length));
}
pictureBox1.Invalidate();    // ←これを書かないと表示が更新されない

追記

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

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

private void Form1_Load(object sender, EventArgs e)
{
        :
        :

    doubleBuffer[0] = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height);
    doubleBuffer[1] = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height);
    showIndex = 0;
    backgroundWorker1.RunWorkerAsync();
}


private void numericUpDown1_Changed(object sender, EventArgs e)
{
    lastLength = Convert.ToInt32(numericUpDown1.Value);
    changeNotify.Set();
}

private int lastLength;
private AutoResetEvent changeNotify = new AutoResetEvent(false);
private Bitmap[] doubleBuffer = new Bitmap[2];
private int showIndex;

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    while(true)
    {
        changeNotify.WaitOne();
        if(backgroundWorker1.CancellationPending )
            break;

        int length = lastLength;

        // 表示中でない方のビットマップに対して描画処理を行う
        int drawIndex = showIndex ^ 1;
        var drawBuffer = doubleBuffer[drawIndex];

        // 重い描画処理
            :
            :

        // 描画したビットマップをPictureBoxに設定して表示を更新
        pictureBox1.Image = drawBuffer;
        showIndex = drawIndex;
        this.Invoke(new Action(pictureBox1.Invalidate));
    }
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/11/08 20:12

    純粋に描画処理そのものが複雑すぎて重くなってこれ以上速くできない場合は、仕方がないので描画処理を別スレッドで行うことになりますね。

    ちなみに、ご質問の下のコードは同期処理のままです。非同期にしたい場合は`Task.Run'等で別スレッドで実行させる必要があります。ただし、それを行った場合は、今度は変化させる度に非同期処理が実行されてしまい、同時にいくつも描画処理が走ることになります。
    また、フォームやコントロールは別スレッドから直接いじれないので、Invokeやら排他制御やら何かと面倒ではあります。

    とりあえず、このケースでのバックグラウンド処理なら、私は`AutoResetEvent`を使います。
    回答の方にサンプルコードを書いておきます。

    キャンセル

  • 2016/11/09 18:11

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

    キャンセル

  • 2016/11/09 18:51

    AutoResetEventはWin32 APIが提供するミューテックスやセマフォなどと並ぶ同期オブジェクトの一つで「イベント」と呼ばれるものをラップしたクラスです。あまりにも一般的な名前なので、検索するときは「同期オブジェクト」も一緒に付けると良いです。

    「イベント」オブジェクトはWindowsのマルチスレッドプログラムにおいて、別スレッドの実行のタイミングを制御するのによく使われます。サンプルコードではそれを利用して、普段は待機させておいて(WaitOneのところ)、UI操作(値の変更)のタイミングでその下の処理を開始させる、というようにしています。そして、イベントオブジェクトは通知をキューイングしないので、処理が間に合わない場合は処理回数を端折る、ということをさせるのに都合が良いです。

    キャンセル

0

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/11/08 17:30

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

    キャンセル

0

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

public partial class TestForm : Form
    {
        private PictureBox pictureBox1 = new PictureBox();
        private NumericUpDown numericUpDown1 = new NumericUpDown();

        public TestForm()
        {
            InitializeComponent();

            // pictureBox1を配置
            pictureBox1.Location = new Point(100, 0);
            pictureBox1.Size = new Size(500, 500);
            pictureBox1.Visible = true;
            pictureBox1.Image = new Bitmap(pictureBox1.Size.Width, pictureBox1.Size.Height);
            Controls.Add(pictureBox1);

            // numericUpDown1を配置
            numericUpDown1.Location = new Point(10, 0);
            numericUpDown1.Visible = true;
            numericUpDown1.ValueChanged += numericUpDown1_Changed;
            numericUpDown1.Maximum = 500;
            Controls.Add(numericUpDown1);
        }

        // numericUpDown1の値を変更した場合
        private void numericUpDown1_Changed(object sender, EventArgs e)
        {
            // 辺の長さ
            int length = Convert.ToInt32(numericUpDown1.Value);

            // Bitmapを生成してpictureBox1に表示する
            MakeBmp(length);
        }

        // Bitmapを生成する関数
        private void MakeBmp(int length)
        {
            // pictureBox1の大きさのBitmapを作成
            Bitmap bmp = (Bitmap)pictureBox1.Image;
            using (Graphics g_bmp = Graphics.FromImage(bmp))
            {
                // 背景クリア
                g_bmp.Clear(this.BackColor);

                // 中央に四角形を作成
                g_bmp.FillRectangle(Brushes.Red, new Rectangle(0, 0, length, length));

                // ランダムに1,000のラインを描画
                Random r = new Random();

                for (int i = 0; i < 1024; i++)
                {
                    g_bmp.DrawLine(Pens.Green, r.Next() % pictureBox1.Width, r.Next() % pictureBox1.Height, r.Next() % pictureBox1.Width, r.Next() % pictureBox1.Height);
                }
            }
            pictureBox1.Refresh();
        }
    }

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/11/08 17:35

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

    キャンセル

0

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/11/08 17:37

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

    キャンセル

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

  • ただいまの回答率 89.52%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる
  • トップ
  • C#に関する質問
  • C#WinFormでコントロールから値が連続して変化した場合の重い処理をなんとかしたいです