PyQt5.15.4(python 32bit)连接mysql 64位数据库

  • 下载mysql-connector-c-6.1.6-win32.msi:版本太新的话,可能不含所需dll文件。

    解压后的文件F.lib.libmysql.dll重命名为libmysql.dll,放入python/Lib/site-packages/PyQt5/Qt5/bin文件夹中

  • 下载qsqlmysql.dll_Qt_SQL_driver_5.15.2_MSVC2019_32-bit.zip,其他版本/位数的dll详见:https://github.com/thecodemonkey86/qt_mysql_driver/releases)

    解压后sqldrivers中的文件qsqlmysql.dllqsqlmysqld.dll两份文件,放入python/Lib/site-packages/PyQt5/Qt5/plugins/sqldrivers文件夹中

  • Complete!

  • 原因:每个PyQt版本都对应一个Qt版本,由于Qt在5.12.x更新后官方不提供mysql的dll,需要自行编译。

  • Pycharm Debug显示数据库driver无mysql,连接失败

    设置-构建,部署,执行-Python调试器-P yQt兼容:自动改为Pyqt5

五分钟刷新一次表:https://stackoverflow.com/questions/60876325/python-pyqt5-table-with-sql-postgrees

QTableWidget

cellchanged/itemchanged 区别

cellChanged is for all QAbstractItemView classes, itemChanged is specific to the convenience views (QListWidget, QTableWidget, QTreeWidget) where you use the corresponding item classes to interact with the model.

ItemDelegate设置数字/空字符

要匹配数字或空字符串’’,即用户没有输入任何输入,请执行此操作

1
(^[0-9]+$|^$)

匹配数字、空字符串或空白字符

1
(^[0-9]+$|^$|^\s$)

designer.exe位置 -PyQt5与PySide2

1
2
3
# 两个designer.exe的文件大小一致,使用起来应该没有区别
C:\Python37\Lib\site-packages\PySide2
C:\Python37\Lib\site-packages\qt5_applications\Qt\bin

PyInstaller打包PyQt应用程序

1
2
3
4
-F, –onefile 打包成一个exe文件。
-D, –onedir 创建一个目录,包含exe文件,但会依赖很多文件(默认选项)
-c, –console, –nowindowed 使用控制台,无界面(默认) - 会弹出一个cmd界面
-w, –windowed, –noconsole 使用窗口,无控制台

不同分辨率的显示器编译代码

  • 不同分辨率的显示器编译PyQt程序,有的控件大小会变(在整个窗口是一种布局的情况下)
1
2
3
4
5
6
if __name__ == '__main__':
QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling # 加上这行代码即可规避分辨率导致的问题
app = QApplication(sys.argv)
win = myWin()
win.show()
sys.exit(app.exec())

QDialog / QMainWindow 设置窗口和Win系统下的任务栏图标

  • Designer下icon->选择资源->选择图片,图片位置与.ui文件在同层目录,
  • pyinstaller打包之后,放在dist文件夹下的一级目录,可正常显示
  • 代码中运行self.setWindowIcon(QIcon("Path"))

Table View/Widget 设置列宽模式

宽/高分配模式

1
2
3
4
5
6
7
8
9
10
# 列宽自动分配
self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
# 行高自动分配
self.tableWidget.verticalHeader().setSectionResizeMode(QHeaderView.Stretch)
# 表格适应内容指定列
self.tableWidget.resizeColumnToContents(column)
# 手动调整
self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
# 固定值:用户无法调整该部分的大小,只能使用resizeSection()以编程方式调整大小,默认为defaultSectionSize。
self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed)、

样式设置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
# 随内容分配列宽
self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.tableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
# 随内容分配行高
self.tableWidget.verticalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.tableWidget.verticalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
# 水平方向标签拓展剩下的窗口部分,填满表格
self.tableWidget.horizontalHeader().setStretchLastSection(True)
# 列宽整个表格为自动分配的,但可单独设置第一列为手动调整宽度
self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.tableWidget.horizontalHeader().setSectionResizeMode(0, QHeaderView.Interactive)
# 自定义列宽
self.tableWidget.setColumnWidth(0, 200)

Table View[可绑定数据库模型]

  • Table View中使用自定义的数据模型来显示表格的内容,通过setModel来绑定数据源

    1
    2
    3
    df = pd.read_csv('file_path')
    model = DataFrameModel(df)
    self.tv_badn.setModel(model)

TableView自定义数据模型 - dataframe

  • 关于自定义的DataFrame数据模型(不会写,copy的),自定义的函数关注dataheaderData

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    # 没有序号的DataFrame - TableModel
    class DataFrameModel(QAbstractTableModel):
    def __init__(self, data):
    QAbstractTableModel.__init__(self)
    self._data = data
    # 行数
    def rowCount(self, parent=None):
    return self._data.shape[0]
    # 列数
    def columnCount(self, parent=None):
    return self._data.shape[1]
    # 数据项的字段
    def data(self, index, role=Qt.DisplayRole):
    if index.isValid():
    if role == Qt.DisplayRole:
    return str(self._data.iloc[index.row(), index.column()])
    return None
    # 表头/行序号数据?
    def headerData(self, col, orientation, role):
    if orientation == Qt.Horizontal and role == Qt.DisplayRole:
    return self._data.columns[col]
    return None

    # 行序号从0开始的DataFrame - TableModel
    class DataFrameModel(QAbstractTableModel):
    def __init__(self, data):
    QAbstractTableModel.__init__(self)
    self._data = data

    def rowCount(self, parent=None):
    return self._data.shape[0]

    def columnCount(self, parent=None):
    return self._data.shape[1]

    def data(self, index, role=Qt.DisplayRole):
    string_role = str(self._data.iloc[index.row(), index.column()]).strip()
    if index.isValid():
    if role == Qt.ForegroundRole:
    if pd.isna(self._data.iloc[index.row(), index.column()]):
    return QColor("red")
    elif string_role == 'FAIL':
    return QColor("red")
    elif str(self._data.iloc[index.row(), index.column()]).strip() == 'PASS':
    return QColor("Green")
    if role == Qt.DisplayRole:
    return str(self._data.iloc[index.row(), index.column()])
    return None

    def headerData(self, section, orientation, role):
    if role != Qt.DisplayRole:
    return QVariant()

    if orientation == Qt.Horizontal:
    try:
    return self._data.columns.tolist()[section]
    except(IndexError, ):
    return QVariant()
    elif orientation == Qt.Vertical:
    try:
    return self._data.index.tolist()[section]
    except(IndexError, ):
    return QVariant()
    return None

    # 序号从1开始的headerData函数
    def headerData(self, section: int, orientation, role: int) -> str:
    # header data (first row and first column)
    if role == Qt.DisplayRole:
    if orientation == Qt.Horizontal:
    return str(self._data.columns[section])

    if orientation == Qt.Vertical:
    if isinstance(self._data.index[section], str):
    return self._data.index[section]
    else:
    return str(self._data.index[section] + 1)
可能会用到的数据模型 含义
QSqlQueryModel 对SQL的查询结果进行封装
QSqlTableModel 对SQL中的表格进行封装
QSortFilterProxyModel 对模型中的数据进行排序或过滤
QStandardItemModel 存储任意层次结构的数据

TableView设置委托:设置单元格不可编辑/输入类型卡控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class EmptyDelegate(QItemDelegate):
def __init__(self, parent):
super(EmptyDelegate, self).__init__(parent)

def createEditor(self, parent: QWidget, option: 'QStyleOptionViewItem', index: QModelIndex):
# 设置委托不返回编辑器 即不可编辑
return None


class IntEditDelegate(QItemDelegate):
def __init__(self, parent):
super().__init__(parent)

def createEditor(self, parent, option, index):
# 设置委托返回QLineEdit编辑器 且设置数据类型为整数
editor = QLineEdit(parent)
editor.setValidator(QIntValidator())
return editor
# 委托的使用:设置第5、6列为整数输入,其余不可编辑。(TableView的列序号从0开始)
self.tableView.setItemDelegate(EmptyDelegate(self))
self.tableView.setItemDelegateForColumn(4, IntEditDelegate(self))
self.tableView.setItemDelegateForColumn(5, IntEditDelegate(self))

Sqlite3数据库连接、查询与显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class EmptyDelegate(QItemDelegate):
def __init__(self, parent):
super(EmptyDelegate, self).__init__(parent)

def createEditor(self, parent: QWidget, option: 'QStyleOptionViewItem', index: QModelIndex):
# 设置委托不返回编辑器 即不可编辑
return None


class IntEditDelegate(QItemDelegate):
def __init__(self, parent):
super().__init__(parent)

def createEditor(self, parent, option, index):
# 设置委托返回QLineEdit编辑器 且设置数据类型为整数
editor = QLineEdit(parent)
editor.setValidator(QIntValidator())
return editor
# 委托的使用:设置第5、6列为整数输入,其余不可编辑。(TableView的列序号从0开始)
self.tableView.setItemDelegate(EmptyDelegate(self))
self.tableView.setItemDelegateForColumn(4, IntEditDelegate(self))
self.tableView.setItemDelegateForColumn(5, IntEditDelegate(self))

Table Widget[可编辑/识别item字段]

  • Table Widget是Table View的子类,只能使用标准的数据模型(QStandardItemModel?),单元格数据通过Table Widget Item对象来实现。

  • 与Table View最大的不同除了数据模型固定以外,可以直接在表格视图中进行数据的修改

  • Table Widget的水平或垂直表头可以直接在designer中设定,也可以使用函数设定,在设定前,需要先初始化行号与列号

    1
    2
    3
    4
    tableWidget.setRowCount(3)
    tableWidget.setColumnCount(3)
    tableWidget.setHorizontalHeaderLabels(['姓名','性别','体重(kg)'])
    tableWidget.setVerticalHeaderLabels(row_list)
  • Designer中的editTriggers中可设定表格的编辑方式,可勾选NoEditTriggers使其为禁止编辑的只读状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 表头为自适应的伸缩模式,可以根据窗口大小来改变网格大小
tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

# 将表格变为禁止编辑
tableWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)

# 设置表格为整行选择
tableWidget.setSelectionBehavior( QAbstractItemView.SelectRows)

#表格表头的显示与隐藏
tableWidget.verticalHeader().setVisible(False)
tableWidget.horizontalHeader().setVisible(True)

# 将行和列的大小设为与内容相匹配(取决于内容的长短/大小)
tableWidget.resizeColumnsToContents()
tableWidget.resizeRowsToContents()

# Table Widget的单元格中还可以放ComboBox(下拉选框)与按钮等控件
comBox = QComboBox()
comBox.addItem("男")
comBox.addItem("女")
comBox.setStyleSheet("QComboBox{margin:3px};")
tableWidget.setCellWidget(0,1,comBox)

searchBtn = QPushButton("修改")
searchBtn.setDown(True)
searchBtn.setStyleSheet("QPushButton{margin:3px};")
tableWidget.setCellWidget(0, 2, searchBtn)

Line Edit

自动补全方式

1
2
3
4
5
6
7
8
9
10
from PyQt5.QtWidgets import QCompleter
from PyQt5.QtCore import Qt

def init_completor(itme_list):
# itme_list 补全选项列表
completer = QCompleter(itme_list)
completer.setFilterMode(Qt.MatchContains) # 模糊匹配
completer.setCompletionMode(QCompleter.PopupCompletion) # 下拉框选项补全
completer.setCaseSensitivity(QCompleter.CaseInsensitive) # 大小写不敏感
return completer
  • 匹配方式:completer.setFilterMode(MatchMode)
    • Qt.MatchStartsWith 开头匹配
    • Qt.MatchContains 内容匹配(模糊匹配)
    • Qt.MatchEndsWith 结尾匹配
  • 补全模式:completer.setCompletionMode(CompletionMode)
    • QCompleter.PopupCompletion 弹出下拉框选项补全
    • QCompleter.InlineCompletion 行内显示补全 (建议开头匹配)
    • QCompleter.UnfilteredPopupCompletion 全显示选项补全
  • 大小写是否敏感:completer.setCaseSensitivity(SensitivityMode)
    • QCompleter.CaseInsensitive 大小写不敏感
    • QCompleter.CaseSensitive 大小写敏感

QCalendar

方法 描述
setDateRange() 设置日期范围选择
setMinimumDate() 设置最小日期
setMaximumDate() 设置最大日期
setSelectedDate 设置一个QDate对象,作为日期控件所选定的日期
setFirstDayOfWeek() 重新设置星期的第一天,默认是星期日,其参数枚举值[Qt.Monday, Qt.Tuesday, Qt.Wednesday, Qt.Thursday, Qt.Friday, Qt.Saturday, Qt.Sunday]
selectedDate() 返回当前选定的日期
minimumDate() 获取日历控件的最小日期
maximumDate() 获取日历控件的最大日期
setGridvisible() 设置日历控件视图是否有网格线

QDate

1
2
date = self.cal.selectedDate()
date.to_string('YYYY-mm-dd')

QComboBox 下拉框

  • 两个下拉框实现级联:父级CurrentIndexChanged信号,关联子级Items刷新
  • addItems([‘str1’, ‘str2’, ‘str3’])
  • QComboBox在designer中可以设置为可输入(Edit)的样式,输入的内容如果和选项模糊匹配会补齐。

复选框:自定义控件,ComboCheckBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""
Check Combo Box
---------------
A QComboBox subclass designed for multiple item selection.
The combo box popup allows the user to check/uncheck multiple items at
once.
"""
import sys
from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtGui import QPalette, QFontMetrics, QStandardItem
from PyQt5.QtWidgets import QComboBox, QStyledItemDelegate, QApplication, qApp, QMainWindow, QWidget


class CheckableComboBox(QComboBox):
# Subclass Delegate to increase item height
class Delegate(QStyledItemDelegate):
def sizeHint(self, option, index):
size = super().sizeHint(option, index)
size.setHeight(20)
return size

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Make the combo editable to set a custom text, but readonly
self.setEditable(True)
self.lineEdit().setReadOnly(True)
# Make the lineedit the same color as QPushButton
palette = qApp.palette()
palette.setBrush(QPalette.Base, palette.button())
self.lineEdit().setPalette(palette)

# Use custom delegate
self.setItemDelegate(CheckableComboBox.Delegate())

# Update the text when an item is toggled
self.model().dataChanged.connect(self.updateText)

# Hide and show popup when clicking the line edit
self.lineEdit().installEventFilter(self)
self.closeOnLineEditClick = False

# Prevent popup from closing when clicking on an item
self.view().viewport().installEventFilter(self)

def resizeEvent(self, event):
# Recompute text to elide as needed
self.updateText()
super().resizeEvent(event)

def eventFilter(self, object, event):

if object == self.lineEdit():
if event.type() == QEvent.MouseButtonRelease:
if self.closeOnLineEditClick:
self.hidePopup()
else:
self.showPopup()
return True
return False

if object == self.view().viewport():
if event.type() == QEvent.MouseButtonRelease:
index = self.view().indexAt(event.pos())
item = self.model().item(index.row())

if item.checkState() == Qt.Checked:
item.setCheckState(Qt.Unchecked)
else:
item.setCheckState(Qt.Checked)
return True
return False

def showPopup(self):
super().showPopup()
# When the popup is displayed, a click on the lineedit should close it
self.closeOnLineEditClick = True

def hidePopup(self):
super().hidePopup()
# Used to prevent immediate reopening when clicking on the lineEdit
self.startTimer(100)
# Refresh the display text when closing
self.updateText()

def timerEvent(self, event):
# After timeout, kill timer, and reenable click on line edit
self.killTimer(event.timerId())
self.closeOnLineEditClick = False

def updateText(self):
texts = []
for i in range(self.model().rowCount()):
if self.model().item(i).checkState() == Qt.Checked:
texts.append(self.model().item(i).text())
text = ", ".join(texts)

# Compute elided text (with "...")
metrics = QFontMetrics(self.lineEdit().font())
elidedText = metrics.elidedText(text, Qt.ElideRight, self.lineEdit().width())
self.lineEdit().setText(elidedText)

def addItem(self, text, data=None):
item = QStandardItem()
item.setText(text)
if data is None:
item.setData(text)


if __name__ == "__main__":
# Example
app = QApplication(sys.argv)
dialog = QMainWindow()
mainWidget = QWidget()
dialog.setCentralWidget(mainWidget)
cb = CheckableComboBox(mainWidget)
items = ['123','456']
cb.addItems(items)
dialog.show()
sys.exit(app.exec_())

QSpinBox 数字写入

方法 说明
setMinimum() / setMaximum() 设置计数器的max min边界值
singleStep() 设置计数器的步长值
setRange() 设置计数器的max min边界值+步长值
setValue() / Value() 设置/获取计数器的当前值

QMessageBox设置弹窗信息:Open File,格式化超链接的绝对路径

1
2
3
4
5
6
7
8
'file:///F:/CSVEdit/test_data/CSV%E5%90%88%E5%B9%B6/001-1%E6%B1%87%E6%80%BB.csv'
os.path.normpath(self.tgt_path) 'F:\\CSVEdit\\test_data\\CSV合并\\001-1汇总.csv'
self.tgt_path 'F:/CSVEdit/test_data/CSV合并/001-1汇总.csv'
os.path.normpath(self.tgt_path)


file_link = bytearray(QUrl.fromLocalFile(os.path.normpath(self.tgt_path)).toEncoded()).decode()
QMessageBox.information(self, "Message", "合并完成!<a href='%s'>Open</a>" % file_link, QMessageBox.Ok, QMessageBox.Ok)

Designer动态加载 自定义控件

  • 在Designer中右键选中的控件 - 提升为:选择对应基类相同的提升的类
  • 新建提升的类:
    • 基类名称
    • 提升的类名称:Class 的类名称,eg. CheckableComboBox
    • 头文件:与相对路径有关,eg.ui.CheckableComboBox , 相对路径为ui/CheckableComboBox.py

工时统计软件设计思路简记

  • 工作日志 (1) :老化日志(1) : 老化箱(1) ,如果涉及到一个工作日志用到多个老化箱,则将工作日志拆分为对应的条数

    遇到这种情况需要解除添加任务时的卡控

  • 老化日志(n) : 老化板(n) (中间表)

  • 与一个工作日志绑定的id,由于与多个老化板关联,因此需要生成多条老化日志