C# - フォントメトリクスを調査してみたらカオスだった件 - 未解決

調査続編（改善版）はこちら → https://qiita.com/kob58im/items/414554ea6f514b11af3a

---

混乱した結果、記事も空中分解気味になってしまったが、とりあえず投稿してみる。

# 経緯

下記みたいな感じで`StringFormat`でセンタリング指定して`DrawString`しても縦位置がずれるので、調べてみた。

```csharp

var sf = new StringFormat();
sf.Alignment = StringAlignment.Center; // 横中央
sf.LineAlignment = StringAlignment.Center; // 縦中央 ・・・ にならない（？）

g.DrawString(text, font, Brushes.Black, new Point(Width/2,Height/2), sf);
```


`g`とか`q`とかの下にはみ出る文字の影響と推測。
参考サイト  [フォント メトリックを取得する - Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/framework/winforms/advanced/how-to-obtain-font-metrics) から抜粋：
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/4aa37708-96c9-cb18-b162-72623ccfaad0.png)

Descentの分だけ上に寄っているとすれば、Descentの半分だけY座標を足して描画すれば大体中央に来そう。

## 謎#1
上記の参考サイトは`Size`を使っているが、`Height`を使わないと何故か位置が合わない。（？）
> `descentPixel = font.Size * descent / fontFamily.GetEmHeight(FontStyle.Regular);`

# いろんなフォントを描画してみる

AscentとHeightの位置に横線を描画してみる。
コード抜粋

```csharp

    FontFamily ff = font.FontFamily;
    int ascent = ff.GetCellAscent(font.Style);
    int emHeight = ff.GetEmHeight(font.Style);

    // 下記の各Y座標(pixel単位)で横線を引いてみた
    float fontHeight = font.GetHeight(g); // g: 描画に使用するGraphicsクラス
    float ascentHeight  = fontHeight * ascent  / emHeight;
```

## システムのデフォルトフォント

それっぽい（※ドキュメントがよく分からないため根拠がない）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/f20ab64f-5345-3802-dcbc-26928b5bb3d7.png)

## ビルマ文字（なぜか、はみ出る）

`Font.GetHeight`がやたら大きい値になったので、縦長の文字を探して色々やってたらビルマ文字[^1]にたどり着いた。なぜかはみ出る。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/a5442998-be4a-aaa0-a274-acf68ec2b24e.png)

# ソースコード

```csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Windows.Forms;

class FontMetricsTest : Form
{
    TrackBar   trackbar;
    PictureBox pct;
    ComboBox   cmbFonts;
    TextBox    txtContent;

    FontMetricsTest()
    {
        Text = "Font metrics test";
        ClientSize = new Size(600, 400);

        cmbFonts = new ComboBox();
        foreach (FontFamily ff in FontFamily.Families) {
            cmbFonts.Items.Add(ff.Name);
        }
        cmbFonts.Text = SystemFonts.DefaultFont.Name;
        cmbFonts.Location = new Point(0,0);
        cmbFonts.Width = 300;
        cmbFonts.DropDownHeight = 500;
        cmbFonts.DropDownStyle = ComboBoxStyle.DropDownList;
        cmbFonts.SelectedIndexChanged += (sender,e)=>{MyRedraw();};
        Controls.Add(cmbFonts);

        trackbar = new TrackBar();
        trackbar.Location = new Point(300,0);
        trackbar.Maximum = 100;
        trackbar.Value   = 50;
        trackbar.Minimum = 1;
        trackbar.TickFrequency = 33;
        trackbar.ValueChanged += (sender,e)=>{MyRedraw();};
        Controls.Add(trackbar);

        txtContent = new TextBox();
        txtContent.Location = new Point(0, 50);
        txtContent.Width = 300;
        txtContent.Text = "g あいう Qiita";
        txtContent.TextChanged += (sender,e)=>{MyRedraw();};
        Controls.Add(txtContent);

        pct = new PictureBox();
        pct.Location = new Point(0, 80);
        pct.Size = new Size(600, 300);
        pct.Image = new Bitmap(600,300);
        Controls.Add(pct);

        MyRedraw();
    }

    void MyRedraw()
    {
        int w = pct.Image.Width;
        int h = pct.Image.Height;

        string fontName = cmbFonts.Text;
        Font font = new Font(fontName, (float)trackbar.Value);
        
        string text = txtContent.Text;
        if (font.Name != fontName) {
            text = "Font unmatch: \"" + font.Name + "\" is loaded.";
        }

        using ( Graphics g = Graphics.FromImage(pct.Image) ) {
            g.Clear(Color.Black);
            
            FontFamily ff = font.FontFamily;

            int ascent = ff.GetCellAscent(font.Style);
            int descent = ff.GetCellDescent(font.Style);
            int emHeight = ff.GetEmHeight(font.Style);
            
            float fontHeight = font.GetHeight(g);
            float ascentHeight  = fontHeight * ascent  / emHeight;
//          float ascentSize    = font.Size  * ascent  / emHeight;

            Console.Write("==== "); Console.Write(font.Name); Console.WriteLine(" ====");
            Console.Write("font.Size: ");  Console.WriteLine(font.Size);
            Console.Write("fontHeight: "); Console.WriteLine(fontHeight);
            Console.Write("ascent: ");     Console.WriteLine(ascent);
            Console.Write("descent: ");    Console.WriteLine(descent);
            Console.Write("sum: ");        Console.WriteLine(ascent+descent);
            Console.Write("emHeight: ");   Console.WriteLine(emHeight);

            Pen pen = new Pen(Color.LightGray, 1.0f);
//          Pen penDash = new Pen(Color.LightGray, 1.0f);
//          penDash.DashStyle = DashStyle.Dash;
            g.DrawLine(pen,     0, fontHeight, w, fontHeight);
            g.DrawLine(pen,     0, ascentHeight, w, ascentHeight);
//          g.DrawLine(penDash, 0, font.Size, w, font.Size);
//          g.DrawLine(penDash, 0, ascentSize, w, ascentSize);

            g.DrawString(text, font, Brushes.White, new PointF(0,0));
        }

        pct.Refresh();
    }
    
    static void DumpSystemFontNames()
    {
        var dict = new Dictionary<string,Font>() {
            {"CaptionFont      " , SystemFonts.CaptionFont     },
            {"DefaultFont      " , SystemFonts.DefaultFont     },
            {"DialogFont       " , SystemFonts.DialogFont      },
            {"IconTitleFont    " , SystemFonts.IconTitleFont   },
            {"MenuFont         " , SystemFonts.MenuFont        },
            {"MessageBoxFont   " , SystemFonts.MessageBoxFont  },
            {"SmallCaptionFont " , SystemFonts.SmallCaptionFont},
            {"StatusFont       " , SystemFonts.StatusFont      }
        };

        foreach (var pair in dict) {
            Console.Write(pair.Key);
            Console.Write(",");
            Console.Write(pair.Value.Size);
            Console.Write(",");
            Console.WriteLine(pair.Value.Name);
        }
    }

    [STAThread]
    static void Main(string[] args)
    {
        // DumpSystemFontNames();
        Application.Run(new FontMetricsTest());
    }
}
```

# 結論

色々やってみたが、結局わからん・・・
実験してみると`Font.Size`≠描画の高さだし[^2]、
`Font.Height`=行間、とドキュメントからは読み取ったけど、はみ出るケースもあるし[^3]、、

<b>特定の文字列を中央表示したいなら、`Graphics.MeasureString`とかを使って、[描画する文字列に応じて調整する](https://dobon.net/vb/dotnet/graphics/measurestring.html)ほうがよいかもしれない。</b>


# 参考サイト

- [フォントの作り方（13）〜メトリクスの話〜](http://www.fontxdesign.net/2017/03/27/フォントの作り方%ef%bc%8813%ef%bc%89〜メトリクスの話〜/)
- [指定したGraphicsオブジェクトで使用できるフォントを取得する - dobon.net](https://dobon.net/vb/dotnet/system/getinstalledfonts.html)
- [フォント メトリックを取得する - Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/framework/winforms/advanced/how-to-obtain-font-metrics)


## そのた
調べてる最中に`System.Windows.Media`のほうの`FontFamily`に迷い込んだりしたが別モノと思われる。

[^1]: 文字は[wikipedia](https://ja.wikipedia.org/wiki/ビルマ文字)からのコピペ。

[^2]: 【追記】ポイントとピクセルの単位系が違うため。[こちら](https://qiita.com/felis_silv/items/efee4b1a397b0b95100a#%E3%82%82%E3%81%86%E4%B8%80%E3%81%A4%E3%81%AE%E6%96%B9%E6%B3%95)が参考になりそう。

[^3]: 【追記】コメント頂いている内容の通り、Fontが代用されているため。