文章目录
前言
用户界面开发,我搞过visual C++ MFC、Delphi VCL、Java Swing/JavaFX、SWT、Electron,当然web里jQuery EasyUI、ExtJS、Dojo以及现在流行的React和Vue也算。如果把Kotlin之Jetpack和SwiftUI/UIKit加上,就琳琅满目、品类齐全了。唉,程序员这一生被技术给耽误了。
PyQt的好处
目前Python上主要的GUI开发工具包括:tkinter、PyQt、PyGTK和wxPython。PyQt最正统,和其它桌面系统相比,有开发速度快,组件丰富,生命力强的优势(受众群体大、社区支持强),当然性能也不差。
PyQt API具有简洁易记的特点。
PyQt背后的Widget都是c++实现的,所以我们看不到python的源码,这对调试不友好。像Java Swing的源码基本都是Java。
PyQt5、PyQt6、PySide2、PySide6
造化弄人,这个世界总是那么纷乱。Trolltech于1992创造了Qt,Riverbank Computing这家公司看到Qt火爆,自己搞了个PyQt。Nokia后来收购了Trolltech,和Riverbank Computing谈LGPL协议,后者不同意,于是Nokia自己于2009年发布了PySide。PySide6 发布于2020年。PySide2 支持Qt 5 版本及以上,与Python 3 兼容。 PySide2 的开发者可以免费将其用于商业目的。 PySide6 是PySide2 的最新版本,它支持Qt 6 版本,并提供了与Qt 6 相关的新特性和功能。PySide可以在LGPL协议下使用,PyQt则在GPL协议下使用。
- 大部分API是兼容的。也就是说,你把PyQt5的代码拿过来,直接在import语句里将PyQt5替换成PySide6是没问题的。
- 部分namespace发生了变化,如Qt.DisplayRole 变成 Qt.ItemDataRole.DisplayRole. PyQt6里的Qt.Alignment.AlignLeft 变成 PySide6里的Qt.AlignmentFlag.AlignLeft
- 有一些函数在PySide6被废弃了。如:
QEnterEvent.globalPos()
QEnterEvent.globalX()
QEnterEvent.globalY()
QEnterEvent.localPos()
QEnterEvent.pos()
QEnterEvent.QEnterEvent.screenPos()
QEnterEvent.QEnterEvent.windowPos()
QEnterEvent.QEnterEvent.x()
QEnterEvent.QEnterEvent.y()
- 推荐使用PySide6,为子孙后代计
从一个最简单的例子入手
if __name__ == '__main__':
app = QApplication(sys.argv)
# main = FixBtn()
main = QPushButton("测试")
main.show()
sys.exit(app.exec_())
任何一个QWidget都能独立show(),这点和其它编程语言的GUI框架不同。一个QWidget可以设置一个layout,通过layout.addWidget()就能添加组件,然后就能独立显示。QMainWindow、QFrame、QDialog只是功能更齐全的窗口而已。
递进一步就是:
if __name__ == '__main__':
app = QApplication(sys.argv)
panel = QWidget()
layout = QHBoxLayout()
btn1 = QPushButton("按钮1")
btn2 = QPushButton("按钮2")
btn3 = QPushButton("按钮3")
layout.addWidget(btn1, 1, Qt.AlignLeft)
layout.addWidget(btn2, 2, Qt.AlignCenter)
layout.addWidget(btn3, 3, Qt.AlignRight)
panel.setLayout(layout)
panel.show()
sys.exit(app.exec_())
PyQt5基础
组件体系
源码结构
- QWidget类是所有用户界面对象的基类。窗口部件(QWidget)是用户界面的一个基本单元:它从窗口系统接收鼠标,键盘和其他事件,并且在屏幕上绘制自己。每个窗口部件都是矩形的,并且它们按Z轴顺时针排列。一个窗口部件可以把他的父窗口部件或者它前面的窗口部件盖住一部分。QMainWindow、QDialog、QFrame是三个直接继承于QWidget的容器类,三者是平级的,能独立存在。
- QMainWindow 类提供一个菜单条、锚接窗口(如工具栏)和一个状态条的主应用程序窗口。主窗口通常用在提供一个大的中央窗口部件以及周围菜单、工具条和一个状态条。QMainWindow常常被继承,因为这使的封装中央部件、菜单和工具以及窗口状态条变得容易,当用户点击菜单项或工具条按钮时,槽会被调用。基于主窗口的应用程序,默认已经有了自己的布局管理器。
- QDialog类是对话框窗口的基类。对话框窗口是主要用于短时期任务以及用户进行简要通讯的顶级窗口。QDialog可以是模态对话框也可以是非模态对话框。QDialog支持扩展性并且可以提供返回值。他们可以有默认按钮。QDialog也可以有一个QSizeGrip在它的右下方,使用setSizeGripEnable()。注意:QDialog使用父窗口部件的方法和Qt中其他类不同。对话框总是顶级窗口部件,但是如果它有一个父对象,它的默认位置就是父对象的中间。他也将和父对象共享工具条条目。
- QFrame类是有框架的窗口部件的基类。它绘制部件并且调用一个虚函数drawContents()函数来填充这个框架。这个函数是被子类重新实现的。QFrame类也可以之间创建没有任何内容的简单框架,尽管通常情况下,要用到QHBox 或QVBox,因为它们可以自动布置你放到框架的窗口部件
- QScrollArea是有滚动条的窗口,继承QFrame
- 组件的Parent。没有parent的QWidget类被认为是最上层的窗体(通常是MainWindow),由于MainWindow的一些操作生成的新窗体对象,parent都应该指向MainWindow。由于parent-child关系的存在,它保证了child窗体在主窗体被回收之时也被回收。parent作为构造函数的最后一个参数被传入,但通常情况下不必显示去指定parent对象。因为当调用局管理器时,部局管理器会自动处理这种parent-child关系。但是在一些特殊的情况下,我们必须显示的指定parent-child关系。如当生成的子类不是QWidget对象但继承了QObject对象,用作dock widgets的QWidget对象。
Qt Designer
先秦的Delphi可视化工具深入人心,现在Qt Designer也不遑多让。
每一次运行QtDesigner修改了UI后,都需要通过PyUIC转换为新的.py文件,都会覆盖掉我们自己的代码,所以一般新建一个新的文件来写入自己的代码。
pyuic5 mainwindow.ui -o MainWindow.py
pyuic5可以将ui文件转为python文件。但Qt6里似乎没有提供pyuic工具,但可以如下使用:
import sys
from PyQt6 import QtWidgets, uic
app = QtWidgets.QApplication(sys.argv)
window = uic.loadUi("mainwindow.ui")
window.show()
app.exec()
当然,安装PyQt6-tools:pip install PyQt6-tools之后就有pyuic6了。和pycharm集成:
上面Arguments配置为:
$FileName$ -o $FileNameWithoutExtension$.py
通过Designer创建一个QWidget,然后用PyUIC转为py文件:
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'StrategyForm.ui'
##
## Created by: Qt User Interface Compiler version 6.6.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QLabel, QSizePolicy, QWidget)
class Ui_StrategyForm(object):
def setupUi(self, StrategyForm):
if not StrategyForm.objectName():
StrategyForm.setObjectName(u"StrategyForm")
StrategyForm.resize(400, 300)
self.label = QLabel(StrategyForm)
self.label.setObjectName(u"label")
self.label.setGeometry(QRect(150, 110, 171, 16))
self.retranslateUi(StrategyForm)
QMetaObject.connectSlotsByName(StrategyForm)
# setupUi
def retranslateUi(self, StrategyForm):
StrategyForm.setWindowTitle(QCoreApplication.translate("StrategyForm", u"Form", None))
self.label.setText(QCoreApplication.translate("StrategyForm", u"\u7b56\u7565\u7ba1\u7406\u754c\u9762", None))
# retranslateUi
然后在别的文件中使用该QWidget:
from PySide6.QtWidgets import QWidget
from .StrategyForm import Ui_StrategyForm
class StrategyStack(QWidget):
def __init__(self):
super(StrategyStack, self).__init__()
def setupUI(self):
form = Ui_StrategyForm()
form.setupUi(self)
基础
- 任何一个QWidget都是一个可以独立显示的窗口,这点和Java Swing不同,Java Swing里JFrame才能独立显示。
- QWidget是个容器,可以往里添加子QWidget,但不能直接加,需要先加到此QWidget的layout里去。
布局
任何一个GUI框架都有布局管理,Java Swing, SWT, ant design都是如此。MFC,Delphi,VB似乎是绝对定位。
常用的布局:QHBoxLayout 、QVBoxLayout、QGridLayout、QFormLayout、QSplitter。前面三个比较好理解。
QFormLayout是label-field式的表单布局,只有两列,这点和jGoodies的FormLayout完全不同。
QSplitter是布局管理器,而不是组件。在Qt Designer中,选中两个控件,再点击垂直或水平拆分,就能添加上QSplitter。
采用布局嵌套就能完成复杂的布局。
QFormLayout布局示例
formLayout = QFormLayout()
# 添加多个标签和字段
formLayout.addRow(QLabel('姓名'), QLineEdit())
formLayout.addRow(QLabel('年龄'), QSpinBox()) # QSpinBox 可以显示和编辑数字
formLayout.addRow(QLabel('地址'), QTextEdit()) # QTextEdit 可以编辑多行文本
# 通过 setSpan 方法设置某一行的部件跨越多列
# 例如,让第一个QLineEdit跨越两列
formLayout.setSpan(0, 1, 2) # 设置第一行的第一个部件跨越两列
伸缩量stretch
通过设置伸缩量stretch可以调节一个layout里组件的比例。主要是两个方法addStretch()和setStretch()。
app = QApplication(sys.argv)
panel = QWidget()
layout = QHBoxLayout()
btn1 = QPushButton("按钮1")
btn2 = QPushButton("按钮2")
btn3 = QPushButton("按钮3")
# layout.addWidget(btn1, 1, Qt.AlignLeft)
# layout.addWidget(btn2, 2, Qt.AlignCenter)
# layout.addWidget(btn3, 4, Qt.AlignRight)
layout.addWidget(btn1)
layout.addWidget(btn2)
layout.addWidget(btn3)
# 设置组件占据的空间比例
layout.setStretch(0, 1)
layout.setStretch(1, 2)
layout.setStretch(2, 4)
layout.setSpacing(10)
panel.setLayout(layout)
panel.resize(600, 280)
panel.show()
sys.exit(app.exec_())
这个stretch值,既可以在addWidget()时设置,也可以是在add后调用addStretch()或setStretch()设置,效果是等同的。但是,layout.addWidget(btn1, 1, Qt.AlignLeft)和layout.addWidget(btn1, 1)是有区别的。
在第一个控件之前或者最后一个控件之后addStretch(0),就可以添加一个空的占位控件,将剩余控件占据掉,这也是布局的一个技巧。
高级界面
比较复杂的控件有:QListView/QListWidget、QTableView/QTableWidget、QTreeView/QTreeWidget,这三个主要的控件同时提供了View和Widget两种类型,QXXXWidget继承QXXXView,QXXXWidget提供了更直接的一些操作。
Model-Delegate-View架构
- Data(源数据)是原始数据,如数据库的一个数据表或SQL查询结果、内存中的一个字符串列表或磁盘文件结构等
- Model(模型/数据模型)与源数据通信,并为视图组件提供数据接口。它从源数据提取需要的数据,用于视图组件进行显示和编辑。model持有数据,通过model修改数据,刷新view, Controller处理用户输入
- View(视图/视图组件)是界面控件,视图从数据模型中根据一定条件(如行号、列号等)获得模型索引(一个指向数据项的引用),然后显示在界面上
- Delegate(代理)在视图与模型之间交互操作时提供临时编辑组件的功能
- 在Qt中,View和Controller界线模糊,通常合在一起了,形成了Model/ViewController架构
- 一种情形是使用QStandardItemModel,另一种情形是自定义Model,继承QAbstractItemModel
模型、视图、代理之间使用信号与槽通信:
- 当源数据发生变化时,数据模型发射信号通知视图组件
- 当用户在界面上操作数据时,视图组件发射信号表示这些操作信息
- 在编辑数据时,代理会发射信号告知数据模型和视图组件编辑器的状态
几个常用的Model: - QStandardItemModel可以使用QStandardItem,通过不断添加子节点,从而构建出list、table、tree结构的数据
- QAbstractItemModel: 适用于有Item的视图,如QTreeView
- 用于QListView的QtCore.QAbstractListModel: 需要实现
rowcount()
和data()
两个方法, - 用于QTableView的QtCore.QAbstractTableModel:需要实现
rowCount()、columnCount()、data()、headerData()
模型数据变化后,调用model.layoutChanged.emit()
刷新视图。
QAbstractItemModel解释
该模型是一个多行多列数据构成,每一个item由(row, column)定位,每个item有一个索引QModelIndex。
要想子类化该类,至少要实现五个方法:index() , parent() , rowCount() , columnCount() , data() .
通过这五个方法,就能建立出这个model的索引树,有了索引树,就能访问model中每个item。
注意,最顶层的索引为QModelIndex()
,对Tree来说,索引构成父子关系,所以访问某个节点下面的子节点,可以用:
model.index(row, col, parentIndex)
一个ModelIndex是和实际数据关联的,通过internalPointer()
可以访问其实际的数据节点。创建索引采用:
QAbstractItemModel.createIndex(self, row, column, data) ## data是该索引关联的数据,调用索引的internalPointer()即为该data
在QTableView中加Custom Widget
原以为在实现QAbstractItemModel的data()方法时,返回一个自定义的Widget就行,发现不行。需要用到Delegate方式。如下在表格中添加按钮的示例:
from PySide6 import QtCore, QtGui, QtWidgets
class PushButtonDelegate(QtWidgets.QStyledItemDelegate):
# clicked = QtCore.pyqtSignal(QtCore.QModelIndex)
clicked = QtCore.Signal(QtCore.QModelIndex)
def paint(self, painter, option, index):
if (
isinstance(self.parent(), QtWidgets.QAbstractItemView)
and self.parent().model() is index.model()
):
self.parent().openPersistentEditor(index)
def createEditor(self, parent, option, index):
button = QtWidgets.QPushButton(parent)
button.clicked.connect(lambda *args, ix=index: self.clicked.emit(ix))
return button
def setEditorData(self, editor, index):
editor.setText(index.data(QtCore.Qt.DisplayRole))
def setModelData(self, editor, model, index):
pass
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QTableView()
model = QtGui.QStandardItemModel(0, 2)
for i in range(10):
it1 = QtGui.QStandardItem()
it1.setData(QtCore.Qt.Checked, QtCore.Qt.CheckStateRole)
it1.setFlags(it1.flags() | QtCore.Qt.ItemIsUserCheckable)
it2 = QtGui.QStandardItem()
it2.setData("text-{}".format(i), QtCore.Qt.DisplayRole)
model.appendRow([it1, it2])
# pass the view as parent
delegate = PushButtonDelegate(w)
w.setItemDelegateForColumn(1, delegate)
def on_clicked(index):
print(index.data())
delegate.clicked.connect(on_clicked)
w.setModel(model)
w.show()
sys.exit(app.exec_())
调用setItemWidget()即可以采用自己的Widget渲染视图。最典型的案例就是定制QListView的item项:
myQCustomQWidget = QCustomQWidget()
myQCustomQWidget.setTextUp(index)
myQCustomQWidget.setTextDown(name)
myQCustomQWidget.setIcon(icon)
# Create QListWidgetItem
myQListWidgetItem = QtGui.QListWidgetItem(self.myQListWidget)
# Set size hint
myQListWidgetItem.setSizeHint(myQCustomQWidget.sizeHint())
# Add QListWidgetItem into QListWidget
self.myQListWidget.addItem(myQListWidgetItem)
self.myQListWidget.setItemWidget(myQListWidgetItem, myQCustomQWidget)
我们经常需要传一个叫role的参数,它其实是用来区分不同内容的显示:
QTreeWidget
tree.setHeaderHidden(True),否则就得设置header,两个都不设,header就会显示1
Web控件
以前是QWebView,Qt5.6 后改为QWebEngineView。通过qwebchannel.js与页面中JS进行交互。
参见:https://doc.qt.io/qt-6/qtwebengine-overview.html
多线程、信号、槽
要搞清楚信号和槽、线程的使用。三步曲:
- 定义信号Signal,信号充当了 发布者(publisher)的角色,负责发布消息
- 定义槽Slot。槽扮演了 订阅者(subscriber)的角色,用于接收消息并作出响应,槽可以订阅一个或多个信号,当信号发生时,槽会自动被调用执行。可以使用 @Slot()装饰器 来定义槽。槽定义方式和普通的函数相同,无非是打上slot标记而已。@Slot()也可以不写,写是为了加强可读性。
- 将信号和槽连接:Signal().connect.Slot()
发送信号可以是UI事件,也可以是在worker线程中调用信号的emit()显式发射。
信号和槽只能在 QObject的子类里使用!!!在定义信号和槽时,必须确保它们的参数类型和个数是相同的,否则无法正确连接和触发信号和槽
推荐使用 QRunnable 和 QThreadPool处理多线程多任务问题。
- QTimer: 提供重复的和音效的定时器
- QThread:要使用QThread开始一个线程,可以创建它的一个子类,然后覆盖其QThread.run()函数。QThread有started和finished信号,可以为这2个信号指定槽函数。
from PySide6.QtCore import QRunnable, Slot, QThreadPool
class Worker(QRunnable):
'''
Worker thread
'''
@Slot() # QtCore.Slot
def run(self):
'''
Your code goes in this function
'''
print("Thread start")
time.sleep(5)
print("Thread complete")
self.threadpool = QThreadPool()
print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())
worker = Worker()
self.threadpool.start(worker)
完整示例:
from PySide6.QtWidgets import QVBoxLayout, QLabel, QPushButton, QWidget, QMainWindow, QApplication
from PySide6.QtCore import QTimer, QRunnable, Slot, Signal, QObject, QThreadPool
import sys
import time
import traceback
class WorkerSignals(QObject):
'''
Defines the signals available from a running worker thread.
Supported signals are:
finished
No data
error
tuple (exctype, value, traceback.format_exc() )
result
object data returned from processing, anything
progress
int indicating % progress
'''
finished = Signal()
error = Signal(tuple)
result = Signal(object)
progress = Signal(int)
class Worker(QRunnable):
'''
Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
'''
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# Add the callback to our kwargs
self.kwargs['progress_callback'] = self.signals.progress
@Slot()
def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.counter = 0
layout = QVBoxLayout()
self.l = QLabel("Start")
b = QPushButton("DANGER!")
b.pressed.connect(self.oh_no)
layout.addWidget(self.l)
layout.addWidget(b)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
self.threadpool = QThreadPool()
print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
def progress_fn(self, n):
print("%d%% done" % n)
def execute_this_fn(self, progress_callback):
for n in range(0, 5):
time.sleep(1)
progress_callback.emit(n*100/4)
return "Done."
def print_output(self, s):
print(s)
def thread_complete(self):
print("THREAD COMPLETE!")
def oh_no(self):
# Pass the function to execute
worker = Worker(self.execute_this_fn) # Any other args, kwargs are passed to the run function
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
worker.signals.progress.connect(self.progress_fn)
# Execute
self.threadpool.start(worker)
def recurring_timer(self):
self.counter +=1
self.l.setText("Counter: %d" % self.counter)
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
也可以:
def _runnable(self):
pass
_run_able = QRunnable.create(self._runnable)
_run_able.setAutoDelete(True) # Auto-deletion is enabled by default
_pool = QThreadPool.globalInstance()
_pool.start(_run_able)
和lambda函数结合
如果槽函数需要传额外的参数,可以使用lambda函数。也可以使用functools.partial。
obj.signal.connect(lambda param1, param2, ..., arg1=val1, arg2=val2, ... : fun(param1, param2,... , arg1, arg2, ....))
def fun(param1, param2,... , arg1, arg2, ....):
[...]
for x in range(10):
btn.pressed.connect(lambda val=x: fun(val))
## 不能直接: btn.pressed.connect(lambda e: fun(x))
注意,如果是在循环中,需要使用named参数,否则始终传的是循环的最后一个值。
参见PyQt sending parameter to slot when connecting to a signal。
此外,还需要注意addBtn.pressed.connect()和addBtn.clicked.connect()传参的不同:
addBtn.clicked.connect(lambda x, code=stock.code,name=stock.name: self.add_selfstock(code, name))
addBtn.pressed.connect(lambda code=stock.code,name=stock.name: self.add_selfstock(code, name))
clicked第一个参数是bool型,不能省略不写。
自定义Worker
采用一个自定义的Worker类,可以封装QRunnable和几个基本信号,你可以用最少的代码去执行一个线程:
task = Worker(lambda progress_callback: self.load_data(code, 365*3))
task.signals.finished.connect(lambda : QMessageBox.information(self, "", f"加载成功"))
QThreadPool.globalInstance().start(task)
因为python里没有匿名类,写法比Java还是要繁琐一些。如果能这么写就好了:
QThreadPool.globalInstance().start(QRunnable({
...
}))
## 或者:
QThreadPool.globalInstance().start(lambda: {
... ## 此处可以写多行语句
})
Worker实现如下:
class WorkerSignals(QObject):
'''
Defines the signals available from a running worker thread.
Supported signals are:
finished
No data
error
tuple (exctype, value, traceback.format_exc() )
result
object data returned from processing, anything
progress
int indicating % progress
'''
finished = Signal()
error = Signal(tuple)
result = Signal(object)
progress = Signal(int)
class Worker(QRunnable):
'''
Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
'''
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# Add the callback to our kwargs
self.kwargs['progress_callback'] = self.signals.progress
@Slot()
def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
列表
QTableWidget 继承自QTableView,主要区别是QTableView 可以使用自定义的数据模型来显示内容(先要通过setModel 来绑定数据源),而QTableWidget 只能使用标准的数据模型,并且其单元格数据是通过QTableWidgetltem 对象来实现的。
图形绘制
在PyQt中常用的图像类有4个,即QPixmap、QImage、QPicture和QBitmap。
- QPixmap是专门为绘图而设计的,在绘制图片时需要使用QPixmap;
- QImage提供了一个与硬件无关的图像表示函数,可以用于图片的像素级访问;
- QPicture是一个绘图设备类,它继承自QPainter类,可以使用QPainter的begin()函数在QPicture上绘图,使用end()函数结束绘图,使用QPicture的save()函数将QPainter所使用过的绘图指令保存到文件中;
- QBitmap是一个继承自QPixmap的简单类,它提供了1bit深度的二值图像的类,QBitmap提供的单色图像可以用来制作游标(QCursor)或者笔刷(QBrush)。
绘图库:PyQt5.QtGui,PyQtGraph,Matpoltlib、Plotly
PyQt5.QtGui
painter = QPainter(self)
painter.setPen(self.pen1)
painter.drawLine(100, 10, 500, 10)
PyQtGraph
PyQtGraph 是一个基于 PyQt 和 Numpy 构建的纯 Python 图形和 GUI 库,应用于数学、科学、等工程数据可视化应用,它的主要目标是提供用于显示数据的快速交互式图形,以及提供应用程序快速开发的工具。
matplotlib和PyQt结合
让plt在画布上画图:
self.fig = plt.figure(figsize=(8, 5))
self.canvas = FigureCanvas(self.fig)
self.chartlayout.addWidget(self.canvas)
ax = self.fig.add_subplot(111)
ax = df.plot(data.......)
self.canvas.draw()
和mplfinance结合
mplfinance是一个画K线图的库。
import mplfinance as mpf
fig, axlist = mpf.plot(ohlcv_dataframe, figratio=(8, 5), returnfig=True)
canvas = FigureCanvas(fig)
chartlayout.addWidget(canvas)
canvas.draw()
工具使用
Qt Designer生成的.ui文件(实质上是XML格式的文件)通过pyuic5工具转换成.py文件。
打包
PyInstaller,加-F参数可以打成一个单一的文件。然后解开,根据需要裁剪瘦身。
python3.8是最后一个支持32位程序的版本,其runtime为最小,如果程序没有加载大数据的需求,建议采用3.8作为rumtime。
学习源码
- shiboken6: https://doc.qt.io/qtforpython-6/shiboken6/index.html
Shiboken is a fundamental piece on the Qt for Python project that serves two purposes:
- Generator: Extract information from C or C++ headers and generate CPython code that allow to bring C or C++ projects to Python. This process uses a library called ApiExtractor which internally uses Clang.
- Module: An utility Python module that exposed new Python types, functions to handle pointers, among other things, that is written in CPython and can use independently of the generator.
参考链接
- Qt代码库
- pyside6-tutorial : 这个非常值得看
- PySide6-Code-Tutorial
- Python PySide/PyQt Tutorial
- Plotly : Low-Code Python Data Apps
- PythonPlotlyCodes: 《Python 数据分析:基于 Plotly 的动态可视化绘图》 源代码