这是我写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中来,具体的实现可以参考代码。
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中。
使用Chunk类要注意的是其不知道两行之间的行间距(Leading),这就是为什么在代码中要设置InitialLeading属性的原因。大家可以试着将其去掉然后再看下重新生存的pdf文档:你会发现所有的文本都写在同一行上。
上图为创建的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都是从新的一行开始。
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或者其它对象。
有了上面介绍的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对象。
虽然这个标题不是完全正确,不过作者总是将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的新功能。listing 2.7显示的是创建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(缩进)有以下三种:
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的内容太长,那么在同一行上还会导致溢出。我们会在后续解决这一问题。
在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");
在前面的例子中我们将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。
往文档中添加内容是有时候需要添加一些比较特殊的东东。比如你可以希望在页面的当前位置添加一个标记(如一个箭头),又或者希望从页面的左边距划一条线到页面的右边距。这些东西可以通过使用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中,这样可以避免分页而导致一些问题
但我们需要画一条直线时,需要知道的是文本的垂直座标。知道只会就可以很方便的使用LineSeparator类。在listing 2.18中,我们就用以下的几个参数创建了一个LineSeparator的实例:
如果这个对象还不满足需求,我们还可以创建自定义的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对象中
在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 读书笔记。