C# - いろんな対策を取り込んだ RichTextBox テンプレ - 全角空白とタブと改行を表示【自分用】

[C# - RichTextBox関連やりたいこと逆引きリファレンス【自分用】 - Qiita](https://qiita.com/kob58im/items/4b210c39232768489a1c#ime%E3%81%8C%E5%8B%9D%E6%89%8B%E3%81%AB%E7%A2%BA%E5%AE%9A%E3%81%99%E3%82%8B%E5%95%8F%E9%A1%8C%E3%81%AE%E5%AF%BE%E7%AD%96)

上記の集大成（？）です。

# 画面キャプチャ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/3dc1945b-8c3d-177d-7300-9cf326f10ce3.png)


# サンプルコード

全角空白だらけにすると、かなり動作が重い

```csharp

using System;
using System.Drawing;
using System.Text;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using System.Windows.Forms;


internal class MyRichTextBox:RichTextBox
{
    private const Int32 WM_PAINT = 0x000F;

    private static readonly int adjX = -2;
    private static readonly int adjY =  2;

    private int lineHeight;
    private RichTextBox dummyRichTextBox; // lineHeight算出用

    //----------------------------------------------------------------
    // Win32
    //
    private const int EM_SETTABSTOPS = 0x00CB;
    private const long IMF_DUALFONT = 0x80;
    private const int WM_SETREDRAW = 0x0008;
    private const int WM_USER = 0x0400;
    private const int EM_SETLANGOPTIONS = WM_USER + 120;
    private const int EM_GETLANGOPTIONS = WM_USER + 121;
    private const int EM_GETTEXTEX = (WM_USER + 94);
    private const int EM_GETTEXTLENGTHEX = (WM_USER + 95);

    // Flags for the GETEXTEX data structure  
    private const int GT_DEFAULT = 0;
    private const int GTL_CLOSE = 4; // Fast computation of a "close" answer 
    private const int GTL_DEFAULT = 0; // Do default (return # of chars) 

    class NativeMethods
    {
        [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
        public extern static IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(IntPtr hWnd, int msg, ref GETTEXTEX wParam, StringBuilder lParam);

        [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(IntPtr hWnd, int msg, ref GETTEXTLENGTHEX wParam, IntPtr lParam);
    }

    [StructLayout(LayoutKind.Sequential)]
    struct GETTEXTEX
    {
        public int cb;
        public int flags;
        public int codepage;
        public IntPtr lpDefaultChar;
        public IntPtr lpUsedDefChar;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct GETTEXTLENGTHEX
    {
        public int flags;
        public int codepage;
    }
    //
    //----------------------------------------------------------------

    private void EnsureDummyRichTextBox(bool forceSetFont)
    {
        if ( dummyRichTextBox == null ) {
            dummyRichTextBox = new RichTextBox();
            dummyRichTextBox.Text = "\n";
            dummyRichTextBox.Font = this.Font;
        }
        else if ( forceSetFont ) {
            dummyRichTextBox.Font = this.Font;
        }
    }

    void RecalcLineHeight(bool fontUpdated)
    {
        EnsureDummyRichTextBox(fontUpdated);
        int y1 = dummyRichTextBox.GetPositionFromCharIndex(dummyRichTextBox.GetFirstCharIndexFromLine(0)).Y;
        int y2 = dummyRichTextBox.GetPositionFromCharIndex(dummyRichTextBox.GetFirstCharIndexFromLine(1)).Y;
        lineHeight = y2 - y1;
    }

    protected override void OnFontChanged(EventArgs e)
    {
        RecalcLineHeight(true);
        SetFixFont();
        SetTabStop(4);
    }

    protected override void WndProc(ref System.Windows.Forms.Message m)
    {
        base.WndProc(ref m);
        if ( m.Msg == WM_PAINT ) {
            RecalcLineHeight(false);
            using ( Graphics graphic = base.CreateGraphics() ) {
                OnPaint(new PaintEventArgs(graphic, base.ClientRectangle));
            }
        }
    }

    protected override void OnPaint(PaintEventArgs pe)
    {
        Brush brush = Brushes.LightGray;
        string s = this.Text;
        int    sLen = s.Length;//this.TextLength;
        Graphics g = pe.Graphics;

        int lineStartI = GetLineFromCharIndex(GetCharIndexFromPosition(new Point(0, 0)));
        int lineEndI   = GetLineFromCharIndex(GetCharIndexFromPosition(new Point(pe.ClipRectangle.Width, pe.ClipRectangle.Height)));

        Regex regex = new Regex(@"[\t　\r\n]"); // tab or 全角空白 or 改行

        // 行ごとに処理
        int nextCharI = -1;
        for ( int lineI = lineStartI; lineI <= lineEndI; lineI++ ) {
            int startCharI;
            if ( lineI == lineStartI ) {
                startCharI = GetFirstCharIndexFromLine(lineI);
            }
            else {
                startCharI = nextCharI;
            }
            nextCharI = GetFirstCharIndexFromLine(lineI+1);
            if (nextCharI < startCharI) {
                nextCharI = sLen;
                if ( startCharI == nextCharI ) {
                    break;
                }
            }
            int lenOfLine = nextCharI - startCharI;

            Match match = regex.Match(s, startCharI, lenOfLine);

            while ( match.Success ) {
                Point point = this.GetPositionFromCharIndex(match.Index);
                //Point point2 = this.GetPositionFromCharIndex(match.Index+match.Length);
                //g.FillRectangle(brush, point.X, point.Y, point2.X - point.X, lineHeight);
                if ( s[match.Index] == '\t' ) {
                    g.DrawString(">", this.Font, brush, adjX+point.X, adjY+point.Y, new StringFormat());
                }
                else if ( s[match.Index] == '　' ) {//全角空白
                    g.DrawString("□", this.Font, brush, adjX+point.X, adjY+point.Y, new StringFormat());
                }
                else if ( s[match.Index] == '\r' ) {
                    g.DrawString(@"\r", this.Font, brush, adjX+point.X, adjY+point.Y, new StringFormat());
                }
                match = match.NextMatch();
            }

            /*
            // ちらつきと残骸が残る。実用レベルではない
            //
            // drawstringでtabの描画で調整がやっかいなので、1文字ずつGetPositionFromCharIndexで場所を取得して描画する
            Point pointNext = new Point(0,0);
            for ( int i=startCharI; i<startCharI+lenOfLine; i++ ) {
                Point point;
                if ( i == startCharI ) {
                    point = this.GetPositionFromCharIndex(i);
                }
                else {
                    point = pointNext;
                }
                pointNext = this.GetPositionFromCharIndex(i+1);
                if ( pointNext.X < 0 ) {
                    continue;
                }
                if ( point.X >= this.Width ) {
                    break;
                }
                g.DrawString(new string(s[i],1), this.Font, Brushes.Blue, adjX+point.X, adjY+point.Y, new StringFormat());
            }
            */

        }
    }


    public override string Text
    {
        get {
            var getLength = new GETTEXTLENGTHEX();
            getLength.flags = GTL_CLOSE; //get buffer size
            getLength.codepage = 1200; //Unicode
            int textLength = (int)(NativeMethods.SendMessage(this.Handle, EM_GETTEXTLENGTHEX, ref getLength, IntPtr.Zero).ToInt64());
            var getText = new GETTEXTEX();
            getText.cb = textLength + 2; //add space for null terminator
            if ( getText.cb < 2 ) { return ""; } // overflow
            getText.flags = GT_DEFAULT;
            getText.codepage = 1200; //Unicode
            var sb = new StringBuilder(getText.cb);
            NativeMethods.SendMessage(this.Handle, EM_GETTEXTEX, ref getText, sb);
            return sb.ToString();
        }
        set { base.Text = value; }
    }

    public override int TextLength
    {
        get {
            var getLength = new GETTEXTLENGTHEX();
            getLength.flags = GTL_DEFAULT; //Returns the number of characters
            getLength.codepage = 1200; //Unicode
            int textLength = (int)(NativeMethods.SendMessage(this.Handle, EM_GETTEXTLENGTHEX, ref getLength, IntPtr.Zero).ToInt64());
            if ( textLength < 0 ) { return 0; } // overflow
            return textLength;
        }
    }

    private void SetFixFont()
    {
        IntPtr lParam = NativeMethods.SendMessage(this.Handle, EM_GETLANGOPTIONS, new IntPtr(0), new IntPtr(0));
        lParam = new IntPtr( ((long)lParam) & (~IMF_DUALFONT));
        NativeMethods.SendMessage(this.Handle, EM_SETLANGOPTIONS, new IntPtr(0), lParam);
    }

    private void SetTabStop(int tabSize)
    {
        int[] tabarray = new int[] { tabSize*4 };
        int wparam = tabarray.Length;

        IntPtr parray = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(int)) * tabarray.Length);
        Marshal.Copy(tabarray, 0, parray, tabarray.Length);
        NativeMethods.SendMessage(this.Handle, EM_SETTABSTOPS, new IntPtr(wparam), parray);
    }
}


class RichTextBoxSample : Form
{
    MyRichTextBox txt;

    RichTextBoxSample()
    {
        ClientSize = new Size(600, 250);

        Controls.Add(txt = new MyRichTextBox(){
            Dock = DockStyle.Fill,
            Multiline = true,
            WordWrap = false,
            AcceptsTab = true,
            ScrollBars = RichTextBoxScrollBars.Both,
        });

        Font baseFont = new Font("ＭＳ ゴシック", 10);
        txt.Font = baseFont;
    }

    [STAThread]
    static void Main()
    {
        Application.Run(new RichTextBoxSample());
    }
}
```