3.1 文本输出
文本输出比图像输出涉及更多的内容和概念。本小节介绍文本输出的基本概念和Windows上文本输出的两种基本方式及其模拟实现方法。下一节“字体管理”是本节内容的一个顺延,也是文本输出所依赖的重要内容。
3.1.1 基本概念
在介绍Windows的文本输出功能及其模拟方法之前,这里先介绍一下一些文本输出的基本概念。这些概念是与具体的平台无关的。了解这些背景知识将有助于后面的功能分析和具体的设计实现。
(1)Glyph
为了将计
算机所存储的字符串显示为可读的文字信息,格式化文本输出过程中的第一步要将字符转换为表示这个字符的图形,这个图形就是Glyph。例如,字符“A”的
ASCII码为0X41,Unicode编码为U+0041,则根据所选择的字体不同,其显示的图形(Glyph)可能是“”、“”或者“”。
需要注意的是,字符与Glyph并不是严格的一对多的关系。在一些特殊情况下(如专业的文字排版),某些字符连在一起输出时可能被显示为一个Glyph。例如,“f”和“l”可能被输出为“”。
(2)字体
字体是具
有同样风格的一系列Glyph的集合。一般地,字体的内容以字体文件的形式存储。打开Windows目录下的Fonts目录,可以看到Windows上安
装了多种字体。字体文件具有一定的公开格式,能够被Windows或者其他操作系统上的某些字体相关工具(如Freetype)读取。通过指定字符,或者
指定特定Glyph在字体中的编号,应用程序可以获得某个字符在字体中对应的图形(Glyph)。不同的字体具有不同的设计风格,应用程序可以根据自己的
需要选取合适的字体。也正因为对多种字体的需求,字体管理成为每个图形用户界面平台的重要任务之一。
(3)Layout
将字符串
中的字符转换为Glyph后,格式化文本输出的第二步是排列这些图形(Glyph)。所谓的Layout,即是图形(Glyph)在平面上的一个排列,也
指代形成这个排列的操作过程。以横向文字自左向右排列为例,基本方法是将所有字符的“基准线(Baseline)”对齐在同一水平线上,并按照每个图形
(Glyph)的宽度,调整后续图形的起始位置。如图3-1所示,是将几个单独的图形Layout的过程。
图3-1 将基准线对齐来做Layout
图中,横线为“Baseline”,在Glyph中的相对位置存储在字体中。在3.2节中还会对字体的相关概念做进一步的解释。
方框表示
的是该Glyph在Layout中所占的位置,也是存储在字体中的。Layout过程将根据这些位置和大小信息决定每个Glyph在Layout中所占的
实际位置。一旦Layout完成,其大小也将固定。这让编程人员在将它输出之前就可以知道这个字符串将占据的空间。
字体(尤
其是TrueType字体)是和显示设备无关的,即根据字体引擎的处理方法不同,Layout的结果可能并不能精确反映字符串在物理显示设备上的输出结
果。物理的显示设备(如屏幕和打印机)显示的最小单元是像素;但某些字体引擎可能使用更精确的浮点数进行Layout。例如,Linux的
FreeType和Mac OS X上的ATSUI使用浮点数进行设备无关的Layout;但在Windows上,Layout的结果只用整数表示。
因此,针对显示设备对Layout进行的调整,实际上是对浮点数的取整。由于取整方式的不同,可能会对最终的Layout结果有细微的影响。在大多数应用中,这点影响是可以忽略的。如果移植的应用程序对文字输出的位置要求极为严格,那么这一点差异需要引起注意。
显示过程就是将Layout结果中的每个Glyph画到显示设备上。每个Glyph的画法也是存储在字体中的。Layout的显示过程将调用设备的绘图功能把Glyph逐一画出。
3.1.2
格式化文本输出
Windows
应用程序的文本输出主要依赖两类API,分别是DrawText(Ex)和(Ext)TextOut。其他如TabbedTextOut、
PolyTextOut等则不太常见。尽管DrawText实际上是USER32子系统提供的函数,由于内容上的相似性,本节还是将这个函数和
ExtTextOut函数一起讨论。
DrawText
和DrawTextEx在指定矩形内输出文本,且有简单的格式化功能。根据输入的参数不同,DrawText和DrawTextEx还可以调整并返回矩形
的长、宽,但并不输出文本。应用程序可以利用这一特性来获得将要输出的文本所占据的确切区域。表3-1分类描述了DrawText和DrawTextEx
的参数及其实现要求。
表3-1
DrawText的参数
参 数
|
描 述
|
实现要求
|
DT_BOTTOM
DT_TOP
DT_VCENTER
|
文字的纵向对齐方式;
只对DT_SINGLELINE有效
|
根据给定的矩形区域和实际文字所占区域调整实际输出文本的位置
|
DT_CENTER
DT_LEFT
DT_RIGHT
|
文字的横向对齐方式
|
DT_END_ELLIPSIS
DT_PATH_ELLIPSIS
DT_WORD_ELLIPSIS
DT_MODIFYSTRING
|
当文字无法全部显示在给定区域时,用“…”替换无法显示的部分文字
|
计算将要输出的文字中每个字所在的位置和大小;
分词功能
|
DT_EXPANDTABS
|
将“\t”显示为空格
|
|
DT_EXTERNALLEADING
|
计算行高时是否考虑字体指定的External Leading
|
取得当前字体的External Leading值;
控制Layout是否计算External Leading,或通过程序从行高中减去这个值
|
DT_HIDEPREFIX
DT_NOPREFIX
DT_PREFIXONLY
|
是否将“&”解析为修饰前缀;是否显示修饰结果或只显示修饰结果
|
预处理输入的文字
|
DT_NOCLIP
|
是否执行裁剪
|
依赖于Screen DC中实现的裁剪功能
|
DT_SINGLELINE
DT_WORDBREAK
|
是否折行
|
计算每个文字的位置和大小;
分词功能
|
DT_CALCRECT
|
返回Layout结果或输出Layout
|
在输出文本之前得到Layout的尺寸
|
DrawText与ExtTextOut相比,最明显的区别就是DrawText具有换行功能。可以利用一些底层的GDI API来模拟DrawText的行为。这些GDI API包括:
— GetTextMetrics:需要其中的external leading。因为DrawText可能在行间插入空白,空白的像素数由external
leading指定,是否执行这样的插入由DT_ EXTERNALLEADING指定。
— GetTextExtentPoint32:需要这个API来计算字符串输出在HDC时所占大小,从而进一步计算换行位置。
— ExtTextOut:执行每一行的文本输出。
DrawText的程序逻辑步骤如下所述。
字符串预处理。根据这些选项预处理字符串:DT_END_ELLIPSIS,DT_EXPANDTABS,DT_HIDEPREFIX,DT_NOPREFIX,DT_PATH_ELLIPSIS,DT_ PREFIXONLY,DT_WORD_ELLIPSIS。
预处理的结果字符串送至下一步,并根据DT_MODIFYSTRING决定是否回填到输入的字符串中。
根据DT_SINGLELINE和DT_WORDBREAK决定是否分行(默认为不分行)。分行的程序逻辑可以这样表述:
— 用GetTextExtentPoint32逐个计算从第一个字符到第N个字符所组成的字符串宽度。
— 一旦计算到N时,字符串宽度超过了所给矩形的宽度,在第N-1处设置换行标志,并将第N个字符作为下一行的第一个字符,继续上一步的操作以决定下一个换行位置,直到整个字符串处理完毕。
根据DT_CALCRECT决定是否输出字符串,或者仅是返回调整过的输出矩形。
逐行输出字符串。根据DT_BOTTOM,DT_CENTER,DT_LEFT,DT_RIGHT,DT_TOP和DT_VCENTER决定每一行的输出位置,并利用ExtTextOut输出。
3.1.3 非格式化文本输出
TextOut和ExtTextOut的格式化功能比DrawText简单,但实现了两个更简单、更底层的文本输出功能:
— 输出指定的Glyph string;
— 精确指定每一个字符的位置。
TextOut和ExtTextOut也能实现一些文本格式化功能,文字的横、纵向对齐方式由当前HDC的text align属性决定。
为了模拟Windows的文本输出功能,需要首先实现三个底层的功能:
— 字体引擎;
— Layout功能;
— 输出Layout。
在Linux上,FreeType是最常用也是最强大的字体引擎,当前的最新版本是2.2.1,它支持大多数字体格式:
— TrueType字体和字体集合;
— Type 1;
— CID-keyed Type 1;
— CFF;
— OpenType;
— 基于SFNT的位图字体;
— X11上的PCF字体;
— Windows上的各种FNT字体;
— BDF;
— PFR;
— Type 42。
FreeType可以将Unicode字符值转换为指定字体中的Glyph索引,并能输出Glyph的位图或返回Glyph的大小信息,为后面的Layout和输出提供了基础功能。
在Windows中,ExtTextOut是所有简单文本输出的基础。如果移植的程序不是专门的文字处理或排版工具,那么以ExtTextOut为基础实现的文本输出功能足以满足要求。在MSDN中观察ExtTextOut,它有几个关键之处:
— 传入的“参考点(Reference Point)”的意义会受到当前HDC的文本对齐方式(Text Align)的影响(可参阅MSDN中的SetTextAlign),见表3-2。
表3-2
参考点的含义
对齐方式
|
意 义
|
TA_BASELINE
|
Ref Point的Y值指示layout的baseline的Y值
|
TA_BOTTOM
|
Ref Point的Y值指示layout底端的Y值
|
TA_TOP
|
Ref Point的Y值指示layout顶端的Y值
|
TA_CENTER
|
Ref Point的X值指示layout的横向中点
|
TA_LEFT
|
Ref Point的X值指示layout的左边
|
TA_RIGHT
|
Ref Point的X值指示layout的右边
|
TA_NOUPDATECP
|
在文本输出时使用Ref Point
|
TA_UPDATECP
|
在文本输出时不使用ExtTextOut或TextOut传入的Ref Point,而是使用HDC中的Current Point,而且在每次文本输出结束后移动Current Point到字符串的末尾
|
注:若未指定当前的文本对齐方式,默认为TA_TOP| TA_LEFT| TA_NOUPDATECP。
例如,如果当前的文本对齐方式为TA_BASELINE|
TA_CENTER,那么输出时的坐标关系如图3-2所示。
图3-2 参考点不在左上角的例子
在Linux上,GDK函数gdk_draw_layout所接受的坐标值永远是layout的左上角的放置位置。因此需要事先计算出layout的大小,然后根据当前的文本对齐方式和Ref Point计算传给gdk_draw_layout的坐标。
— ExtTextOut的最后一个参数(lpDx)是一个数组。如果不为空,这个数组指定的是每个字符在layout方向上的偏移量。以layout从左向右为例,输出字符串“Print”时,如果lpDx的内容为{9,
5, 3, 6},那么“r”的左上角将距离“P”的左上角9个logical pixel;“i”的左上角距离“r”的左上角5个logical pixel,依此类推。
如前所
述,在Linux上,GDK采用Pango作为字体输出引擎。以Pango为例,可以用下面的代码设置layout中各个字符的位置。其中,
PangoLayout代表一个Layout,用户可以使用pango_layout_set_text函数将需要Layout的一系列字符传达给
PangoLayout对象。Pango会在需要的时候实际完成Layout过程。
PangoLayoutLine *pLine =
pango_layout_get_line(layout, 0); //[1]
GSList *pRunList = pLine->runs; //[2]
PangoLayoutRun *pRun = (PangoLayoutRun
*)pRunList->data; //[3]
PangoGlyphString *pGlyphs = pRun->glyphs; //[4]
for (int i = 0; i < nCount; ++i)
{
pGlyphs->glyphs[i].geometry.width
= pDeltaX[i] * PANGO_SCALE; // [5]
}
对这段代码的解释如下所述。
—
从一个PangoLayout(代表一个Layout)中获得一个PangoLayoutLine(Layout好的文本的一行内容)。这里认为该
PangoLayout中只有一行。由于TextOut和ExtTextOut每次都只输出一行文本,没有换行功能,可以相应地把每个
PangoLayout都设置为单行模式(用pango_layout_set_single_paragraph_mode可以做到这一点)。
— PangoLayoutLine的定义如下:
struct PangoLayoutLine
{
PangoLayout *layout;
gint start_index; /*
start of line as byte index into layout->text */
gint length; /*
length of line in bytes */
GSList *runs;
};
其中,
“runs”中是这一行中所包含的PangoLayoutRun(Layout中具有相同属性的一系列Glyph被集合成一个run,同一个run中的
Glyph具有相同的属性。例如,都来自同一种字体,字号都一样等)。在ExtTextOut的模拟中,由于每一次输出都只使用当前HDC中的文本属性,
即在同一行内不会出现多种文本属性,所以一个PangoLayoutLine中也只可能有一个PangoLayoutRun。但是在RTL(从右到左)和
LTR(丛左到右)混排的情况下,情况要复杂一些。
— 从一系列run中拿到一个PangoLayoutRun。PangoLayoutRun的定义如下:
struct PangoLayoutRun
{
PangoItem
*item;
PangoGlyphString *glyphs;
};
— 从PangoLayoutRun中提取PangoGlyphString。PangoGlyphString是一个Glyph组成的数组,其定义如下:
struct PangoGlyphString
{
gint num_glyphs;
PangoGlyphInfo *glyphs;
gint *log_clusters;
gint space;
}
其中,“glyphs”是一个由若干PangoGlyphInfo(描述一个Glyph)组成的数组,数组中元素的个数由另一个成员“num_glyphs”指定。
— 通过设置PangoGlyphInfo中的geometry.width,可以精确地指定各个字符之间的间距。
当一个Layout准备好之后,可以调用GDK的函数gdk_draw_layout,将Layout中的文本显示到屏幕上。
3.2 字体管理
3.2.1 字体管理的一般概念
在3.1节中已经对“字体”、“Glyph”和“Layout”,以及它们之间的联系做过介绍。这里将讲述其他与字体相关的一般概念。
(1)字体:字体(Font)是Glyph的集合。Glyph依据统一的风格设计,表示一种或多种语言中的字符。
(2)字
体族:字体族(Font Family)是一组字体的集合。这些字体有共同的名字和统一的设计,分别表示字体族中的一种风格(Style或Type
Face)。例如,“Times”是一个字体族的名字,而“Times Bold”、“Times Italic”和“Times
Regular”则是族中不同风格的字体名字。
(3)字
符编码:字符编码(Character
Encoding)是一个映射,它为每一个字符指定一个数字。这一概念的引入是因为在计算机中,字符是表示为数字的。现实中存在着不计其数的字符编码。例
如,著名的ASCII,为字符“Lower Case A”指定的数字为97;为字符“White
Space”指定的数字为32;等等。每个国家都可能为自己使用的语言编制一套字符编码;每个软件开发商也可能制定自己的编码规则;每种操作系统也可能有
自己的编码。这种混乱的情况为程序的移植和相互通信带来了很大的困难。因此,现在广泛使用的是一种名为Unicode的字符编码。
(4)
Glyph代码(Glyph
Code):每种字体都有一套规则,将存在于该字体中的所有Glyph一一编码,并根据一种字符编码,制定出一套将字符编码对应到Glyph代码的规则。
一般而言,字体会支持几种不同的字符编码,这由字体的设计者决定。但一般的字体都会支持Unicode。
(5)下面是字体度量的一些概念。
— Bounding Box:能包括一个Glyph的最小矩形。
— Baseline:Baseline是一条虚拟的水平线。Baseline的位置由字体指定。在layout的时候,默认的方式是一行中的各个glyph按baseline对齐。
— Glyph Origin:定义一个字符的原点位置。对于水平字体,Glyph Origin位于Baseline上Glyph的最左端。
— Advance Width:一个Glyph在Layout中默认占据的宽度,即从一个Glyph Origin到下一个Glyph Origin的距离。
— Left-side Bearing:从Glyph Origin到Bounding Box最左端的距离。
— Rightside Bearing:从Bounding Box最右端到下一个Glyph Origin的距离。
— Ascent:从Baseline到Glyph最上端的距离(注意:不一定是Bounding Box的最上端)。
— Descent:从Baseline到Glyph最下端的距离(注意:不一定是Bounding Box的最下端)。
字体度量的一些概念如图3-3所示。
图3-3 字体度量的一些概念
(6)字体变量(Font Variations):字体沿着一个字体变量的变化方向会表现出一些递增(或递减)的变化。例如,字体的“重量”(Weight)是一个字体变量,沿着这个变化方向,字体所表现出的变化如图3-4所示。
图3-4 字体变化
(7)Kerning和Kerning Pair:Kerning是对特定的两个(或多个)相邻Glyph之间的空间所做的调整。Kerning Pair由两个Glyph和一个表示调整幅度的数值组成。字体中会保存所有产生Kerning的Kerning
Pair。图3-5显示了Kerning的效果。
图3-5 Kerning的作用
(8)定宽和变宽字体:定宽(Fixed)字体是指,在指定字体大小的前提下,字体中所有的Glyph宽度相同。变宽字体是指,即便是在相同的字体大小的前提下,字体中所有的Glyph宽度也彼此不同,例如,“i”的宽度一般会小于“m”的宽度。
“Courier”和“Courier New”是典型的定宽字体。
3.2.2 Linux下的字体管理
Linux
下有一个名为FontConfig的小程序,Linux上所有的字体文件相关的管理工作都直接或间接地通过这个程序来完成。关于FontConfig的详
细信息,可以通过man
page中的fontconfig(3)和FontConfig的网站(http://www.fontconfig.org/)获得。
默认情况
下,FontConfig在/usr/X11R6/lib/X11/lib/fonts/中查找系统范围内的字体,在~/.fonts/中查找用户安装的
字体。在这些目录中添加字体文件将会使FontConfig在最近一次被调用时发现新安装的字体,并把它纳入FontConfig所管理的字体集合中。
通过FontConfig提供的API,可以在程序中收集系统当前的可用字体。将这一功能与FreeType相结合,模拟层能够模拟Win32下绝大多数字体管理的功能。
Pango也有自己的字体管理功能,通过对FontConfig和FreeType(或Xft)的调用实现。目前,Pango对字体管理的功能支持有限,用它来模拟Win32字体管理功能可能无法满足一些对字体信息要求较多的应用程序。
3.2.3
GDI中的字体管理
(1)字体的表现形式
Windows下,字体表示为HFONT类型。HFONT是HGDIOBJ的一种,通过CreateFont或CreateFontIndirect被创建。一个HFONT所表示的是一个逻辑字体在被Select到一个HDC中以前,它仅仅是对一个字体的逻辑描述。
(2)字体的创建
CreateFont
和CreateFontIndirect可以创建字体。观察这两个函数的参数表,可以发现这两个函数实际上完成的是一项功能,即从一个LOGFONT创建
一个HFONT。因此,可以用创建一般HGDIOBJ的过程来创建一个HFONT,而一个HFONT在被创建时仅包含创建时输入的LOGFONT信息就足
够了。
(3)字体的使用
HDC通过SelectObject选取要使用的HFONT。一个HFONT中所包含的逻辑信息将会对这个HDC上的文本输出产生影响(HFONT和HDC共同作用的结果)。同一个HFONT在不同HDC上所产生的文本输出结果未必是一样的。
(4)字体信息的获取
在构建与文本输出相关的应用程序时,经常需要查询当前字体的信息。例如,程序可能希望得知当前字体的高度和字符的平均宽度,以便决定将要显示的对话框的大小,当输出到打印机时,程序也许不希望使用BITMAP字体,因为这种字体不能缩放,无法在打印机上得到理想的效果。
Windows主要通过一个名为TEXTMETRIC的结构向应用程序提供当前字体的信息。程序可以用GetTextMetrics获得这样一个结构,结构中的各项数值描述了指定HDC上当前选中的字体。
(5)查看系统的可用字体
对于一个
有字处理功能的应用程序来说,其基本功能之一就是提供一个字体列表以便用户选择。Windows提供一个EnumFontFamiliesEx的API,
程序可以通过这个API获得当前设备上的所有可用字体。除了EnumFontFamiliesEx之外,Windows还有另外几个API具有类似的功
能:EnumFonts和EnumFontFamilies。它们的功能都是EnumFontFamiliesEx的子集。
3.2.4 几个关键API的模拟实现
(1)CreateFont和CreateFontIndirect
这
里模拟的是字体的创建过程。CreateFont和CreateFontIndirect这两个API实际上是一个函数,
CreateFontIndirect使用一个LOGFONT结构来创建字体,而CreateFont则将LOGFONT中的各项分散在自己的参数表里。
HFONT
的创建实际上是一个逻辑过程——CreateFont并没有与任何一个HDC发生联系——所以,这里只需要创建一个类型为OBJ_FONT的GDI
OBJECT,并将给定的LOGFONT(逻辑字体信息)记录在其中即可。当一个HFONT被SelectObject引用时,将根据选择它的HDC被实
例化。例如,在Linux上,一个被选择到SCREEN
DC上的HFONT可能被实例化为一个PangoFontDescription,而被选择到PRINTER
DC上的HFONT可能被实例化为一个PostScript中的Font Dictionaray。
在LOGFONT中,需要注意字体高度的指定。
— 如果lfHeight大于0,那么指定的是“cell height”,即字体的ascent与descent之和等于指定的lfHeight。
—
如果lfHeight小于0,那么指定的是“character
height”,即字体的ascent与descent之和再减去字体的internal
leading,结果等于指定的lfHeight的绝对值。关于internal leading,将在下一节中进一步讨论。
— 如果lfHeight等于0,表示使用默认的字体高度。
如果指定的字体名不存在,则在实例化时,需要使用与LOGFONT中各项数值最为接近的字体,通常会选择系统的默认字体。但需要注意的是,由于字体的映射是发生在SelectObject的过程中的,所以字体映射的结果反映在HDC中,并不会反映在HFONT中。
(2)GetTextMetrics
GetTextMetrics是Windows字体信息模拟的最基本功能。TEXTMETRIC的各项数值见表3-3。
表3-3
TEXTMETRIC的成员
名 称
|
概 念
|
使用频度
|
tmHeight
|
字体高度。在Window中,它被定义为“tmAscent+tmDescent”。但在大多数其他平台的字体引擎中不这样定义,模拟时请注意参看所使用字体引擎的定义
|
经常
|
tmAscent
|
相同于“一般概念”中的ascent
|
经常。因为需要以这两个值来决定tmHeight
|
tmDescent
|
相同于“一般概念”中的descent
|
tmInternalLeading
|
Windows上特有的概念。例如,
当创建字体时指定的lfHeight为负数时,所得到的tmHeight为lfHeight的绝对值与tmInternalLeading之和
|
经常。当lfHeight<0时,需要这个数值来决定实际字体的高度
|
tmExternalLeading
|
被用来指定行与行之间被插入的高度。DrawText可以指定在输出时是否使用这一数值
|
有时
|
tmAveCharWidth
|
平均字符宽度。一般是字母x的宽度
|
经常。许多对话框根据该数值决定自己的宽度
|
tmMaxCharWidth
|
最大字符宽度
|
有时
|
tmWeight
|
自体“重量”,和LOGFONT中的lfHeight是同一数值
|
很少
|
tmOverhang
|
由于字体变体而在文本输出时附加的字符串宽度
|
很少
|
tmDigitizedAspectX
|
指定字体水平方向的角度
|
几乎不用。对于大多数字体来说,该项数值为0
|
tmDigitizedAspectY
|
指定字体垂直方向的角度
|
tmFirstChar
|
指定字体所包含的第一个和最后一个字符。用GetFontUnicode Ranges可以获得关于字体所包含字符的详细信息
|
很少
|
tmLastChar
|
tmDefaultChar
|
指定字体的默认字符。当遇到字体中不包含的字符时,将用这个字符来代替
|
很少
|
tmBreakChar
|
指定字体中用来表示分词的字符。如英文字体中的空格(0x20)
|
很少
|
tmItalic
|
字体是否为斜体。与LOGFONT中指定的lfItalic相同
|
很少
|
tmUnderlined
|
字体是否有下画线。与LOGFONT中指定的lfUnderline相同
|
很少
|
tmStruckOut
|
字体是否有删除线。与LOGFONT中指定的lfStrikeOut相同
|
很少
|
tmPitchAndFamily
|
包含两项信息:
① 字体是变宽还是定宽的,是否TRUE_TYPE,是否矢量字体
② 字体的风格。注意不要将这一概念与“一般概念”中的Font Family相混淆
|
有时。通常情况下,程序会使用这一字段来判定字体是否可以缩放
|
tmCharset
|
字体的字符集。如果字体支持LOGFONT中指定的lfCharset,那么这个数值与其相同;否则将使用字体的默认字符集
|
有时。当应用程序需要考虑国际化时,这一字段就变得重要起来
|
通过Linux上的FontConfig,可以获取一些基本的字体信息。
const FcPattern *pat = ……; //
[1]
FcValue value;
if (FcResultMatch ==
FcPatternGet(pat, FC_FAMILY, 0, &value))
{
family_name
= strdup((const char*)value.u.s); //
[2]
}
if (FcResultMatch ==
FcPatternGet(pat, FC_STYLE, 0, &value))
{
style_name
= strdup((const char*)value.u.s); //
[3]
}
if (FcResultMatch ==
FcPatternGet(pat, FC_FILE, 0, &value))
{
file_name
= strdup((const char*)value.u.s); //
[4]
}
if (FcResultMatch ==
FcPatternGet(pat, FC_SLANT, 0, &value))
{
slant = value.u.i; //
[5]
}
if (FcResultMatch ==
FcPatternGet(pat, FC_WEIGHT, 0, &value))
{
weight
= value.u.i; //
[6]
}
上述代码的解释如下:
— [1]:首先通过FontConfig的API获得一个有效的FcPattern。例如,使用FcConfigGetFonts可以获得一个FcPattern的数组。
— [2]:使用FcPatternGet并指定FC_FAMILY为参数,可以获得字体名称。如“Luxi Sans”、“Helvetica”等。
—
[3]:使用FcPatternGet并指定FC_STYLE为参数,可以获得字体的风格名。常见的有“italic(斜体)”、“bold(粗体)”或
“bold italic(粗斜体)”,还有“light”, “black”, “ultra light”, “book”等。
— [4]:使用FcPatternGet并指定FC_FILE为参数,可以获得字体的文件名。建议将字体的文件名保存,以便将来使用字体引擎(如FreeType)来获得更详细的字体信息。
— [5]:使用FcPatternGet并指定FC_SLANT为参数,可以获得字体是否为斜体。
—
[6]:使用FcPatternGet并指定FC_WEIGHT为参数,可以获得字体的“重量”。但需要注意的是,这一数值与TEXTMETRIC中的
tmWeight虽然意义相同,但数值不同。tmWeight的数值范围是100 ~ 900,而FontConfig中的数值范围是0~200。
有了从FontConfig中取得的字体文件名,就可以利用FreeType来获得一些更详细的字体信息。首先介绍一下FreeType中一个关键的数据结构FT_FaceRec。在FreeType中,它表示一个字体,见表3-4。
表3-4
FT_FaceRec的成员
成员名称
|
数据类型
|
携带信息
|
face_flags
|
FT_Long
|
字体属性
如果FT_FACE_FLAG_SCALABLE被设置,对应tmPitchAndFamily被设置了TMPF_VECTOR;如果FT_FACE_FLAG_FIXED_SIZES被设置,说明这是一个bitmap字体;如果FT_FACE_FLAG_FIXED_WIDTH被设置,说明这是一个定宽字体;如果FT_FACE_FLAG_SFNT被设置,对应tmPitchAndFamily被设置了TMPF_TRUETYPE。
|
style_flags
|
FT_Long
|
风格属性。指示字体是否是斜体或粗体
|
family_name
|
FT_String *
|
与FontConfig中得到的数据相同
|
style_name
|
FT_String *
|
num_fixed_sizes
|
FT_Int
|
如果是一个bitmap字体,这两项有效
|
available_sizes
|
FT_Bitmap_Size *
|
units_per_EM
|
FT_Ushort
|
指定一些数值的单位。对于TrueType字体,这个值一般是2048;对于bitmap字体,这个值是1,以下的数值对bitmap字体没有意义。
通俗来说,这个数值说明一个字体的每个glyph是画在一个什么样精度的画布上。2048说明画布在横向和纵向上都有2048个小格子,而下面的各项数值则表示它们各自占据多少个这样的小格子
|
bbox
|
FT_BBox
|
指定字体的“bounding box”
|
ascender
|
FT_Short
|
对应tmAscent
|
descender
|
FT_Short
|
对应tmDescent。这个值一般是负数
|
续表
成员名称
|
数据类型
|
携带信息
|
height
|
FT_Short
|
字体输出时baseline的间距。注意:这个值不对应tmHeight。tmHeight应该用tmAscent+tmDescent来计算。在FT_FaceRec中,height – ascenter –
abs(descender)对应tmExternalLeading。
|
max_advance_width
|
FT_Short
|
对应tmMaxCharWidth
|
max_advance_height
|
FT_Short
|
只对纵向字体有效
|
underline_position
|
FT_Short
|
字体建议的下画线位置。注意:Windows下画线的位置与此不同。这个值一般是在baseline或偏下一点,而Windows下画线是画在整个字下面的
|
underline_thickness
|
FT_Short
|
字体建议的下画线粗细。注意:Windows的下画线粗细不仅与字体有关,还与字体大小和字体重量有关
|
glyph
|
FT_GlyphSlot
|
可以使用FT_Load_Glyph来填充,其中的数值描述具体Glyph的属性。例如,要计算tmAveCharWidth,可以装载’x’的glyph,并从这里获取它的宽度
|
(3)EnumFontFamilesEx
在Linux
上,可以利用FontConfig的FcConfigGetFonts来获得当前所有系统可用的字体。因此最容易想到的是在
EnumFontFamilesEx中调用FcConfigGetFonts,并对每个返回的FcPattern加以处理,再调用
EnumFontFamilesEx中指定的Callback函数。但是,作为一个模拟字体子系统中的一个API,需要考虑以下因素。
— 每次的SelectObject(OBJ_FONT)操作都需要做字体映射,故需要知道系统中有哪些字体。
— 每次的GetTextMetrics操作都需要得到一个TEXTMETRIC,每次都从FcPattern重新计算,很费时间。
— EnumFontFamiliesEx对每个字体都要计算得到一个LOGFONT和TEXTMETRIC。
综合以上
考虑,效率最高的做法是,在应用程序启动或模拟层启动的时候构建一个字体列表,并为列表中的每个字体计算出对应的LOGFONT和TEXTMETRIC
(至少计算出一个中间结果,如一个默认的LOGFONT,以及LOGFONT和TEXTMETRIC的对应关系)。这样,后续的SelectObject
(OBJ_FONT)或EnumFontFamiliesEx操作都可以通过简单的查表和少量的计算完成。
EnumFontFamiliesEx
并不是只有获取所有系统可用字体这样简单,这里有一个细节:EnumFontFamilesEx以一个LOGFONT作为输入,当输入的LOGFONT不
同时会有不同的行为。如果lfFaceName为空字符串,EnumFontFamiliesEx列举所有字体,但不包括变体;如果lfFaceName
指定一个有效字体名,则列举该字体的所有变体;如果lfFaceName指定的不是一个有效的字体名,则不列举任何字体。
考虑到这
一特性,可以将字体列表设计为三层结构:FontTable→FontFamily→
FontFace。Linux系统字体的表现形式(如FcPattern)与LOGFONT、TEXTMETRIC的转换由FontFace完成,而
FontFamily的引入是为了给模拟提供方便。如果应用程序对字体映射的要求较高,还可以引入一个FontMap,用来在SelectObject
(OBJ_FONT)时选择要使用的FontFace。Font Table的结构如图3-6所示。
图3-6
FontTable的结构
在这种设计中,EnumFontFamiliesEx可以被实现为:
int CommonDeviceContext::EnumFontFamiliesExA(
LPLOGFONTA lpfont,
FONTENUMPROCA
proc,
LPARAM lparam,
DWORD dwFlag)
{
if (lpfont->lfFaceName[0])
{
if (there is
a FontFamily in FontTable named as "lfFaceName")
{
for
each FontFace in the FontFamily found
{
get
LOGFONT from FontFace;
get
TEXTMETRIC from FontFace;
get
FontType from FontFace;
Call
Callback "proc" with <LOGFONT, TEXTMETRIC and FontType>
}
}
}
else
{
for each
FontFamily in FontTable
{
get
default FontFace from FontFamily;
get
LOGFONT from FontFace;
get
TEXTMETRIC from FontFace;
get
FontType from FontFace;
Call
Callback "proc" with <LOGFONT, TEXTMETRIC and FontType>
}
}
}
3.3 小结
作为
GDI的一个部分,因其复杂而丰富的内容,文本输出和字体管理被单独作为一章介绍。尽管如此,本章并不涉及打印,以及复杂字符编码、BiDi输出等内容。
对于“文本输出”,本章集中介绍了DrawText和ExtTextOut函数在模拟层的实现;对于“字体管理”,介绍了通过FontConfig等
Linux模块实现EnumFontFamilesEx等GDI字体管理函数的方法。
模拟层的
文本输出,尤其是字形排列(layout)严重依赖于pango。这样做的原因在于,充分发挥pango的功能可以节省巨大的开发代价。更重要的是,
pango作为GTK的一个模块,随着GNOME平台的升级而升级,应用程序因而可以自动获得pango升级带来的改进。另外,使用pango还可以使得
应用程序的外观具有和桌面平台一致的风格(native
appearance)。