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)。