C#のTextBoxを本当の意味でAutoSizeにする

# はじめに

Labelコントロールはテキストに応じて自動でサイズを変更してくれます。これをTextBoxでもできないか調べてみましたがスマートな方法が見つからなかったのでごり押しで作成してみました。  

![textbox.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3193449/85bc6e77-7c0f-38f3-90d9-5940960f6ca7.gif)  

# 概要

1. TextBoxのテキストを読み取る。
2. 非表示のLabel(AutoSize=true)を作成する。
3. LabelのFont、TextをTextBoxと同じとする。自動でLabelのサイズが変更される。
4. LabelのサイズをTextBoxに適用する。
5. Labelを削除する。

# TextBox(単一行)の場合

概要の通り。

```cs:TextBoxAutoSize.cs
//コンパイル方法
//C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc /target:winexe TextBoxAutoSize.cs
using System;
using System.Windows.Forms;

namespace TextBoxAutoSize
{
    class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }

    class Form1 : Form
    {
        private TextBox textBox;

        public Form1()
        {
            this.textBox = new TextBox();
            this.textBox.Multiline = false;
            this.textBox.Location = new System.Drawing.Point(13, 13);
            this.textBox.TextChanged += new System.EventHandler(this.textBox_TextChanged);
            this.Controls.Add(this.textBox);
        }

        private void textBox_TextChanged(object sender, EventArgs e)
        {
            //マージン
            int margin = 20;

            //サイズ判定用ラベルを作成
            Label dummy = new Label();
            dummy.Visible = false;
            dummy.Font = this.textBox.Font;
            dummy.Text = this.textBox.Text;
            dummy.AutoSize = true;
            this.Controls.Add(dummy);

            //テキストボックスのサイズを変更
            this.textBox.Width = dummy.Width + margin;

            //コントロール削除
            this.Controls.Remove(dummy);
        }
    }
}

```

# TextBox(複数行)の場合

基本的に概要の通りですが、「単一行Labelの高さ」と「単一行TextBoxの高さ」の差分で高さ補正をしています。またテキストの最後が改行だけの場合、1行分高さが小さくなるのでその分も補正しています。ソースコードとしては**TextBox(単一行)の場合**のコードの
*  `this.textBox.Multiline = false` を`this.textBox.Multiline = true`に変更
* textBox_TextChanged関数を以下に置き換え。

```cs:TextBoxAutoSize_multiline.cs
private void textBox_TextChanged(object sender, EventArgs e)
{
    //マージン
    int margin = 20;

    //サイズ判定用ラベル
    Label dummy = new Label();
    dummy.Visible = false;
    dummy.Font = this.textBox.Font;
    dummy.Text = this.textBox.Text;
    dummy.AutoSize = true;
    this.Controls.Add(dummy);

    //「単一行Labelの高さ」取得用
    Label dummy2 = new Label();
    dummy2.Visible = false;
    dummy2.Font = this.textBox.Font;
    dummy2.AutoSize = true;
    dummy2.Text = "A";
    this.Controls.Add(dummy2);

    //「単一行TextBoxの高さ」取得用
    TextBox dummy3 = new TextBox();
    dummy3.Visible = false;
    dummy3.Font = this.textBox.Font;
    dummy3.BorderStyle = this.textBox.BorderStyle;
    dummy3.Text = "A";
    this.Controls.Add(dummy3);

    //「単一行Labelの高さ」と「単一行TextBoxの高さ」の差分
    int diffh = dummy3.Height - dummy2.Height;

    //テキストの最後が改行だけの場合
    if (this.textBox.Text.EndsWith(System.Environment.NewLine))
    {
        dummy.Text = this.textBox.Text + "A";
    }

    this.textBox.Width = dummy.Width + margin;
    this.textBox.Height = dummy.Height + diffh;

    //コントロール削除
    this.Controls.Remove(dummy);
    this.Controls.Remove(dummy2);
    this.Controls.Remove(dummy3);
}

```

# 入力中(変換確定前)の段階でもサイズを変更したい場合

上記の方法だとテキストの入力が確定していないとサイズを変更できませんが、以下のようにすると入力中(変換確定前)の段階でも自動でサイズを変更できるみたいです。

1. 入力中かどうかを調べて、入力中であれば入力中のテキストを取得
2. キャレットがある行において、「キャレットがある位置までのテキスト＋入力中のテキスト」と
「キャレットがある行のテキスト」を比較。前者のほうが長い場合は必要に応じてサイズ補正をする。

一応動作しますが、きちんと作成できているかあやしいです。。。
```cs:TextBoxAutoSize_multiline_IME.cs
//コンパイル方法
//C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc /target:winexe TextBoxAutoSize_multiline_IME.cs
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace TextBoxAutoSize
{
    class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }

    class Form1 : Form
    {
        [DllImport("imm32.dll")]
        public static extern IntPtr ImmGetContext(IntPtr hWnd);

        [DllImport("Imm32.dll")]
        public static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hIMC);

        [DllImport("Imm32.dll", CharSet = CharSet.Unicode)]
        private static extern int ImmGetCompositionStringW(IntPtr hIMC, int dwIndex, byte[] lpBuf, int dwBufLen);

        private const int GCS_COMPSTR = 8;
        private ImeStatus ime_state;//IME入力中かどうか

        private TextBox textBox;
        private int currentWidth;//現在のテキストボックスの幅

        public Form1()
        {
            this.textBox = new TextBox();
            this.textBox.Multiline = true;
            this.textBox.Location = new System.Drawing.Point(13, 13);
            this.textBox.TextChanged += new System.EventHandler(this.textBox_TextChanged);
            this.textBox.KeyUp += new System.Windows.Forms.KeyEventHandler(this.textBox_KeyUp);
            this.Controls.Add(this.textBox);

            ime_state = new ImeStatus();
            currentWidth = this.textBox.Width;
        }

        private void textBox_TextChanged(object sender, EventArgs e)
        {
            changeWidth(this.textBox.Text);
            currentWidth = this.textBox.Width;
        }

        private void textBox_KeyUp(object sender, KeyEventArgs e)
        {
            //IME入力中でなければ何もしない
            if (!ime_state.Compositing)
            {
                return;
            }

            var c = ImmGetContext(textBox.Handle);

            if (c == IntPtr.Zero)
            {
                return;
            }

            try
            {
                var buf = new byte[256];

                var length = ImmGetCompositionStringW(c, GCS_COMPSTR, buf, buf.Length);
                if (length >= 0)
                {
                    // IME入力中のテキストを取得
                    var composition = System.Text.Encoding.Unicode.GetString(buf, 0, length);

                    // 現在のテキスト内容
                    string text0 = textBox.Text.Substring(0, textBox.SelectionStart);
                    string text1 = textBox.Text.Substring(textBox.SelectionStart);

                    // 現在のテキスト内容にIME入力中のテキストを結合
                    List<string> splittext = new List<string>(text1.Replace(System.Environment.NewLine, "\n").Split('\n'));
                    splittext.RemoveAt(0);

                    text1 = (text1.Contains(System.Environment.NewLine) ? System.Environment.NewLine : "")
                        + (splittext.Count > 0 ? String.Join(System.Environment.NewLine, splittext.ToArray()) : "");

                    string currentText = text0 + composition + text1;

                    changeWidth(currentText, true);
                }
                else
                {
                    ;
                }
            }
            finally
            {
                ImmReleaseContext(textBox.Handle, c);
            }
        }

        private void changeWidth(string text, bool ime = false)
        {
            //マージン
            int margin = 20;

            //サイズ判定用ラベル
            Label dummy = new Label();
            dummy.Visible = false;
            dummy.Font = this.textBox.Font;
            dummy.Text = text;
            dummy.AutoSize = true;
            this.Controls.Add(dummy);

            //「単一行Labelの高さ」取得用
            Label dummy2 = new Label();
            dummy2.Visible = false;
            dummy2.Font = this.textBox.Font;
            dummy2.AutoSize = true;
            dummy2.Text = "A";
            this.Controls.Add(dummy2);

            //「単一行TextBoxの高さ」取得用
            TextBox dummy3 = new TextBox();
            dummy3.Visible = false;
            dummy3.Font = this.textBox.Font;
            dummy3.BorderStyle = this.textBox.BorderStyle;
            dummy3.Text = "A";
            this.Controls.Add(dummy3);

            //「単一行Labelの高さ」と「単一行TextBoxの高さ」の差分
            int diffh = dummy3.Height - dummy2.Height;

            //テキストの最後が改行だけの場合
            if (text.EndsWith(System.Environment.NewLine))
            {
                dummy.Text = text + "A";
            }

            if (!ime)
            {
                this.textBox.Width = dummy.Width + margin;
            }
            else
            {
                this.textBox.Width = currentWidth < dummy.Width + margin ? dummy.Width + margin : currentWidth;
            }
            this.textBox.Height = dummy.Height + diffh;

            //コントロール削除
            this.Controls.Remove(dummy);
            this.Controls.Remove(dummy2);
            this.Controls.Remove(dummy3);
        }

        //IME入力中かどうかを調べる
        // ウィンドウメッセージを処理するために
        // IMessageFilter インターフェイスを実装します。
        public class ImeStatus : IMessageFilter
        {
            public ImeStatus()
            {
                // コンストラクター内で
                // メッセージフィルターとして自身を登録します。
                Application.AddMessageFilter(this);
            }

            public bool Compositing
            {
                get; private set;
            }

            // アプリケーションがウィンドウメッセージを処理する前に
            // 呼び出されます。
            public bool PreFilterMessage(ref Message m)
            {
                const int WmStartComposition = 0x10D;
                const int WmEndComposition = 0x10E;
                switch (m.Msg)
                {
                    case WmStartComposition:
                        // 日本語入力の開始
                        Compositing = true;
                        break;
                    case WmEndComposition:
                        // 日本語入力の終了
                        Compositing = false;
                        break;
                }
                return false;
            }
        }
    }
}
```

# 引用元

入力中かどうかの確認には以下のコードを引用  
[日本語入力中か否かを調べる方法 - Qiita](https://qiita.com/hiddengoat88/items/1f4074038bf78e806a8a)

入力中のテキストの取得方法は以下のコードを引用  
[C# TextChangedイベントを日本語変換前でも発生させる - スタック・オーバーフロー](https://ja.stackoverflow.com/questions/32794/c-textchanged%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%82%92%E6%97%A5%E6%9C%AC%E8%AA%9E%E5%A4%89%E6%8F%9B%E5%89%8D%E3%81%A7%E3%82%82%E7%99%BA%E7%94%9F%E3%81%95%E3%81%9B%E3%82%8B)

# 参考文献

[ImmGetContext 関数 (imm.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/api/imm/nf-imm-immgetcontext)  
[ImmReleaseContext 関数 (imm.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/api/imm/nf-imm-immreleasecontext)  
[ImmGetCompositionStringW 関数 (imm.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/api/imm/nf-imm-immgetcompositionstringw)  

中身を見れていませんが、以下も参考になるかもしれません。  
[winforms - C# Resize textbox to fit content - Stack Overflow](https://stackoverflow.com/questions/8048455/c-sharp-resize-textbox-to-fit-content)