| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512 |
- """
- 打印功能模块
- 使用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)
|