前言
這是我寫iText in Action 2nd讀書筆記的第二篇,但在上一篇有資源下載和代碼的一些說明,如果大家對iTextSharp和PDF有興趣,希望還是先看第一篇。現在我們將重點集中到第四步:添加內容。這里說的添加內容都是通過Document.Add()方法調用,也就是通過一些high-level的對象實現內容的添加。這一節如標題要介紹Chunk、Phrase、Paragraph和List對象的屬性和使用。Document的Add方法接受一個IElement的接口,我們先來看下實現此接口的UML圖:
上圖是iText中對應的類圖,在iTextSharp中,只要將接口的名稱前加上I即可如(IElement,ITextElementArray)。
為了模擬一些有意義的數據,本書的作者還創建了一個名叫Movies的數據庫,里面包含有120 movies,80 directors和32 countries。以下為database的ERD圖:
作者使用了HSQL 數據引擎來實現數據的讀取,HSQL是java中一個輕量級不需要安裝的數據庫引擎,在使用時只需要引用其jar包就好了。我這里就使用了SQLite來替換,數據也已經存入上篇中movies.db3文件中,大家也可以將重點轉移到iText中來,具體的實現可以參考代碼。
Adding Chunk, Phrase, Paragraph, and List objects
The Chunk object: a String, a Font, and some attributes
Chunk類是可以添加到Document中最小的文本片段。Chunk類中包含一個StringBuilder對象,其代表了有相同的字體,字體大小,字體顏色和字體格式的文本,這些屬性定義在Chunk類中Font對象中。其它的屬性如背景色(background),text rise(用來模擬下標和上標)還有underline(用來模擬文本的下划線或者刪除線)都定義在其它一系列的屬性中,這些屬性都可以通過settter 方法或者C#中的property進行設置。以下的代碼在pdf文檔中寫入32個countries的名稱和ID,這里的代碼只用到了Chunk對象。
listing 2.2 CountryChunks.cs
// step 1 Document document = new Document(); using (document) { // step 2 PdfWriter.GetInstance(document, new FileStream(fileName, FileMode.Create)).InitialLeading = 16; // step 3 document.Open(); // step 4 // database connection and statement string connStr = ConfigurationManager.AppSettings["SQLiteConnStr"]; SQLiteConnection conn = new SQLiteConnection(connStr); using (conn) { SQLiteCommand cmd = conn.CreateCommand(); cmd.CommandText = "SELECT COUNTRY, ID FROM FILM_COUNTRY ORDER BY COUNTRY"; cmd.CommandType = CommandType.Text; conn.Open(); SQLiteDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { // add a country to the document as a Chunk document.Add(new Chunk(reader.GetString(0))); document.Add(new Chunk(" ")); Font font = new Font(Font.FontFamily.HELVETICA, 6, Font.BOLD, BaseColor.WHITE); // add the ID in another font Chunk id = new Chunk(reader.GetString(1), font); id.SetBackground(BaseColor.BLACK, 1f, 0.5f, 1f, 1.5f); // with a background color id.SetTextRise(6); document.Add(id); document.Add(Chunk.NEWLINE); } } }
以上的代碼比較少見:在大部分情況下我們都使用Chunk類來構建其它的文本對象如Pharse和Paragraphs。總而言之除了一些比較特殊的Chunk對象(比如Chunk.NewLine),我們很少直接將Chunk加入到Document中。
行間距: LEADING
使用Chunk類要注意的是其不知道兩行之間的行間距(Leading),這就是為什么在代碼中要設置InitialLeading屬性的原因。大家可以試着將其去掉然后再看下重新生存的pdf文檔:你會發現所有的文本都寫在同一行上。
THE FONT OBJECT
上圖為創建的pdf文檔和其包含的字體,我們可以從文件菜單中選擇屬性,然后在點擊字體標簽就可以看到文檔中的所有字體。從圖中可以得知文檔使用了兩個字體:Helvetica和Helvetica-Bold,但在windows中打開pdf文檔時,Adobe Reader會將Helvetica替換為ArialMT字體,Helvetica-Bold替換為ArialBoldMT字體,因為這些字體看起來很類似。以下的代碼就使用了默認的字體:
document.Add(new Chunk(reader.GetString(0))); document.Add(new Chunk(" "));
在iText中默認的字體為Helvetica,字體大小12pt,如果需要其它的字體可以從文件或者其它資源文件中選擇需要的字體。
Font font = new Font(Font.FontFamily.HELVETICA, 6, Font.BOLD, BaseColor.WHITE);
以上我們設置了同一個font-family但不同類型的字體:Helvetica-Bold,並設置字體大小為6pt,字體顏色為白色。白色的字體在白色的頁面中是看不到的,因此后面通過SetBackground方法設置了Chunk類的背景色,后續我們還調用了SetTextRise方法將country ID作為上標打印出來,SetTextRise方法的參數表示的是離這一行的基准線(baseline)的距離,如果為正數就會模擬上標,負數就會模擬為下標。
最后調用Chunk.NewLine換行,這樣就保證每一個country name都是從新的一行開始。
The Phrase object: a List of Chunks with leading
Chunk類在iText是最小的原子文本,而Phrase類被定義為 "a string of word",因此Phrase是一個組合的對象。轉換為java和iText來說,phrase就是包含了Chunk類的一個ArraryList。在iTextSharp中這種組合關系是用List<IElement>表示。
不同的字體
當我們用不同的Chunk類來構建Phrase對象時,都會創建一些不同的Font常量以便代碼中使用。
listing 2.3 DirectorPhrases1.cs
Font BOLD_UNDERLINED = new Font(Font.FontFamily.TIMES_ROMAN, 12, Font.BOLD | Font.UNDERLINE); Font NORMAL = new Font(Font.FontFamily.TIMES_ROMAN, 12); public virtual Phrase CreateDirectorPharse(IDataReader reader) { Phrase director = new Phrase(); director.Add(new Chunk(reader.GetString(0), BOLD_UNDERLINED)); director.Add(new Chunk(",", BOLD_UNDERLINED)); director.Add(new Chunk(" ", NORMAL)); director.Add(new Chunk(reader.GetString(1), NORMAL)); return director; }
以上代碼中的CreateDirectorPharse方法構建了我們需要的Phrase對象。我們會循環調用80次以便從movie數據庫中獲取80個不同的導演信息(directors)。這里也推薦大家使用CreateObject方法來構建需要的Chunk、Phrase或者其它對象。
Phrase的行間距(Leading)
有了上面介紹的CreateDirectorPharse方法,下面創建pdf文檔就方便多了:
listing 2.4 DirectorPhrases1.cs
// step 1 Document document = new Document(); using (document) { // step 2 PdfWriter.GetInstance(document, new FileStream(fileName, FileMode.Create)); // step 3 document.Open(); string connStr = ConfigurationManager.AppSettings["SQLiteConnStr"]; SQLiteConnection conn = new SQLiteConnection(connStr); using (conn) { SQLiteCommand cmd = conn.CreateCommand(); cmd.CommandText = "SELECT name, given_name FROM film_director ORDER BY name, given_name;"; cmd.CommandType = CommandType.Text; conn.Open(); SQLiteDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { document.Add(CreateDirectorPharse(reader)); document.Add(Chunk.NEWLINE); } } }
仔細觀察以上代碼,你會發現InitialLeading屬性沒有出現了,這里使用了默認的leading。在iText中,如果沒有顯示的設置leading,那么iText會在加入到document中的Phrase或者Paragraph中查找其字體大小,然后乘以1.5就是最后的leading。如果有phrase對象,字體為10,那么leading就是15。對應默認的字體(12pt)默認的leading就是18。
嵌入字體
目前為止,我們都是通過Font類來創建字體。這樣創建的字體也通常稱之為 standard Type 1 fonts,這些字體是不會被iText嵌入到文檔中。standard Type 1 fonts過去也叫做內置字體(bulit-in fonts)或者Base 14 fonts。這14種字體(四種類型[normal、Bold、Italic、BoldItalic]的Helvetica,Times-Roman,Courier加上Symbol和ZapfDingbats)過去常常隨着PDF viewer發布。但我們要注意的是:這些字體只支持 American/Western-European 字符集,如果要添加簡體中文或者繁體中文就必須選擇其它的字體。
listing 2.5 DirectorPhrases2.cs
public static Font BOLD; public static Font NORMAL; static DirectorPhrases2() { BaseFont timesdb = null; BaseFont times = null; try { // create a font that will be embedded timesdb = BaseFont.CreateFont(@"c:/windows/fonts/timesbd.ttf", BaseFont.WINANSI, BaseFont.EMBEDDED); // create a font that will be embedded times = BaseFont.CreateFont(@"c:/windows/fonts/times.ttf", BaseFont.WINANSI, BaseFont.EMBEDDED); } catch (Exception) { Environment.Exit(1); } BOLD = new Font(timesdb, 12); NORMAL = new Font(times, 12); } public override Phrase CreateDirectorPharse(IDataReader reader) { Phrase director = new Phrase(); Chunk name = new Chunk(reader.GetString(0), BOLD); name.SetUnderline(0.2f, -2f); director.Add(name); director.Add(new Chunk(",", BOLD)); director.Add(new Chunk(" ", NORMAL)); director.Add(new Chunk(reader.GetString(1), NORMAL)); director.Leading = 24; return director; }
上面的代碼中我們通過BaseFont類從文件中獲取了Time New Roman(times.ttf)和Times new Roman Bold(timesdb.ttf)字體,並且設置為ANSI 字符集(BaseFont.WINANSI)並嵌入此字體(BaseFont.EMBEDDED)。BaseFont的詳細介紹在chapter11有說明,這里只要知道通過BaseFont和一個表示字體大小的float值來創建一個Font實例就足夠了。
以上兩圖是通過phrase對象創建的兩個pdf文檔的對比。仔細觀察會發現通過第二張圖創建的文檔的行間距更大,因為listing 2.5中設置了自定義更大的Leading。導演(directors)的名稱都有下划線,但具體的實現方式不同:listing 2.3使用的是有下划線屬性的Font,listing 2.5使用的是Chunk的SetUnderline方法(第一個參數表示的是線的厚度 0.2pt,第二個參數表示是離基准線的y坐標 -2pt表示在基准線下面2pt )。SetUnderline還有一個接受六個參數的方法,有點復雜具體可以參考書上的介紹。
圖中還有一個怪異的事情:兩個pdf文檔中都有Helvetica字體的存在,但代碼中沒有明確的引用此字體。其實這個字體是在以下代碼中被添加的:
document.Add(Chunk.NEWLINE);
Chunk.NewLine包含一個默認字體的換行符,而默認字體為Helvetica。我們可以通過以下代碼來避免:
document.Add(new Chunk("\n", NORMAL));
但一個更好的解決方法就是使用Paragraph對象。
Paragraph object: a Phrase with extra properties and a newline
雖然這個標題不是完全正確,不過作者總是將Phrase和Paragraph類比HTML中的span和div。如果在前一個例子中用Paragraph代替Phrase,就沒有必要添加document.Add(Chunk.NEWLINE)。代碼就可以這樣寫:
listing 2.6 MovieTitles.cs
List<Movie> movieCollection = PojoFactory.GetMovies(conn); foreach (Movie movie in movieCollection) { document.Add(new Paragraph(movie.Title)); }
Paragraph繼承Phrase類,因此我們在創建Paragraph類時和創建Phrase完全一致,不過Paragraph擁有更多的屬性設置:定義文本的對齊方式、不同的縮進和設置前后的空間大小。
Paragraph之新功能
我們通過以下代碼來體會一些Paragraph的新功能。listing 2.7顯示的是創建Paragraph
- CreateYearAndDuration()方法中用Chunk類組成Paragraph對象
- CreateMovieInfomatin()方法中則由Phrase和可以被當作Phrase的Paragraph對象組成。
listing 2.7 MovieParagraphs1
protected Paragraph CreateMovieInformation(Movie movie) { Paragraph p = new Paragraph(); p.Font = FilmFonts.NORMAL; p.Add(new Phrase("Title: ", FilmFonts.BOLDITALIC)); p.Add(PojoToElementFactory.GetMovieTitlePhrase(movie)); p.Add(" "); if (movie.OriginalTitle != null) { p.Add(new Phrase("Orginal title: ", FilmFonts.BOLDITALIC)); p.Add(PojoToElementFactory.GetOriginalTitlePhrase(movie)); p.Add(" "); } p.Add(new Phrase("Country: ", FilmFonts.BOLDITALIC)); foreach (Country country in movie.Countries) { p.Add(PojoToElementFactory.GetCountryPhrase(country)); p.Add(" "); } p.Add(new Phrase("Director: ", FilmFonts.BOLDITALIC)); foreach (Director director in movie.Directors) { p.Add(PojoToElementFactory.GetDirectorPhrase(director)); p.Add(" "); } p.Add(CreateYearAndDuration(movie)); return p; } protected Paragraph CreateYearAndDuration(Movie movie) { Paragraph info = new Paragraph(); info.Font = FilmFonts.NORMAL; info.Add(new Chunk("Year: ", FilmFonts.BOLDITALIC)); info.Add(new Chunk(movie.Year.ToString(), FilmFonts.NORMAL)); info.Add(new Chunk(" Duration: ", FilmFonts.BOLDITALIC)); info.Add(new Chunk(movie.Duration.ToString(), FilmFonts.NORMAL)); info.Add(new Chunk(" minutes", FilmFonts.NORMAL)); return info; }
以上代碼中我們將Font對象統一聚合在FilmFonts類中,並且選用了一些通用的名稱:NORMAL,BOLD,BOLDITAL和ITALIC。這樣的好處就是如果以后要修改字體的話名稱不就需要修改,而且如果以后要將字體從Helevetica修改為Times,我們也只要修改此處即可。
CreateMovieInfomatin方法在listing 2.8中代碼使用:
listing 2.8 MovieParagraphs1.cs
foreach (Movie movie in PojoFactory.GetMovies(conn)) { Paragraph p = CreateMovieInformation(movie); p.Alignment = Element.ALIGN_JUSTIFIED; p.IndentationLeft = 18; p.FirstLineIndent = -18; document.Add(p); }
接下來我們使用PojoToElementFactory類中方法將POJO對象轉換為Phrase對象。隨着應用程序的不斷增大,將一些可重用的方法如GetMovieTitlePhrase和GetDirectorPhrase組合在單獨的factory中是蠻有好處的。
listing 2.9 MovieParagraphs2.cs
foreach (Movie movie in movies) { // Create a paragraph with the title Paragraph title = new Paragraph(PojoToElementFactory.GetMovieTitlePhrase(movie)); title.Alignment = Element.ALIGN_LEFT; document.Add(title); // Add the original title next to it using a dirty hack if (movie.OriginalTitle != null) { Paragraph dummy = new Paragraph("\u00a0", FilmFonts.NORMAL); dummy.Leading = -18; document.Add(dummy); Paragraph orignialTitle = new Paragraph(PojoToElementFactory.GetOriginalTitlePhrase(movie)); orignialTitle.Alignment = Element.ALIGN_RIGHT; document.Add(orignialTitle); } // Info about the director Paragraph director; float indent = 20; // Loop over the directors foreach (Director item in movie.Directors) { director = new Paragraph(PojoToElementFactory.GetDirectorPhrase(item)); director.IndentationLeft = indent; document.Add(director); indent += 20; } // Info about the country Paragraph country; indent = 20; // Loop over the countries foreach (Country item in movie.Countries) { country = new Paragraph(PojoToElementFactory.GetCountryPhrase(item)); country.Alignment = Element.ALIGN_RIGHT; country.IndentationRight = indent; document.Add(country); indent += 20; } // Extra info about the movie Paragraph info = CreateYearAndDuration(movie); info.Alignment = Element.ALIGN_CENTER; info.SpacingAfter = 36; document.Add(info); }
以上代碼產生的pdf文檔會列出數據庫中所有的movie信息:title,OriginalTitle(如果存在的話),diector和countries以及production year和run length。具體的數據大家可以用可視化工具SQLite Expert profession工具打開Movies.db3查看。
在listing 2.8中我們設置了Paragraph的Alignment屬性為Element.ALIGN_JUSTIFIED,這會導致iText在內部改變單詞以每個字母之間的距離從而保證文本有相同的左邊距和右邊距。listing 2.9還調用Element.ALIGN_LEFT,Element.ALIGN_RIGHT。Element.ALIGN_JUSTIFIED_ALL和Element.ALIGN_JUSTIFIED的效果類似,區別就是Element.ALIGN_JUSTIFIED_ALL會將最后一行也進行對齊操作。沒有設置的話默認為Element.ALIGN_LEFT。
Paragraph的indentation(縮進)有以下三種:
- IndentationLeft
- IndentationRight
- FirstLineIndent
listing 2.8中我們先設置了IndentationLeft為18pt,但后續又設置FirstLineIndent為-18pt。這樣的話第一行實際上就沒有縮進了,而第二行開始就會有18pt的左縮進,這樣第一行和其它行就比較方便區別。區分不同的Paragraph可以還設置SpacingAfter和SpacingBefore屬性。
最后在listing 2.9中為了實現將movie的title(左對齊)和orginal title(右對齊)打印在同一行上,代碼使用了一些特殊的辦法:在之間添加了dummy的Paragraph,然后設置其Leading為-18,這樣會導致當前頁的當前座標往上移了一行。在這個例子中這種方法還不錯,但其它情況下就不太一樣,比如說如果前一行導致了第二頁,那么就不可能重新回到前一頁中。還有如果title和orginal title的內容太長,那么在同一行上還會導致溢出。我們會在后續解決這一問題。
Distributing text over different lines
在listing 2.8生成的movie_paragraphs_1.pdf文檔中,所有的信息都包含在一個Paragraph類中。對於大部分的movie來說,Paragraph的內容都會超過一行,因此iText就會內容分布在不同的行上。默認情況下iText會盡可能多將完整的單詞添加在一行里面。在iText中會將空格和hypen(符號'-')當作分割字符(split character),不過也可以通過代碼重新定義分隔字符。
分隔字符
如果我們想在同一行上將兩個單詞通過空格分開,不能使用常用的空格符(char)32,而應該使用nonbreaking space character (char)160。下面的代碼我們用StringBuilder包含Stanley Kubrick導演的所有電影名稱,然后用pipe符號('|')連接成一個很長的字符串。電影名稱中我們還將普通的空格符用nonbreaking space character代替。
listing 2.10 MovieChain.cs
// create a long Stringbuffer with movie titles StringBuilder buf1 = new StringBuilder(); foreach (Movie movie in kubrick ) { // replace spaces with non-breaking spaces buf1.Append(movie.Title.Replace(' ', '\u00a0')); // use pipe as separator buf1.Append('|'); } // Create a first chunk Chunk chunk1 = new Chunk(buf1.ToString()); // wrap the chunk in a paragraph and add it to the document Paragraph paragraph = new Paragraph("A:\u00a0"); paragraph.Add(chunk1); paragraph.Alignment = Element.ALIGN_JUSTIFIED; document.Add(paragraph); document.Add(Chunk.NEWLINE); // define the pipe character as split character chunk1.SetSplitCharacter(new PipeSplitCharacter()); // wrap the chunk in a second paragraph and add it paragraph = new Paragraph("B:\u00a0"); paragraph.Add(chunk1); paragraph.Alignment = Element.ALIGN_JUSTIFIED; document.Add(paragraph); document.Add(Chunk.NEWLINE);
因為我們替換了空格符,iText在chunk1對象中就找不到默認的分隔字符,所以當一行中不能容納多余的字符時iText會將一些單詞分隔在不同的行顯示。接下來再次添加相同的內容,不過這里定義了pipe符號('|')為分隔字符。下面的代碼是接口ISplitCharacter的具體實現類PipeSplitCharacter。使用是只需調用Chunk類的SetSplitCharacter()方法即可。
listing 2.11 PipeSplitCharacter.cs
public bool IsSplitCharacter(int start, int current, int end, char[] cc, iTextSharp.text.pdf.PdfChunk[] ck) { char c; if (ck == null) { c = cc[current]; } else { c = (char)ck[Math.Min(current, ck.Length - 1)].GetUnicodeEquivalent(cc[current]); } return (c == '|' || c <= ' ' || c == '-'); }
上面的方法看起來有點復雜,不過在大部分情況下我們只要copy return那一行的代碼。下圖就是代碼生成的pdf文檔:
在段落A中,內容文本分隔的不太正常,如單詞"Love"被分隔為"Lo"和"ve"。段落B中定義了pipe符號('|')為分隔字符。段落C中的內容是沒有將正常空格替換為nonbreaking spaces的情況。
連接字符
以下的代碼和listing 2.10類似,不過這里沒有替換正常的空格。但調用了Chunk類的一個方法SetHyphenation。(連接字符的意思是:如果在一行的結尾處不能容納整個單詞,但可以容納單詞的部分字符,那么這個單詞就會被分隔不同行,但會用連接符('-')連接起來以表示其為一個單詞。)
list 2.11 MovieChain.cs
// create a new StringBuffer with movie titles StringBuilder buf2 = new StringBuilder(); foreach (Movie movie in kubrick) { buf2.Append(movie.Title); buf2.Append('|'); } // Create a second chunk Chunk chunk2 = new Chunk(buf2.ToString()); // wrap the chunk in a paragraph and add it to the document paragraph = new Paragraph("C:\u00a0"); paragraph.Add(chunk2); paragraph.Alignment = Element.ALIGN_JUSTIFIED; document.Add(paragraph); document.NewPage(); // define hyphenation for the chunk chunk2.SetHyphenation(new HyphenationAuto("en", "US", 2, 2)); // wrap the second chunk in a second paragraph and add it paragraph = new Paragraph("D:\u00a0"); paragraph.Add(chunk2); paragraph.Alignment = Element.ALIGN_JUSTIFIED; document.Add(paragraph); // go to a new page document.NewPage(); // define a new space/char ratio writer.SpaceCharRatio = PdfWriter.NO_SPACE_CHAR_RATIO; // wrap the second chunk in a third paragraph and add it paragraph = new Paragraph("E:\u00a0"); paragraph.Add(chunk2); paragraph.Alignment = Element.ALIGN_JUSTIFIED; document.Add(paragraph);
以上的代碼中我們創建了HyphenationAuto類的一個實例,iText會在一些命名為en_US.xml或者en_GB.xml文件中找到字符連接的規制。代碼中傳入了四個參數,前兩個引用的為就剛剛提到的xml文件,第三個和第四個參數表示的是從單詞的開頭或者單詞的結尾開始有多少個字符可以被單獨拿出來以便於連接。比如第三個參數為1那么單詞"elephant"就會被連接為"e-lephant",但只有一個字符被連接的話看起來總是不太正常。Paragraph對象D和E都有一個兩端對齊的選項和連接字符。對齊是通過在單詞之間和單詞的字符之間添加格外的空間實現。段落D使用的是默認設置。默認設置的比率為2.5,意思是單詞之間的格外空間是單詞內字符之間格外空間的2.5倍。但可以通過PdfWriter.SpaceCharRatio來設置自定義的比率。在段落E中設置的就是PdfWriter.NO_SPACE_CHAR_RATIO,這樣的話單詞內字符之間的空間就沒有格外添加,而僅僅是在單詞之間添加一些格外的空間。
最后要注意的是調用Chunk類的SetHyphenation方法時要引用格外的dll:itext-hyph-xml.dll,這里的引用不是在project上添加引用,而是在代碼中將其加入到資源文件:
BaseFont.AddToResourceSearch(@"D:\itext-hyph-xml.dll");
The List object: a sequence of Paragraphs called ListItem
在前面的例子中我們將Movies,directors和countries的信息全部列舉出來。接下來我們會重復這一過程,但不同的是:我們會先創建countries的列表,然后此列表中添加movie列表,movie下再添加directors列表。
有序列表和無序列表
為了實現此功能,我們會用到List類和一系列的ListItem類。在UML圖中可以得知ListItem是繼承與Paragraph類的,主要的區別在於ListItem類有格外的一個Chunk變量,此變量代表的就是列表符號。以下代碼生成的report就使用了有序和無序列表。有序列表的列表符號可以是數字或者字母(默認為數字),字母可以為大寫和小寫(默認為大寫)。無序列表的列表符號為連接符"-"。
listing 2.13 MovieLists1.cs
List list = new List(List.ORDERED); using (conn) {
……
while (reader.Read()) { ListItem item = new ListItem(string.Format("{0}: {1} movies", reader.GetString(1), reader.GetInt32(2)), FilmFonts.BOLDITALIC); List movieList = new List(List.ORDERED, List.ALPHABETICAL); movieList.Lowercase = List.LOWERCASE; string country_id = reader.GetString(0); foreach (Movie movie in PojoFactory.GetMovies(conn, country_id)) { ListItem movieItem = new ListItem(movie.Title); List directorList = new List(List.UNORDERED); foreach (Director director in movie.Directors) { directorList.Add(string.Format("{0}, {1}", director.Name, director.GivenName)); } movieItem.Add(directorList); movieList.Add(movieItem); } item.Add(movieList); list.Add(item); } } document.Add(list);
以上代碼中我們發現可以直接將string類型的變量加入到List類中而不需要創建ListItem類。不過iText內部會自動創建ListItem來包裹此字符串。
改變列表符號
listing 2.14 MovieLists2.cs
List list = new List(); list.Autoindent = false; list.SymbolIndent = 36; using (conn) { …… while (reader.Read()) { ListItem item = new ListItem(string.Format("{0}: {1} movies", reader.GetString(1), reader.GetInt32(2)), FilmFonts.BOLDITALIC); item.ListSymbol = new Chunk(reader.GetString(0)); List movieList = new List(List.ORDERED, List.ALPHABETICAL); movieList.Alignindent = false; string country_id = reader.GetString(0); foreach (Movie movie in PojoFactory.GetMovies(conn, country_id)) { ListItem movieItem = new ListItem(movie.Title); List directorList = new List(List.ORDERED); directorList.PreSymbol = "Director "; directorList.PostSymbol = ": "; foreach (Director director in movie.Directors) { directorList.Add(string.Format("{0}, {1}", director.Name, director.GivenName)); } movieItem.Add(directorList); movieList.Add(movieItem); } item.Add(movieList); list.Add(item); } } document.Add(list);
對於countries的列表,我們設置其列表符號的縮進,每個列表選項都定義其列表符號為數據庫中的ID號。和前一個例子對比,movie的列表有些小不同:我們設置了Alignindent屬性。在listing 2.13中,iText將每個movie的列表選項都設置了同一個縮進。但在listing 2.14中由於Alignindent屬性的設置,每個列表選項有基於自己列表符號的縮進。
從上圖可以得知每個有序列表的列表符號后都有一個period(.)符號,不過iText中也可以通過PreSymbol和PostSymbol來設置。如在listing 2.14中我們會獲取類似於"Director 1:","Director 2:"的列表符號。
特殊列表
上圖中有4個更多的列表類型,以下代碼中我們創建了RomanList,GreekList和ZapfDingbatsNumberList。
listing 2.15 MovieList3.cs
List list = new RomanList(); List movieList = new GreekList(); movieList.Lowercase = List.LOWERCASE; List directorList = new ZapfDingbatsList(0);
但如果列表有很多選項的話還是不要選擇ZapfDingbatsNumberList類型,ZapfDingbatsNumberList類型在列表選項超過10的時候就不能正確的顯示。ZapfDingbats是14個standard Type 1 font中一個,其包含了一些特殊的符號,對應的列表類型為ZapfDingbatsList。
The DrawInterface: vertical position marks, separators, and tabs
往文檔中添加內容是有時候需要添加一些比較特殊的東東。比如你可以希望在頁面的當前位置添加一個標記(如一個箭頭),又或者希望從頁面的左邊距划一條線到頁面的右邊距。這些東西可以通過使用IDrawInterface來實現。以下為接口IDrawInterface的類圖:
水平標記符號
假設我們要創建一個directors的列表,然后列出每個director所導演的電影。對於這個列表我們希望如果這個director有超過兩部的電影,那么在director的左邊有一個箭頭標識,對於電影我們希望如果制作日期在2000年或者之后也有一個箭頭標識。大家可以看下圖所示:
這里是通過繼承VerticalPositionMark類來實現此功能。
listing 2.17 PositionedArrow.cs
public class PositionedArrow : VerticalPositionMark { protected Boolean left; … public static readonly PositionedArrow LEFT = new PositionedArrow(true); public static readonly PositionedArrow RIGHT = new PositionedArrow(false); …… public override void Draw(PdfContentByte canvas, float llx, float lly, float urx, float ury, float y) { canvas.BeginText(); canvas.SetFontAndSize(zapfdingbats, 12); if (left) { canvas.ShowTextAligned(Element.ALIGN_CENTER, ((char)220).ToString(), llx - 10, y, 0); } else { canvas.ShowTextAligned(Element.ALIGN_CENTER, ((char)220).ToString(), urx + 10, y + 8, 180); } canvas.EndText(); } }
具體使用時可以用Document.Add方法將PositionedArrow類的實例添加進去。因為PositionedArrow類繼承VerticalPositionMark,VerticalPositionMark又實現了IElement接口。當這種類型的IElement被添加到文檔中時自定義的Draw方法就會被調用,而且這個方法可以獲取其引用內容被添加的面板(canvas),此方法還知道頁邊距的坐標(其中 (llx,lly)為頁邊距的左下角坐標,(urx,ury)為頁邊距的右上角坐標) 和當前的y坐標。
listing 2.18 DirectorOverview1.cs
Director director; // creating separators LineSeparator line = new LineSeparator(1, 100, null, Element.ALIGN_CENTER, -2); Paragraph stars = new Paragraph(20); stars.Add(new Chunk(StarSeparator.LINE)); stars.SpacingAfter = 50; using (conn) { // step 4 …… // looping over the directors while (reader.Read()) { // get the director object and use it in a Paragraph director = PojoFactory.GetDirector(reader); Paragraph p = new Paragraph(PojoToElementFactory.GetDirectorPhrase(director)); int count = Convert.ToInt32(reader["c"]); // if there are more than 2 movies for this director // an arrow is added to the left if (count > 2) { p.Add(PositionedArrow.LEFT); } p.Add(line); // add the paragraph with the arrow to the document document.Add(p); // Get the movies of the directory, ordered by year int director_id = Convert.ToInt32(reader["ID"]); List<Movie> movies = PojoFactory.GetMovies(conn, director_id); var sortedMovies = from m in movies orderby m.Year select m; // loop over the movies foreach (Movie movie in sortedMovies) { p = new Paragraph(movie.Title); p.Add(" ; "); p.Add(new Chunk(movie.Year.ToString())); if (movie.Year > 1999) { p.Add(PositionedArrow.RIGHT); } document.Add(p); } // add a star separator after the director info is added document.Add(stars); } }
這里要注意的是PositionedArrow並不是直接加入到Document中,其被Paragraph對象包裹起來,因此PostionedArrow引用的是Paragraph,所以下一步就要將Paragraph添加到Document中,這樣可以避免分頁而導致一些問題
LINE SEPARATORS
但我們需要畫一條直線時,需要知道的是文本的垂直座標。知道只會就可以很方便的使用LineSeparator類。在listing 2.18中,我們就用以下的幾個參數創建了一個LineSeparator的實例:
- The line width(線的寬度),在這里是1pt。
- The percentage that needs to be covered(需要覆蓋的百分比),這里是100%。
- A color(線的顏色),null就表示使用默認的顏色。
- The alignment(對齊設置),只有在百分比不是100%才有意義。
- The offset(偏移量),這里是基准線往下2pt。
如果這個對象還不滿足需求,我們還可以創建自定義的VerticalPositionMark子類,或者直接實現IDrawInterface接口。
listing 2.19 StarSeparator.cs
public class StarSeparator : IDrawInterface { ……
#region IDrawInterface Members public void Draw(iTextSharp.text.pdf.PdfContentByte canvas, float llx, float lly, float urx, float ury, float y) { float middle = (llx + urx) / 2; canvas.BeginText(); canvas.SetFontAndSize(bf, 10); canvas.ShowTextAligned(Element.ALIGN_CENTER, "*", middle, y, 0); canvas.ShowTextAligned(Element.ALIGN_CENTER, "* *", middle, y - 10, 0); canvas.EndText(); } #endregion }
這里要注意的是StarSeparator類沒有實現IElement接口,所以不能直接添加到document中,具體使用時會將其包裹在Chunk對象中
SEPARATOR CHUNKS
在listing 2.9是代碼應用了一個類似與hack的方法將movie的title和orginal title打印在同一行上。這里介紹一個正確的使用方法:
list 2.20 DirectorOverview2.cs
// get the director object and use it in a Paragraph director = PojoFactory.GetDirector(reader); Paragraph p = new Paragraph(PojoToElementFactory.GetDirectorPhrase(director)); // add a dotted line separator p.Add(new Chunk(new DottedLineSeparator())); // adds the number of movies of this director int count = Convert.ToInt32(reader["c"]); p.Add(string.Format("movies: {0}", count)); document.Add(p); // Creates a list List list = new List(List.ORDERED); list.IndentationLeft = 36; list.IndentationRight = 36; // Gets the movies of the current director int director_id = Convert.ToInt32(reader["ID"]); List<Movie> movies = PojoFactory.GetMovies(conn, director_id); var sortedMovies = from m in movies orderby m.Year select m; ListItem movieItem; // loops over the movies foreach (Movie movie in sortedMovies) { // creates a list item with a movie title movieItem = new ListItem(movie.Title); // adds a vertical position mark as a separator movieItem.Add(new Chunk(new VerticalPositionMark())); // adds the year the movie was produced movieItem.Add(new Chunk(movie.Year.ToString())); // add an arrow to the right if the movie dates from 2000 or later if (movie.Year > 1999) { movieItem.Add(PositionedArrow.RIGHT); } // add the list item to the list list.Add(movieItem); } // add the list to the document document.Add(list);
在以上的代碼中我們將DottedLineSeparator類包裹在Chunk類中,然后使用次Chunk類來分隔director和其導演的電影數目。DottedLineSeparator類是LineSeparator,的子類,主要的區別是DottedLineSeparator畫的是虛線。下圖為效果圖:
使用標簽(tab)也可以在一行中分布內容。
TAB CHUNKS
下圖呈現的就是使用tabs在一行或者多行上對title,orginal title,runlength和produce year進行分布。如果使用通常的separator Chunks,文檔中內容就不會按照列的形勢對齊。
如果title和其對應的original title太長的話,iText會創建新的一行,因為我們已經在tab chunk中定義了此種情況。如果你將tab chunk構造器中的true修改為false的話,就不會有換行,文本就會覆蓋。
listing 2.21 DirectorOverview3.cs
// creates a paragraph with the director name director = PojoFactory.GetDirector(reader); Paragraph p = new Paragraph(PojoToElementFactory.GetDirectorPhrase(director)); // adds a separator p.Add(CONNECT); // adds more info about the director p.Add(string.Format("movies: {0}", reader["c"])); // adds a separator p.Add(UNDERLINE); // adds the paragraph to the document document.Add(p); // gets all the movies of the current director int director_id = Convert.ToInt32(reader["ID"]); List<Movie> movies = PojoFactory.GetMovies(conn, director_id); var sortedMovies = from m in movies orderby m.Year select m; // loop over the movies foreach (Movie movie in sortedMovies) { // create a Paragraph with the movie title p = new Paragraph(movie.Title); // insert a tab p.Add(new Chunk(tab1)); // add the origina title if (movie.OriginalTitle != null) { p.Add(new Chunk(movie.OriginalTitle)); } // insert a tab p.Add(new Chunk(tab2)); // add the run length of the movie p.Add(new Chunk(string.Format("{0} minutes", movie.Duration))); // insert a tab p.Add(new Chunk(tab3)); // add the production year of the movie p.Add(new Chunk(movie.Year.ToString())); // add the paragraph to the document document.Add(p); } document.Add(Chunk.NEWLINE);
總結
這一節中我們實現介紹了Chunk類以及其大部分的屬性,后續還會介紹一些其它屬性。接下來使用了Phrase和Paragraph類,期間還說明了Font和BaseFont的用法。然后使用ListItem構建的List來呈現文檔,最后還討論separator Chunks的不同用法。
代碼和圖片有點多,不過創建的pdf都比較結合實踐,希望這邊文章各位能夠喜歡。代碼大家可以點擊這里下載。
同步
此文章已同步到目錄索引:iText in Action 2nd 讀書筆記。