【.NET】文字列が表示しきれないときに省略記号"..."を表示する

# はじめに
多言語対応の依頼があったのですが、1つの言語を切り替えて表示するのではなく複数言語（日本語と英語など）を一度に表示する方式でした。ラベルコントロールに1つに複数言語を表示するとなると改行して表示することになります。
長い文字列があり表示しきれない場合、省略記号"..."を末尾に表示する機能が、WinFormではラベルコントロールの`AutoEllipsis`プロパティを`True`にすることで実現できます。
しかし、言語ごとに実現したいので省略記号に変換した文字列を取得した上で改行して文字列連結するようにしたい。

```:例
Evacuation Sh...
避難場所
```

下記サイトに省略させる方法があるが、表示のみで省略された文字自体を取得することができない。
「[文字列が表示しきれないときに"..."を表示する - dobon.net](https://dobon.net/vb/dotnet/graphics/stringtrimming.html)」
いい方法がないか検索したところ、下記サイトのプログラムを見つけたので、これを改良することにした。
[Auto Ellipsis - CODE PROJECT](https://www.codeproject.com/Articles/37503/Auto-Ellipsis)

パス名の機能は除外し、DataGridView等に対応できるように引数を見直した。WinFormとWPFの両方作成した。

# 仕様
EllipsisクラスのTrimmingメソッドで省略された文字を取得することができます。
省略位置は、EllipsisFormatで指定します。Wordは他と組み合わせて指定します。
省略文字を変更することができます。

|EllipsisFormat|省略方法|
|:--|:--|
|None|省略記号なし|
|End|末尾に省略記号を表示する|
|Start|先頭に省略記号を表示する|
|Middle|真ん中に省略記号を表示する|
|Word|単語単位で省略します|

"Government of the people, by the people,"
![Ellipsis.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/295caf0b-b678-c60a-84f3-261c270ac01c.png)
"あいうえいお かきくけこ さしすせそ たちつてと"
![Ellipsis2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/64581ec7-d6ff-80b2-a7f2-e1aff8f92dda.png)
上から順に、`None`、`End`、`Start`、`Middle`、`Middle | Word` で表示しています。

# 動作環境
* Windows 10 Home
* .NET Framework 4.7.2
* Visual Studio 2019 Communication

# 使用例
## WinForm
```csharp:使用例
private void button1_Click(object sender, EventArgs e)
{
    string s = "Government of the people, by the people,";
    SetEllipsis(this.label1, s, Ellipsis.EllipsisFormat.End | Ellipsis.EllipsisFormat.Word);
}

private void SetEllipsis(Control ctrl, string text, Ellipsis.EllipsisFormat format)
{
    Graphics dc = ctrl.CreateGraphics();
    Font font = ctrl.Font;
    int width = ctrl.Width;

    ctrl.Text = Ellipsis.Trimming(text, dc, font, width, format);
}
```

## WPF
```csharp:使用例
private void Button_Click(object sender, RoutedEventArgs e)
{
    string s = "Government of the people, by the people,";
    SetEllipsis(this.Label1, s, Ellipsis.EllipsisFormat.Middle | Ellipsis.EllipsisFormat.Word);
}

private void SetEllipsis(ContentControl ctrl, string text, Ellipsis.EllipsisFormat format)
{
    double width = ctrl.Width;
    double fontSize = ctrl.FontSize;
    Typeface face = new Typeface(ctrl.FontFamily, ctrl.FontStyle, ctrl.FontWeight, ctrl.FontStretch);

    ctrl.Content = Ellipsis.Trimming(text, face, fontSize, width, format);
}
```

# プログラム
Ellipsisクラスになります。

## WinForm
```csharp:Ellipsis.cs
using System;
using System.Drawing;
using System.Text.RegularExpressions;
using System.Windows.Forms;

public class Ellipsis
{
    [Flags]
    public enum EllipsisFormat
    {
        None = 0,
        End = 1,
        Start = 2,
        Middle = 3,
        Word = 4
    }

    public static string EllipsisChars = "...";

    private static Regex prevWord = new Regex(@"\W*\w*$");
    private static Regex nextWord = new Regex(@"\w*\W*");

    public static string Trimming(string text, Graphics dc , Font font, int width, EllipsisFormat options)
    {
        if (string.IsNullOrEmpty(text))
            return text;

        if ((EllipsisFormat.Middle & options) == 0)
            return text;

        if (dc == null)
            throw new ArgumentNullException("dc");

        Size s = TextRenderer.MeasureText(dc, text, font);

        if (s.Width <= width)
            return text;

        string mid = text;
        string fit = "";
        int len = 0;
        int seg = mid.Length;

        while (seg > 1)
        {
            seg -= seg / 2;

            int left = len + seg;
            int right = mid.Length;

            if (left > right)
                continue;

            if ((EllipsisFormat.Middle & options) == EllipsisFormat.Middle)
            {
                right -= left / 2;
                left -= left / 2;
            }
            else if ((EllipsisFormat.Start & options) != 0)
            {
                right -= left;
                left = 0;
            }

            if ((EllipsisFormat.Word & options) != 0)
            {
                if ((EllipsisFormat.End & options) != 0)
                {
                    int left2 = left - prevWord.Match(mid, 0, left).Length;
                    if (left2 != 0) left = left2; 
                }
                if ((EllipsisFormat.Start & options) != 0)
                {
                    int right2 = right + nextWord.Match(mid, right).Length;
                    if (right2 != 0) right = right2;
                }
            }

            string str = mid.Substring(0, left) + EllipsisChars + mid.Substring(right);
            s = TextRenderer.MeasureText(dc, str, font);
            if (s.Width <= width)
            {
                len += seg;
                fit = str;
            }
        }

        return fit;
    }
}
```

## WPF
```csharp:Ellipsis.cs
using System;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Media;

public class Ellipsis
{
    [Flags]
    public enum EllipsisFormat
    {
        None = 0,
        End = 1,
        Start = 2,
        Middle = 3,
        Word = 4
    }

    public static string EllipsisChars = "...";

    private static Regex prevWord = new Regex(@"\W*\w*$");
    private static Regex nextWord = new Regex(@"\w*\W*");

    public static string Trimming(string text, Typeface face, double fontSize, double width, EllipsisFormat options)
    {
        if (string.IsNullOrEmpty(text))
            return text;

        if ((EllipsisFormat.Middle & options) == 0)
            return text;

        if (face == null)
            throw new ArgumentNullException("face");

        Size s = MeasureText(text, face, fontSize);

        if (s.Width <= width)
            return text;

        string mid = text;
        string fit = "";
        int len = 0;
        int seg = mid.Length;

        while (seg > 1)
        {
            seg -= seg / 2;

            int left = len + seg;
            int right = mid.Length;

            if (left > right)
                continue;

            if ((EllipsisFormat.Middle & options) == EllipsisFormat.Middle)
            {
                right -= left / 2;
                left -= left / 2;
            }
            else if ((EllipsisFormat.Start & options) != 0)
            {
                right -= left;
                left = 0;
            }

            if ((EllipsisFormat.Word & options) != 0)
            {
                if ((EllipsisFormat.End & options) != 0)
                {
                    int left2 = left - prevWord.Match(mid, 0, left).Length;
                    if (left2 != 0) left = left2; 
                }
                if ((EllipsisFormat.Start & options) != 0)
                {
                    int right2 = right + nextWord.Match(mid, right).Length;
                    if (right2 != 0) right = right2;
                }
            }

            string str = mid.Substring(0, left) + EllipsisChars + mid.Substring(right);
            s = MeasureText(str, face, fontSize);
            if (s.Width <= width)
            {
                len += seg;
                fit = str;
            }
        }

        return fit;
    }

    static private Size MeasureText(string text, Typeface face, double fontSize)
    {
        var formattedText = new FormattedText(text,
                                              CultureInfo.
                                              CurrentCulture,
                                              FlowDirection.LeftToRight,
                                              face,
                                              fontSize,
                                              Brushes.Black,
                                              new NumberSubstitution(),
                                              1);

        return new Size(formattedText.Width, formattedText.Height);
    }
}
```

# ライセンスっぽいこと
コード改変や配布は自由です。
このプログラムによる義務／責任を何ら負いません。

# 最後に
Visual Basic版も記述しようと思ったのですが、WinFormとWPFもとなると長くなりすぎるので Visual Basic版に関しては、「[Telerik Code Converter](http://converter.telerik.com/)」のサイトで Visual Basicに変換してください。
[Visual BasicからC#への変換について](https://qiita.com/yaju/items/3afb486bfbbddc1e6aa8)

# 参照
* [Auto Ellipsis - CODE PROJECT](https://www.codeproject.com/Articles/37503/Auto-Ellipsis)
* [文字列が表示しきれないときに"..."を表示する - dobon.net](https://dobon.net/vb/dotnet/graphics/stringtrimming.html)
* [How to calculate WPF TextBlock width for its known font size and characters? - stack overflow](https://stackoverflow.com/questions/9264398/how-to-calculate-wpf-textblock-width-for-its-known-font-size-and-characters)
* [ボタンやラベルのテキストの末尾に省略記号を表示するには？［2.0のみ］](https://www.atmarkit.co.jp/fdotnet/dotnettips/419autoellipsis/autoellipsis.html)
* [【WPF】TextBlockで文字を省略したい](https://qiita.com/pregum/items/c8bcbff7aabfdb92e45b)