Python桌面开发pyqt5—最佳实践

特殊事件

延迟点击按钮

延迟3s后点击按钮:

# close dialog after 3s
btn_ok.animateClick(3000)

窗体相关事件

窗口显示

页面显示时,自动触发:

  • 调用show方法时
  • 最小化窗口后再次打开窗口时
def showEvent(self, a0: QShowEvent) -> None:
    """页面显示事件"""
    print('show...')
    # 初始化工作

如果想仅在手动调用show方法时触发,最小化再打开后不触发:

def showEvent(self, evt):
    """页面手动调用show()方法时调用"""
    # 判断事件是否是由用户手动调用show()方法触发的
    if evt.spontaneous():
        return
    # 页面显示时的初始化操作
    print('show...')

窗口隐藏

页面隐藏时,自动触发:

  • 调用hide方法时
  • 最小化窗口时
def hideEvent(self, evt):
    """页面隐藏事件"""
    print('hide...')

如果想仅在手动调用show方法时触发,最小化再打开后不触发:

def hideEvent(self, evt):
    """页面手动调用hide()方法时调用"""
    # 判断事件是否是由用户手动调用hide()方法触发的
    if evt.spontaneous():
        return
    # 页面显示时的初始化操作
    print('hide...')

窗口关闭

页面关闭时,自动触发:

  • 调用close方法时

  • 点击右上角X按钮时

def closeEvent(self, evt) -> None:
    print('close...')
    reply = QMessageBox.question(self, '提示',"确定关闭吗?",QMessageBox.Yes|QMessageBox.No,QMessageBox.No)
    # evt.ignore()

制作带行号的文本框

参考

效果

Python桌面开发pyqt5—最佳实践_第1张图片

实现—自定义组件

from PyQt5.QtCore import Qt, QRect, QSize, QPoint
from PyQt5.QtGui import QColor, QPainter, QTextFormat, QKeyEvent, QWheelEvent, QMouseEvent, QTextCursor
from PyQt5.QtWidgets import QWidget, QTextEdit, QPlainTextEdit


class QPlainTextEditWithNu(QPlainTextEdit):
    def __init__(self, parent=None, rect=None):
        super().__init__(parent)
        # 尺寸
        self.setGeometry(rect[0], rect[1], rect[2], rect[3])
        self.setLineWrapMode(QPlainTextEdit.NoWrap)  # 不自动换行
        self.lineNumberArea = LineNumPaint(self)
        self.document().blockCountChanged.connect(self.update_line_num_width)
        self.document().cursorPositionChanged.connect(self.highlightCurrentLine)  # 高亮当前行
        self.verticalScrollBar().sliderMoved.connect(self.scroll_event)  # 滚动条移动更新行号
        self.update_line_num_width()

    def wheelEvent(self, d: QWheelEvent) -> None:
        self.scroll_event(d)
        super().wheelEvent(d)

    def scroll_event(self, event: QWheelEvent = None):
        self.lineNumberArea.update()

    def keyPressEvent(self, e: QKeyEvent) -> None:
        super().keyPressEvent(e)
        self.lineNumberArea.update()

    def mousePressEvent(self, e: 'QMouseEvent') -> None:
        super().mousePressEvent(e)
        self.update()
        self.highlightCurrentLine()

    def lineNumberAreaWidth(self):
        block_count = self.document().blockCount()
        max_value = max(1, block_count)
        d_count = len(str(max_value))
        _width = self.fontMetrics().width('9') * d_count + 5
        return _width

    def update_line_num_width(self):
        self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)

    def resizeEvent(self, event):
        super().resizeEvent(event)
        cr = self.contentsRect()
        self.lineNumberArea.setGeometry(QRect(cr.left(), cr.top(), self.lineNumberAreaWidth(), cr.height()))

    def highlightCurrentLine(self):
        extraSelections = []
        if not self.isReadOnly():
            selection = QTextEdit.ExtraSelection()
            lineColor = QColor(Qt.blue).lighter(190)
            selection.format.setBackground(lineColor)
            selection.format.setProperty(QTextFormat.FullWidthSelection, True)
            selection.cursor = self.textCursor()
            selection.cursor.clearSelection()
            extraSelections.append(selection)
        self.setExtraSelections(extraSelections)

    def lineNumberAreaPaintEvent(self, event):
        cursor = QTextCursor(self.document())
        painter = QPainter(self.lineNumberArea)

        painter.fillRect(event.rect(), Qt.lightGray)
        line_height = self.fontMetrics().lineSpacing()  # 包含行间距的行高

        block_number = self.cursorForPosition(QPoint(0, int(line_height / 2))).blockNumber()
        first_visible_block = self.document().findBlock(block_number)
        blockNumber = block_number
        cursor.setPosition(self.cursorForPosition(QPoint(0, int(line_height / 2))).position())
        rect = self.cursorRect()
        scroll_compensation = rect.y() - int(rect.y() / line_height) * line_height
        top = scroll_compensation
        last_block_number = self.cursorForPosition(QPoint(0, self.height() - 1)).blockNumber()

        height = self.fontMetrics().height()
        block = first_visible_block
        while block.isValid() and (top <= event.rect().bottom()) and blockNumber <= last_block_number:
            # cur_line_count = block.lineCount()
            if block.isVisible():
                number = str(blockNumber + 1)
                painter.setPen(Qt.black)
                # print((0, top, self.lineNumberArea.width(), height), number)
                painter.drawText(0, top, self.lineNumberArea.width(), height, Qt.AlignCenter, number)
            block = block.next()
            top = top + line_height
            blockNumber += 1


class LineNumPaint(QWidget):
    def __init__(self, q_edit):
        super().__init__(q_edit)
        self.q_edit_line_num = q_edit

    def sizeHint(self):
        return QSize(self.q_edit.lineNumberAreaWidth(), 0)

    def paintEvent(self, event):
        self.q_edit_line_num.lineNumberAreaPaintEvent(event)

实例化(调用)

如果要将此控件作为其他Dialog的子控件,只需要将父控件的实例作为参数传递进去即可

self.te = QPlainTextEditWithNu(parent)

如果此控件本身就是一个窗口:

import sys
from PyQt5.QtWidgets import QApplication
from QPlainTextEditWithNu import QPlainTextEditWithNu

if __name__ == '__main__':
    app = QApplication(sys.argv)
    codeEditor = QPlainTextEditWithLineNum()
    codeEditor.setWindowTitle("带行号的编辑器")
    codeEditor.setGeometry(100, 100, 800, 600)
    codeEditor.show()
    sys.exit(app.exec_())

QStandardItemModel相关

设置标题

self.model.setHorizontalHeaderLabels(['id', 'username', 'create_time', 'status'])

DataFrame渲染数据的套路

通过下面的代码,可以实现自动将标题设置为df的列名,无需像上面一样手动指定

# 表格追加数据显示csv
df = pd.read_csv('./test.csv')
# 设置标题
self.model.setHorizontalHeaderLabels(df.columns.tolist())
# 渲染数据
for row in df.values.tolist():
    self.model.appendRow([QStandardItem(str(item)) for item in row])

Python桌面开发pyqt5—最佳实践_第2张图片

渲染后端接口的数据

假设,后端传来的数据格式为:{'title':['id','username','status'], 'user_list':[xxx]},那么如何优雅地渲染?(至少不需要appendRow的时候手动指定key)

# tableView标题
self.model.setHorizontalHeaderLabels(data['title'])
# 刷新tableView
self.model.removeRows(0, self.model.rowCount())
for item in data['user_list']:
    self.model.appendRow([QStandardItem(str(item[title])) for title in data['title']])

清除所有数据(除了标题)

# clear不推荐,因为会清除标题HorizontalHeaderLabels
# self.model.clear()
# 推荐用法
self.model.removeRows(0, self.model.rowCount())

设置Role数据

item = QStandardItem()
item.setData(user['username'], Qt.DisplayRole)
item.setData(user['id'], Qt.BackgroundRole)

自定义单元格样式

比如,想要让item全部居中

Python桌面开发pyqt5—最佳实践_第3张图片

定义

from PyQt5.Qt import QStandardItemModel, Qt

class MyQStandardItemModel(QStandardItemModel):
    """重写QStandardItemModel的data函数,使QTableView全部item居中"""

    def data(self, index, role=None):
        if role == Qt.TextAlignmentRole:
            return Qt.AlignCenter
        return QStandardItemModel.data(self, index, role)

调用

# 实例化Model
self.model = MyQStandardItemModel(self)
# TableView关联Modal
self.tableView.setModel(self.model)

QListView相关

双击条目事件

def listView_doubleClick(self, idx):
    """双击条目"""
    data = idx.data()	# 获取选中的文本
    data_bg = idx.data(Qt.BackgroundRole)	# 获取bg角色的data

QTableView相关

双击条目事件

def tableView_doubleClick(self, idx):
    """双击条目"""
    # (row, 0).data
    data = self.page4_tableView.model().index(idx.row(), 0).data()
    # (row, 1).data_bg
    data_bg = self.page4_tableView.model().index(idx.row(), 1).data(Qt.BackgroundRole)

操作选中行

获取选中行对应的id,将id发送给后台接口进行某些操作(如删除数据、更新等):

selected_indexes = self.tableView.selectedIndexes()
if len(selected_indexes):
    if prompt_question(self, '确认要xxx吗?'):
        for selected_index in selected_indexes:
            selected_row = selected_index.row()
            id = self.tableView.model().index(selected_row, 0).data()	# (row, 0)
            print(id)	# do something

只选中一行:

selected_indexes = self.tableView.selectedIndexes()
if len(selected_indexes):
    if prompt_question(self, '确认要xxx吗?'):
        selected_index = selected_indexes[0]
        selected_row = selected_index.row()
        id = self.page4_tableView.model().index(selected_row, 0).data()
        print(id)	# do something

获取选中行所有数据,组成列表(选中多行,就是二维列表):

selected_indexes = self.tableView.selectedIndexes()
if len(selected_indexes):
    if prompt_question(self, '确认要xxx吗?'):
        datas = []
        for selected_index in selected_indexes:
            selected_row = selected_index.row()
            # 获取选中行对应的数据
            data = []
            for column in range(self.model.columnCount()):
                item = self.tableView.model().index(selected_row, column).data()
                data.append(item)
            datas.append(data)
        print(datas)	# do something

避免索引重复:如果限定tableView的一行只能选中某一个单元格,那么上面两个示例是没问题的;但如果可以选中一行中的多个,上述代码的selected_row就会重复。正确做法是,使用set()存储选中的索引值:

# 使用set,避免tableView选中同一行的单元格,造成返回值重复的问题
selected_indexes = set()
for idx in self.page4_tableView.selectedIndexes():
    selected_indexes.add(idx.row())
if len(selected_indexes):
    if prompt_question(self, '确认要进行错误上报吗?'):
        for selected_index in selected_indexes:
            id = self.model.item(selected_row, 0).data()	# (row, 0)
            print(id)	# do something

设置标题样式

设置标题颜色为京东红,字体为微软雅黑,加粗

self.tableView.horizontalHeader().setStyleSheet("""
QHeaderView::section{
	font: 10pt '微软雅黑';
	font-weight: 600;
	color: #f10215;
}""")

Python桌面开发pyqt5—最佳实践_第4张图片

编辑策略

默认策略:双击、选中后按任意键都可编辑

Python桌面开发pyqt5—最佳实践_第5张图片

禁用编辑:只能选中不能编辑

self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers)

选中策略selectionMode

Python桌面开发pyqt5—最佳实践_第6张图片

  • ExtendedSelection:默认策略,可以选中多行
  • NoSelection:不可选中

选中行为selectionBehavior

Python桌面开发pyqt5—最佳实践_第7张图片

  • SelectItems:默认行为,只选中单元格
  • SelectRows:选中行
  • SelectColumns:选中列

列展示、拉伸策略

**默认:**不占满列、以最窄的宽度显示,但可自由伸缩

self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)

Python桌面开发pyqt5—最佳实践_第8张图片

宽度固定:不可拖动改变宽度

self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed)

Python桌面开发pyqt5—最佳实践_第9张图片

每列等宽:水平方向上平分,每列等宽,不可拖动改变宽度

self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

Python桌面开发pyqt5—最佳实践_第10张图片

随内容自适应:但多列内容长度差别过大显得特别难看

self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)

Python桌面开发pyqt5—最佳实践_第11张图片

各列自定义比例填充

  • 可以使用 setColumnWidth 方法来设置每一列的宽度,
  • 必须在model.append(row)之后操作,之前操作不生效!
# 以下语句必须在model加载完数据之后
tableView.setColumnWidth(0, 500)
tableView.setColumnWidth(1, 100)

Python桌面开发pyqt5—最佳实践_第12张图片

最后一列填满:在Qt Designer中,启用horizontalHeaderStretchLastSection

Python桌面开发pyqt5—最佳实践_第13张图片

技巧:假如有n列,可以只设置前n-1列的宽度,然后指定最后一列填满。这样可以做到n列刚好填满tableView、不出现水平滚动条。但是这种方法仅适用于n不是很大的情况!

# 设置列宽
self.tableView.horizontalHeader().setStretchLastSection(True)
self.tableView.setColumnWidth(0, 100)
self.tableView.setColumnWidth(1, 250)
self.tableView.setColumnWidth(2, 350)

Python桌面开发pyqt5—最佳实践_第14张图片

文件上传与下载

文件上传

只需要使用request.post()方法中,传入files参数即可

服务端

@app.route('/upload', methods=['POST'])
def upload():
    file = request.files['upload_file']
    filename = secure_filename(file.filename)
    file.save(os.path.join(paths, filename))
    return '文件上传成功'

客户端

url = 'http://localhost:5000/upload'
files = {'upload_file': open('./HDFS_2k.log','rb')}
response = requests.post(url, files=files)
print(response)

携带其他参数

客户端:如果需要上传文件的同时,传递参数(如user_id),则需要在post()方法中传递data表单参数。

注意,其他参数必须通过form的方式(在post方法传递data=xxx),而不是json(即不能在post方法传递json=xxx)

url = 'http://localhost:5000/upload'
form_data = {'user_id': 1}
files = {'upload_file': open('./HDFS_2k.log','rb')}
response = requests.post(url, data=form_data, files=files)
print(response)

服务端:使用request.form[key]接收form参数

注意,其他参数必须通过form方式(使用request.form[key]接收),而不是json(即不能request.json[key]接收)

@app.route('/upload', methods=['POST'])
def upload():
    user_id = request.form['user_id']
    file = request.files['upload_file']
    # userId_rawFilename => 1_666.png
    filename = '{}_{}'.format(user_id, secure_filename(file.filename))
    file.save(os.path.join(paths, filename))
    return '文件上传成功'

"加载中"模态框

定义组件

# LoadingDialog.py
from PyQt5.Qt import QProgressDialog
from PyQt5.QtCore import Qt


class LoadingDialog:
    def __init__(self, root, message):
        self.root = root
        self.pd = QProgressDialog(message, '', 1, 100, root)
        self.root.setEnabled(False)
        # 无限加载效果
        self.pd.setRange(0, 0)
        # 居中显示
        self.pd.move((self.root.width() - self.pd.width()) / 2, (self.root.height() - self.pd.height()) / 2)
        # 隐藏"取消"按钮
        self.pd.setCancelButton(None)
        self.pd.setWindowFlags(Qt.CustomizeWindowHint)
        self.pd.setStyleSheet("""background-color:#ddd;""")
        # 必须使用show
        self.pd.show()

    def exec_(self):
        self.pd.exec_()

    def close(self):
        self.pd.close()
        self.root.setEnabled(True)

调用

# 弹出模态框: 后台线程开始前
self.pd = LoadingDialog(root=self, message='请求中...')
# 关闭: 后台线程结束的槽函数中关闭
self.pd.close()

效果

Python桌面开发pyqt5—最佳实践_第15张图片

PyQt5多线程

参考

演示多线程

Python桌面开发pyqt5—最佳实践_第16张图片

import sys
import time

from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QApplication, QLabel, QWidget, QGridLayout, QPushButton


class Worker(QObject):
    finished = pyqtSignal()
    intReady = pyqtSignal(int)

    @pyqtSlot()
    def work(self):
        for i in range(1, 100):
            time.sleep(0.1)
            print(i)
            self.intReady.emit(i)
        self.finished.emit()


class Form(QWidget):
    def __init__(self):
        super().__init__()
        self.label = QLabel("0")

        # 1 - create Worker and Thread inside the Form
        self.worker = Worker()
        self.thread = QThread()

        self.worker.intReady.connect(self.updateLabel)
        self.worker.moveToThread(self.thread)
        self.worker.finished.connect(self.thread.quit)
        self.thread.started.connect(self.worker.work)
        # self.thread.finished.connect(app.exit)

        self.initUI()

    def start(self):
        print('开始了......')
        self.thread.start()

    def initUI(self):
        grid = QGridLayout()
        self.setLayout(grid)
        grid.addWidget(self.label, 0, 0)
        self.setFixedSize(800, 600)
        self.move(300, 150)
        self.setWindowTitle('多线程经典案例')
        btn = QPushButton('按钮', self)
        btn.move(400, 300)
        btn.clicked.connect(self.start)

    def updateLabel(self, i):
        self.label.setText("{}".format(i))


if __name__ == '__main__':
    app = QApplication(sys.argv)
    form = Form()
    form.show()
    sys.exit(app.exec_())

进度条

Python桌面开发pyqt5—最佳实践_第17张图片

import sys
import time

from PyQt5.Qt import *
from ui_py.ui_loading import Ui_Form


class Runthread(QThread):
    #  通过类成员对象定义信号对象
    _signal = pyqtSignal(int)

    def __init__(self):
        super(Runthread, self).__init__()

    def run(self):
        for i in range(100):
            time.sleep(0.05)
            self._signal.emit(i)  # 注意这里与_signal中的类型相同
        self._signal.emit(100)


class Loading_Page(QWidget, Ui_Form):
    # 信号
    loading_end_signal = pyqtSignal(bool)

    def __init__(self, parent=None, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)
        self.setAttribute(Qt.WA_StyledBackground, True)
        self.setupUi(self)
        self.loading_thread()

    def loading_thread(self):
        self.thread = Runthread()
        self.thread._signal.connect(self.loading)
        self.thread.start()

    def loading(self, val):
        self.qpb.setValue(val)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    root = Loading_Page()
    root.show()
    sys.exit(app.exec_())

表格控件

Python桌面开发pyqt5—最佳实践_第18张图片

import csv
import sys

from PyQt5 import QtCore, QtGui, QtWidgets


class MyWindow(QtWidgets.QWidget):
    def __init__(self, fileName, parent=None):
        super(MyWindow, self).__init__(parent)
        self.fileName = fileName

        self.model = QtGui.QStandardItemModel(self)

        self.tableView = QtWidgets.QTableView(self)
        self.tableView.setModel(self.model)
        self.tableView.horizontalHeader().setStretchLastSection(True)

        self.pushButtonLoad = QtWidgets.QPushButton(self)
        self.pushButtonLoad.setText("Load Csv File")
        self.pushButtonLoad.clicked.connect(self.on_pushButtonLoad_clicked)

        self.pushButtonWrite = QtWidgets.QPushButton(self)
        self.pushButtonWrite.setText("Write Csv File")
        self.pushButtonWrite.clicked.connect(self.on_pushButtonWrite_clicked)

        self.layoutVertical = QtWidgets.QVBoxLayout(self)
        self.layoutVertical.addWidget(self.tableView)
        self.layoutVertical.addWidget(self.pushButtonLoad)
        self.layoutVertical.addWidget(self.pushButtonWrite)

    def loadCsv(self, fileName):
        with open(fileName, mode='r') as fp:
            for row in csv.reader(fp):
                items = [
                    QtGui.QStandardItem(field)
                    for field in row
                ]
                self.model.appendRow(items)

    def writeCsv(self, fileName):
        with open(fileName, mode='w', newline='') as fp:
            writer = csv.writer(fp)
            for rowNumber in range(self.model.rowCount()):
                fields = [
                    self.model.data(
                        self.model.index(rowNumber, columnNumber),
                        QtCore.Qt.DisplayRole
                    )
                    for columnNumber in range(self.model.columnCount())
                ]
                writer.writerow(fields)

    @QtCore.pyqtSlot()
    def on_pushButtonWrite_clicked(self):
        self.writeCsv(self.fileName)

    @QtCore.pyqtSlot()
    def on_pushButtonLoad_clicked(self):
        self.loadCsv(self.fileName)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    app.setApplicationName('MyWindow')
    main = MyWindow("./gao.csv")
    main.show()
    sys.exit(app.exec_())

支持打印机

效果

Python桌面开发pyqt5—最佳实践_第19张图片

代码

import sys

from PyQt5.QtGui import QFont, QTextDocument, QTextCursor
from PyQt5.QtPrintSupport import QPrinter, QPrintDialog, QPrintPreviewDialog
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QAction, QDialog

# 打印文本---《心经》
the_text = '''
观自在菩萨,行深般若波罗蜜多时,照见五蕴皆空,度一切苦厄。
舍利子,色不异空,空不异色,色即是空,空即是色,受想行识亦复如是。
舍利子,是诸法空相,不生不灭,不垢不净,不增不减。
是故空中无色,无受想行识,无眼耳鼻舌身意,无色声香味触法,无眼界乃至无意识界,无无明亦无无明尽,乃至无老死,亦无老死尽,无苦集灭道,无智亦无得。
以无所得故,菩提萨埵,依般若波罗蜜多故,心无挂碍;无挂碍故,无有恐怖,远离颠倒梦想,究竟涅槃。
三世诸佛,依般若波罗蜜多故,得阿耨多罗三藐三菩提。
故知般若波罗蜜多,是大神咒,是大明咒,是无上咒,是无等等咒,能除一切苦,真实不虚。
故说般若波罗蜜多咒,即说咒曰:揭谛揭谛,波罗揭谛,波罗僧揭谛,菩提萨婆诃。
'''


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setWindowTitle(self.tr("支持打印机Demo"))
        self.createLabel()
        self.createMenus()

    def createLabel(self):
        """ 创建文本框 """
        self.label = QLabel()
        self.label.setFont(QFont("Roman times", 12, QFont.Bold))
        self.label.setText(the_text)
        self.setCentralWidget(self.label)


    def createMenus(self):
        """ 创建菜单栏 """
        # 创建动作一
        self.printAction1 = QAction(self.tr("打印无预览"), self)
        self.printAction1.triggered.connect(self.on_printAction1_triggered)

        # 创建动作二
        self.printAction2 = QAction(self.tr("打印有预览"), self)
        self.printAction2.triggered.connect(self.on_printAction2_triggered)

        # 创建动作三
        self.printAction3 = QAction(self.tr("直接打印"), self)
        self.printAction3.triggered.connect(self.on_printAction3_triggered)

        # 创建动作四
        self.printAction4 = QAction(self.tr("打印到PDF"), self)
        self.printAction4.triggered.connect(self.on_printAction4_triggered)

        # 创建菜单,添加动作
        self.printMenu = self.menuBar().addMenu(self.tr("打印"))
        self.printMenu.addAction(self.printAction1)
        self.printMenu.addAction(self.printAction2)
        self.printMenu.addAction(self.printAction3)
        self.printMenu.addAction(self.printAction4)

    def on_printAction1_triggered(self):
        """ 动作一: 打印,无预览 """
        printer = QPrinter()
        printDialog = QPrintDialog(printer, self)
        if printDialog.exec_() == QDialog.Accepted:
            self.handlePaintRequest(printer)

    def on_printAction2_triggered(self):
        """ 动作二: 打印,有预览 """
        dialog = QPrintPreviewDialog()
        dialog.paintRequested.connect(self.handlePaintRequest)
        dialog.exec_()

    def on_printAction3_triggered(self):
        """ 动作三: 直接打印 """
        printer = QPrinter()
        self.handlePaintRequest(printer)

    def on_printAction4_triggered(self):
        """ 动作四: 打印到pdf """
        printer = QPrinter()
        printer.setOutputFormat(QPrinter.PdfFormat)
        printer.setOutputFileName("D:/pdf直接打印测试.pdf")
        self.handlePaintRequest(printer)

    def handlePaintRequest(self, printer):
        """ 打印函数 """
        document = QTextDocument()
        cursor = QTextCursor(document)
        cursor.insertText(self.label.text())
        document.print(printer)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    main = MainWindow()
    main.show()
    sys.exit(app.exec_())

文件对话框

选择文件

下面是通用框架,点击btn_select按钮触发下面的槽函数,选择文件后将内容加载到te_log多行文本域中。

需要注意:必须是file_path, ok...而不是self.file_path, ok...,因为后者测试发现了BUG,即第1次选择了文件,第2次再次点击按钮弹出文件选择框但没有选文件时,self.file_path就变成了'',此时再点击"上传"按钮就会报错。

def btn_select_click(self):
    file_path, ok = UIUtils.prompt_filedialog(self, '上传文件', './HDFS', '日志文件(*.log *.txt)')
    # 选择了文件
    if ok:
        self.file_path = file_path
        with open(self.file_path, mode='r', encoding='utf-8') as fp:
            self.log_text = fp.read()
            self.te_log.setPlainText(self.log_text)
            self.statusbar.showMessage('当前选中:[{}]'.format(self.file_path))
            # 启用btn_file_upload
            self.btn_upload.setEnabled(True)
            # ...

保存文件

下面是通用框架,点击btn_save按钮触发下面的槽函数,填入文件名后将内容保存至本地txt文件。其中方法的参数3是directory,传入一个目录表示保存在哪个文件夹下。

如果传入的是目录directory,必须手动填入文件名才可以报错,就像下面这样:

def btn_save_click(self):
    save_path = QFileDialog.getSaveFileName(self, '保存文件', './models', '文本文件(*.txt)')[0]
    if len(save_path):
        print('成功保存文件: {}'.format(save_path))
        # ...

Python桌面开发pyqt5—最佳实践_第20张图片

如果传入的是一个文件file,则保存的文件名会给出默认提示,就像下面这样:

def btn_save_click(self):
    save_path = QFileDialog.getSaveFileName(self, '保存文件', './models', '文本文件(*.txt)')[0]
    if len(save_path):
        print('成功保存文件: {}'.format(save_path))
        # ...

Python桌面开发pyqt5—最佳实践_第21张图片

工作路径问题

场景复现

**这是一个很容易出错的问题,情景如下:**某个Pyqt5应用由3个界面组成:win_register.py,win_login.py,win_main.py,它们的路径关系如下图:

Python桌面开发pyqt5—最佳实践_第22张图片

在3个界面的py文件中,分别都指定了工作路径为项目logmeta-front的根目录,如下:

import os
os.chdir('../')

然后,在index.py中,将3个界面组合起来形成入口:

class Index:
    def __init__(self):
        self.init_ui()
        # ...
        
    def init_ui(self):
        self.window_login = WinLogin()
        self.window_register = WinRegister()
        self.window_main = WinMain()
        self.window_login_show()

问题描述

现在出现的问题是,单独运行每个界面,资源加载都没问题

Python桌面开发pyqt5—最佳实践_第23张图片

但运行入口文件index.py,则所有资源都丢失了:

Python桌面开发pyqt5—最佳实践_第24张图片

原因分析

出现上述BUG的原因是,每个界面文件都将工作路径重置为../,但我们运行入口文件index.py时,界面文件重置工作路径的操作其实是相对于当前真正运行的文件(即index.py)的,所以修改后的工作路径就不是项目根目录了,而是根目录的上级目录(即index.py根目录的父目录),所以会造成上述错误。

解决方法:工作路径设置为绝对路径

import os
os.chdir(os.path.join(os.path.dirname(__file__), '../'))

QLabel加载网络图片

封装

def load_url_img(label, url):
	"""加载网络图片"""
	try:
		img = QImage.fromData(requests.get(url).content)
		label.setPixmap(QPixmap.fromImage(img))
		label.setScaledContents(True)
		label.repaint()
	except Exception as _:
		label.setPixmap(QPixmap(None))

调用

load_url_img(self.lb_img, response['data']['img_url'])

进度条

import sys
import time
from PyQt5.Qt import *
from PyQt5 import QtCore
from resources.ui_loading import Ui_Form

app = QApplication(sys.argv)


class Runthread(QThread):
	#  通过类成员对象定义信号对象
	_signal = pyqtSignal(int)
	def __init__(self):
		super(Runthread, self).__init__()
	def __del__(self):
		self.wait()
	def run(self):
		for i in range(100):
			time.sleep(0.05)
			self._signal.emit(i)
		self._signal.emit(100)


class Loading_Page(QWidget, Ui_Form):
	# 信号
	loading_end_signal = pyqtSignal(bool)

	def __init__(self, parent=None, *args, **kwargs):
		super().__init__(parent, *args, **kwargs)
		self.setAttribute(Qt.WA_StyledBackground, True)
		self.setupUi(self)
		self.loading_thread()

	def loading_thread(self):
		self.thread = Runthread()
		self.thread._signal.connect(self.loading)
		self.thread.start()
	
	def loading(self,val):
		self.qpb.setValue(val)

        
if __name__ == '__main__':
	root = Loading_Page()
	root.show()
	sys.exit(app.exec_())

你可能感兴趣的:(Python,#,桌面开发pyqt5,python,qt)