最近項目中需要將查詢出來的表格數據以PDF形式導出,并且表格的形式包含橫向行與縱向列的單元格合并操作,導出的最終效果如圖所示:
首先引入操作依賴
<!--導出pdf所需包--><dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.10</version></dependency><dependency><groupId>com.itextpdf</groupId><artifactId>itext-asian</artifactId><version>5.2.0</version></dependency>
最上面的基本信息是固定死的就是4*4的表格,這個創建起來 就比較簡單,主要是下面這個表格,需要從數據庫查出數據并循環進行展示,并且內容相同的列要進行合并。直接展示代碼:
主類對外調用方法:
@Operation(summary = "導出PDF")@PostMapping("/download")@SneakyThrows(Exception.class)public void download(Long id,HttpServletResponse response, HttpServletRequest request) {// 防止日志記錄獲取session異常request.getSession();// 設置編碼格式response.setContentType("application/pdf;charset=UTF-8");response.setCharacterEncoding("utf-8");String fileName = URLEncoder.encode("調試PDF", "UTF-8");response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".pdf");contractDemandThreeRequestBaseService.download(id,response);}
具體實現類:
@Overridepublic void download(Long id, HttpServletResponse response) {//要下載的數據查詢數據部分我去掉了有需要自己根據業務取ContractDemandThreeRequestBaseDO baseDO = contractDemandThreeRequestBaseMapper.selectById(id);ContractDemandThreeRequestBaseRespVO base = ContractDemandThreeRequestBaseConvert.INSTANCE.convert(baseDO);base.setDeptName(ObjectUtil.isEmpty(deptService.getDept(base.getDeptId())) ? null : deptService.getDept(base.getDeptId()).getName());base.setProjectLeaderName(ObjectUtil.isEmpty(userService.getUser(base.getProjectLeaderId())) ? null : userService.getUser(base.getProjectLeaderId()).getNickname());//子表數據List<ContractDemandThreeQuestionDO> details = contractDemandThreeQuestionMapper.selectList(Wrappers.lambdaQuery(ContractDemandThreeQuestionDO.class).eq(ContractDemandThreeQuestionDO::getDemandId, id));//下面進行表格的創建、字體設置、合并單元格// 定義全局的字體靜態變量Font titlefont;Font headfont;Font keyfont = null;Font textfont = null;Font content = null;BaseFont bfChinese = null;// 最大寬度try {// 不同字體(這里定義為同一種字體:包含不同字號、不同style)這里我用的最后一個 content字體bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);titlefont = new Font(bfChinese, 16, Font.BOLD);headfont = new Font(bfChinese, 14, Font.BOLD);keyfont = new Font(bfChinese, 10, Font.BOLD);textfont = new Font(bfChinese, 15, Font.NORMAL);content = new Font(bfChinese, 10, Font.NORMAL);} catch (Exception e) {e.printStackTrace();}BaseFont bf;Font font = null;try {//創建字體bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H",BaseFont.NOT_EMBEDDED);//使用字體并給出顏色font = new Font(bf, 20, Font.BOLD, BaseColor.BLACK);} catch (Exception e) {e.printStackTrace();}Document document = new Document();try {PdfWriter pdfWriter = PdfWriter.getInstance(document, response.getOutputStream());//打開生成的pdf文件document.open();//設置內容Paragraph paragraph = new Paragraph("采購需求三問", font);paragraph.setAlignment(1);//引用字體document.add(paragraph);// 設置表格的列寬和列數float[] widths = {25f, 25f, 25f, 25f};PdfPTable table = new PdfPTable(widths);table.setSpacingBefore(20f);// 設置表格寬度為100%table.setWidthPercentage(100.0F);table.setHeaderRows(1);table.getDefaultCell().setHorizontalAlignment(1);PdfPCell cell = null;//第一行:表格的名字cell = new PdfPCell(new Paragraph("采購項目名稱", content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);cell.setFixedHeight(30);table.addCell(cell);//表格里面的值(下面都是同樣的操作)cell = new PdfPCell(new Paragraph(base.getProjectName(), content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table.addCell(cell);//第二行cell = new PdfPCell(new Paragraph("項目編號", content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);cell.setFixedHeight(30);table.addCell(cell);cell = new PdfPCell(new Paragraph(base.getProjectCode(), content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table.addCell(cell);cell = new PdfPCell(new Paragraph("資金類型", content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table.addCell(cell);cell = new PdfPCell(new Paragraph(getDitcValue("FundType", base.getFundType()), content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table.addCell(cell);//第三行cell = new PdfPCell(new Paragraph("支出類別", content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);cell.setFixedHeight(30);table.addCell(cell);cell = new PdfPCell(new Paragraph(getDitcValue("ExpenditureCategory", base.getExpenditureCategory()), content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table.addCell(cell);cell = new PdfPCell(new Paragraph("預算含稅總金額(萬元)", content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table.addCell(cell);cell = new PdfPCell(new Paragraph(String.valueOf(base.getBudgetIncludingTaxTotalMoney()), content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table.addCell(cell);//第三行cell = new PdfPCell(new Paragraph("采購內容", content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);cell.setFixedHeight(30);table.addCell(cell);cell = new PdfPCell(new Paragraph(base.getProcureContent(), content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table.addCell(cell);//第四行cell = new PdfPCell(new Paragraph("需求部門", content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);cell.setFixedHeight(30);table.addCell(cell);cell = new PdfPCell(new Paragraph(base.getDeptName(), content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table.addCell(cell);cell = new PdfPCell(new Paragraph("項目負責人", content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);cell.setFixedHeight(30);table.addCell(cell);cell = new PdfPCell(new Paragraph(base.getProjectLeaderName(), content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table.addCell(cell);// 設置表格的列寬和列數float[] widths2 = {25f, 25f, 25f};PdfPTable table2 = new PdfPTable(widths2);table2.setSpacingBefore(20f);// 設置表格寬度為100%table2.setWidthPercentage(100.0F);table2.setHeaderRows(1);table2.getDefaultCell().setHorizontalAlignment(1);//需求三問詳情信息標題欄cell = new PdfPCell(new Paragraph("需求三問", content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);cell.setFixedHeight(20);table2.addCell(cell);cell = new PdfPCell(new Paragraph("選擇項", content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table2.addCell(cell);cell = new PdfPCell(new Paragraph("內容項", content));cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);table2.addCell(cell);//下面的代碼就是樣圖中底下的表格,需要動態構建數據//人員列表數據-第五行List<List<String>> doList = new ArrayList<>();if (CollectionUtils.isNotEmpty(details)) {//組裝數據for (ContractDemandThreeQuestionDO detail : details) {String natureType = "項目" + getDitcValue("QuestionType", detail.getDemandNatureType());String deviceGoodsType = getDitcValue(baseDO.getModelType(), detail.getItemValue());doList.add(Arrays.asList(natureType, deviceGoodsType, detail.getItemContent()));}makeData(doList, content, table2);//為兩個表格添加標題document.add(new Paragraph("\n"));document.add(new Paragraph("▋ 基本信息", content));document.add(new Paragraph("\n"));document.add(table);document.add(new Paragraph("\n"));document.add(new Paragraph("▋ 采購需求三問內容", content));document.add(new Paragraph("\n"));//將table2中數據相同的列合并單元格(橫向合并)document.add(table2);// 加水印(水印組成:下載人姓名-手機號-部門)PdfContentByte waterMar = pdfWriter.getDirectContentUnder();AtomicReference<String> text = new AtomicReference<>("");Long loginUserId = getLoginUserId();AdminUserRedisVO adminUserRedisVO = adminUserRedisDAO.get(loginUserId);Optional.ofNullable(adminUserRedisVO).ifPresent(vo -> {DeptDO dept = deptService.getDept(adminUserRedisVO.getDeptId());String deptName = Objects.nonNull(dept) ? dept.getName() : "";text.set(vo.getNickname() + "-" + vo.getMobile() + "-" + deptName);});addTextFullWaterMark(waterMar, text.get(), bfChinese);//關閉文檔document.close();} catch (DocumentException | IOException e) {e.printStackTrace();log.error("導出pdf失敗:{}", e);}}
補充說明:List<List<String>> doList = new ArrayList<>();
這個是我構建底下表格數據的格式,舉個例子,類似于:
List<List<String>> headList = new ArrayList<>(); headList.add(Arrays.asList(new String[]{"1", "2", "3"})); headList.add(Arrays.asList(new String[]{"2", "6", "10"})); headList.add(Arrays.asList(new String[]{"1", "12", "13"})); headList.add(Arrays.asList(new String[]{"2", "9", "11"})); headList.add(Arrays.asList(new String[]{"1", "8", "12"}));
根據自己的實際業務構建,我底下的表格是一個n*3的表格,只有三列,所以數據結構就相當于上面的兩層List結構,最外層的集合代表有多少行,嵌套的結合相當于多少列(這里就是三列),我將對象集合查詢出來后使用循環,將我所用到的數據組裝成這個示例的樣子。
重點來了,以下是合并單元格的方法:
makeData(doList, content, table2);
/*** 合并單元格方法** @param list 表頭數據 list中相連下標位置內容如果相同自動合并 上下位置內容相同自動合并* @param fontChinese 支持轉換中文的Font對象* @return*/private void makeData(List<List<String>> list, Font fontChinese, PdfPTable table2) {List<List<PdfPCell>> aa = new ArrayList<>();int length = list.get(0).size();//循環在外層,這里代表的是有幾列(其實是固定的,因為上面構建的時候,列數就是三列)//下面的循環不用管,是將你組裝的數據設置到單元格里for (int i = 0; i < list.size(); i++) {List<String> strings = list.get(i);int colNum = 1;List<PdfPCell> bb = new ArrayList<>();for (int j = 0; j < strings.size(); j++) {if (j + 1 < strings.size()) {if (strings.get(j).equals(strings.get(j + 1))) {colNum++;} else {PdfPCell cell = new PdfPCell();//合并列cell.setColspan(colNum);Paragraph elements = new Paragraph(strings.get(j), fontChinese);elements.setAlignment(1);cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);cell.setFixedHeight(30);cell.addElement(elements);bb.add(cell);for (int a = 1; a < colNum; a++) {bb.add(null);}colNum = 1;}} else {PdfPCell cell = new PdfPCell();cell.setColspan(colNum);Paragraph elements = new Paragraph(strings.get(j), fontChinese);cell.setVerticalAlignment(Element.ALIGN_MIDDLE);cell.setHorizontalAlignment(Element.ALIGN_CENTER);elements.setAlignment(1);cell.setFixedHeight(30);cell.addElement(elements);bb.add(cell);for (int a = 1; a < colNum; a++) {bb.add(null);}colNum = 1;}}aa.add(bb);}//合并算法for (int i = 0; i < length; i++) {int rowSpan = 1;for (int j = 0; j < aa.size(); j++) {if (aa.get(j).get(i) == null) {continue;}if (j + 1 < aa.size()) {if (aa.get(j + 1).get(i) != null&& aa.get(j).get(i).getCompositeElements().get(0).toString().equals(aa.get(j + 1).get(i).getCompositeElements().get(0).toString())) {rowSpan++;} else {aa.get(j - rowSpan + 1).get(i).setRowspan(rowSpan);for (int a = 1; a < rowSpan; a++) {aa.get(j - rowSpan + 1 + a).set(i, null);}rowSpan = 1;}} else {aa.get(j - rowSpan + 1).get(i).setRowspan(rowSpan);for (int a = 1; a < rowSpan; a++) {aa.get(j - rowSpan + 1 + a).set(i, null);}rowSpan = 1;}}break;}for (List<PdfPCell> a : aa) {for (PdfPCell pCell : a) {if (pCell != null) {table2.addCell(pCell);}}}}
這里將合并算法進行一個說明:外層循環其實還就是表示列,我這里是三列,他會依次循環三列,并將橫向與縱向的表格中有相同數據的都進行合并,也就是值相同的行進行合并,值相同的列也進行合并。但我的業務要求就是只合并第一列,所以我這里也沒有改算法,偷了個懶,直接在第一次循環后加了break,直接終止后面的循環。這個自己看自己的業務要求。代碼里的getDictValue方法是我自己獲取字典值的方法,可自行定義
加水印:
public static void addTextFullWaterMark(PdfContentByte waterMar, String text, BaseFont bf) {waterMar.beginText();PdfGState gs = new PdfGState();// 設置填充字體不透明度為0.2fgs.setFillOpacity(0.2f);waterMar.setFontAndSize(bf, 20);// 設置透明度waterMar.setGState(gs);// 設置水印對齊方式 水印內容 X坐標 Y坐標 旋轉角度for (int x = 0; x <= 900; x += 200) {for (int y = -50; y <= 800; y += 200) {waterMar.showTextAligned(Element.ALIGN_RIGHT, text, x, y, 35);}}// 設置水印顏色waterMar.setColorFill(BaseColor.GRAY);//結束設置waterMar.endText();waterMar.stroke();}
這個操作PDF的類很強大,基本上你想怎么導出,都可以進行調整