C# Programming

ImageGDI+ (Font)

開発環境: Visual Studio 2003 

1.目次

2.目的

GDI+ の特にフォント周りについてです。基本的なことなので、押さえておかないとね。

3.参考

(1) Microsoft チュートリアル

4.フォントの扱い

Font には、ご存知のように山のようにフォントがあります。これは、コントロールパネル→フォントで確認できます。
これらのフォントのうち、Courier のようなビットマップフォントと Arial のようなアウトラインフォントがありますが、どちらも同じインターフェースでアクセスできます。ただし、フォントのアウトラインパスをとる場合は、当然アウトラインフォントだけにしか適用できません。

Fontスタイルには、次のようなものがあります。

フォントスタイル説明
FontStyle.Regular標準体
FontStyle.Bold太字テキスト
FontStyle.Italic斜体テキスト
FontStyle.Strikeout中央に線が引かれているテキスト
FontStyle.Underline下線付きテキスト

フォントを指定して文字を書く。
SolidBrush brush = new SolidBrush(SystemColors.WindowText);
Font font = new Font("Courier", 12);
g.DrawString("文字列", font, brush, x, y);
フォントのスタイルを指定して文字を書く。
SolidBrush brush = new SolidBrush(SystemColors.WindowText);
Font font = new Font("Courier", 12, FontStyle.Bold);
g.DrawString("文字列", font, brush, x, y);

注意 フォントのデフォルト色は? 
Windows のお作法に従い、SystemColors.WindowText を使用すべきです。

ここでは、C#から、どのようなフォントがあるのか実際に確認して、プログラム上の注意点をまとめます。
OnPaint イベントで、FontFamily すべてと FontStyle の組み合わせのすべてをプリントして確認してみましょう。

Image

注意 すべてのフォントスタイルがサポートされているとは限らない 
ここでは、IsStyleAvailable() でサポートされていないフォントスタイルの場合には※※※NA※※※ をプリントするようにしています。
上の図でわかるように、フォントファミリーによってはすべてのフォントスタイルがサポートされているとは限りません。
FontStyle でサポートされていないフォントを作成しようとすると、System.ArgumentException があがります。
FontStyle がサポートされているかどうか FontFamily.IsStyleAvailable() で確認するか、例外をキャッチする必要があります。


注意 同じフォントサイズを指定してもフォントの高さが異なる。 
上の図で、すべて同じフォントサイズを指定します。それにもかかわらず、フォントの行間隔が異なっていることがわかります。
これはこのプログラムのバグではなくって、同じフォントサイズを指定してもフォントが返す高さ自体が異なっています。
ものによっては Gautami フォントのようにどうしてこんな変な幅を返すのかわからないものもありますが、フォントによって幅だけでなく高さも異なることを注意しておくべきです。それでないと、フォントが違ったときに表示位置が異なってしまう場合があります。

4-1.フォントのスタイル確認用テストコード

2003/1/18 初版作成  簡単なコードなので、コード中のコメントを読んでもらえればわかると思います。

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

namespace EnumFonts
{
    /// <summary>
    /// Form1 の概要の説明です。
    /// </summary>
    public class Form1 : System.Windows.Forms.Form
    {
        /// <summary>
        /// 必要なデザイナ変数です。
        /// </summary>
        private System.ComponentModel.Container components = null;

        public Form1()
        {
            //
            // Windows フォーム デザイナ サポートに必要です。
            //
            InitializeComponent();

            //
            // TODO: InitializeComponent 呼び出しの後に、コンストラクタ コードを追加してください。
            //
            DrawFonts();
        }

        /// <summary>
        /// 使用されているリソースに後処理を実行します。
        /// </summary>
        protected override void Dispose( bool disposing )
        {
            if( disposing )
            {
                if (components != null) 
                {
                    components.Dispose();
                }
            }
            base.Dispose( disposing );
        }

                #region Windows Form Designer generated code
        /// <summary>
        /// デザイナ サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディタで変更しないでください。
        /// </summary>
        private void InitializeComponent()
        {
            this.pictureBox1 = new System.Windows.Forms.PictureBox();
            this.SuspendLayout();
            // 
            // pictureBox1
            // 
            this.pictureBox1.Name = "pictureBox1";
            this.pictureBox1.Size = new System.Drawing.Size(200, 176);
            this.pictureBox1.TabIndex = 0;
            this.pictureBox1.TabStop = false;
            this.pictureBox1.Paint += new System.Windows.Forms.PaintEventHandler(this.pictureBox1_Paint);
            // 
            // Form1
            // 
            this.AutoScaleBaseSize = new System.Drawing.Size(5, 12);
            this.AutoScroll = true;
            this.ClientSize = new System.Drawing.Size(292, 266);
            this.Controls.AddRange(new System.Windows.Forms.Control[] {
                                                                          this.pictureBox1});
            this.Name = "Form1";
            this.Text = "Form1";
            this.ResumeLayout(false);

        }
                #endregion

        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main() 
        {
            Application.Run(new Form1());
        }

        private System.Windows.Forms.PictureBox pictureBox1;

        private Bitmap bitmap = null;
            
        /// <summary>
        /// 表示速度の高速化のため、一度すべてのフォントを Bitmap に描画してから、
        /// PictureBox に OnPaint で描画するようにしています。
        /// </summary>
        private void DrawFonts()
        {   
            float x = 0f;   // フォントのX 描画位置
            float y = 0f;   // フォントのY 描画位置
            // おおもとのBitmap の大きさは、決めうちでかなり大きめに取ってます。
            Size winSize = SystemInformation.PrimaryMonitorMaximizedWindowSize;
            bitmap = new Bitmap(winSize.Width, winSize.Height*2);
            Graphics g = Graphics.FromImage(bitmap);
            // フォントスタイルを列挙して、Array に入れておく。
            Array fontStyles = Enum.GetValues(typeof(FontStyle));

            Font font = null;
            // デフォルトのフォント色は、SystemColors.WindowText がお勧め。
            SolidBrush brush = new SolidBrush(SystemColors.WindowText);
            SolidBrush redBrush = new SolidBrush(Color.Red);
                        
            // 横方向にフォントスタイル
            foreach(FontStyle fs in fontStyles)
            {
                // 縦方向にフォントファミリー
                float width = 0f;
                foreach(FontFamily ff in FontFamily.Families)
                {
                    // スタイルがサポートされているかどうか確認する。
                    if(ff.IsStyleAvailable(fs))
                    {
                        font = new Font(ff, 10, fs);
                        g.DrawString(ff.Name, font, brush, x, y);
                        width = Math.Max(width, g.MeasureString(ff.Name, font).Width);
                    }
                    else
                    {
                        // スタイルがサポートされていなければ、デフォルトフォントで書く。
                        g.DrawString("※※※NA※※※", this.Font, redBrush, x, y);                                      
                        width = Math.Max(width, g.MeasureString("※※※NA※※※", this.Font).Width);
                    }
                    y += font.Height;
                    this.pictureBox1.Height = (int)y;
                }
                x += width;
                this.pictureBox1.Width = (int)x;
                y = 0;
            }
        }

        /// <summary>
        /// PictureBox の OnPaint で Bitmap から DrawImage することにより、
        /// 毎回フォントを作成して描画することを避ける。
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void pictureBox1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
        {
            Graphics g = e.Graphics;

            Rectangle rect = new Rectangle(new Point(0, 0), this.pictureBox1.Size);
            g.DrawImage(bitmap, rect,  rect, GraphicsUnit.Pixel);
        }
    }
}

5.フォントのパス

GraphicsPath.AddString(...) でフォントのアウトラインを取得することができます。(下図)
Image
フォントのアウトラインパスを取得する。
        private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
        {
            // MS明朝のアウトラインを描く。
            Graphics g = e.Graphics;
            GraphicsPath gPath = new System.Drawing.Drawing2D.GraphicsPath();
            SolidBrush brush = new SolidBrush(SystemColors.WindowText);
            FontFamily ff = new FontFamily("MS 明朝");
            gPath.AddString("MS明朝", ff, (int)FontStyle.Bold, 
                50, new Point(0, 0), StringFormat.GenericDefault);
            g.DrawPath(new Pen(Color.Black, 1f), gPath);

            GraphicsState gs = g.Save();
            g.TranslateTransform(0f, 50f);
            gPath.Reset();
            gPath.AddString("目", ff, (int)FontStyle.Bold, 
                25, new Point(0, 0), StringFormat.GenericDefault);
            g.DrawPath(new Pen(Color.Black, 1f), gPath);
            g.TranslateTransform(50f, 0.5f);
            g.DrawPath(new Pen(Color.Black, 1f), gPath);
            g.Restore(gs);

            // XX を重ねて描く。
            g.TranslateTransform(0f, 50f);
            gPath.Reset();
            gPath.AddString("X", ff, (int)FontStyle.Bold, 
                200, new Point(0, 0), StringFormat.GenericDefault);
            gPath.AddString("X", ff, (int)FontStyle.Bold, 
                200, new Point(20, 0), StringFormat.GenericDefault);
            gPath.AddString("X", ff, (int)FontStyle.Bold, 
                200, new Point(40, 0), StringFormat.GenericDefault);
            gPath.FillMode = FillMode.Alternate;
            g.FillPath(new SolidBrush(Color.Cyan), gPath);
            g.DrawPath(new Pen(Color.Black, 1f), gPath);
        }
注意 小さなフォントのパスを処理する場合の注意点
このアウトラインパスになった段階で、フォントのヒント情報(線幅を同じにするようにピクセル上に線を描画するための情報など)が欠落します。
したがって、細かい字のパスを描画した場合、例えば線幅が同じにならない可能性があります。

どういうことかというと、上の図で"目"が2つ描かれていますが、微妙に線幅が違うのがわかりますか?
上のソースを見てもらえばわかりますが、フォントパスは同じですが、右の"目"は、左の"目"より、0.5ピクセル下に描画したものです。
拡大図を下図に示します。アウトラインフォントは通常、線幅を調整するためのヒント情報持っており、DrawString で描画した場合は、線の幅は同じになるように自動的に調整してくれます。しかし、ヒント情報はピクセルに対して、いかにフォントをレイアウトするかと言う情報ですから、ピクセルに関係しないフォントパスにはこのヒント情報は含まれません。このため、下図のように小さいフォントのパスを描画すると線幅が異なってしまう場合があります。
一言で言うと、アウトライン情報を Pixel に量子化するときの誤差が顕在化してしまうということです。これは Pixel に対して、パスが小さい場合に顕著になりますが、大きいフォントの場合は量子化誤差は無視できる程度なので問題なくなります。

Image

Xを3つ重ねているのは、こんなこともできるという意味なしプログラムです。^^;