C#のWinformでControl.Invalidate()とPaintイベント

コンピュータ

PictureBoxなどのコントロールを再描画するためにControl.Invalidate()を実行することがあります。
何気に使っていて、ふと気になったのですが、Invalidate()を発行すると再描画されるのでPaintイベントが発生するわけですが、Invalidate()はPaint()イベントの終了待ちをするのでしょうか?Invalidate()の次の行では描画が完了していることを想定してコードを書くこと出来るのか気になりました。

UIのスレッドはシングルスレッドぽいので、順次処理のような気がします。ただ命令の順番を貯めるキューのような物があって、「再描画をしなさい」とキューに命令を追加して、終了を待たずにInvalidate()に制御を戻す可能性もあります。

基本的な事柄ですので調べれば答えが探せそうですが、コードを書いて試してみたいと思います。

namespace InvalidPaint01;

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        Bitmap bmp = new(256, 256);
        using(var g = Graphics.FromImage(bmp))
        {
            g.Clear(Color.Blue);    // 青
        }

        PictureBox picbox = new()
        {
            Location = new (10, 10),
            SizeMode = PictureBoxSizeMode.AutoSize,
            Image = bmp, // 初期は青
            Parent = this,
        };

        Button button = new()
        {
            Location = new (10, 10+256+10),
            Text = "Invalidate()",
            Parent = this,
        };
        // ボタンクリックイベント
        button.Click += (s, e) =>
        {
            Console.WriteLine("Invalidate前");
            using(var g = Graphics.FromImage(bmp))
            {
                g.Clear(Color.Green);    // 緑
            }
            picbox.Invalidate();
            Console.WriteLine("Invalidate後");
        };
        // ピクチャボックスペイントイベント
        picbox.Paint += (s, e) =>
        {
            Console.WriteLine("Paint");
        };
        
    }
}

起動した状態はピクチャボックスが青で、Invalidate()ボタンを押すと緑色へ変化します。

Invalidate()ボタンを押す。

Invalidate()の次の行で一時停止してみる。この時点でピクチャボックスは青のまま、Paintイベントでの文字出力もされていません。

再開して、Paintイベントの文字出力で一時停止してみます。この時点でも青色のままでした。
Paintイベントの後はピクチャボックスが緑色に更新されていました。

デバック実行が実際の動作と同じだとすると、Invalidate()は再描画の命令を発行するだけで再描画の終了を待つわけでは無いようです。

少し意地悪な修正をしてみます。Invalidate()の発効後に白色の四角形を描画してみます。

        // ボタンクリックイベント
        button.Click += (s, e) =>
        {
            Console.WriteLine("Invalidate前");
            var g = Graphics.FromImage(bmp);
            g.Clear(Color.Green);    // 緑
            picbox.Invalidate();
            g.FillRectangle(Brushes.White, 20, 20, 128, 128);
            Console.WriteLine("Invalidate後");
        };

順番からすると四角形の描画の前にInvalidate()で再描画を指示していますので、四角形はピクチャボックスボックスに反映されないように見えます。


ところが実行するとピクチャボックスに四角形の描画が反映しています。
多分、四角形の描画がPictureBoxの再描画の前に終了したためこのような結果になったと思われます。

それでは四角形の描画までにウエイトを入れて描画のタイミングを遅くしてみます。

        // ボタンクリックイベント
        button.Click += async (s, e) =>
        {
            Console.WriteLine("Invalidate前");
            var g = Graphics.FromImage(bmp);
            g.Clear(Color.Green);    // 緑
            picbox.Invalidate();
            await Task.Delay(100);
            g.FillRectangle(Brushes.White, 20, 20, 128, 128);
            Console.WriteLine("Invalidate後");
        };


今度はピクチャボックスに四角形の描画が反映されていません。

先ほどは「Invalidate()前」→「Invalidate()後」→「Paint」の順番でしたが、今回は「Invalidate()前」→「Paint」→「Invalidate()後」となっています。

今回試してみて、Invalidate()は再描画を指示しますが、再描画の終了を待つわけでは無いので、連続して画像を書き換えるような場合Paintイベントの終了を待つ必要がありそうです。と書いてはみましたが、パフォーマンスが低下しそうですしデッドロックがおきそうな予感がします。

コメント