在传统西文印刷中,一些字体会在显示不同字符的时候,使用不同的宽度,比如“m”会比“i”会更宽,这就是比例字体(proportional font),它可以提高单词的可读性。但早期的电脑显示器、打字机,由于技术的局限,无法进行字母宽度的比例调整,因此将每个字元都制作成一样的宽度,从而形成了等宽字体(monospaced fonts)。在等宽字体中,字母“m”,“i”所占宽度一致,多余部分用空白填充,这使得字母“i”、“j”显得两侧空白较多,而字母“w”、“m”等的笔画显得相当拥挤。

    但是随着GUI的流行以及处理比例字体的突破,因此排版上显得比较自然的比例字体的使用已经相当普及,我们在Word等文档里大部分的字体都是比例字体。

如何区分字体是否为等宽字体


▲ 比例字体和等宽字体

    等宽字体顾名思义就是所有的字符宽度都是相等的。最直观的就是判断一些比如“i”、“m”、空格所占的宽度,比如上图。

    在“C:\Windows\Fonts”下面有很多字体,我们可以选择两个字体"Arial"和"Courier New" 打开来看一下:

    可以看到"Arial"是典型的比例字体,“123456789”这是9个字符,跟总宽度相等的"abcdefghijkl"有12个字符,可以很明显的看到ij这类字符要比hk要窄。而"Courier New"则是等宽字体,如果以上区别不明显,我们还可以在Word中显示网格线,并输入一些字符来查看差别,用两种字体分别输入了0、o、O、i、I(大写的i)、l(小写的L)和L,以及数字0,1,2,3,4,5,6 来查看区别:

▲等宽字体和比例字体的对不同字符显示所使用宽度的区别

    可以看到,比例字体的大写O几乎占了3个宽度,而i只占用了1个宽度。另外,在比例字体中,大写的i和小写的L,完全无法区分😂。

代码中应该使用的字体


   现实生活中大部分都是比例字体,比如微信默认字体,因为看起来可能更加紧凑美观。但程序开发方面,一般都会使用等宽字体,相比于比例字体,等宽字体有以下优点:

  • 更好辨认,比如一些比如容易混淆的字符,0和O,数字1、小写字母i、大写字母I、小写字母l、大写字母L。
  • 美观以及IDE对齐,常见的比如语句块{}的对齐,另外对于一些对缩进要求严格的语言,比如Python更是如此。
  • 很多编辑器提供列编辑的功能,比如同时在多行的同一位置处增删字符。

    最近微软推出了cascadia-code代码专用字体,可以看到Stack Overflow和本博客的代码已经使用了该字体。

   比如,Visual Studio和Visual Studio Code安装后,默认使用的字体Consolas就是等宽字体。

▲ Visual Studio中默认的字体

▲Visual Studio Code默认字体

如果通过代码判断字体是否等宽


    在某些情况下,比如我们可能需要列出所有计算机中的等宽字体供用户选择。做法很简单,那就是列出计算机内安装的所有字体,然后计算该字体下"w“和”i”的宽度,如果两者宽度相等,则是等宽字体。具体代码如下:

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Drawing;
using System.Drawing.Text;

namespace MonospaceFonts
{
    static class FontHelper
    {

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        class LOGFONT
        {
            public int lfHeight;
            public int lfWidth;
            public int lfEscapement;
            public int lfOrientation;
            public int lfWeight;
            public byte lfItalic;
            public byte lfUnderline;
            public byte lfStrikeOut;
            public byte lfCharSet;
            public byte lfOutPrecision;
            public byte lfClipPrecision;
            public byte lfQuality;
            public byte lfPitchAndFamily;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
            public string lfFaceName;
        }

        static bool IsMonospaced(Graphics g, Font f)
        {
            float w1, w2;

            w1 = g.MeasureString("i", f).Width;
            w2 = g.MeasureString("W", f).Width;
            return w1 == w2;
        }

        static bool IsSymbolFont(Font font)
        {
            const byte SYMBOL_FONT = 2;

            LOGFONT logicalFont = new LOGFONT();
            font.ToLogFont(logicalFont);
            return logicalFont.lfCharSet == SYMBOL_FONT;
        }

        /// <summary>
        /// Tells us, if a font is suitable for displaying document.
        /// </summary>
        /// <remarks>Some symbol fonts do not identify themselves as such.</remarks>
        /// <param name="fontName"></param>
        /// <returns></returns>
        static bool IsSuitableFont(string fontName)
        {
            return !fontName.StartsWith("ESRI") && !fontName.StartsWith("Oc_");
        }

        public static List<string> GetMonospacedFontNames()
        {
            List<string> fontList = new List<string>();
            InstalledFontCollection ifc;

            ifc = new InstalledFontCollection();
            using (Bitmap bmp = new Bitmap(1, 1))
            {
                using (Graphics g = Graphics.FromImage(bmp))
                {
                    foreach (FontFamily ff in ifc.Families)
                    {
                        if (ff.IsStyleAvailable(FontStyle.Regular) && ff.IsStyleAvailable(FontStyle.Bold)
                            && ff.IsStyleAvailable(FontStyle.Italic) && IsSuitableFont(ff.Name))
                        {
                            using (Font f = new Font(ff, 10))
                            {
                                if (IsMonospaced(g, f) && !IsSymbolFont(f))
                                {
                                    fontList.Add(ff.Name);
                                }
                            }
                        }
                    }
                }
            }
            return fontList;
        }
    }
}

    我这里使用的是.NET Core 2.0,需要引用System.Drawing.Common NuGet包,注意,这里最新的6.0版本可能会报“InstalledFontCollection”不支持该平台的bug,这里使用的是4.7版本,且该NuGet仅支持Windows平台。程序的做法很简单,首先列出本机所有的安装的字体,然后新建一个Graphics,测量"w"和“i”的宽度,如果相等,则为等宽字体。

    再新建一个控制台程序:

class Program
{
    static void Main(string[] args)
    {
        List<string> fonts = FontHelper.GetMonospacedFontNames();
        fonts.ForEach(Console.WriteLine);
        Console.ReadKey();
    }
}

     运行之后,就能列出本机安装的所有等宽字体了,我本机运行效果如下:

Cascadia Code
Cascadia Code ExtraLight
Cascadia Code Light
Cascadia Code SemiBold
Cascadia Code SemiLight
Cascadia Mono
Cascadia Mono ExtraLight
Cascadia Mono Light
Cascadia Mono SemiBold
Cascadia Mono SemiLight
Consolas
Courier New
DejaVu Sans Mono
HoloLens MDL2 Assets
Lucida Console
MS Gothic
Segoe MDL2 Assets
SimSun-ExtB
仿宋
宋体
幼圆
新宋体
楷体
細明體-ExtB
細明體_HKSCS-ExtB
隶书
黑体

       其实等宽字体不多,大部分都是比例字体。

 

参考资料