PdfPCell类继承于Rectangle类,因此也继承了很多修改边框和背景色的属性和方法,后续我们会讨论到,但现在我们先要说明PdfPCell的内容模式。在iText的内部PdfPCell的内容被保存在ColumnText对象里面,如果你对ColumnText类有比较清晰的了解那么PdfPCell就很好理解了。但如果大家跳过了第三节直接到这里的话,那还是希望大家先学习第三节中关于ColumnText的内容。
在这个子节中我们会用Phrase和Chunk对象作为填充表格的内容,以下为效果图:
这里不能使用Paragraph,List或者Image对象,因为我们处于文本模式(text mode),而且要注意的时:对齐选项和行高是通过PdfPCell类的属性来设置的。
好了,直接上代码
listing 4.6 MovieTextMode.cs
List<Movie> movies = PojoFactory.GetMovies(conn); foreach (Movie movie in movies) { PdfPTable table = new PdfPTable(2); table.SetWidths(new int[] { 1, 4 }); PdfPCell cell; cell = new PdfPCell(new Phrase(movie.Title, FilmFonts.BOLD)); cell.HorizontalAlignment = Element.ALIGN_CENTER; cell.Colspan = 2; table.AddCell(cell); if (movie.OriginalTitle != null) { cell = new PdfPCell(PojoToElementFactory.GetOriginalTitlePhrase(movie)); cell.Colspan = 2; cell.HorizontalAlignment = Element.ALIGN_RIGHT; table.AddCell(cell); } List<Director> directors = movie.Directors; cell = new PdfPCell(new Phrase("Directors:")); cell.Rowspan = directors.Count; cell.VerticalAlignment = Element.ALIGN_MIDDLE; table.AddCell(cell); int count = 0; foreach (Director pojo in directors) { cell = new PdfPCell(PojoToElementFactory.GetDirectorPhrase(pojo)); cell.Indent = 10 * count++; table.AddCell(cell); } table.DefaultCell.HorizontalAlignment = Element.ALIGN_RIGHT; table.AddCell("Year:"); table.AddCell(movie.Year.ToString()); table.AddCell("Run Length :"); table.AddCell(movie.Duration.ToString()); List<Country> countries = movie.Countries; cell = new PdfPCell(new Phrase("Countries:")); cell.Rowspan = countries.Count; cell.VerticalAlignment = Element.ALIGN_BOTTOM; table.AddCell(cell); foreach (Country country in countries) { table.AddCell(country.C_Country); } document.Add(table); }
在上面的代码中,单元格是通过以下两种方式添加到表格中的:
就如同ColumnText对象一样我们可以设置Indent(第一行的缩进)和FollowingIndent,SpaceCharRatio等属性。
上图中右边的单元格显示的是有不同的行高(leading)和padding。大家要注意的是在文本模式下单元格中内容的行高就等于字体的大小,在内部其是如同这样设置的:SetLeading(0,1)。所以Phrase在PdfPcell中和不在其中的行高是不同的。不过我们可以像ColumnText对象一样通过SetLeading方法来改变此设置。以下为图中前五行的设置代码:
listing 4.7 Spacing.cs
PdfPCell cell = new PdfPCell(p); //row 1 table.AddCell("default leading/ spacing"); table.AddCell(cell); //row 2 table.AddCell("absolute leading: 20"); cell.SetLeading(20f, 0f); table.AddCell(cell); //row 3 table.AddCell("absolute leading: 3; relative leading: 1.2"); cell.SetLeading(3f, 1.2f); table.AddCell(cell); //row 4 table.AddCell("absolute leading: 0; relative leading: 1.2"); cell.SetLeading(0f, 1.2f); table.AddCell(cell); //row 5 table.AddCell("no leading at all"); cell.SetLeading(0f, 0f); table.AddCell(cell);
这里要说明的是:行高设置为零是不推荐的做法,从图上可以看到行高为零的内容会溢出单元格并和前一列的内容重叠了。PdfPCell内的padding的默认大小为2pt,但也可以通过Padding属性来改变,以下为6到8行的padding设置代码:
listing 4.8 Spacing.cs (contiuned)
//row 6 cell = new PdfPCell(new Phrase("Dr. iText or: How I Learned to Stop Worrying and Love PDF")); table.AddCell("padding 10"); cell.Padding = 10; table.AddCell(cell); //row 6 table.AddCell("padding 0"); cell.Padding = 0; table.AddCell(cell); //row 7 table.AddCell("different padding for left,right, top and bottom"); cell.PaddingLeft = 20; cell.PaddingRight = 50; cell.PaddingTop = 0; cell.PaddingBottom = 5; table.AddCell(cell);
以上代码中的Padding属性和HTML中table标签的cellPadding属性有类似的行为,但在iText中没有对应的CellSpacing属性,不过我们会在第五节是通过单元格的事件来达到同样的效果。
我们可以根据单元格第一行的ascender来调整padding值。bottom padding的值则适应单元格中的最后一行的descender值。以下的代码为12行的设置代码:
listing 4.9 Spacing.cs (contiuned)
table.DefaultCell.UseDescender = true; table.DefaultCell.UseAscender = true; table.AddCell("padding 2; ascender, descender"); cell.Padding = 2; cell.UseAscender = true; cell.UseDescender = true; table.AddCell(p);
这里的ascender和descender是在前几节中丈量文本时已经提到的:ascender是在基准线上需要的空间,descender是在基准线下需要的空间。通过行间距,padding和ascender,descender会对单元格的高度有影响,扩展起来就是对一行的高度有影响。
每一行的高度是需要计算的,但iText并不是每次都有足够的数据来计算,具体参考以下代码:
listing 4.10 TableHeight.cs
PdfPTable table = CreateFirestTable(); document.Add(new Paragraph(string.Format("Table height before document.Add(): {0}", table.TotalHeight))); document.Add(new Paragraph(string.Format("Height of the first row : {0}", table.GetRowHeight(0)))); document.Add(table); document.Add(new Paragraph(string.Format("Table height after document.Add(): {0}", table.TotalHeight))); document.Add(new Paragraph(string.Format("Height of the firest row: {0}", table.GetRowHeight(0)))); table = CreateFirestTable(); document.Add(new Paragraph(string.Format("Table height before SetTotalWidth(): {0}", table.TotalHeight))); document.Add(new Paragraph(string.Format("Height of the first row: {0}", table.GetRowHeight(0)))); table.TotalWidth = 50; table.LockedWidth = true; document.Add(new Paragraph(string.Format("Table height after SetTotalWidth(): {0}", table.TotalHeight))); document.Add(new Paragraph(string.Format("Height of the first row: {0}", table.GetRowHeight(0)))); document.Add(table);
在以上的代码中,表格的高度在添加到Document之前为零,添加之后为48。这个是正常的:在iText不知道表格的宽度时是不能计算出高度的。同样的table在设置了50pt的固定宽度时就有192pt的高度,但由于其宽度远远小余页面宽度的80%,里面的文本就被包裹起来。但也可以设置NoWrap属性来改变。具体的说明参考下图和代码:
liting 4.11 CellHeights.cs
// a long phrase with newlines p = new Phrase("Dr. iText or:\nHow I Learned to Stop Worrying\nand Love PDF."); cell = new PdfPCell(p); // the phrase fits the fixed height table.AddCell("fixed height(more than sufficient)"); cell.FixedHeight = 72f; table.AddCell(cell); // the phrase doesn't fit the fixed height table.AddCell("fixed height(not sufficient)"); cell.FixedHeight = 36f; table.AddCell(cell); // The minimum height is exceeded table.AddCell("minimum height"); cell = new PdfPCell(new Phrase("Dr. iText")); cell.MinimumHeight = 36f; table.AddCell(cell); // The last row is extended table.ExtendLastRow = true; table.AddCell("extend last row"); table.AddCell(cell); document.Add(table);
通过以上的代码和图大家可以看到如果设置NoWrap为True的话文本就有可能溢出单元格,所以一般情况下还是不要设置。以上的行高都是计算出来的,但我们也可以通过FixedHeight 属性来设置行高,以上代码中我们设置了两个行高:72和36,然后将文本:Dr.iText or : How I learned to stop worrying and love PDF添加进单元格,从图中可以看到36pt的行高不够因此and Love PDF就没有显示出来。这就可以回答了在学习ColumnText时提到的问题:如果文本不能完全填充时有什么结果。所以我们调用ColumnText的方法时要确认文本能够完全填充或者你希望通过这个方法减少一些文本。设置FixedHeight的值太少会导致文本减少,但我们还可以设置MinimumHeight属性:如果文本可以填充的话行高就不变,如果不能填充行高就会相应的变大。最后一行的单元格延伸到了页面的下边距,但这不是PdfPCell的属性,是通过PdfPTable的ExtendLastRow来实现的。
目前为止我们都是在文本模式的情况下讨论PdfPCell的属性,但除了leading, horizontal alignment和indentation属性之外,其它的属性如cell height,padding等在组合模式时也是有效的。有效的属性也包括从Rectangle类继承的属性。
这里就直接上图和代码:
listing 4.12 RotationAndColors.cs
// row 1, cell 1 cell = new PdfPCell(new Phrase("COLOR")); cell.Rotation = 90; cell.VerticalAlignment = Element.ALIGN_TOP; table.AddCell(cell); // row 1, cell 2 cell = new PdfPCell(new Phrase("red/ no borders")); cell.Border = Rectangle.NO_BORDER; cell.BackgroundColor = BaseColor.RED; table.AddCell(cell); // row 1, cell 3 cell = new PdfPCell(new Phrase("green/ black bottom border")); cell.Border = Rectangle.BOTTOM_BORDER; cell.BorderColor = BaseColor.BLACK; cell.BorderWidthBottom = 10f; cell.BackgroundColor = BaseColor.GREEN; table.AddCell(cell); // row 1, cell 4 cell = new PdfPCell(new Phrase("cyan / blue top border + padding")); cell.Border = Rectangle.TOP_BORDER; cell.UseBorderPadding = true; cell.BorderWidthTop = 5f; cell.BorderColorTop = BaseColor.BLUE; cell.BackgroundColor = BaseColor.CYAN; table.AddCell(cell); // row 2, cell 1 cell = new PdfPCell(new Phrase("GRAY")); cell.Rotation = 90; cell.VerticalAlignment = Element.ALIGN_MIDDLE; table.AddCell(cell); // row 2, cell 2 cell = new PdfPCell(new Phrase("0.6")); cell.Border = Rectangle.NO_BORDER; cell.GrayFill = 0.6f; table.AddCell(cell);
通过单元格的Rotation属性我们设置其内容的旋转,和Image对象一样默认是逆时针旋转。如第二行的第一个单元格是通过Rotaion和VerticalAlignment来实现的。从图上大家会发现这个表格比较花哨,其中包含了很多的颜色。第一行的第二个单元格就是通过BackgroundColor 来设置背景色,而第二行的背景色是通过GrayFill来设置。边框的设置是通过Border属性设置,其中Rectangle.NO_BORDER就是没有边框,上下左右的边框对应的就是TOP_BORDER,BOTTOM_BORDER,LEFT_BORDER,RIGHT_BORDER。大家记得在学习Image的时候Rectangle.BOX会同时设置上下左右边框,这个值也是单元格的默认选项。
以上的代码还设置了UseBorderPadding属性为True,这样border就会计算在padding内,如果不设置的话文本就会覆盖border,就如同第三行的第二个单元格一样。border的设置可以通过Border的相应属性统一设置,也可以分别设置,但他们之间还是有点小区别:
listing 4.13 RotationAndColors.cs (continued)
// row 3, cell 3 cell = new PdfPCell(new Phrase("with correct padding")); cell.UseBorderPadding = true; cell.BorderWidthLeft = 16f; cell.BorderWidthBottom = 12f; cell.BorderWidthRight = 8f; cell.BorderWidthTop = 4f; cell.BorderColorLeft = BaseColor.RED; cell.BorderColorBottom = BaseColor.ORANGE; cell.BorderColorRight = BaseColor.YELLOW; cell.BorderColorTop = BaseColor.GREEN; table.AddCell(cell); // row 3, cell 4 cell = new PdfPCell(new Phrase("red border")); cell.BorderWidth = 8f; cell.BorderColor = BaseColor.RED; table.AddCell(cell);
但我们设置某一个border时,UseVariableBorders属性就会被设置为true,这就会将border画进单元格里面。但如果通过统一的Border属性设置的话,UseVariableBorders属性就不会变(当然我们是可以手动将其设置为true),所以图中的最后一个单元格的border就超出了单元格的边界。
在文本模式下,我们只可以添加Chunk和Phrase对象,如果要添加Lists和Image就需要组合模式。大家要注意的是代码:
PdfPCell cell = new PdfPCell(new Paragraph("some text"));
和代码:
PdfPCell cell = new PdfPCell(); cell.AddElement(new Paragraph("some text"));
以上的两份代码是有很大的区别 在第一个代码中,Paragraph对象的一些属性如leading,alignment是不起作用的,相反要设置对应PdfPCell对象的属性。在第二个代码中,我们通过AddElement方法转换为组合模式,这里PdfPCell对象的leading,alignment,indentation属性就不起作用了。这种架构和我们学到的ColumnText对象一样。
现在我们用table来创建电影列表信息。具体看下图:
listing 4.14 MovieCompositeMode.cs
cell = new PdfPCell(); // a cell with paragraphs and lists Paragraph p = new Paragraph(movie.Title, FilmFonts.BOLD); p.Alignment = Element.ALIGN_CENTER; p.SpacingAfter = p.SpacingBefore = 5; cell.AddElement(p); cell.Border = PdfPCell.NO_BORDER; if (movie.OriginalTitle != null) { p = new Paragraph(movie.OriginalTitle, FilmFonts.ITALIC); p.Alignment = Element.ALIGN_RIGHT; cell.AddElement(p); } list = PojoToElementFactory.GetDirectorList(movie); list.IndentationLeft = 30; cell.AddElement(list); p = new Paragraph(string.Format("Year: {0}", movie.Year), FilmFonts.NORMAL); p.IndentationLeft = 15; p.Leading = 24; cell.AddElement(p); p = new Paragraph(string.Format("Run length: {0}", movie.Duration), FilmFonts.NORMAL); p.Leading = 14; p.IndentationLeft = 30; cell.AddElement(p); list = PojoToElementFactory.GetCountryList(movie); list.IndentationLeft = 40; cell.AddElement(list); table.AddCell(cell);
以上代码中table包含两个单元格:第一个为电影的海报,第二个为电影的其他信息。第二个单元格由不同alignment,leading,spacing和indentation的Paragraph,List对象组成。因为处于组合模式下我们要为单独的类设置属性。对于Image对象我们可以设置是否将其缩放。
好了直接上图:
listing 4.15 XMen.cs
// first movie table.DefaultCell.HorizontalAlignment = Element.ALIGN_CENTER; table.DefaultCell.VerticalAlignment = Element.ALIGN_TOP; table.AddCell("X-Men"); // we wrap he image in a PdfPCell PdfPCell cell = new PdfPCell(img[0]); table.AddCell(cell); // second movie table.DefaultCell.VerticalAlignment = Element.ALIGN_MIDDLE; table.AddCell("X2"); // we wrap the image in a PdfPCell and let iText scale it cell = new PdfPCell(img[1], true); table.AddCell(cell); // third movie table.DefaultCell.VerticalAlignment = Element.ALIGN_BOTTOM; table.AddCell("X-Men:The Last Stand"); // we add the image with addCell() table.AddCell(img[2]); // fourth movie table.AddCell("Superman Returns"); cell = new PdfPCell(); // we add it with addElement(); it can only take 50% of the width. img[3].WidthPercentage = 50; cell.AddElement(img[3]); table.AddCell(cell); // we complete the table (otherwise the last row won't be rendered) table.CompleteRow(); document.Add(table);
上图中我们用4种不同的方法显示4部电影海报。其中前两个图片(X战警1和2)是将Image对象作为pdfPCell的构造器参数传入,第三个图片(X战警3)Image对象是直接通过AddCell方法添加,最后一个图片(超人归来)Image是被PdfPCell的AddElement方法添加进去。将Image对象作为PdfPCell的构造器参数时,单元格的默认padding就从2pt变为0pt。PdfPCell的相应构造器还有一个bool类型的参数,为true时iText会将图片缩放,但默认为false,因此如果没设置的画图片可能会溢出单元格。通过AddCell方法添加图片时图片会自动缩放,但默认单元格的设置会影响图片的属性,如第三张图片的padding就为2pt,而且是向下对其。最后通过AddElement方法添加时图片就会缩放以便100%填充单元格的宽度,但可以通过WidthPercentage来设置。除了Image之外,我们还可以在表中添加表格,也就是嵌套表格。
在iText以前还不支持rowspan属性的时候,要完成类似的功能就只能用嵌套表格的方法。下图就是一个嵌套表格的效果图:
在上图中单元格1.1和1.2为一个嵌入的表格,单元格12.1和单元格12.2也为一个嵌入的表格,通过这种方法单元格13,14,15看起来就有一个rowspan为2的属性。一下为具体的代码:
listing 4.16 NestedTable.cs
PdfPTable table = new PdfPTable(4); PdfPTable nested1 = new PdfPTable(2); nested1.AddCell("1.1"); nested1.AddCell("1.2"); PdfPTable nested2 = new PdfPTable(1); nested2.AddCell("12.1"); nested2.AddCell("12.2"); for (int k = 0; k < 16; k++) { if (k == 1) { table.AddCell(nested1); } else if (k == 12) { table.AddCell(new PdfPCell(nested2)); } else { table.AddCell("cell" + k); } } document.Add(table);
就如同添加Image对象一样,当PdfPTable对象通过AddCell方法直接添加时padding为2pt,但将PdfPTable对象包裹在PdfPCell中时padding就为0pt。
直接上图:
listing 4.17 NestedTables.cs
public override void CreateFile(string fileName) { // step 1 Document document = new Document(); using (document) { // step 2 PdfWriter writer = PdfWriter.GetInstance(document, new FileStream(fileName, FileMode.Create)); // step 3 document.Open(); string connStr = ConfigurationManager.AppSettings["SQLiteConnStr"]; SQLiteConnection conn = new SQLiteConnection(connStr); using (conn) { conn.Open(); List<DateTime> days = PojoFactory.GetDays(conn); foreach (DateTime day in days) { document.Add(GetTable(conn, day)); document.NewPage(); } } } } public PdfPTable GetTable(IDbConnection conn, DateTime day) { // Create a table with only one column PdfPTable table = new PdfPTable(1); table.WidthPercentage = 100; // add the cell with the date … List<Screening> screeings = PojoFactory.GetScreenings(conn, day); foreach (Screening screening in screeings) { table.AddCell(GetTable(conn, screening)); } return table; } private PdfPTable GetTable(IDbConnection conn, Screening screening) { // Create a table with 4 columns PdfPTable table = new PdfPTable(4); table.SetWidths(new int[] { 1, 5, 10, 10 }); // Get the movie Movie movie = screening.Movie; // A cell with the title as a nested table spanning the complete row PdfPCell cell = new PdfPCell(); // nesting is done with addElement() in this example cell.AddElement(FullTitle(screening)); cell.UseAscender = cell.UseDescender = true; … // cell with the list of countries … return table; } private PdfPTable FullTitle(Screening screening) { PdfPTable table = new PdfPTable(3); … return table; }
FullTitle方法创建的table是通过AddElement方法添加的,这个和通过AddCell方法以及PdfPCell构造器有一些区别。AddElement方法表格会被添加到单元格中的ColumnText对象中,而且我们还可以同个单元格中添加其他的内容。
这一些主要是介绍PdfPCell的文本模式和组合模式,其架构和ColumnText一致。表格除了添加一些基本构建块之外还可以添加图片,我们还可以通过嵌套表格来完成一些复杂的页面布局。不过这里的数据量比较少,下一节会介绍处理大数据量的内容,最后是代码下载。
此文章已同步到目录索引:iText in Action 2nd 读书笔记。