import io import os import traceback from typing import Callable from docx import Document from docx.enum.section import WD_ORIENT from docx.enum.table import WD_ALIGN_VERTICAL, WD_ROW_HEIGHT_RULE from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Pt, Cm from docx.text.run import Run from module import LOGLEVEL from module.achievement_excel import ExcelReader class DocxWriter: full_file_path: str def __init__(self, save_path: str, filename: str, excel_reader: ExcelReader, signal: Callable[[str, str], None] = lambda x, y: print(x)): super().__init__() self.save_path = save_path self.filename = filename self.excel_reader = excel_reader self.full_file_path = os.path.join(save_path, filename) + '.docx' self.signal = signal def write(self) -> int: try: if not os.path.exists(self.save_path): os.makedirs(self.save_path) if os.path.exists(self.full_file_path): self.signal(f"文件'{self.filename}'已存在,将覆盖原文件", LOGLEVEL.WARNING) doc = Document() for section in doc.sections: new_width, new_height = Cm(29.7), Cm(21) # A4纸张 # 设置纸张方向为横向 section.orientation = WD_ORIENT.LANDSCAPE # 设置页边距(例如:上下左右均为2厘米) section.top_margin = Cm(2) section.bottom_margin = Cm(2) section.left_margin = Cm(2) section.right_margin = Cm(1) section.page_width = new_width section.page_height = new_height paragraph = doc.add_paragraph() # 设置段落居中对齐 paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER run = paragraph.add_run(f"《{self.excel_reader.course_name}》课程目标达成情况评价报告") self.set_run_font(run, 16, '黑体', '黑体', True) paragraph = doc.add_paragraph() run = paragraph.add_run( f"1.班级{'(年级)' if len(self.excel_reader.class_list) == 1 else ''}课程目标达成情况分析") self.set_run_font(run, 14, 'Times New Roman', '黑体', True) # 班级课程目标达成情况分析表 class_info_str = "班级:{} 学生人数:{} 任课教师:{}" for r_index in range(len(self.excel_reader.class_list)): self.signal(f"正在生成 {self.excel_reader.class_list[r_index]} 表格", LOGLEVEL.INFO) paragraph = doc.add_paragraph() # 设置段落为分散对齐 paragraph.alignment = WD_ALIGN_PARAGRAPH.DISTRIBUTE run = paragraph.add_run(class_info_str.format( self.excel_reader.class_list[r_index], self.excel_reader.class_number[r_index], self.excel_reader.course_teacher_name )) self.set_run_font(run, 10.5, 'Times New Roman', '宋体') if len(self.excel_reader.class_list) == 1: rows = 14 + self.excel_reader.class_number[r_index] + self.excel_reader.kpi_number else: rows = 12 + self.excel_reader.class_number[r_index] + self.excel_reader.kpi_number cols = 2 + self.calculate_columns(self.excel_reader.achievement_level[r_index]) # 加上第一列 table = doc.add_table(rows=rows, cols=cols) # 设置外侧框线粗1.5磅,内侧框线粗0.5磅 self.set_table_borders(table) # 合并前六行头两个单元格 for row in range(6): cell_start = table.cell(row, 0) cell_end = table.cell(row, 1) cell_start.merge(cell_end) # 合并前三行的单元格 for row in range(3): col_span = 2 # 从第三列开始 for performance in self.excel_reader.achievement_level[r_index]: non_none_count = 3 - performance.scores.count(None) if non_none_count > 1: cell_start = table.cell(row, col_span) cell_end = table.cell(row, col_span + non_none_count - 1) cell_start.merge(cell_end) col_span += non_none_count X = self.excel_reader.kpi_number + 5 # X的值 if len(self.excel_reader.class_list) == 1: X += 2 # 第一、二行合并第二个单元格 for row in range(rows - X, rows - X + 2): table.cell(row, 0).merge(table.cell(row, 1)) # 第三行的合并操作 # 第一个单元格与第三+len(a[0])-1行第二个单元格合并 table.cell(rows - X + 2, 0).merge(table.cell(rows - X + 2 + self.excel_reader.kpi_number - 1, 1)) for i in range(self.excel_reader.kpi_number): # 第三个与第四个单元格合并 table.cell(rows - X + 2 + i, 2).merge(table.cell(rows - X + i + 2, 3)) # 从第五个单元格合并到最后 start_cell = table.cell(rows - X + 2 + i, 4) end_cell = table.cell(rows - X + 2 + i, cols - 1) start_cell.merge(end_cell) # 第三+len(a[0])行的合并操作 # 合并第一个到第二个单元格 table.cell(rows - X + 2 + self.excel_reader.kpi_number, 0).merge( table.cell(rows - X + 2 + self.excel_reader.kpi_number, 1)) # 合并第三个到最后一个单元格 start_cell = table.cell(rows - X + 2 + self.excel_reader.kpi_number, 2) end_cell = table.cell(rows - X + 2 + self.excel_reader.kpi_number, cols - 1) start_cell.merge(end_cell) if len(self.excel_reader.class_list) == 1: # 合并前两个单元格 for i in range(2): start = rows - X + 3 + i + self.excel_reader.kpi_number table.cell(start, 0).merge(table.cell(start, 1)) # 依次合并后面的单元格 for row in range(start, start + self.excel_reader.kpi_number): col_span = 2 # 从第三列开始 for performance in self.excel_reader.achievement_level[r_index]: non_none_count = 3 - performance.scores.count(None) if non_none_count > 1: cell_start = table.cell(row, col_span) cell_end = table.cell(row, col_span + non_none_count - 1) cell_start.merge(cell_end) col_span += non_none_count start = rows - X + 3 + self.excel_reader.kpi_number if len(self.excel_reader.class_list) == 1: start += 2 end = rows for row in range(start, end): start_cell = table.cell(row, 1) end_cell = table.cell(row, cols - 1) start_cell.merge(end_cell) # 填充表格数据 self.put_data_to_table(table, self.excel_reader.get_word_template, r_index) doc.add_page_break() if len(self.excel_reader.class_list) > 1: paragraph = doc.add_paragraph() run = paragraph.add_run("2.年级课程目标达成情况分析") self.set_run_font(run, 14, 'Times New Roman', '黑体', True) # 2.年级课程目标达成情况分析 class_info_str = "班级:{} 课程负责人:{}" paragraph = doc.add_paragraph() paragraph.alignment = WD_ALIGN_PARAGRAPH.DISTRIBUTE run = paragraph.add_run(class_info_str.format( self.excel_reader.total_class_str, self.excel_reader.course_lead_teacher_name )) self.set_run_font(run, 10.5, "Times New Roman", '宋体') rows = 10 + len(self.excel_reader.class_list) cols = 2 + self.calculate_columns(self.excel_reader.achievement_level[0]) # 加上第一列 table = doc.add_table(rows=rows, cols=cols) # 设置外侧框线粗1.5磅,内侧框线粗0.5磅 self.set_table_borders(table) # 合并前六行头两个单元格 for row in range(rows - 2): cell_start = table.cell(row, 0) cell_end = table.cell(row, 1) cell_start.merge(cell_end) # 合并前三行的单元格 for row in range(3): col_span = 2 # 从第三列开始 for performance in self.excel_reader.achievement_level[0]: non_none_count = 3 - performance.scores.count(None) if non_none_count > 1: cell_start = table.cell(row, col_span) cell_end = table.cell(row, col_span + non_none_count - 1) cell_start.merge(cell_end) col_span += non_none_count # 合并中间X行的单元格 for row in range(6, 8 + len(self.excel_reader.class_list)): col_span = 2 for performance in self.excel_reader.achievement_level[0]: non_none_count = 3 - performance.scores.count(None) if non_none_count > 1: cell_start = table.cell(row, col_span) cell_end = table.cell(row, col_span + non_none_count - 1) cell_start.merge(cell_end) col_span += non_none_count # 合并最后两行的单元格 for row in range(2): cell_start = table.cell(rows - row - 1, 1) cell_end = table.cell(rows - row - 1, cols - 1) cell_start.merge(cell_end) # 填充数据 self.put_data_to_table(table, self.excel_reader.get_word_template_part_2) doc.add_page_break() # 3. 课程目标达成情况的合理性评价 paragraph = doc.add_paragraph() run = paragraph.add_run(f"{3 if len(self.excel_reader.class_list) > 1 else 2}" f". 课程目标达成情况的合理性评价") self.set_run_font(run, 14, 'Times New Roman', '黑体', True) rows = 9 cols = 4 table = doc.add_table(rows=rows, cols=cols) # 设置外侧框线粗1.5磅,内侧框线粗0.5磅 self.set_table_borders(table) # 合并第一行 cell_start = table.cell(0, 0) cell_end = table.cell(0, cols - 1) cell_start.merge(cell_end) # 合并第二行至最后 for i in range(1, 9): if i == 2: continue cell_start = table.cell(i, 1) cell_end = table.cell(i, cols - 1) cell_start.merge(cell_end) # 填充数据 self.put_data_to_table(table, self.excel_reader.get_word_template_part_3) # 应用样式 self.signal("正在应用文字样式", LOGLEVEL.INFO) # 遍历文档中的所有段落 for index, paragraph in enumerate(doc.paragraphs): if index == 0: self.set_paragraph_format(paragraph, WD_LINE_SPACING.ONE_POINT_FIVE) else: self.set_paragraph_format(paragraph) # 遍历文档中的所有表格及其单元格 t_len = len(doc.tables) part_1_table_index = [x for x in range(len(self.excel_reader.class_list))] part_2_table_index = [len(part_1_table_index)] if len(self.excel_reader.class_list) != 1 else [] part_3_table_index = [t_len - 1] for t_index, table in enumerate(doc.tables): self.set_table_borders(table) for r_index, row in enumerate(table.rows): row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST row.height = Cm(0.7) for c_index, cell in enumerate(row.cells): cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER self.set_cell_margins(cell, start=57, end=57) for paragraph in cell.paragraphs: for run in paragraph.runs: self.set_run_font(run, 10.5, 'Times New Roman', '宋体') self.set_paragraph_format(paragraph) special_cell = [] if t_index in part_1_table_index: ll = [(9 + x + self.excel_reader.class_number[t_index], 4) for x in range(self.excel_reader.kpi_number)] start_line = 9 + self.excel_reader.kpi_number special_cell.extend(ll) special_cell.extend([ (1, 2), (2, 2), (start_line + self.excel_reader.class_number[t_index], 2), ]) if len(self.excel_reader.class_list) == 1: special_cell.extend([ (start_line + 3 + self.excel_reader.class_number[t_index], 1), (start_line + 4 + self.excel_reader.class_number[t_index], 1), ]) lst = [(start_line + 3 + self.excel_reader.class_number[t_index], 1)] else: special_cell.extend([ (start_line + 1 + self.excel_reader.class_number[t_index], 1), (start_line + 2 + self.excel_reader.class_number[t_index], 1), ]) lst = [(start_line + 1 + self.excel_reader.class_number[t_index], 1)] if (r_index, c_index) in lst: self.insert_image_from_data(cell, self.excel_reader.pic_list[t_index], Cm(7 * self.excel_reader.kpi_number) ) if (len(self.excel_reader.class_list) == 1 and r_index == start_line + 2 + self.excel_reader.class_number[t_index]): self.change_font_to_wingding2(paragraph) elif t_index in part_2_table_index: special_cell = [ (1, 2), (2, 2), (8 + len(self.excel_reader.class_list), 1), (9 + len(self.excel_reader.class_list), 1), ] if r_index == 7 + len(self.excel_reader.class_list): self.change_font_to_wingding2(paragraph) elif t_index in part_3_table_index: special_cell = [ (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1), (8, 1), ] if r_index == 0: for run in paragraph.runs: self.set_run_font(run, 10.5, 'Times New Roman', '宋体', True) else: self.change_font_to_wingding2(paragraph) if any(r_index == pair[0] and c_index >= pair[1] for pair in special_cell): paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT else: paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER doc.save(self.full_file_path) except PermissionError as e: raise Exception(f""" 简要错误信息:{str(e)} 这可能是由于Word文件被占用所导致的,请关闭相应文件后再试。 """) except Exception as _: error_message = traceback.format_exc() raise Exception(f""" 原始错误信息: {error_message} """) def set_save_path(self, save_path: str): self.save_path = save_path self.full_file_path = os.path.join(save_path, self.filename) def set_filename(self, filename: str): self.filename = filename self.full_file_path = os.path.join(self.save_path, filename) def set_run_font(self, run: Run, size, western_font, chinese_font, bold=False): run.font.size = Pt(size) run.font.name = western_font run.bold = bold run._element.rPr.rFonts.set(qn('w:eastAsia'), chinese_font) def set_cell_border(self, cell, **kwargs): """ 设置单元格边框 kwargs: top, bottom, left, right, inside_h, inside_v """ tc = cell._tc tcPr = tc.get_or_add_tcPr() # 创建一个新的'TC边框'元素 tcBorders = OxmlElement('w:tcBorders') # 对于每个边框方向 for key, value in kwargs.items(): if value is not None: tag = 'w:{}'.format(key) border = OxmlElement(tag) border.set(qn('w:val'), 'single') border.set(qn('w:sz'), str(int(value * 8))) border.set(qn('w:space'), '0') border.set(qn('w:color'), 'auto') tcBorders.append(border) # 将边框添加到单元格属性中 tcPr.append(tcBorders) def set_paragraph_format(self, paragraph, line_spacing_rule: WD_LINE_SPACING = WD_LINE_SPACING.SINGLE): paragraph_format = paragraph.paragraph_format paragraph_format.line_spacing_rule = line_spacing_rule # 单倍行距 paragraph_format.space_before = Pt(0) # 段前间距 paragraph_format.space_after = Pt(0) # 段后间距 def calculate_columns(self, performance_list): """ 计算表格的列数 :param performance_list: :return: """ col_count = 0 for performance in performance_list: col_count += 3 - performance.scores.count(None) # 非None值的数量 return col_count def put_data_to_table(self, table, gen_func, *args): """填充表格数据""" data = gen_func(*args) for i, row in enumerate(table.rows): for j, cell in enumerate(row.cells): if cell.text == "": # 填充数据 # 这里需要根据您的数据结构来调整 d = next(data) cell.text = str(d) def set_cell_margins(self, cell, **kwargs): """ 设置单元格的边距。 cell: 要修改的单元格实例 kwargs: 边距值,以twips为单位(1/1440英寸) 示例:set_cell_margins(cell, top=50, start=50, bottom=50, end=50) """ tc = cell._tc tcPr = tc.get_or_add_tcPr() tcMar = OxmlElement('w:tcMar') for m in ["top", "start", "bottom", "end"]: if m in kwargs: node = OxmlElement("w:{}".format(m)) node.set(qn('w:w'), str(kwargs.get(m))) node.set(qn('w:type'), 'dxa') tcMar.append(node) tcPr.append(tcMar) def is_start_of_merged_cells(self, cell): """ 检查单元格是否是合并单元格的起始单元格 """ merge = cell._element.xpath('./w:tcPr/w:vMerge') if merge: return merge[0].get(qn('w:val')) == 'restart' return False def is_merged_vertically(self, cell): """ 检查单元格是否垂直合并 """ merge = cell._element.xpath('./w:tcPr/w:vMerge') return bool(merge) def set_table_borders(self, table, outside_sz=1.5, inside_sz=0.5): """ 设置整个表格的边框样式 outside_sz: 外侧边框大小 inside_sz: 内部分割线大小 """ for row in table.rows: for cell in row.cells: self.set_cell_border(cell, top=inside_sz, bottom=inside_sz, left=inside_sz, right=inside_sz) # 设置外侧边框 for cell in table.rows[0].cells: self.set_cell_border(cell, top=outside_sz) for cell in table.rows[-1].cells: self.set_cell_border(cell, bottom=outside_sz) for row in table.rows: self.set_cell_border(row.cells[0], left=outside_sz) self.set_cell_border(row.cells[-1], right=outside_sz) def insert_image_from_data(self, cell, image_data, width=Cm(21), height=Cm(4.5)): # 创建一个临时文件 with io.BytesIO(image_data) as image_stream: image_stream.seek(0) # 插入图片 # 在单元格中添加一个段落 paragraph = cell.paragraphs[0] # 在段落中插入图片 run = paragraph.add_run() run.add_picture(image_stream, width=width, height=height) def is_chinese(self, char): """判断字符是否为中文""" if '\u4e00' <= char <= '\u9fff': return True return False def change_font_to_wingding2(self, paragraph): for run in paragraph.runs: if not run.text.strip(): # 跳过不包含文本的runs continue new_text = "" for char in run.text: if char in ['R', '£']: # 对于特殊字符,创建新的run if new_text: new_run = paragraph.add_run(new_text) self.set_run_font(new_run, 10.5, 'Times New Roman', '宋体') new_text = "" special_run = paragraph.add_run(char) self.set_run_font(special_run, 10.5, 'Wingdings 2', '宋体') else: # 累积其他字符 new_text += char if new_text: # 创建剩余文本的run new_run = paragraph.add_run(new_text) self.set_run_font(new_run, 10.5, 'Times New Roman', '宋体') run.clear()