袁創(chuàng):文本編輯器中文字?jǐn)嘈屑芭虐嫠惴ㄑ芯?/span>
袁永福 2025-9-19
文本編輯器是一種非常復(fù)雜的圖形軟件,涉及到的很多開發(fā)技巧和軟件結(jié)構(gòu)都是傳統(tǒng)的數(shù)據(jù)庫(kù)程序開發(fā)中所從未應(yīng)用的,因此掌握相關(guān)技術(shù)的人是非常的少的。在其中文字?jǐn)嘈屑芭虐嫠惴ㄊ蔷庉嬈鏖_發(fā)中的核心算法之一。如果沒有掌握這個(gè)算法,那只能在開源軟件的基礎(chǔ)上小打小鬧了。
本文就討論一下編輯器中文檔斷行及排版算法。
文字排版大致分為以下幾個(gè)步驟:
- 測(cè)量各個(gè)字符的寬度和高度。[袁永福版權(quán)所有]
- 計(jì)算文檔容器的客戶區(qū)寬度。比如設(shè)置的紙張寬度減去左頁(yè)邊距和右頁(yè)邊距的寬度。這里的文檔容器不僅僅指大的正文區(qū)域,還包括單元格、文本框之類的文檔結(jié)構(gòu)。
- 斷行,也就是將各個(gè)字符從左到右,從上到下的依次放置在文檔容器中。產(chǎn)生一行行文本,實(shí)現(xiàn)一種流式排版。
- 行內(nèi)排版,也就是在文檔行中進(jìn)行字符排版,特別是為了完成文檔內(nèi)容兩邊對(duì)齊功能。
- 分頁(yè)。
■■測(cè)量字符大小
排版的第一步就是計(jì)算文檔中各個(gè)字符的寬度和高度。筆者是使用C#開發(fā)的,因此可以調(diào)用System.Drawing.Graphics.MeasureString方法來測(cè)量字符的寬度和高度。由于文檔中字符個(gè)數(shù)很多,比如幾萬(wàn)個(gè),則一個(gè)個(gè)測(cè)量是非常消耗時(shí)間的,為此需要采用很多優(yōu)化手段來加速測(cè)量。[袁永福版權(quán)所有]
說到測(cè)量字符,就涉及到等寬字體和比例字體的概念了。等寬字體就是使用該字體繪制字符,字符的寬度是一樣的,比如“宋體”,它就是等寬字體,用它來測(cè)量和繪制字母“W”和“i”,其寬度是一樣的。比例字體就是使用該字體測(cè)量和繪制字符,其寬度是不一樣的,比如“Times new roman”,用它來測(cè)量字母“W”和“i”,其寬度是不一樣的。
對(duì)于等寬字體,可以事先測(cè)量一個(gè)字符的寬度,比如“W”,則以后遇到其他字符就使用這個(gè)已經(jīng)測(cè)量好的寬度;而對(duì)于比例字體,則需要進(jìn)行實(shí)時(shí)的測(cè)量。
不過一般來說,對(duì)于等寬字體和比例字體,中文符號(hào)的寬度還是一致的。因此可以實(shí)現(xiàn)測(cè)量一個(gè)中文字符的寬度,以后遇到中文字符就采用這個(gè)事先測(cè)好的寬度。
這里帶來一個(gè)問題,如何判斷一個(gè)字符是否為中文字符,那就需要參照GB3212,GBK等計(jì)算機(jī)字符集的標(biāo)準(zhǔn)來判斷了。一般來說Unicode編碼范圍從19968至40869的字符為中文字符,當(dāng)然為了進(jìn)一步的優(yōu)化,可以知道一些全角符號(hào),它們的寬度也等于中文字符。
不過僅僅依照UNICODE編碼來判斷是否是中文字符是不可靠的。因?yàn)橐粯拥腢NICODE字符在不同的字體中其意義可能是不一樣的。[袁永福版權(quán)所有]
比如對(duì)于字體“Wingdings”,所有的字符在這個(gè)字體中完全變味了,就表示一個(gè)個(gè)特定形狀的符號(hào),判斷是否是中文就毫無意義了;另外對(duì)于條碼字體也有這種情況。
最為保險(xiǎn)的做法就是直接解析字體二進(jìn)制文件(擴(kuò)展名為ttf或ttc),獲得其中的字體輪廓信息,然后根據(jù)字符的UNICODE編碼值來計(jì)算出字符的寬度,這樣做是最為準(zhǔn)確可靠的。筆者猜測(cè)Graphics.MeasureString方法內(nèi)部也可能采用這種方法。不過編輯器自己解析字體二進(jìn)制文件進(jìn)行字符測(cè)量,繞過底層諸多的調(diào)用層次,其速度可以非常的快,可以在幾十毫秒內(nèi)完成幾萬(wàn)個(gè)字符的測(cè)量。[袁永福版權(quán)所有]
不過解析字體二進(jìn)制文件信息還是要花掉不少時(shí)間的,比如對(duì)于宋體,其字體文件名simsun.ttc,文件大小15MB,含28762個(gè)字符輪廓信息。但分析所得的結(jié)果信息量很小,只有1424 字節(jié),為此需要將分析結(jié)果保存在一個(gè)臨時(shí)文件中,下次就無需分析這個(gè)字體二進(jìn)制文件了。
■■斷行
測(cè)量完字符的大小后,編輯器程序開始在內(nèi)存中構(gòu)造排版對(duì)象模型,不斷的將字符填充到最后一個(gè)文檔行,若文檔行的字符寬度和加上準(zhǔn)備添加的字符的寬度大于文檔容器客戶區(qū)寬度時(shí),就進(jìn)行斷行,另起一行開始填充字符。
不過也存在提前斷行的情況。為了盡量保證連續(xù)的英文字母字符和阿拉伯?dāng)?shù)字之間不能出現(xiàn)斷行,這樣會(huì)導(dǎo)致同一個(gè)邏輯上密切相關(guān)的單詞被拆散放在兩行了。因此遇到這種情況需要提前斷行。
為此程序在執(zhí)行斷行的時(shí)候需要進(jìn)行判斷,如果下一個(gè)字符和文檔行中最后幾個(gè)字符都是英文字母字符或阿拉伯?dāng)?shù)字字符時(shí),需要從右到左遍歷最后一個(gè)文檔行,將相關(guān)字符抽取出來,準(zhǔn)備放置在下一行中。[袁永福版權(quán)所有]
當(dāng)然這樣的操作也不是絕對(duì)的,比如遇到連續(xù)的超級(jí)長(zhǎng)的“單詞”時(shí),比如100個(gè)連續(xù)字符“a”,雖然基本上沒有實(shí)際意義,但這是一種必需考慮的邊界條件,很容易導(dǎo)致程序運(yùn)行錯(cuò)誤。因此在提前斷行時(shí)需要進(jìn)行這樣的判斷,若真的出現(xiàn)這種情況,那就取消提前斷行。
※前置標(biāo)點(diǎn)和后置標(biāo)點(diǎn)
不能出現(xiàn)在行尾的符號(hào)稱為前置標(biāo)點(diǎn),例如“([{·‘“〈《「『【〔〖(.[{£¥”;不能出現(xiàn)在行首的符號(hào)稱為后置標(biāo)點(diǎn),例如“!),.:;?]}¨·ˇˉ―‖’”…∶、。〃々〉》」』】〕〗!"'),.:;?]`|}~¢”。
比如一個(gè)文本行內(nèi)容為“?張三李四王五【”,這就是一種不和規(guī)范的文本行,需要避免這種情況。
在進(jìn)行文字?jǐn)嘈袝r(shí),若這個(gè)文檔行的最后一個(gè)字符是前置標(biāo)點(diǎn)時(shí),需要進(jìn)行提前斷行;如果斷行后第一個(gè)要排版的字符為后置標(biāo)點(diǎn)時(shí),也需要進(jìn)行提前斷行。
在進(jìn)行斷行的時(shí)候,對(duì)于段落符號(hào)要進(jìn)行一些特殊處理。段落符號(hào)本身是有一定的寬度的,但當(dāng)文檔行要執(zhí)行斷行時(shí),參與計(jì)算時(shí)的寬度就可以當(dāng)做零了。
在排版的編程實(shí)踐中,筆者采用堆棧的方式實(shí)現(xiàn)斷行。首先將所有要排版的字符壓入一個(gè)堆棧中,然后循環(huán)從堆棧中Peek獲得一個(gè)字符元素,然后試圖添加到當(dāng)前文檔行中,若文檔行剩余空間足夠容納新字符,則將該新字符添加到文檔行中,同時(shí)堆棧執(zhí)行Pop操作。若文檔行剩余空間不夠,則不執(zhí)行Pop操作,新建一個(gè)文檔行,從而開始新的循環(huán)。如果出現(xiàn)提前斷行,則需要將當(dāng)前文檔行中的若干個(gè)字符元素移出來,并壓入堆棧中等著下一次循環(huán)中使用。
當(dāng)堆棧內(nèi)容為空時(shí),就跳出循環(huán),完成文檔的斷行操作。[袁永福版權(quán)所有]
※停止行
用戶在編輯的時(shí)候會(huì)頻繁的輸入字符,這就使得程序頻繁的進(jìn)行文檔排版操作。當(dāng)文檔內(nèi)容比較多,比如上萬(wàn)個(gè)字符時(shí),進(jìn)行整個(gè)文檔范圍的字符排版及重新繪制用戶界面可能要花上幾百毫秒的,這樣就導(dǎo)致用戶輸入字符時(shí)編輯器反應(yīng)遲鈍。
為此在用戶編輯錄入的時(shí)候,需要進(jìn)行文檔內(nèi)容的部分區(qū)域的文字排版,而其他區(qū)域的排版就不要?jiǎng)恿恕榇嗽诰幊讨胁捎昧艘环N技巧來減輕排版的工作量,筆者稱之為停止行技巧。
在排版前,首先備份文檔容器的文檔行信息。在每完成一個(gè)斷行,形成一個(gè)新的文檔行時(shí)。遍歷備份的文檔行信息,從最后一行開始和新的文檔行內(nèi)容進(jìn)行比較,比較內(nèi)容主要是文檔行中的文檔元素是否完全一致,當(dāng)然還有一些其他判斷。當(dāng)新舊兩個(gè)文檔行內(nèi)容一致時(shí),這個(gè)舊的文檔行稱為停止行。此時(shí)文檔內(nèi)容斷行提前結(jié)束。然后進(jìn)行新文檔行的行內(nèi)排版,最后新文檔行和一部分舊的文檔行合并,形成新的文檔排版。這樣就能比較大的降低運(yùn)行時(shí)排版工作量。[袁永福版權(quán)所有]
■■行內(nèi)排版
文字?jǐn)嘈型瓿珊螅枰M(jìn)行行內(nèi)排版。
文檔行中各個(gè)字符的寬度之和不大可能正好等于文檔容器的客戶區(qū)寬度。兩者會(huì)有空白差。
由于中文字符和英文字符寬度不一樣,對(duì)于不等寬字體,各個(gè)英文字符、數(shù)字字符等寬度還不一樣。使得各個(gè)文本行的字符寬度之和是不一樣的,使得各個(gè)文檔行右邊緣是參差不齊的。這樣比較嚴(yán)重的影響美觀。
為此需要將文檔行的寬度拉長(zhǎng)成文檔容器客戶區(qū)寬度,由此會(huì)額外的制造出不少空白,此時(shí)需要將這些空白比較均勻的分?jǐn)偟礁鱾€(gè)字符上。此處是比較均勻的分?jǐn)偅皇峭耆鶆颍怯幸欢ǖ姆植妓惴ǖ摹?/span>
同一行中,字符不是相對(duì)孤立的,而且從邏輯上分為一組一組的,對(duì)于漢字和標(biāo)點(diǎn)符號(hào),它們是各自為政,自己組成一組。對(duì)于連續(xù)的英文字母字符和阿拉伯?dāng)?shù)字,它們邏輯上是同一組的,一起構(gòu)成一個(gè)完整的單詞,因此同一組之間的字符之間應(yīng)該是緊密連接在一起,不得拆開。[袁永福版權(quán)所有]
為此要分?jǐn)傆捎谖淖謨蛇厡?duì)齊而造成的額外空間時(shí),首先要對(duì)文檔行的字符進(jìn)行分組,然后將額外的空白平均分?jǐn)偟阶址M上。
例如對(duì)于文字“DCWriter電子病歷文本編輯器。”,其分組為“[DCWriter][/電][子][病][歷][文][本][編][輯][器][。]”,其中一對(duì)方括號(hào)之間就是一組字符,這樣就分成11組。如果額外的空白寬度為20個(gè)單位,則需要將空白平均分?jǐn)偟竭@些字符組上面,最后一組不分?jǐn)偅谑乔懊?0組分配得到20÷(11-1)=2個(gè)單位的空白寬度。在排版時(shí)將這10個(gè)2單位的空白寬度插入到字符組之間,這樣就能拉長(zhǎng)文檔行的寬度正好等于文檔容器的客戶區(qū)寬度。
■■分頁(yè)
分頁(yè)本質(zhì)上說就是計(jì)算分頁(yè)線的位置。其過程如下
- 首先計(jì)算出標(biāo)準(zhǔn)頁(yè)的高度,也就是紙張高度減去上下頁(yè)邊距的值,還需要考慮到頁(yè)眉頁(yè)腳的修正量。
- 設(shè)置當(dāng)前分頁(yè)線的位置,也就是上一個(gè)分頁(yè)線的位置加上標(biāo)準(zhǔn)頁(yè)高。
- 遍歷文檔行,若分頁(yè)線的位置在文檔行中間,說明該行文字被分割到兩頁(yè)中,此時(shí)將分頁(yè)線的位置向上移動(dòng),使得分頁(yè)線在當(dāng)前文檔行的上邊緣和上一個(gè)文檔行下邊緣的中間。
- 如此循環(huán),使得所有的文檔頁(yè)的高度和大于等于文檔的內(nèi)容高度。[袁永福版權(quán)所有]
在進(jìn)行分頁(yè)時(shí),也需要判斷很多邊界條件,比如當(dāng)某個(gè)文檔行非常高,比如中間放置了一個(gè)超高的圖片,使得這個(gè)文檔行的高度大于標(biāo)準(zhǔn)頁(yè)高,此時(shí)就不能隨便移動(dòng)分頁(yè)線的位置了。
另外當(dāng)文檔中有表格時(shí),則需要深入到表格單元格內(nèi)部進(jìn)行修正分頁(yè)線位置的操作,這是一種遞歸操作。
在電子病歷業(yè)務(wù)中有著繼續(xù)打印的功能,在筆者的實(shí)現(xiàn)中,續(xù)打位置實(shí)際上就算是一種特殊的分頁(yè)線,這樣就能避免在續(xù)打時(shí)文字被分割打印的情況。
文字?jǐn)嘈泻团虐嫠惴ㄊ欠浅?fù)雜的,即使筆者經(jīng)過長(zhǎng)期的重構(gòu)再重構(gòu),優(yōu)化再優(yōu)化,也還是花費(fèi)了一萬(wàn)多行的C#代碼來實(shí)現(xiàn)這個(gè)功能,而且還有不少地方仍然需要優(yōu)化。
一些人認(rèn)為C#無法開發(fā)高性能的程序,編輯器這樣程序應(yīng)該需要用C++開發(fā)。筆者經(jīng)過實(shí)踐認(rèn)為,所謂C#性能不高的說法是不對(duì)的,關(guān)鍵還是算法。C#程序只是啟動(dòng)有些慢,運(yùn)行起來后仍然可以達(dá)到很高的性能。[袁永福版權(quán)所有]
posted on 2018-03-09 10:34 袁永福 電子病歷,醫(yī)療信息化 閱讀(1006) 評(píng)論(1) 收藏 舉報(bào)
浙公網(wǎng)安備 33010602011771號(hào)