テキストボックスに文字列を追加し続けると遅くなる

ログ出力などの用途でTextBoxを利用し、文字列を追加し続けると処理に時間がかかり、動作速度が遅くなります。この現象により、アプリケーションの動作速度の低下や、アプリケーションが反応しなくなりフリーズしてしまうことがあります。この記事ではTextBoxに文字を追加し続ける場合の対策等を紹介します。

原因

TextBoxのTextプロパティに長い文字列を設定すると時間がかかるためです。

対策

対策1:Textプロパティの設定回数を減らす

TextBoxのTextプロパティに長い文字列を設定する回数を減らす方法により高速化できます。
以下の対策があります。
  • あらかじめテキストボックスに設定する文字列を string型で用意しておき、まとめてTextBoxに設定する
  • StringBuilderでテキストボックスに設定する文字列を作成し、TextBoxに設定する

対策2:WordWrapを無効にする

大幅な高速化にはなりませんが、WordWrapを無効にするとパフォーマンスが改善します。

対策3:SendMessageを用いる

Textプロパティへの値の設定ではなく、Windows APIを用いて直接TextBoxにテキストを設定する方法です。SendMessageでEM_REPLACESEL メッセージをTextBoxに送信します。

サンプルプログラムとパフォーマンス比較 (string, StringBuilder)

コードの実装と、どの程度の速度差があるかを比較します。

UI

下図のUIを作成します。Buttonを3つ、MultiLineプロパティをtrueにしたTextBox、Labelを1つ配置しています。

コード

下記のコードを記述します。ButtonのClickイベントを実装しています。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace BigText
{
  public partial class FormMain : Form
  {
    public FormMain()
    {
      InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
      Stopwatch sw = new Stopwatch();
      sw.Start(); 

      for (int i = 0; i < 1000; i++) {
        Guid g = Guid.NewGuid();
        textBox1.Text += string.Format("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 :{0:s} : {1:d}\r\n", g.ToString(), sw.ElapsedMilliseconds);
      }

      label1.Text = string.Format("処理終了 : {0:d}\r\n", sw.ElapsedMilliseconds);
      sw.Stop();
    }

    private void button2_Click(object sender, EventArgs e)
    {
      Stopwatch sw = new Stopwatch();
      sw.Start();

      string strtext = "";
      for (int i = 0; i < 1000; i++) {
        Guid g = Guid.NewGuid();
        strtext += string.Format("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 :{0:s} : {1:d}\r\n", g.ToString(), sw.ElapsedMilliseconds);
      }
      textBox1.Text = strtext;

      label1.Text = string.Format("処理終了 : {0:d}\r\n", sw.ElapsedMilliseconds);
      sw.Stop();
    }

    private void button3_Click(object sender, EventArgs e)
    {
      Stopwatch sw = new Stopwatch();
      sw.Start();

      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < 1000; i++) {
        Guid g = Guid.NewGuid();
        sb.Append(string.Format("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 :{0:s} : {1:d}\r\n", g.ToString(), sw.ElapsedMilliseconds));
      }
      textBox1.Text = sb.ToString();

      label1.Text = string.Format("処理終了 : {0:d}\r\n", sw.ElapsedMilliseconds);
      sw.Stop();
    }
  }
}

解説

Button1では、ループ内でTextBoxのTextプロパティに文字列を追加しています。
button1
private void button1_Click(object sender, EventArgs e)
{
  Stopwatch sw = new Stopwatch();
  sw.Start(); 

  for (int i = 0; i < 1000; i++) {
    Guid g = Guid.NewGuid();
    textBox1.Text += string.Format("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 :{0:s} : {1:d}\r\n",
      g.ToString(), sw.ElapsedMilliseconds);
  }

  label1.Text = string.Format("処理終了 : {0:d}\r\n", sw.ElapsedMilliseconds);
  sw.Stop();
}
Button2では、string型の変数を用意し、ループ内で変数にテキスト情報を設定し、ループを抜けた後でTextBoxのTextプロパティに文字列を設定しています。
button2
private void button2_Click(object sender, EventArgs e)
{
  Stopwatch sw = new Stopwatch();
  sw.Start();

  string strtext = "";
  for (int i = 0; i < 1000; i++) {
    Guid g = Guid.NewGuid();
    strtext += string.Format("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 :{0:s} : {1:d}\r\n",
      g.ToString(), sw.ElapsedMilliseconds);
  }
  textBox1.Text = strtext;

  label1.Text = string.Format("処理終了 : {0:d}\r\n", sw.ElapsedMilliseconds);
  sw.Stop();
}
Button3ではButton2と同じですが、string型の変数の代わりにStringBuilderを用いています。
button3
private void button3_Click(object sender, EventArgs e)
{
  Stopwatch sw = new Stopwatch();
  sw.Start();

  StringBuilder sb = new StringBuilder();
  for (int i = 0; i < 1000; i++) {
    Guid g = Guid.NewGuid();
    sb.Append(string.Format("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 :{0:s} : {1:d}\r\n",
      g.ToString(), sw.ElapsedMilliseconds));
  }
  textBox1.Text = sb.ToString();

  label1.Text = string.Format("処理終了 : {0:d}\r\n", sw.ElapsedMilliseconds);
  sw.Stop();
}

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


左の[TextBox追加]ボタンをクリックします。テキストボックスへの文字列の追加が始まります。処理が終わるとラベルに所要時間が表示されます。17.3秒かかっています。


アプリケーションを再起動し、真ん中の[string +]ボタンをクリックします。処理が実行されます。0.04秒で処理が完了しています。


同様に[StringBuilder]ボタンをクリックします。0.03秒で処理が完了しています。

サンプルプログラムとパフォーマンス比較 (WordWrap=false)

続いて、TextBoxに単純に追加するコードでWordWrapがFalseの場合、どの程度改善するかを比較します。

TextBoxのWordWrapプロパティをFalseに設定します。


プロジェクトを実行し、[TextBox追加]ボタンをクリックします。テキストボックスへの文字の追加が始まります。文字の追加が完了すると所要時間がラベルに表示されます。WordWrapをFalseにした場合は、10.2秒で処理が完了しています。WordWrap=trueの場合と比較すると1.5倍以上の速度改善が得られます。

サンプルプログラムとパフォーマンス比較 (SendMessage)

続いてSendMessageを用いた場合

UI

下図のUIを作成します。

コード

下記のコードを記述します。コードを見やすくするため、Button1,Button2,Button3のClickイベントハンドラは省略しています。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace BigText
{
  public partial class FormMain : Form
  {
    const int WM_GETTEXTLENGTH = 0x000E;
    const int EM_SETSEL = 0x00B1;
    const int EM_REPLACESEL = 0x00C2;

    [DllImport("User32.dll")]
    public static extern int SendMessage(IntPtr hWnd, int uMsg, int wParam, string lParam);

    [DllImport("User32.dll")]
    public static extern int SendMessage(IntPtr hWnd, int uMsg, int wParam, int lParam);

    public FormMain()
    {
      InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
      //(省略)
    }

    private void button2_Click(object sender, EventArgs e)
    {
      //(省略)
    }

    private void button3_Click(object sender, EventArgs e)
    {
      //(省略)
    }

    private void button4_Click(object sender, EventArgs e)
    {
      Stopwatch sw = new Stopwatch();
      sw.Start();

      for (int i = 0; i < 1000; i++) {
        Guid g = Guid.NewGuid();
        SendMessage(textBox1.Handle, EM_REPLACESEL, 1, 
          string.Format("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 :{0:s} : {1:d}\r\n", 
            g.ToString(), sw.ElapsedMilliseconds));
      }

      label1.Text = string.Format("処理終了 : {0:d}\r\n", sw.ElapsedMilliseconds);
      sw.Stop();

    }
  }
}

解説

ループ内で、SendMessage関数を呼び出しTextBoxにテキストを追加します。EM_REPLACESELメッセージは選択範囲を書き換えるウィンドウメッセージですが、このコードでは選択領域を指定していないため、テキストの挿入の動作になります。

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


[SendMessage]ボタンをクリックします。テキストボックスにテキストの追加が実行されます。SendMessageを用いた場合は、Textプロパティに追加する場合と異なり、テキストが追加されるたびにテキストボックスの画面が更新され、下にスクロールしていきます。WordWrap=Trueの場合は、2.0秒で処理が終了しました。


WordWrap=Falseの場合は1.8秒で処理が完了しています。若干の動作速度向上があるようです。

まとめ

TextBoxのTextプロパティに長い文字列を設定するのは動作速度の低下原因となるため、string型の変数やTextBuilderなどで文字列を準備してからまとめてTextBoxに設定すると動作速度の低下を防げます。リアルタイムのログなどで逐次TextBoxへの出力が必要な場合は、Windows APIのSendMessageを用いてEM_REPLACESEL メッセージを送信してテキストの追加をすると動作速度の低下を防げます。
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
作成日: 2014-04-21
Copyright © iPentec all rights reserverd.