ワードラップの座標を計算して文字を描画する - C#

ワードラップの座標を計算して文字を描画するコードを紹介します。

概要

こちらの記事では、指定した範囲内でワードラップして文字列を描画するコードを紹介しました。 単純にワードラップされた文字列を描画する場合は、紹介した記事の方法で問題ありませんが、 個々の文字のワードラップされた座標値を求めたい場合には、文字の位置を計算により求める必要があります。 この記事では、文字の座標値を計算してワードラップ処理をし文字列を描画するコードを紹介します。

方針

文字ごとの座標値を求めるには、GetTextExtentExPoint()関数を利用します。
文字の座標を求めたのち、指定された範囲に収まる文字数を計算して描画してワードラップされた文字列を描画できます。
補足
この記事で紹介している方法では、横幅ぴったりに文字を表示するため、アルファベットの単語の途中でワードラップの分割が発生します。 英単語の途中で極力分割をしないワードラップのロジックについてはこちらの記事を参照してください。

プログラム例:GetTextExtentExPointとExtTextOut関数を利用する場合

UI

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

コード

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

namespace DrawStringWordWrap
{
  public partial class FormWordWrapCalcDraw : Form
  {
    public enum BkMode
    {
      TRANSPARENT = 1,
      OPAQUE = 2
    }

    [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;
    }
    

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

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

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

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

    [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", 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);

    [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);


    public FormWordWrapCalcDraw()
    {
      InitializeComponent();
    }

    private void panel1_Paint(object sender, PaintEventArgs e)
    {

      int areaLeft = 128;
      int areaTop = 32;
      int areaWidth = 128;
      int areaHeight = 160;
      Pen p = new Pen(Color.FromArgb(0x45, 0x72, 0xAA));
      Rectangle r = new Rectangle(areaLeft, areaTop, areaWidth, areaHeight);
      e.Graphics.DrawRectangle(p, r);


      Font f = new Font("Yu Gothic UI", 14);
      string DocText = "ぺんぎんクッキー(Penguin Cookie)を食べれば、みんなHappy Time!!";

      IntPtr hDC = e.Graphics.GetHdc();
      
      SetTextColor(hDC, ColorTranslator.ToWin32(Color.Black));
      IntPtr oldfnt = SelectObject(hDC, f.ToHfont());

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

      for (int i = 0; i < Dx.Length; i++) {
        textBox1.Text += string.Format("{0:d}\r\n",Dx[i]);
      }

      int idx = 0;
      int startidx = 0;
      int startpos = 0;
      int posx = areaLeft;
      int posy = areaTop;

      RECT rect = new RECT() { _Left=areaLeft, _Top=areaTop, _Right=areaLeft+areaHeight, _Bottom= areaTop+areaHeight};

      SetBkMode(hDC, (int)BkMode.TRANSPARENT);

      while (idx < Dx.Length-1) {
        if (areaWidth < Dx[idx + 1] - startpos) {
          string drawText = DocText.Substring(startidx, idx - startidx+1);
          ExtTextOut(hDC, posx, posy, (uint)ETOOptions.ETO_CLIPPED, ref rect, drawText, (uint)drawText.Length, IntPtr.Zero);
          posy += sz.cy;
          startidx = idx+1;
          startpos = Dx[idx];
        }
        idx++;
      }

      string lastdrawText = DocText.Substring(startidx);
      ExtTextOut(hDC, posx, posy, (uint)ETOOptions.ETO_CLIPPED, ref rect, lastdrawText, (uint)lastdrawText.Length, IntPtr.Zero);

      IntPtr cfont = SelectObject(hDC, oldfnt);
      DeleteObject(cfont);

      e.Graphics.ReleaseHdc();
    }
  }
}

解説

ワードラップのテキストを描画するエリアの枠を描画します。枠線の色は青色にします。
  int areaLeft = 128;
  int areaTop = 32;
  int areaWidth = 128;
  int areaHeight = 160;
  Pen p = new Pen(Color.FromArgb(0x45, 0x72, 0xAA));
  Rectangle r = new Rectangle(areaLeft, areaTop, areaWidth, areaHeight);
  e.Graphics.DrawRectangle(p, r);

描画するフォントを準備します。
  Font f = new Font("Yu Gothic UI", 14);

描画する文字列を準備します。
  string DocText = "ぺんぎんクッキー(Penguin Cookie)を食べれば、みんなHappy Time!!";

GetTextExtentExPoint 関数を呼び出す際にデバイスコンテキストのハンドルが必要になるため、GraphicsオブジェクトのGetHdc メソッドを 呼び出して、デバイスコンテキストのハンドルを取得します。
  IntPtr hDC = e.Graphics.GetHdc();

グラフィックスのデバイスコンテキストに文字の描画色と描画のフォントを設定します。
  SetTextColor(hDC, ColorTranslator.ToWin32(Color.Black));
  IntPtr oldfnt = SelectObject(hDC, f.ToHfont());

GetTextExtentExPoint 関数を呼び出して各文字の座標値を求めます。
文字の座標はDx配列に設定されます。
  int[] Dx = new int[DocText.Length];
  int FitCount;
  SIZE sz;
  GetTextExtentExPoint(hDC, DocText, DocText.Length, 1024, out FitCount, Dx, out sz);

テキストボックスにDx配列の値を表示します。
  for (int i = 0; i < Dx.Length; i++) {
    textBox1.Text += string.Format("{0:d}\r\n",Dx[i]);
  }

テキストを描画するエリアのRECTオブジェクトを作成します。
  int idx = 0;
  int startidx = 0;
  int startpos = 0;
  int posx = areaLeft;
  int posy = areaTop;

  RECT rect = new RECT() { _Left=areaLeft, _Top=areaTop, _Right=areaLeft+areaHeight, _Bottom= areaTop+areaHeight};

SetBkMode関数を呼び出し、ExtTextOut関数でテキストを描画する背景色を透明色に設定します。
  SetBkMode(hDC, (int)BkMode.TRANSPARENT);

Dxの値を確認して描画エリアに入る文字数を調べ、描画エリアの横幅を超えたら、手前の文字までを画面に描画します。
次の行の高さは、GetTextExtentExPoint関数の戻り値のSIZEオブジェクトのsz変数のcyに設定されていますので、描画位置のY座標にsz.cyの値を加算します。
  while (idx < Dx.Length-1) {
    if (areaWidth < Dx[idx + 1] - startpos) {
      string drawText = DocText.Substring(startidx, idx - startidx+1);
      ExtTextOut(hDC, posx, posy, (uint)ETOOptions.ETO_CLIPPED, ref rect, drawText, (uint)drawText.Length, IntPtr.Zero);
      posy += sz.cy;
      startidx = idx+1;
      startpos = Dx[idx];
    }
    idx++;
  }

  string lastdrawText = DocText.Substring(startidx);
  ExtTextOut(hDC, posx, posy, (uint)ETOOptions.ETO_CLIPPED, ref rect, lastdrawText, (uint)lastdrawText.Length, IntPtr.Zero);

フォントを元のフォントに戻し、作成したフォントを削除します。
  IntPtr cfont = SelectObject(hDC, oldfnt);
  DeleteObject(cfont);

最後にデバイスコンテキストのハンドルを開放します。
  e.Graphics.ReleaseHdc();

実行結果

プロジェクトを実行します。Panelの青色の枠内にワードラップされたテキストが描画されます。

プログラム例:GetTextExtentExPoint関数とTextRendererクラスを利用する場合

GetTextExtentExPoint関数とTextRendererクラスを利用する場合のコードを紹介します。

UI

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

コード

以下のコードを記述します。
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;

namespace DrawStringWordWrap
{
  public partial class FormWordWrapCalcDrawTextRenderer : Form
  {
    [StructLayout(LayoutKind.Sequential)]
    public struct SIZE
    {
      public int cx;
      public int cy;

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

    [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", 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);


    public FormWordWrapCalcDrawTextRenderer()
    {
      InitializeComponent();
    }

    private void panel1_Paint(object sender, PaintEventArgs e)
    {
      Font f = new Font("Yu Gothic UI", 14);
      string DocText = "ぺんぎんクッキー(Penguin Cookie)を食べれば、みんなHappy Time!!";

      int areaLeft = 128;
      int areaTop = 32;
      int areaWidth = 128;
      int areaHeight = 160;
      Pen p = new Pen(Color.FromArgb(0x45, 0x72, 0xAA));
      Rectangle r = new Rectangle(areaLeft, areaTop, areaWidth, areaHeight);

      e.Graphics.DrawRectangle(p, r);

      int[] Dx = new int[DocText.Length];
      int FitCount;
      SIZE sz;

      IntPtr hDC = e.Graphics.GetHdc();

      IntPtr oldfnt = SelectObject(hDC, f.ToHfont());
      GetTextExtentExPoint(hDC, DocText, DocText.Length, 1024, out FitCount, Dx, out sz);
      IntPtr cfont = SelectObject(hDC, oldfnt);
      DeleteObject(cfont);

      e.Graphics.ReleaseHdc();

      for (int i = 0; i < Dx.Length; i++) {
        textBox1.Text += string.Format("{0:d}\r\n", Dx[i]);
      }

      int idx = 0;
      int startidx = 0;
      int startpos = 0;
      int posx = areaLeft;
      int posy = areaTop;

      while (idx < Dx.Length - 1) {
        if (areaWidth < Dx[idx + 1] - startpos) {
          string drawText = DocText.Substring(startidx, idx - startidx + 1);
          TextRenderer.DrawText(e.Graphics, drawText, f, new Rectangle(posx,posy,areaWidth,sz.cy),
            Color.Black, TextFormatFlags.Default | TextFormatFlags.NoPadding);

          posy += sz.cy;
          startidx = idx + 1;
          startpos = Dx[idx];
        }
        idx++;
      }

      string lastdrawText = DocText.Substring(startidx);
      TextRenderer.DrawText(e.Graphics, lastdrawText, f, new Rectangle(posx, posy, areaWidth, sz.cy),
        Color.Black, TextFormatFlags.Default | TextFormatFlags.NoPadding);
    }
  }
}

解説

ワードラップのテキストを描画するエリアの枠を描画します。枠線の色は青色にします。
  int areaLeft = 128;
  int areaTop = 32;
  int areaWidth = 128;
  int areaHeight = 160;
  Pen p = new Pen(Color.FromArgb(0x45, 0x72, 0xAA));
  Rectangle r = new Rectangle(areaLeft, areaTop, areaWidth, areaHeight);

  e.Graphics.DrawRectangle(p, r);

デバイスコンテキストのハンドを取得し、SelectObject関数で描画に使用するフォントをデバイスコンテキストに割り当てます。 その後、GetTextExtentExPoint関数を実行し、各文字の座標値を求めます。
  int[] Dx = new int[DocText.Length];
  int FitCount;
  SIZE sz;

  IntPtr hDC = e.Graphics.GetHdc();

  IntPtr oldfnt = SelectObject(hDC, f.ToHfont());
  GetTextExtentExPoint(hDC, DocText, DocText.Length, 1024, out FitCount, Dx, out sz);
  IntPtr cfont = SelectObject(hDC, oldfnt);
  DeleteObject(cfont);

  e.Graphics.ReleaseHdc();

Dx変数の値をテキストボックスに表示します。
  for (int i = 0; i < Dx.Length; i++) {
    textBox1.Text += string.Format("{0:d}\r\n", Dx[i]);
  }

Dxの値を確認して描画エリアに入る文字数を調べ、描画エリアの横幅を超えたら、手前の文字までを画面に描画します。
画面描画は、TextRendererクラスのDrawTextメソッドを利用します。パディングは設定しないため、TextFormatFlags.Default | TextFormatFlags.NoPadding を TextFormatFlagsオプションに指定して画面を描画します。
次の行の高さは、GetTextExtentExPoint関数の戻り値のSIZEオブジェクトのsz変数のcyに設定されていますので、 描画位置のY座標にsz.cyの値を加算します。
  int idx = 0;
  int startidx = 0;
  int startpos = 0;
  int posx = areaLeft;
  int posy = areaTop;

  while (idx < Dx.Length - 1) {
    if (areaWidth < Dx[idx + 1] - startpos) {
      string drawText = DocText.Substring(startidx, idx - startidx + 1);
      TextRenderer.DrawText(e.Graphics, drawText, f, new Rectangle(posx,posy,areaWidth,sz.cy),
        Color.Black, TextFormatFlags.Default | TextFormatFlags.NoPadding);

      posy += sz.cy;
      startidx = idx + 1;
      startpos = Dx[idx];
    }
    idx++;
  }

  string lastdrawText = DocText.Substring(startidx);
  TextRenderer.DrawText(e.Graphics, lastdrawText, f, new Rectangle(posx, posy, areaWidth, sz.cy),
    Color.Black, TextFormatFlags.Default | TextFormatFlags.NoPadding);

実行結果

プロジェクトを実行します。Panelの青色の枠内にワードラップされたテキストが描画されます。

著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
掲載日: 2023-02-03
iPentec all rights reserverd.