""" 打印功能模块 使用PyQt5的QPrinter实现打印功能 """ from PyQt5.QtPrintSupport import QPrinter, QPrintDialog, QPrintPreviewDialog from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QPainter, QFont, QFontMetrics, QPen from PyQt5.QtCore import Qt, QRect, QSizeF, QSize from typing import Dict import platform class Printer: """打印类""" def __init__(self): # 100mm x 150mm 纸张尺寸 self.page_width_mm = 100 # 100mm self.page_height_mm = 150 # 150mm # 边距(mm) self.margin_mm = 3 # 上下左右各3mm边距 # 计算可用尺寸 # 可用高度 = 150mm - 上下边距(6mm) = 144mm # 可用宽度 = 100mm - 左右边距(6mm) = 94mm available_height_mm = self.page_height_mm - (2 * self.margin_mm) # 144mm available_width_mm = self.page_width_mm - (2 * self.margin_mm) # 94mm # 12行数据行 + 1行标题行 = 13行 # 标题行占用10mm,剩余134mm给12行数据行 # 每行高度 = 134mm / 12 ≈ 11.17mm num_data_rows = 12 # 12行数据行 title_height_mm = 10.0 # 标题行高度 data_rows_height_mm = available_height_mm - title_height_mm # 134mm self.line_height_mm = data_rows_height_mm / num_data_rows # 约11.17mm每行 self.title_line_height_mm = title_height_mm # 10mm标题行 # 4列布局:左侧label + 左侧内容 + 右侧label + 右侧内容 # 每列约 = 94mm / 4 ≈ 23.5mm # 但实际分配:左侧部分和右侧部分各占50% # 左侧:label(约25%) + 内容(约25%) # 右侧:label(约25%) + 内容(约25%) column_width_mm = available_width_mm / 4 # 约23.5mm每列 # 字体大小(增加3倍) self.title_font_size_mm = 2.1 # 标题字体大小(mm),增加3倍 self.content_font_size_mm = 1.68 # 内容字体大小(mm),增加3倍 self.label_font_size_mm = 1.68 # 标签字体大小(mm),增加3倍(不加粗) # 单元格内边距和间距 self.cell_padding_mm = 2.0 # 单元格内边距(mm) self.label_content_spacing_mm = 2.5 # label和内容之间的间距(mm) # 这些值将在draw_weighing_slip中根据实际页面尺寸动态计算 self.title_font_size = None self.content_font_size = None self.label_font_size = None self.line_height = None self.title_line_height = None self.cell_padding = None self.label_content_spacing = None self.label_width_2col = None self.label_width_4col = None self.label_width_cargo_name = None self.label_width_business_type = None self.label_width_supplier_consignee = None self.label_width_time = None self.label_width_planned_executed = None def _calculate_dimensions_from_page(self, page_width, page_height, dpi, painter=None): """ 根据实际页面尺寸计算所有尺寸 :param page_width: 页面宽度(points) :param page_height: 页面高度(points) :param dpi: 打印机DPI :param painter: QPainter对象(可选,用于更准确地计算字体宽度) """ # 将mm转换为points:1mm = (dpi / 25.4) points mm_to_point = dpi / 25.4 # 计算实际页面尺寸(mm) actual_width_mm = page_width / mm_to_point actual_height_mm = page_height / mm_to_point # 基于实际尺寸计算缩放比例(相对于100mm x 150mm) width_scale = actual_width_mm / self.page_width_mm height_scale = actual_height_mm / self.page_height_mm scale = min(width_scale, height_scale) # 使用较小的缩放比例,确保内容不超出 # 计算字体大小(points) self.title_font_size = int(self.title_font_size_mm * mm_to_point * scale) self.content_font_size = int(self.content_font_size_mm * mm_to_point * scale) self.label_font_size = int(self.label_font_size_mm * mm_to_point * scale) # 计算行高和间距(points) self.line_height = int(self.line_height_mm * mm_to_point * scale) self.title_line_height = int(self.title_line_height_mm * mm_to_point * scale) self.cell_padding = int(self.cell_padding_mm * mm_to_point * scale) self.label_content_spacing = int(self.label_content_spacing_mm * mm_to_point * scale) # 创建临时字体来计算label宽度(不加粗) two_char_width = 0 try: label_font = QFont("微软雅黑", self.label_font_size) # 不加粗 if painter: # 使用painter的font metrics,更准确 painter.setFont(label_font) label_metrics = painter.fontMetrics() else: # 如果没有painter,使用QFontMetrics label_metrics = QFontMetrics(label_font) two_char_width = label_metrics.boundingRect("测试").width() except: # 如果无法创建QFontMetrics,使用估算值 # 基于字体大小估算两个字的宽度 two_char_width = int(self.label_font_size * 2.2) # 计算label宽度(基于mm,然后转换为points) # 100mm纸张,减去边距6mm = 94mm可用宽度 # 4列布局:每列约23.5mm # 两列布局:每列约47mm,label占用约40% = 18.8mm # 四列布局:每列约23.5mm,label占用约40% = 9.4mm available_width_mm = self.page_width_mm - (2 * self.margin_mm) # 94mm # 两列布局:label宽度约为可用宽度的20%(给内容更多空间) label_width_2col_mm = available_width_mm * 0.20 * scale # 约18.8mm # 四列布局:label宽度约为单列宽度的40% label_width_4col_mm = (available_width_mm / 4) * 0.40 * scale # 约9.4mm self.label_width_2col = int(label_width_2col_mm * mm_to_point) + two_char_width self.label_width_4col = int(label_width_4col_mm * mm_to_point) + two_char_width # 特定字段的label宽度(基于实际字符宽度) # 货物名称、供货单位、收货单位的label较长,需要更多宽度 self.label_width_cargo_name = int(available_width_mm * 0.25 * scale * mm_to_point) + two_char_width self.label_width_business_type = int(label_width_4col_mm * 1.1 * mm_to_point) + two_char_width self.label_width_supplier_consignee = int(available_width_mm * 0.25 * scale * mm_to_point) + two_char_width self.label_width_time = int(label_width_4col_mm * 1.1 * mm_to_point) + two_char_width self.label_width_planned_executed = int(label_width_4col_mm * 1.1 * mm_to_point) + two_char_width def get_printer(self) -> QPrinter: """ 获取配置好的打印机对象 :return: QPrinter对象 """ # 使用高分辨率打印机 printer = QPrinter(QPrinter.HighResolution) # 设置自定义页面大小:100mm x 150mm printer.setPageSize(QPrinter.Custom) # 设置页面尺寸(单位:毫米) # 使用setPaperSize设置自定义尺寸 try: printer.setPaperSize(QSizeF(self.page_width_mm, self.page_height_mm), QPrinter.Millimeter) except (AttributeError, TypeError): # 如果setPaperSize不存在或参数错误,尝试使用物理尺寸 try: # 计算points尺寸:100mm = 283.46 points (72 DPI), 150mm = 425.19 points # 但在高分辨率打印机上,需要使用实际DPI dpi = printer.resolution() width_points = self.page_width_mm * (dpi / 25.4) height_points = self.page_height_mm * (dpi / 25.4) printer.setPaperSize(QSizeF(width_points, height_points), QPrinter.Point) except: pass # 设置页边距(单位:毫米) printer.setPageMargins( self.margin_mm, self.margin_mm, self.margin_mm, self.margin_mm, QPrinter.Millimeter ) return printer def preview_weighing_slip(self, slip: Dict): """ 预览地磅单 :param slip: 地磅单数据字典 """ printer = self.get_printer() # 创建打印预览对话框 preview_dialog = QPrintPreviewDialog(printer) preview_dialog.setWindowTitle("打印预览 - 地磅单") # 定义绘制函数 def paint_page(printer_obj): painter = QPainter(printer_obj) try: self.draw_weighing_slip(painter, printer_obj, slip) finally: painter.end() # 连接预览绘制信号 preview_dialog.paintRequested.connect(paint_page) # 显示预览对话框 preview_dialog.exec_() def print_weighing_slip(self, slip: Dict, show_preview: bool = False): """ 打印地磅单 :param slip: 地磅单数据字典 :param show_preview: 是否先显示预览(默认False,直接打印) """ if show_preview: # 显示预览 self.preview_weighing_slip(slip) return printer = self.get_printer() # 显示打印对话框 print_dialog = QPrintDialog(printer) if print_dialog.exec_() != QPrintDialog.Accepted: return # 开始打印 painter = QPainter() painter.begin(printer) try: self.draw_weighing_slip(painter, printer, slip) finally: painter.end() def draw_weighing_slip(self, painter: QPainter, printer: QPrinter, slip: Dict): """ 绘制地磅单内容 :param painter: QPainter对象 :param printer: QPrinter对象 :param slip: 地磅单数据字典 """ # 获取页面尺寸(已减去边距) page_rect = printer.pageRect() width = page_rect.width() height = page_rect.height() # 获取打印机DPI dpi = printer.resolution() # 根据实际页面尺寸计算所有尺寸(传入painter以便更准确地计算字体宽度) self._calculate_dimensions_from_page(width, height, dpi, painter) x = 0 y = 0 # 设置字体 title_font = QFont("微软雅黑", self.title_font_size, QFont.Bold) # label字体不加粗,与内容字体一样 label_font = QFont("微软雅黑", self.label_font_size) # 不加粗 content_font = QFont("微软雅黑", self.content_font_size) # 绘制标题 - 确保居中 painter.setFont(title_font) title = "山峡化工地磅单" title_metrics = QFontMetrics(title_font) # 使用boundingRect获取更准确的文本宽度 title_rect = title_metrics.boundingRect(title) title_width = title_rect.width() # 使用页面实际宽度计算居中位置(相对于pageRect,x从0开始) title_x = (width - title_width) / 2 # 确保标题垂直居中在标题行内 # 使用更精确的垂直居中:单元格中心 + 文本rect的top偏移 title_baseline = y + self.title_line_height / 2 + title_rect.height() / 2 - title_rect.bottom() # 使用QRect和Qt.AlignCenter来确保完美居中 title_draw_rect = QRect(0, y, width, self.title_line_height) painter.drawText(title_draw_rect, Qt.AlignCenter, title) # 标题后直接开始数据行,不增加额外间距(因为行高已经合理分配) y += self.title_line_height # 设置边框画笔(2px边框线) # 在Qt打印系统中,使用逻辑单位,设置为2点(points)即可 # 这样在打印时会自动按比例缩放,保持2像素的视觉效果 border_width_points = 2 # 2px边框宽度(逻辑单位) border_pen = QPen(Qt.black, border_width_points) border_pen.setStyle(Qt.SolidLine) painter.setPen(border_pen) # 绘制表格内容(带边框) painter.setFont(label_font) content_metrics = QFontMetrics(content_font) label_metrics = QFontMetrics(label_font) # 计算文本垂直居中位置(通用函数) def get_text_baseline(cell_y_pos): max_metrics = label_metrics if label_metrics.height() > content_metrics.height() else content_metrics return cell_y_pos + self.line_height / 2 + max_metrics.ascent() - max_metrics.height() / 2 # 辅助函数:绘制两列单元格(label + 内容) def draw_2col_row(label_text, value_text, y_pos, custom_label_width=None): cell_y = y_pos # 绘制边框(确保所有参数都是整数) painter.drawRect(int(x), int(cell_y), int(width), int(self.line_height)) text_baseline = get_text_baseline(cell_y) # 使用自定义label宽度或默认宽度 label_width = custom_label_width if custom_label_width is not None else self.label_width_2col # 绘制标签(固定宽度) painter.setFont(label_font) label_x = x + self.cell_padding painter.drawText(int(label_x), int(text_baseline), label_text) # 绘制内容(从固定label宽度后开始,剩余空间全部给内容) if value_text: painter.setFont(content_font) content_x = label_x + label_width + self.label_content_spacing # 计算内容区域的最大宽度,避免超出边界 content_max_width = width - content_x - self.cell_padding painter.drawText(int(content_x), int(text_baseline), str(value_text)) return y_pos + self.line_height # 辅助函数:绘制四列单元格(label1 + 内容1 + label2 + 内容2) def draw_4col_row(label1_text, value1_text, label2_text, value2_text, y_pos, custom_label1_width=None, custom_label2_width=None, left_content_max_width=None, custom_line_height=None, draw_center_line=False): cell_y = y_pos # 使用自定义行高或默认行高(如果包含多行文本,使用更大的行高) actual_line_height = custom_line_height if custom_line_height is not None else self.line_height # 如果value2包含换行符(多行文本),增加行高(减小增加比例) if value2_text and "\n" in str(value2_text): actual_line_height = actual_line_height * 1.4 # 增加40%的行高来容纳两行文本(减小了) # 绘制边框(确保转换为整数) painter.drawRect(int(x), int(cell_y), int(width), int(actual_line_height)) text_baseline = get_text_baseline(cell_y) # 使用自定义label宽度或默认宽度 label1_width = custom_label1_width if custom_label1_width is not None else self.label_width_4col label2_width = custom_label2_width if custom_label2_width is not None else self.label_width_4col # 左侧部分(占一半宽度,但如果指定了left_content_max_width,则调整) if left_content_max_width is not None: # 计算左侧内容区域的结束位置 label1_x = x + self.cell_padding content1_x = label1_x + label1_width + self.label_content_spacing left_end_x = content1_x + left_content_max_width # 右侧部分从左侧内容区域结束后开始 right_start_x = left_end_x + self.cell_padding else: # 默认:左侧部分(占一半宽度) left_half_width = width / 2 right_start_x = x + left_half_width # 左侧label painter.setFont(label_font) label1_x = x + self.cell_padding painter.drawText(int(label1_x), int(text_baseline), label1_text) # 左侧内容 if value1_text: painter.setFont(content_font) content1_x = label1_x + label1_width + self.label_content_spacing # 左侧内容最大宽度 if left_content_max_width is not None: content1_max_width = left_content_max_width else: content1_max_width = right_start_x - content1_x - self.cell_padding painter.drawText(int(content1_x), int(text_baseline), str(value1_text)) # 右侧label painter.setFont(label_font) label2_x = right_start_x + self.cell_padding painter.drawText(int(label2_x), int(text_baseline), label2_text) # 右侧内容 if value2_text: content2_x = label2_x + label2_width + self.label_content_spacing # 右侧内容最大宽度 content2_max_width = width - content2_x - self.cell_padding value2_str = str(value2_text) # 检查是否包含换行符(多行文本,通常是时间字段) if "\n" in value2_str: # 多行文本(时间字段):使用更小的字体 time_font_size = int(self.content_font_size * 0.75) # 减小25%的字体大小 time_font = QFont("微软雅黑", time_font_size) painter.setFont(time_font) # 多行文本:使用QRect和TextWordWrap content2_rect = QRect( int(content2_x), int(cell_y + self.cell_padding), int(content2_max_width), int(actual_line_height - 2 * self.cell_padding) ) painter.drawText(content2_rect, Qt.AlignLeft | Qt.AlignTop | Qt.TextWordWrap, value2_str) else: # 单行文本:使用原来的字体 painter.setFont(content_font) painter.drawText(int(content2_x), int(text_baseline), value2_str) # 如果需要绘制中间竖线,在行的正中间往左30px位置绘制(20px + 10px) if draw_center_line: # 将30px转换为逻辑单位(points) # 在Qt打印系统中,width是points单位 # 屏幕96 DPI:1 pixel = 72/96 = 0.75 points # 所以:30px = 30 * 0.75 = 22.5 points offset_points = 30 * 72.0 / 96.0 # 约22.5 points center_x = x + width / 2 - offset_points painter.drawLine(int(center_x), int(cell_y), int(center_x), int(cell_y + actual_line_height)) return y_pos + actual_line_height # 表格第一行:车号(两列:label + 内容) y = draw_2col_row("车号:", slip['vehicle_no'], y) # 表格第二行:计划号(两列:label + 内容) y = draw_2col_row("计划号:", slip.get('plan_no', '') or '', y) # 表格第三行:货物名称(两列:label + 内容) y = draw_2col_row("货物名称:", slip['cargo_name'], y, self.label_width_cargo_name) # 表格第四行:业务类型、批号(四列:label1 + 内容1 + label2 + 内容2) y = draw_4col_row( "业务类型:", slip['business_type'], "批号:", slip.get('batch_no', '') or '', y, custom_label1_width=self.label_width_business_type, draw_center_line=True ) # 表格第五行:供货单位(两列:label + 内容) y = draw_2col_row("供货单位:", slip['supplier'], y, self.label_width_supplier_consignee) # 表格第六行:收货单位(两列:label + 内容) y = draw_2col_row("收货单位:", slip['consignee'], y, self.label_width_supplier_consignee) # 格式化时间字符串,将日期和时间分两行显示 def format_time_to_two_lines(time_str): """将时间字符串格式化为两行:日期和时间分开""" if not time_str: return "" # 将空格替换为换行符 # 例如:"2025-11-20 09:08:01" -> "2025-11-20\n09:08:01" return str(time_str).replace(" ", "\n", 1) # 表格第七行:毛重(t)、毛重时间(四列:label1 + 内容1 + label2 + 内容2) # 将kg转换为t(除以1000) gross_weight_t = slip['gross_weight'] / 1000.0 # 格式化毛重时间,分成两行显示 gross_weight_time_formatted = format_time_to_two_lines(slip['gross_weight_time']) # 限制毛重内容区域宽度,让时间部分往左移 # 4列布局:左侧50%中,label占40%,内容占60%的左侧部分 = 约28mm # 但为了给时间更多空间,限制毛重内容为左侧部分的50% gross_weight_content_width = int(width * 0.22) y = draw_4col_row( "毛重(t):", f"{gross_weight_t:.2f}", "毛重时间:", gross_weight_time_formatted, y, custom_label2_width=self.label_width_time, left_content_max_width=gross_weight_content_width, draw_center_line=True ) # 表格第八行:皮重(t)、皮重时间(四列:label1 + 内容1 + label2 + 内容2) # 将kg转换为t(除以1000) tare_weight_t = slip['tare_weight'] / 1000.0 # 格式化皮重时间,分成两行显示 tare_weight_time_formatted = format_time_to_two_lines(slip['tare_weight_time']) # 限制皮重内容区域宽度,让时间部分往左移 # 4列布局:与毛重相同,限制皮重内容为左侧部分的50% tare_weight_content_width = int(width * 0.22) y = draw_4col_row( "皮重(t):", f"{tare_weight_t:.2f}", "皮重时间:", tare_weight_time_formatted, y, custom_label2_width=self.label_width_time, left_content_max_width=tare_weight_content_width, draw_center_line=True ) # 表格第九行:扣杂(t)、净重(t)(四列:label1 + 内容1 + label2 + 内容2) # 将kg转换为t(除以1000) deduction_t = slip.get('deduction', 0) / 1000.0 net_weight_t = slip['net_weight'] / 1000.0 # 使用与皮重时间相同的左侧内容宽度,确保右侧label对齐 y = draw_4col_row( "扣杂(t):", f"{deduction_t:.2f}", "净重(t):", f"{net_weight_t:.2f}", y, left_content_max_width=tare_weight_content_width, draw_center_line=True ) # 表格第十行:计划量(t)、已执行量(t)(四列:label1 + 内容1 + label2 + 内容2) # 将kg转换为t(除以1000) planned_qty = slip.get('planned_quantity') planned_qty_t = planned_qty / 1000.0 if planned_qty else None executed_qty = slip.get('executed_quantity') executed_qty_t = executed_qty / 1000.0 if executed_qty else None # 使用与皮重时间相同的左侧内容宽度,确保右侧label对齐 y = draw_4col_row( "计划量(t):", f"{planned_qty_t:.2f}" if planned_qty_t else "", "已执行量(t):", f"{executed_qty_t:.2f}" if executed_qty_t else "", y, custom_label1_width=self.label_width_planned_executed, custom_label2_width=self.label_width_planned_executed, left_content_max_width=tare_weight_content_width, draw_center_line=True ) # 表格第十一行:磅点(两列:label + 内容) y = draw_2col_row("磅点:", slip.get('weighing_point', '山峡地磅'), y) # 表格第十二行:备注(两列:label + 内容) y = draw_2col_row("备注:", slip.get('remarks', '') or '', y)