Yu Gothic UI (悠ゴシック UI) で文字列を描画すると、文字列をまとめて描画した場合と一文字ずつ文字を描画した場合で長さが異なる - C#

Yu Gothic UI (悠ゴシック UI) で文字列を描画すると、文字列をまとめて描画した場合と 一文字ずつ文字を描画した場合で長さが異なる現象と対処法を紹介します。

概要

こちらの記事では 文字列をまとめて描画した場合と一文字ずつ文字を描画した場合で長さが異なる現象と対処法を紹介しました。
DrawStringメソッドを利用して描画すると、描画位置にずれが発生するため、 TextRenderer を利用するか、Windows APIのExtTextOut 関数を利用することで、文字列をまとめて描画した場合と、 一文字ずつ描画した場合の結果が同じになります。
しかし、対処した方法を利用しても Yu Gothic UIのフォントを利用した場合にはずれが発生します。 この記事では、Yu Gothic UIフォントを利用した場合に、文字列をまとめて描画した場合と 一文字ずつ文字を描画した場合で長さが異なる現象を確認して、対処法のコードを紹介します。

現象の確認1:TextRenderer.DrawText を利用した場合

UI

下図のフォームを作成します。Panelコントロールをフォームに配置します。

コード

次のコードを記述します。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Text;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace KerningDemo
{
  public partial class FormTextDraw3 : Form
  {
    public FormTextDraw3()
    {
      InitializeComponent();
    }

    private void panel1_Paint(object sender, PaintEventArgs e)
    {
      string DrawText = "ぺんぎんクッキー";
      Font f = new Font("Yu Gothic UI", 28);
      Brush b = new SolidBrush(Color.Black);
      e.Graphics.TextRenderingHint = TextRenderingHint.AntiAlias;

      TextRenderer.DrawText(e.Graphics, DrawText, f,new Point(0, 10), Color.Black);

      int xpos = 0;
      for (int i = 0; i < DrawText.Length; i++)
      {
        TextRenderer.DrawText(e.Graphics, DrawText.Substring(i,1), f, new Point(xpos, 70), Color.Black);
        xpos += TextRenderer.MeasureText(e.Graphics, DrawText.Substring(i, 1), f).Width;
      }

      xpos = 0;
      for (int i = 0; i < DrawText.Length; i++)
      {
        TextRenderer.DrawText(e.Graphics, DrawText.Substring(i, 1), f, new Point(xpos, 130), Color.Black);
        xpos += TextRenderer.MeasureText(e.Graphics, DrawText.Substring(i, 1), f, new Size(100,100),TextFormatFlags.NoPadding).Width;
      }
    }
  }
}

解説

こちらの記事で紹介しているコードと同様です。
上から順番に
  • TextRenderer.DrawTextメソッドを利用してPanelに文字列を一括で描画します。
  • TextRenderer.DrawTextメソッドを利用して一文字ずつ文字を描画します。文字の幅は、TextRenderer.MeasureTextで求めます。
  • TextRenderer.DrawTextメソッドを利用して一文字ずつ文字を描画します。文字の幅は、TextFormatFlags.NoPaddingオプションを設定して、TextRenderer.MeasureTextを呼び出して求めます。

実行結果

上記のプロジェクトを実行します。下図のウィンドウが表示されます。
2番目の表示はTextRenderer.MeasureTextの戻り値に前後の余白が含まれているため、字間が広がってしまいますが、 3番目の表示はまとめて描画した場合と同じ結果になります。しかし、Yu Gothiuc UIフォントで描画した場合は一文字ずつ描画した結果が、 わずかに横幅が大きくなっています。


線を描画して比較します。「ぺんぎん」の"ぎ"の文字の横幅が一括で描画した場合と、一文字づつ描画した場合で異なるため、 以後の文字の位置がずれてしまっています。


なお、フォントが「メイリオ」の場合は、まとめて描画した場合と一文字ずつ描画した場合で同じ描画結果になります。

現象の確認2:ExtTextOut を利用した場合

ExtTextOut() Windows APIを利用した場合でも動作を確認します。

UI

下図のフォームを作成します。Panelコントロールをフォームに配置します。

コード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace KerningDemo
{
  public partial class FormTextDraw4 : Form
  {
    [DllImport("gdi32.dll", EntryPoint = "SelectObject")]
    public static extern IntPtr SelectObject([In] IntPtr hdc, [In] IntPtr hgdiobj);

    [DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool DeleteObject([In] IntPtr hObject);


    [DllImport("gdi32.dll")]
    public static extern int SetBkMode(IntPtr hdc, int iBkMode);

    public enum BkMode
    {
      TRANSPARENT = 1,
      OPAQUE = 2
    }

    [DllImport("gdi32.dll")]
    public static extern uint SetTextColor(IntPtr hdc, int crColor);

    [Flags]
    public enum ETOOptions : uint
    {
      ETO_CLIPPED = 0x4,
      ETO_GLYPH_INDEX = 0x10,
      ETO_IGNORELANGUAGE = 0x1000,
      ETO_NUMERICSLATIN = 0x800,
      ETO_NUMERICSLOCAL = 0x400,
      ETO_OPAQUE = 0x2,
      ETO_PDY = 0x2000,
      ETO_RTLREADING = 0x800,
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
      public int _Left;
      public int _Top;
      public int _Right;
      public int _Bottom;
    }

    [DllImport("gdi32.dll", EntryPoint = "ExtTextOutW")]
    public static extern bool ExtTextOut(IntPtr hdc, int X, int Y, uint fuOptions,
    [In] ref RECT lprc, [MarshalAs(UnmanagedType.LPWStr)] string lpString, uint cbCount, [In] IntPtr lpDx);

    [DllImport("gdi32.dll", CharSet = CharSet.Unicode)]
    public static extern bool GetTextExtentPoint32(IntPtr hdc, string lpString, int cbString, out LPSIZE lpSize);


    public FormTextDraw4()
    {
      InitializeComponent();
    }

    private void panel1_Paint(object sender, PaintEventArgs e)
    {
      string DrawText = "ぺんぎんクッキー";
      Font f = new Font("Yu Gothic UI", 28);

      RECT rect;
      rect._Top = 0;
      rect._Left = 0;
      rect._Bottom = 1000;
      rect._Right = 1000;

      IntPtr hDC = e.Graphics.GetHdc();

      SetBkMode(hDC, (int)BkMode.TRANSPARENT);
      SetTextColor(hDC, ColorTranslator.ToWin32(Color.Black));
      IntPtr oldfnt = SelectObject(hDC, f.ToHfont());
      ExtTextOut(hDC, 0, 10, (uint)ETOOptions.ETO_CLIPPED, ref rect, DrawText, (uint)DrawText.Length, IntPtr.Zero);
      DeleteObject(SelectObject(hDC, oldfnt));

      e.Graphics.ReleaseHdc(hDC);


      //
      hDC = e.Graphics.GetHdc();
      SetBkMode(hDC, (int)BkMode.TRANSPARENT);
      SetTextColor(hDC, ColorTranslator.ToWin32(Color.Black));
      oldfnt = SelectObject(hDC, f.ToHfont());

      int xpos = 0;
      for (int i = 0; i < DrawText.Length; i++)
      {
        ExtTextOut(hDC, xpos, 70, (uint)ETOOptions.ETO_CLIPPED, ref rect, DrawText.Substring(i,1), 1, IntPtr.Zero);
        xpos += GetTextWidth(DrawText.Substring(i, 1), 1, hDC, f);
      }

      DeleteObject(SelectObject(hDC, oldfnt));
      e.Graphics.ReleaseHdc(hDC);

    }

    private int GetTextWidth(string text, int SourceLength, IntPtr hdc, Font f)
    {
      LPSIZE textSize;
      IntPtr oldfnt = SelectObject(hdc, f.ToHfont());
      GetTextExtentPoint32(hdc, text, SourceLength, out textSize);
      DeleteObject(SelectObject(hdc, oldfnt));
      return textSize.cx;
    }
  }
}

解説

こちらの記事で紹介しているコードと同様です。
上から順番に
  • ExtTextOut関数を利用してPanelに文字列を一括で描画します。
  • ExtTextOut関数利用して一文字ずつ文字を描画します。文字の幅は、GetTextExtentPoint32で求めます。

実行結果

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


Yu Gothic UIフォントwで描画した場合、こちらの結果も一文字づつ描画した場合は文字の幅がわずかに大きくなる結果が確認できます。

原因

Yu Gothic UIは「プロポーショナル メトリクス」フォントの機能に対応しているフォントです。
プロポーショナル メトリクスに対応している場合、前後の文字の形状に合わせて文字単位で詰める処理をするため、 一文字づつ描画した場合より横幅が短くなる場合や長くなる場合があります。
今回の例の場合は、「ぎ」と「ん」の文字の形状の関係で、通常より「ぎ」の幅を短くしてより左側に「ん」を描画したほうが きれいに配置できるため、通常の幅よりも短く描画されています。



対処法

プロポーショナル メトリクスの配置情報を取得する方法として、Windows API の GetTextExtentExPoint 関数を利用すると、 個々の文字の位置を取得でき、プロポーショナルメトリクス適用後の文字列の幅を求められます。

UI

下図のフォームを作成します。Panelを1つ配置します。

コード

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.Runtime.InteropServices;
using System.Security.Policy;

namespace KerningDemo
{
  public partial class FormTextDraw5 : Form
  {
    [DllImport("gdi32.dll", EntryPoint = "SelectObject")]
    public static extern IntPtr SelectObject([In] IntPtr hdc, [In] IntPtr hgdiobj);

    [DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool DeleteObject([In] IntPtr hObject);


    [DllImport("gdi32.dll")]
    public static extern int SetBkMode(IntPtr hdc, int iBkMode);

    public enum BkMode
    {
      TRANSPARENT = 1,
      OPAQUE = 2
    }

    [DllImport("gdi32.dll")]
    public static extern uint SetTextColor(IntPtr hdc, int crColor);

    [Flags]
    public enum ETOOptions : uint
    {
      ETO_CLIPPED = 0x4,
      ETO_GLYPH_INDEX = 0x10,
      ETO_IGNORELANGUAGE = 0x1000,
      ETO_NUMERICSLATIN = 0x800,
      ETO_NUMERICSLOCAL = 0x400,
      ETO_OPAQUE = 0x2,
      ETO_PDY = 0x2000,
      ETO_RTLREADING = 0x800,
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
      public int _Left;
      public int _Top;
      public int _Right;
      public int _Bottom;
    }

    [DllImport("gdi32.dll", EntryPoint = "ExtTextOutW")]
    public static extern bool ExtTextOut(IntPtr hdc, int X, int Y, uint fuOptions,
    [In] ref RECT lprc, [MarshalAs(UnmanagedType.LPWStr)] string lpString, uint cbCount, [In] IntPtr lpDx);

    [DllImport("gdi32.dll", CharSet = CharSet.Unicode)]
    public static extern bool GetTextExtentPoint32(IntPtr hdc, string lpString, int cbString, out LPSIZE lpSize);

    [DllImport("gdi32.dll", EntryPoint = "GetTextExtentExPointW")]
    static extern bool GetTextExtentExPoint(IntPtr hdc, [MarshalAs(UnmanagedType.LPWStr)] string lpszStr,
      int cchString, int nMaxExtent, out int lpnFit, int[] alpDx, out SIZE lpSize);


    [StructLayout(LayoutKind.Sequential)]
    public struct SIZE
    {
      public int cx;
      public int cy;

      public SIZE(int cx, int cy)
      {
        this.cx = cx;
        this.cy = cy;
      }
    }


    public FormTextDraw5()
    {
      InitializeComponent();
    }

    private void panel1_Paint(object sender, PaintEventArgs e)
    {
      string DrawText = "ぺんぎんクッキー";
      Font f = new Font("Yu Gothic UI", 28);

      RECT rect;
      rect._Top = 0;
      rect._Left = 0;
      rect._Bottom = 1000;
      rect._Right = 1000;

      IntPtr hDC = e.Graphics.GetHdc();

      SetBkMode(hDC, (int)BkMode.TRANSPARENT);
      SetTextColor(hDC, ColorTranslator.ToWin32(Color.Black));
      IntPtr oldfnt = SelectObject(hDC, f.ToHfont());
      ExtTextOut(hDC, 0, 10, (uint)ETOOptions.ETO_CLIPPED, ref rect, DrawText, (uint)DrawText.Length, IntPtr.Zero);
      DeleteObject(SelectObject(hDC, oldfnt));

      e.Graphics.ReleaseHdc(hDC);


      //
      hDC = e.Graphics.GetHdc();
      SetBkMode(hDC, (int)BkMode.TRANSPARENT);
      SetTextColor(hDC, ColorTranslator.ToWin32(Color.Black));
      oldfnt = SelectObject(hDC, f.ToHfont());

      int xpos = 0;
      for (int i = 0; i < DrawText.Length; i++) {
        ExtTextOut(hDC, xpos, 70, (uint)ETOOptions.ETO_CLIPPED, ref rect, DrawText.Substring(i, 1), 1, IntPtr.Zero);
        xpos += GetTextWidth(DrawText.Substring(i, 1), 1, hDC, f);
      }


      //
      int FitCount;
      int[] Dx = new int[DrawText.Length];
      SIZE sz;
      GetTextExtentExPoint(hDC, DrawText, DrawText.Length, 1024, out FitCount, Dx, out sz);

      for (int i = 0; i < DrawText.Length; i++) {
        int posx;
        if (i == 0) posx = 0; else posx = Dx[i - 1];
          ExtTextOut(hDC, posx, 130, (uint)ETOOptions.ETO_CLIPPED, ref rect, DrawText.Substring(i, 1), 1, IntPtr.Zero);
      }

      //
      DeleteObject(SelectObject(hDC, oldfnt));
      e.Graphics.ReleaseHdc(hDC);

    }

    private int GetTextWidth(string text, int SourceLength, IntPtr hdc, Font f)
    {
      LPSIZE textSize;
      IntPtr oldfnt = SelectObject(hdc, f.ToHfont());
      GetTextExtentPoint32(hdc, text, SourceLength, out textSize);
      DeleteObject(SelectObject(hdc, oldfnt));
      return textSize.cx;
    }
  }
}

解説

上から順番に
  • ExtTextOut関数を利用してPanelに文字列を一括で描画します。
  • ExtTextOut関数利用して一文字ずつ文字を描画します。文字の幅は、GetTextExtentPoint32で求めます。
  • ExtTextOut関数利用して一文字ずつ文字を描画します。文字の幅は、GetTextExtentExPointの情報を利用します。

GetTextExtentExPointを呼び出します。描画したい文字列の各文字の位置を取得します。
GetTextExtentExPointのパラメーターは次の通りです。
  • 第一引数: デバイスコンテキストのhDC
  • 第二引数: 座標を取得したい(描画テキスト)の文字列
  • 第三引数: 第二引数の文字列の長さ
  • 第四引数: 描画文字列の最大ピクセル幅を与えます。今回は1,024が固定値で与えていますが、クライアント領域の横幅の値にしても良いかと思われます。
  • 第五引数: 座標が設定できた文字数
  • 第六引数: それぞれの文字の座標値
  • 第七引数: 描画文字全体の幅と高さのサイズを返します。

  int FitCount;
  int[] Dx = new int[DrawText.Length];
  SIZE sz;
  GetTextExtentExPoint(hDC, DrawText, DrawText.Length, 1024, out FitCount, Dx, out sz);

forループで一文字づつPaneに文字を描画します。最初の文字は0から開始します。Dx[i]はi文字目の右端座標値が設定されますので、 i文字目を描画するときのx座標は、(最初の文字のx座標)+Dx[i-1] となります。
文字の描画は、ExtTextOutを利用します。
  for (int i = 0; i < DrawText.Length; i++) {
    int posx;
    if (i == 0) posx = 0; else posx = Dx[i - 1];
      ExtTextOut(hDC, posx, 130, (uint)ETOOptions.ETO_CLIPPED, ref rect, DrawText.Substring(i, 1), 1, IntPtr.Zero);
  }

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。
GetTextExtentExPoint を利用して一文字づつ文字を描画した結果は、ExtTextOutでまとめて文字を描画した結果と一致していることがわかります。


「ぎ」と「ん」の文字間の間隔も詰められて描画できています。


Yu Gothic UIのフォントを利用した場合でも、一文字ずつ描画した場合でも、まとめて描画した場合と同様にずれなく描画できました。
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
掲載日: 2023-01-06
iPentec all rights reserverd.