いろいろ備忘録日記

主に .NET と Python絡みのメモを公開しています。

フォームの描画速度の違いについて (Load, Shown, Application.DoEvents)


ネタ元はCodeProjectの以下の記事です。
元記事が英語なので、後から自分が見て思い出せるようメモしときます。


通常フォームを表示する際に表示データを読み込んだり、加工したりして
画面コントロールに設定したりしますが、その際は大抵Loadイベントを利用します。
VisualStudio上のデザイナでフォームをダブルクリックしてもLoadイベントのハンドラ
が作成されます。


でも、Loadイベントはロード時のイベントなので当然ながらそのイベントハンドラにて
行われる処理が全部完了しないと画面は表示されません。


イベントハンドラの中での処理がすぐ終わるものならいいのですが
長い処理を行うと中々画面が表示されないという事態になります。


通常、このような場合、よく行われるのが時間がかかる処理は非同期処理に
してしまって、とりあえず画面は先に表示してしまったりします。


人は、画面がいつまでも表示されない状態で待つのは苦痛ですが
一旦画面が表示されてしまった後は、同じ時間でも待ったりしてくれます。


で、本題ですがCodeProjectの記事では、わざわざ非同期処理を行わなくても
それに近い状態を見せてくれています。


やり方は、実にシンプルです。

Loadイベントで処理を行わず、Shownイベントで処理を行う。


これだけです。なるほどなって思いました。
Shownイベントは画面が初期表示された時に発生します。


このタイミングでは画面の基礎部分は表示できているので
通常よりも早く画面が表示できます。


でも、Shownイベントにそのまま重い処理を書いてしまうと
画面は表示されますが、データ処理中のコントロールの描画が
追いつきません。必然的に微妙な描画状態でフォームが表示されます。


なので、以下のようにします。

Application.DoEvents();
[処理・・・]


一旦DoEventsを呼んで現在の状態で描画させておいて
その後で処理を行います。


これだと、画面はデータが設定される前で確実に描画されているので
基本的な見た目は確保したままデータ設定処理に入れます。


以下、サンプルです。

[メインフォーム (Form1)]

using System;
using System.Windows.Forms;

namespace Gsf.Samples
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            InitializeEvents();
        }

        private void InitializeEvents()
        {            
            btnProcLoadEvents.Click += (s, e) =>
            {
                new ChildForm1().ShowDialog(this);
            };

            btnProcShownEvent.Click += (s, e) =>
            {
                new ChildForm2().ShowDialog(this);
            };

            btnProcShownEventWithDoEvents.Click += (s, e) => 
            {
                new ChildForm3().ShowDialog(this);
            };
        }
    }
}


[Loadイベントで処理を行うサブフォーム (ChildForm1)]

using System;
using System.Linq;
using System.Windows.Forms;

namespace Gsf.Samples
{
    public partial class ChildForm1 : Form
    {
        public ChildForm1()
        {
            InitializeComponent();
            InitializeControls();
            InitializeEvents();
        }

        private void InitializeControls()
        {
            SuspendLayout();

            Controls.Add(new ListBox
            {
                Dock = DockStyle.Top,
                Name = "listBox"
            });

            FlowLayoutPanel contentPane = new FlowLayoutPanel
            {
                FlowDirection = FlowDirection.LeftToRight,
                WrapContents = true,
                Dock = DockStyle.Bottom,
                AutoScroll = true,
                Name = "contentPane"
            };

            Enumerable.Range(1, 100).ToList().ForEach((cnt) => 
                {
                    contentPane.Controls.Add(new TextBox{});
                }
            );

            Controls.Add(contentPane);

            ResumeLayout();        
        }

        private void InitializeEvents()
        {
            Load += (s, e) => 
            {
                Cursor = Cursors.WaitCursor;

                this.FindControl<ListBox>("listBox").LoadSampleData();
                this.FindControl<FlowLayoutPanel>("contentPane").LoadSampleData();

                Cursor = Cursors.Default;
            };

            FormClosed += (s, e) =>
            {
                (Controls[0] as ListBox).Items.Clear();
                GC.Collect();
            };
        }
    }
}

[Shownイベントで処理を行うサブフォーム(DoEvents無し) (ChildForm2)]

using System;
using System.Linq;
using System.Windows.Forms;

namespace Gsf.Samples
{
    public partial class ChildForm2 : Form
    {
        public ChildForm2()
        {
            InitializeComponent();
            InitializeControls();
            InitializeEvents();
        }

        private void InitializeControls()
        {
            SuspendLayout();

            Controls.Add(new ListBox
            {
                Dock = DockStyle.Fill,
                Name = "listBox"
            });

            FlowLayoutPanel contentPane = new FlowLayoutPanel
            {
                FlowDirection = FlowDirection.LeftToRight,
                WrapContents = true,
                Dock = DockStyle.Bottom,
                AutoScroll = true,
                Name = "contentPane"
            };

            Enumerable.Range(1, 100).ToList().ForEach((cnt) =>
                {
                    contentPane.Controls.Add(new TextBox{});
                }
            );

            Controls.Add(contentPane);

            ResumeLayout();
        }

        private void InitializeEvents()
        {
            Shown += (s, e) =>
            {
                Cursor = Cursors.WaitCursor;

                this.FindControl<ListBox>("listBox").LoadSampleData();
                this.FindControl<FlowLayoutPanel>("contentPane").LoadSampleData();

                Cursor = Cursors.Default;
            };

            FormClosed += (s, e) =>
            {
                (Controls[0] as ListBox).Items.Clear();
                GC.Collect();
            };
        }
    }
}

[Shownイベントで処理を行うサブフォーム(DoEvents有り)(ChildForm3)]

using System;
using System.Linq;
using System.Windows.Forms;

namespace Gsf.Samples
{
    public partial class ChildForm3 : Form
    {
        public ChildForm3()
        {
            InitializeComponent();
            InitializeControls();
            InitializeEvents();
        }

        private void InitializeControls()
        {
            SuspendLayout();

            Controls.Add(new ListBox
            {
                Dock = DockStyle.Fill,
                Name = "listBox"
            });

            FlowLayoutPanel contentPane = new FlowLayoutPanel
            {
                FlowDirection = FlowDirection.LeftToRight,
                WrapContents = true,
                Dock = DockStyle.Bottom,
                AutoScroll = true,
                Name = "contentPane"
            };

            Enumerable.Range(1, 100).ToList().ForEach((cnt) =>
                {
                    contentPane.Controls.Add(new TextBox{});
                }
            );

            Controls.Add(contentPane);

            ResumeLayout();
        }

        private void InitializeEvents()
        {
            Shown += (s, e) =>
            {
                //
                // 一旦DoEventsをコールして基本部分の描画は
                // 行っておく.
                //
                Application.DoEvents();

                Cursor = Cursors.WaitCursor;

                this.FindControl<ListBox>("listBox").LoadSampleData();
                this.FindControl<FlowLayoutPanel>("contentPane").LoadSampleData();

                Cursor = Cursors.Default;
            };

            FormClosed += (s, e) =>
            {
                (Controls[0] as ListBox).Items.Clear();
                GC.Collect();
            };
        }
    }
}


[内部で利用している拡張クラス]

using System;
using System.Linq;
using System.Windows.Forms;

namespace Gsf.Samples
{
    public static class ControlExtensions
    {
        public static T FindControl<T>(this Control self, string name) where T : Control
        {
            if(self == null) {
                return default(T);
            }

            if(string.IsNullOrEmpty(name)) {
                return default(T);
            }
            
            Control[] ctls = self.FindForm().Controls.Find(name, true);
            if(ctls.Length == 0) {
                return default(T);
            }

            return (T) ctls[0];
        }

        public static void LoadSampleData(this ListBox self)
        {
            if(self == null) {
                return;
            }

            self.Items.Clear();
            Enumerable.Range(1, 100000).ToList().ForEach((cnt) => { self.Items.Add(cnt.ToString()); });
        }

        public static void LoadSampleData(this FlowLayoutPanel self)
        {
            if(self == null) {
                return;
            }

            for(int i = 0; i < self.Controls.Count; i++)
            {
                self.Controls[i].Text = i.ToString();
            }
        }
    }
}


サンプルを以下の場所にあげておきました。
実際にコンパイルして実行していただくと違いが分かると思います。