PyQt5中使用Qprinter打印热敏小票

《PyQt5中使用QWebChannel和内嵌网页进行js交互》一文中,我记录了如何使用QWebchannel与内嵌网页进行js交互,其根本目标在于使用Qt5调起打印机服务。在这篇文章中我将介绍一下具体使用Qprinter打印超市热敏小票的过程。

>>>原文地址

PyQt5中使用Qprinter打印热敏小票

参考内容:
Qt5官方文档
《pyqt5的 QPrinter 使用模板》 by 一心狮

本文包含以下内容:
1.使用html进行热敏打印的方法
2.分析存在的问题
3.提出另一种打印方法来解决问题

使用html进行热敏打印

python端代码

# -*- coding:utf-8 -*-
# webprint.py


from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QObject, pyqtSlot, QUrl, QSizeF
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtPrintSupport import QPrinter, QPrinterInfo
from PyQt5.QtGui import QTextDocument
import sys


class Printer:
    def __init__(self):
        self.p = QPrinterInfo.defaultPrinter()  # 获取默认打印机
        self.print_device = QPrinter(self.p)  # 指定打印所使用的装置

    def print(self, content):
        # 设置打印内容的宽度,否则打印内容会变形
        self.print_device.setPageSizeMM(QSizeF(110, 250))
        d = QTextDocument()  # 使用QTextDcument对html进行解析
        d.setDocumentMargin(0)  # 将打印的边距设为0
        # 设置全局生效的默认样式
        d.setDefaultStyleSheet('''
        * {padding:0;margin: 0;}
        h1 {font-size: 20px;}
        h3 {font-size: 16px;}
        .left {float: left;}
        .right {float:right;}
        .clearfix {clear: both;}
        ul {list-style: none;}
        .print_container {width: 250px;}
        .section2 label {display: block;}
        .section3 label {display: block;}
        .section4 .total label {display: block;}
        .section4 {border-bottom: 1px solid #DADADA;}
        .section5 label {display: block;}
        ''')
        d.setHtml(content)  # 注入html内容
        d.print(self.print_device)  # 调用打印机进行打印


class Print(QObject):
    def __init__(self):
        super().__init__()
        self.printer = Printer()

    @pyqtSlot(str, result=str)
    def print(self, content):
        self.printer.print(content)
        return


if __name__ == '__main__':
    app = QApplication(sys.argv)
    browser = QWebEngineView()
    browser.setWindowTitle('使用PyQt5打印热敏小票')
    browser.resize(900, 600)
    channel = QWebChannel()
    printer = Print()
    channel.registerObject('printer', printer)
    browser.page().setWebChannel(channel)
    url_string = "file:///python/print/webprint.html"  # 内置的网页地址
    browser.load(QUrl(url_string))
    browser.show()
    sys.exit(app.exec_())

网页端代码

<!-- webprint.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>使用PyQt5打印热敏小票</title>
</head>
<style type="text/css">
    * {padding:0;margin: 0;}
    h1 {font-size: 20px;}
    h3 {font-size: 16px;}
    .left {float: left;}
    .right {float:right;}
    .clearfix {clear: both;}
    ul {list-style: none;}
    .print_container {width: 250px;}
    .section2 label {display: block;}
    .section3 label {display: block;}
    .section4 .total label {display: block;}
    .section4 {border-bottom: 1px solid #DADADA;}
    .section5 label {display: block;}
</style>
<body>
<div id="capture">
    <div class="print_container">
        <h3>便利店</h3>
        <span>***************************************</span>
        <div class="section3">
            <label>订单号:700001001201811161631123558</label>
            <label>下单时间:2018-10-16 16:31:14</label>
            <label>收银员:王小明</label>
        </div>
        <span>***************************************</span>
        <div class="section4">
            <div style="border-bottom: 1px solid #DADADA;">
                <table style="width: 100%;">
                    <thead>
                    <tr>
                        <td width="60%">品名</td>
                        <td width="20%">数量</td>
                        <td width="20%">金额</td>
                    </tr>
                    </thead>
                    <tbody>
                    <tr>
                        <td>今麦郎</td>
                        <td>1</td>
                        <td>100.00</td>
                    </tr>
                    </tbody>
                </table>
            </div>
            <div class="total">
                <label class="left">合 计</label>
                <label class="right">100.00</label>
                <div class="clearfix"></div>
                <label class="left">收款金额</label>
                <label class="right">100</label>
                <div class="clearfix"></div>
                <label class="left">找零金额</label>
                <label class="right">0.00</label>
                <div class="clearfix"></div>
            </div>
            <div style="text-align: right;">
                <label>顾客已付款</label>
            </div>
            <span>***************************************</span>
        </div>
        <div class="section5">
            <label>电话:</label>
        </div>
        <span>***************************************</span>
        <div class="section5">
            <label>欢迎光临,谢谢惠顾!</label>
            <label>便利店</label>
        </div>
    </div>
</div>
<div>
    <button onclick="do_print()">进行html打印</button>
</div>
<script src="qwebchannel.js" type="text/javascript"></script>
<script>
    window.onload = function() {
        new QWebChannel(qt.webChannelTransport, function (channel) {
            window.printer = channel.objects.printer;
        });
    }

    function do_print() {
        if (printer !== null) {
            var html = document.querySelector('#capture').innerHTML;
            printer.print(html);
        }
    }
</script>
</body>
</html>

关于使用qwebchannel进行js交互的内容这里不再赘述,请查阅本文开头提到的文章。

问题分析

运行上述代码,我们可以成功地调起打印机服务。但是打印出来的内容却惨不忍睹,热敏小票的左边和顶部空出一大片空白,以至于打印出来的票据内容丢失了大半!

惨不忍睹的html打印

为什么会这样呢?在代码中我们已经对QTextDocument进行了setDocumentMargin设置,打印时却依然有巨大的边距。
一开始我以为是margin设置无效,后来查看了pyqt5的源码以及在Google上搜索,才得知QTextDocument强制左边和顶部留白。事实上默认的margin已经是0了。这样一来使用QTextDocument进行打印的计划宣告破产,我不得不苦苦思索,在互联网上胡搜一通,看看是否有人遇到相同的问题。
值得一提的是,热心的(:p)简书网友一心狮,他向我提供了一种思路:

先在项目内,放置一个已经排版好的excel文件。然后用win32com,对这个 execl文件,进行操作,如赋值,如打印

这确实是一个不错的方法,遗憾的是对我来说不太适用。超市热敏小票的内容不是固定长度的,而且我打算用pyinstaller将所有代码封装成一个独立的可执行程序(exe),放置一个excel文件也不太方便。
后来在Stackoverflow我偶然的看见了一个同样的问题,具体链接我忘了保存。下面只有一个回答,答者粗略地提到一个解决方法——Qt5打印图片不会留边距,可以从这个角度着手,把要打印的内容转为图片再打印。这条曲线救国的思路真是太棒了!

使用图片进行热敏打印

想要使用图片打印,首先就要把文字内容转成图片才行。幸好这世上已经有人提供了简单方便的html转图片方案,而且是在网页端进行的!这个方案就是使用起来方便简单的html2canvas

The script allows you to take "screenshots" of webpages or parts of it, directly on the users browser. The screenshot is based on the DOM and as such may not be 100% accurate to the real representation as it does not make an actual screenshot, but builds the screenshot based on the information available on the page.

简而言之就是支持对html页面的部分进行“截屏”操作。
使用方法极其简单:

html2canvas(document.querySelector("#capture")).then(canvas => {
    document.body.appendChild(canvas)
});

转好了图片,我们只需在python端对图片数据流使用QPainter连接打印机进行打印即可。

python端代码

# -*- coding:utf-8 -*-

from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QObject, pyqtSlot, QUrl
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtPrintSupport import QPrinter, QPrinterInfo
from PyQt5.QtGui import QPainter, QImage
import sys, base64


class Printer:
    def __init__(self):
        self.p = QPrinterInfo.defaultPrinter()
        self.print_device = QPrinter(self.p)

    def print_(self, data_url):
        image_content = base64.b64decode(data_url)  # 数据流base64解码
        image = QImage()
        image.loadFromData(image_content)  # 使用QImage构造图片
        painter = QPainter(self.print_device)  # 使用打印机作为绘制设备
        painter.drawImage(0, 0, image)  # 进行绘制(即调起打印服务)
        painter.end()  # 打印结束


class Print(QObject):
    def __init__(self):
        super().__init__()
        self.printer = Printer()

    @pyqtSlot(str, result=str)
    def print_(self, data_url):
        # 去除头部标识
        self.printer.print_(data_url.replace('data:image/png;base64,', ''))
        return


if __name__ == '__main__':
    app = QApplication(sys.argv)
    browser = QWebEngineView()
    browser.setWindowTitle('使用PyQt5打印热敏小票')
    browser.resize(900, 600)
    channel = QWebChannel()
    printer = Print()
    channel.registerObject('printer', printer)
    browser.page().setWebChannel(channel)
    url_string = "file:///python/print/webprint.html"  # 内置的网页地址
    browser.load(QUrl(url_string))
    browser.show()
    sys.exit(app.exec_())

网页端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>使用PyQt5打印热敏小票</title>
</head>
<style type="text/css">
    * {padding:0;margin: 0;}
    h1 {font-size: 20px;}
    h3 {font-size: 16px;}
    .left {float: left;}
    .right {float:right;}
    .clearfix {clear: both;}
    ul {list-style: none;}
    .print_container {width: 250px;}
    .section2 label {display: block;}
    .section3 label {display: block;}
    .section4 .total label {display: block;}
    .section4 {border-bottom: 1px solid #DADADA;}
    .section5 label {display: block;}
</style>
<body>
<div id="capture">
    <div class="print_container">
        <h3>便利店</h3>
        <span>***************************************</span>
        <div class="section3">
            <label>订单号:700001001201811161631123558</label>
            <label>下单时间:2018-10-16 16:31:14</label>
            <label>收银员:王小明</label>
        </div>
        <span>***************************************</span>
        <div class="section4">
            <div style="border-bottom: 1px solid #DADADA;">
                <table style="width: 100%;">
                    <thead>
                    <tr>
                        <td width="60%">品名</td>
                        <td width="20%">数量</td>
                        <td width="20%">金额</td>
                    </tr>
                    </thead>
                    <tbody>
                    <tr>
                        <td>今麦郎</td>
                        <td>1</td>
                        <td>100.00</td>
                    </tr>
                    </tbody>
                </table>
            </div>
            <div class="total">
                <label class="left">合 计</label>
                <label class="right">100.00</label>
                <div class="clearfix"></div>
                <label class="left">收款金额</label>
                <label class="right">100</label>
                <div class="clearfix"></div>
                <label class="left">找零金额</label>
                <label class="right">0.00</label>
                <div class="clearfix"></div>
            </div>
            <div style="text-align: right;">
                <label>顾客已付款</label>
            </div>
            <span>***************************************</span>
        </div>
        <div class="section5">
            <label>电话:</label>
        </div>
        <span>***************************************</span>
        <div class="section5">
            <label>欢迎光临,谢谢惠顾!</label>
            <label>便利店</label>
        </div>
    </div>
</div>
<div>
    <button onclick="do_print_()">进行图像打印</button>
</div>
<script src="qwebchannel.js" type="text/javascript"></script>
<script src="html2canvas.min.js" type="text/javascript"></script>
<script>
    window.onload = function() {
        new QWebChannel(qt.webChannelTransport, function (channel) {
            window.printer = channel.objects.printer;
        });
    }

    function do_print_() {
        if (printer !== null) {
            html2canvas(document.querySelector("#capture")).then(canvas => {
                var data_url = canvas.toDataURL();
                printer.print_(data_url);
            });
        }
    }
</script>
</body>
</html>

运行代码,我们欣喜地发现,热敏小票的排版终于正常了!


令人欣喜的图片打印

事实上,无论是图片打印,或者excel打印,都是同样的曲线救国思路。在得知第一种方法的情况下,我却没能想到第二种方法,看来我的联想能力还有待锻炼。

以上,就是我的解决历程。

附:完整代码(包含两种打印方式)

python端代码

# -*- coding:utf-8 -*-

from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QObject, pyqtSlot, QUrl, QSizeF
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtPrintSupport import QPrinter, QPrinterInfo
from PyQt5.QtGui import QTextDocument, QPainter, QImage
import sys, base64


class Printer:
    def __init__(self):
        self.p = QPrinterInfo.defaultPrinter()
        self.print_device = QPrinter(self.p)

    def print(self, content):
        self.print_device.setPageSizeMM(QSizeF(110, 250))
        d = QTextDocument()
        d.setDocumentMargin(0)
        d.setDefaultStyleSheet('''
        * {padding:0;margin: 0;}
        h1 {font-size: 20px;}
        h3 {font-size: 16px;}
        .left {float: left;}
        .right {float:right;}
        .clearfix {clear: both;}
        ul {list-style: none;}
        .print_container {width: 250px;}
        .section2 label {display: block;}
        .section3 label {display: block;}
        .section4 .total label {display: block;}
        .section4 {border-bottom: 1px solid #DADADA;}
        .section5 label {display: block;}
        ''')
        d.setHtml(content)
        d.print(self.print_device)

    def print_(self, data_url):
        image_content = base64.b64decode(data_url)  # 数据流base64解码
        image = QImage()
        image.loadFromData(image_content)  # 使用QImage构造图片
        painter = QPainter(self.print_device)  # 使用打印机作为绘制设备
        painter.drawImage(0, 0, image)  # 进行绘制(即调起打印服务)
        painter.end()  # 打印结束


class Print(QObject):
    def __init__(self):
        super().__init__()
        self.printer = Printer()

    @pyqtSlot(str, result=str)
    def print(self, content):
        self.printer.print(content)
        return

    @pyqtSlot(str, result=str)
    def print_(self, data_url):
        # 去除头部标识
        self.printer.print_(data_url.replace('data:image/png;base64,', ''))
        return


if __name__ == '__main__':
    app = QApplication(sys.argv)
    browser = QWebEngineView()
    browser.setWindowTitle('使用PyQt5打印热敏小票')
    browser.resize(900, 600)
    channel = QWebChannel()
    printer = Print()
    channel.registerObject('printer', printer)
    browser.page().setWebChannel(channel)
    url_string = "file:///python/print/webprint.html"  # 内置的网页地址
    browser.load(QUrl(url_string))
    browser.show()
    sys.exit(app.exec_())

网页端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>使用PyQt5打印热敏小票</title>
</head>
<style type="text/css">
    * {padding:0;margin: 0;}
    h1 {font-size: 20px;}
    h3 {font-size: 16px;}
    .left {float: left;}
    .right {float:right;}
    .clearfix {clear: both;}
    ul {list-style: none;}
    .print_container {width: 250px;}
    .section2 label {display: block;}
    .section3 label {display: block;}
    .section4 .total label {display: block;}
    .section4 {border-bottom: 1px solid #DADADA;}
    .section5 label {display: block;}
</style>
<body>
<div id="capture">
    <div class="print_container">
        <h3>便利店</h3>
        <span>***************************************</span>
        <div class="section3">
            <label>订单号:700001001201811161631123558</label>
            <label>下单时间:2018-10-16 16:31:14</label>
            <label>收银员:王小明</label>
        </div>
        <span>***************************************</span>
        <div class="section4">
            <div style="border-bottom: 1px solid #DADADA;">
                <table style="width: 100%;">
                    <thead>
                    <tr>
                        <td width="60%">品名</td>
                        <td width="20%">数量</td>
                        <td width="20%">金额</td>
                    </tr>
                    </thead>
                    <tbody>
                    <tr>
                        <td>今麦郎</td>
                        <td>1</td>
                        <td>100.00</td>
                    </tr>
                    </tbody>
                </table>
            </div>
            <div class="total">
                <label class="left">合 计</label>
                <label class="right">100.00</label>
                <div class="clearfix"></div>
                <label class="left">收款金额</label>
                <label class="right">100</label>
                <div class="clearfix"></div>
                <label class="left">找零金额</label>
                <label class="right">0.00</label>
                <div class="clearfix"></div>
            </div>
            <div style="text-align: right;">
                <label>顾客已付款</label>
            </div>
            <span>***************************************</span>
        </div>
        <div class="section5">
            <label>电话:</label>
        </div>
        <span>***************************************</span>
        <div class="section5">
            <label>欢迎光临,谢谢惠顾!</label>
            <label>便利店</label>
        </div>
    </div>
</div>
<div>
    <button onclick="do_print()">进行html打印</button>
    <button onclick="do_print_()">进行图像打印</button>
</div>
<script src="qwebchannel.js" type="text/javascript"></script>
<script src="html2canvas.min.js" type="text/javascript"></script>
<script>
    window.onload = function() {
        new QWebChannel(qt.webChannelTransport, function (channel) {
            window.printer = channel.objects.printer;
        });
    }

    function do_print() {
        if (printer !== null) {
            var html = document.querySelector('#capture').innerHTML;
            printer.print(html);
        }
    }

    function do_print_() {
        if (printer !== null) {
            html2canvas(document.querySelector("#capture")).then(canvas => {
                var data_url = canvas.toDataURL();
                printer.print_(data_url);
            });
        }
    }
</script>
</body>
</html>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260