【PyQT】实现表格增删改查(无qss)
项目说明#
项目描述#
以爬取的微博数据为例,使用pyqt开发一个队数据进行增删改查的界面
需求分析#
自动连接数据库
excel文件导入数据库中
把数据库的数据导出为Excel文件
可以在界面增加、修改、删除数据、
通过下拉框选择值查询数据
实现步骤#
安装qt designer插件#
教程:https://blog.csdn.net/qq_32892383/article/details/108867482?spm=1001.2014.3001.5506
设计ui界面#
2.1 初始化设置页面(数据库连接页面)
(1)鼠标移至上方,点击空白处,选择栅格布局,点击stackedWidget,选择栅格布局,
2.2 设置数据表界面
该界面由四部分组成:导航栏、操作模块、数据预览模块、翻页模块
包括数据表、查询选择框、导入、导出、增加、删除、全部删除、修改、批量修改、查询、清楚查询、保存、刷新按钮,以及翻页功能
(1) 界面排版
(2)对翻页模块选择栅格布局、操作模块的按钮水平布局、查询选择模块栅格布局
(3)点击stackedWidget,对整体栅格布局
(4)预览界面
通过布局可以解决窗口拖动界面比例不变的问题,窗口拖动大小时,界面比例也随之变化。
导航栏-选项卡切换#
- 将设计好的MainWindow.ui通过PyUic转换为py文件
- 在main.py中实例化主窗口,此时还无法切换标签页
import sys
from PyQt5.QtWidgets import *
from PyQt5.Qt import *
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtGui import QTextList
from ui.MainWindow import *
class MainWindow(QtWidgets.QMainWindow,Ui_MainWindow):
def __init__(self):
super(MainWindow,self).__init__()
self.setupUi(self)
if __name__ == '__main__':
app=QApplication(sys.argv)
widget=MainWindow()
widget.show()
sys.exit(app.exec_())
- 实现标签页切换
# 定义标签名列表
tab=['初始化设置','表1','表2','表3']
tab=['初始化设置','表1','表2','表3']
class MainWindow(QtWidgets.QMainWindow,Ui_MainWindow):
def __init__(self):
super(MainWindow,self).__init__()
self.setupUi(self)
self.menu_btn1.clicked.connect(lambda:self.switch_tab(self.menu_btn1))
self.menu_btn2.clicked.connect(lambda:self.switch_tab(self.menu_btn2))
def switch_tab(self,_btn):
# index=tab.index(_btn.text())+1
self.stackedWidget.setCurrentIndex(tab.index(_btn.text()))
标签页1功能实现#
(1) 在def __init__中设置退出按钮默认
self.p1_btn_quit.setDisabled(True)
(2)绑定按钮确定、取消、保存、退出的点击事件
# 确定按钮
@pyqtSlot()
def on_p1_btn_sure_clicked(self):
if len(self.username.text())==0:
QMessageBox.warning(self,'警告','请输入密码')
else:
# 启动连接数据库线程
pass
# 取消按钮
@pyqtSlot()
def on_p1_btn_cancel_clicked(self):
self.username.setText('127.0.0.1')
self.password.clear()
self.operator.setText('admin')
# 退出连接
@pyqtSlot()
def on_p1_btn_quit_clicked(self):
res = QMessageBox.warning(self, '警告', '退出连接将无法使用系统功能,是否确定退出?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if res == QMessageBox.Yes:
self.p1_btn_sure.setDisabled(False)
self.textBrowser.setText(self.textBrowser.toPlainText() + '\n' + datetime.datetime.now().strftime(
'%Y-%m-%d %H:%M:%S') + ' 退出连接')
self.p1_btn_quit.setDisabled(True)
# 保存按钮
@pyqtSlot()
def on_p1_btn_save_clicked(self):
# 将密码保存在本地
file = open(f'{os.getcwd()}/data/pwd.pickle', 'wb')
pickle.dump(self.password.text(),file)
file.close()
self.textBrowser.setText( self.textBrowser.toPlainText() + '\n' + datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ' 保存成功')
(3) 创建MySQL连接线程ThreadMysql
class ThreadMysql(QThread):
# signal_connect_log = pyqtSignal(str)
signal_connect_status = pyqtSignal(list)
def __init__(self,passsword):
super().__init__()
self.pwd=passsword
def run(self):
try:
self.db=pymysql.connect(
host='127.0.0.1',
port=3306,
user='root',
password=self.pwd
)
self.cursor=self.db.cursor()
self.connect()
except Exception as e:
print(e.args)
self.signal_connect_status.emit([False])
def connect(self):
# 创建数据库
sql='create database if not exists %s' %('test_db')
self.cursor.execute(sql)
self.cursor.execute('use test_db')
# 创建表1
sq="""
create table if not exists typhoon_tb ()
ID INT4 NOT NULL AUTO_INCREMENT PRIMARY KEY,
uid varchar(25),
content varchar(255),
create_at date,
ip varchar(15),
class varchar(15)
"""
self.cursor.execute(sql)
self.signal_connect_status.emit([self.cursor,self.db,True])
(4)在确定按钮的点击事件中加入连接数据库的线程
当成功连接数据库时,按钮状态发生变化
self.thread1=ThreadMysql(self.password.text())
self.thread1.start()
self.thread1.signal_connect_status.connect((self.p1_connect_status))
def p1_connect_status(self,list):
if True in list:
# 设置按钮状态
self.p1_btn_save.setDisabled(True)
self.p1_btn_quit.setDisabled(False)
str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + " 数据库连接成功!"
self.p1_btn_quit.setDisabled(False)
else:
str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + " 数据库连接失败!"
if len(self.textBrowser.toPlainText()) == 0:
self.textBrowser.setText(str)
else:
self.textBrowser.setText(self.textBrowser.toPlainText() + '\n' + str)
(5)自动连接数据库
def auto_connect(self):
if os.path.exists('./data/pwd.pickle'):
# 读取密码文件
try:
file = open('./data/pwd.pickle', 'rb')
res = pickle.load(file)
# 写入密码框
self.password.setText(res)
# 启动数据库连接线程
self.thread_mysql = ThreadMysql(passsword=res)
self.thread_mysql.start()
self.thread_mysql.signal_connect_status.connect(self.p1_connect_status)
except Exception as e:
pass
实现表格展示数据#
(1)表格初始化
def init_tb(self,columns,tb):
tb.setEditTriggers(QAbstractItemView.NoEditTriggers) # 设置不可编辑
tb.setColumnCount(len(columns)) #设置列数
tb.setHorizontalHeaderLabels(columns) # 设置列标签
由于考虑到需要勾选部分数据进行操作,所以第一列需要实现一个勾选框。这里引入一个自定义表头,重新定义初始化方法如下:
from PyQt5.QtWidgets import QHeaderView, QStyle,QStyleOptionButton
from PyQt5.QtCore import Qt, pyqtSignal, QRect
class CheckBoxHeader(QHeaderView):
"""自定义表头类"""
# 自定义 复选框全选信号
select_all_clicked = pyqtSignal(bool)
# 这4个变量控制列头复选框的样式,位置以及大小
_x_offset = 0
_y_offset = 0
_width = 20
_height = 20
# all_header_combobox=[]
def __init__(self, all_header_combox, orientation=Qt.Horizontal, parent=None):
super(CheckBoxHeader, self).__init__(orientation, parent)
self.setSectionsClickable(True)
self.isOn = False
self.sort_indicator_order=True
self.all_header_combobox = all_header_combox
def paintSection(self, painter, rect, logicalIndex):
painter.save()
super(CheckBoxHeader, self).paintSection(painter, rect, logicalIndex)
painter.restore()
self._y_offset = int((rect.height() - self._width) / 2.)
if logicalIndex == 0:
option = QStyleOptionButton()
option.rect = QRect(rect.x() + self._x_offset, rect.y() + self._y_offset, self._width, self._height)
option.state = QStyle.State_Enabled | QStyle.State_Active
if self.isOn:
option.state |= QStyle.State_On
else:
option.state |= QStyle.State_Off
self.style().drawControl(QStyle.CE_CheckBox, option, painter)
def mousePressEvent(self, event):
index = self.logicalIndexAt(event.pos())
if 0 == index:
x = self.sectionPosition(index)
if x + self._x_offset < event.pos().x() < x + self._x_offset + self._width and self._y_offset < event.pos().y() < self._y_offset + self._height:
if self.isOn:
self.isOn = False
else:
self.isOn = True
# 当用户点击了行表头复选框,发射 自定义信号 select_all_clicked()
self.select_all_clicked.emit(self.isOn)
self.updateSection(0)
super(CheckBoxHeader, self).mousePressEvent(event)
# 自定义信号 select_all_clicked 的槽方法
def change_state(self, isOn):
# 如果行表头复选框为勾选状态
if isOn:
# 将所有的复选框都设为勾选状态
for i in self.all_header_combobox:
i.setCheckState(Qt.Checked)
else:
for i in self.all_header_combobox:
i.setCheckState(Qt.Unchecked)
def init_tb(self,columns,tb,header):
tb.setEditTriggers(QAbstractItemView.NoEditTriggers) # 设置不可编辑
tb.setColumnCount(len(columns)) #设置列数
tb.setHorizontalHeader(header) # 设置表头
tb.setHorizontalHeaderLabels(columns) # 设置列标签
(2)表格加载数据
定义一个方法display()用于显示数据,需要传入查询语句,获取数据表的所有数据,再使用tableWidget对象进行设置
def display(self,tb,sql):
self.cursor.execute(sql)
self.db.commit()
res=self.cursor.fetchall()
# 设置行数
tb.setRowCount(len(res))
for i in range(len(res)):
for j in range(1,tb.columnCount()):
item=res[i][j-1]
if item is None:
item=""
newItem = QTableWidgetItem(str(item))
newItem.setTextAlignment(Qt.AlignCenter | Qt.AlignCenter) # 设置居中
tb.setItem(i, j, newItem)
(3) 显示勾选框
此时表格能正常加载数据,但每行第一列没有显示勾选框
在display中补充如下代码,且增加形参header
# 显示勾选框
for i in range(len(res)):
checkbox = QTableWidgetItem()
checkbox.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
checkbox.setCheckState(Qt.Unchecked)
header.all_header_combobox.append(checkbox)
tb.setItem(i, 0, checkbox)
(4)勾选框联动
当表头勾选框勾选时,表格的当页面所有数据应该都被选中
header.select_all_clicked.connect(header.change_state)
实现导入#
该功能不会默认自动导入全部sheet,而是读取文件中的sheet,弹出选择sheet的窗口,选择sheet后开始导入
(1)创建select_sheets_dialog
由listWidget和buttonBox组成
(2)创建存储对话框的Dialogs.py,在此实例化窗口
class SelectSheetsDialog(QtWidgets.QDialog,Ui_Dialog):
signal_sheets=pyqtSignal(list)
def __init__(self):
super(SelectSheetsDialog, self).__init__()
self.setupUi(self)
self.listWidget.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.lst=[]
# 设置按钮点击事件,返回一个sheet列表
@pyqtSlot()
def on_buttonBox_accepted(self):
self.lst=self.listWidget.selectedItems()
self.lst=[ i.text() for i in list(self.lst)]
self.signal_sheets.emit(self.lst)
在main.py的main方法实例化窗口对象
select_sheets_dialog = SelectSheetsDialog()
(3)绑定导入按钮的点击事件
@pyqtSlot()
def on_p2_btn_ipt_clicked(self):
# 判断数据库是否已连接
if self.cursor is None:
QMessageBox.warning(self, '警告', '无法操作!数据库未连接,请先连接数据库。')
else:
# 设置禁用
self.p2_btn_ipt.setDisabled(True)
self.input_data()
def input_data(self):
try:
# 文件对话框
file, _ = QFileDialog.getOpenFileName(self, 'Open file', 'd:\\', '(*.xlsx *.xls *.csv)')
# 当选中窗口
if file:
# 判断文件格式是否正确
if file.endswith('xlsx') or file.endswith('xls') :
workbook = pd.ExcelFile(file)
sheets = workbook.sheet_names
select_sheets_dialog.listWidget.clear()
select_sheets_dialog.listWidget.addItems(sheets)
select_sheets_dialog.signal_sheets.connect(
lambda sheets: self.get_select_sheet(sheets, workbook, file, type))
select_sheets_dialog.show()
elif file.endswith('csv'):
pass
else:
QMessageBox.warning(self, '警告', '请导入xlsx或xls文件!')
except Exception as e:
print(e.args)
(4)将select_sheets_dialog信号中的列表数据传入get_select_sheet()中处理
def get_select_sheet(self, sheets, workbook, file):
select_sheets_dialog.signal_sheets.disconnect() # 解除信号
app.setOverrideCursor(Qt.BusyCursor) # 光标设置为忙碌状态
# 调用导入线程
self.InputPorcesshread_1 = InputPorcesshread(file, sheets, workbook, self.cursor, self.db)
self.InputPorcesshread_1.start()
self.InputPorcesshread_1.signal_input_fail.connect(self.__signal_input_fail)
self.InputPorcesshread_1.signal_input_success.connect(self.__signal_input_success)
(5)定义导入线程
class InputPorcesshread(QThread):
signal_input_fail = pyqtSignal()
signal_input_success = pyqtSignal()
def __init__(self, file, sheet, workbook, cursor, db):
super(InputPorcesshread, self).__init__()
self.file = file
self.sheets = sheet
self.workbook = workbook
self.cursor = cursor
self.db = db
def run(self) -> None:
try:
self.input_process()
except Exception as e:
# datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + f' 导入失败'
self.signal_input_fail.emit()
print("operateThread", e.args)
def input_process(self):
input_dfs = pd.read_excel(self.workbook, sheet_name=self.sheets)
input_dfs = pd.concat([input_dfs[i] for i in self.sheets])
insert_sql = "INSERT ignore INTO typhoon_tb (uid,content,create_at,ip,class)" \
"VALUES (%s,%s,%s," \
"%s,%s)"
# 处理nan值
input_dfs = input_dfs.where(input_dfs.notnull(), None)
input_dfs = input_dfs.replace(np.nan, None)
# 所有内容
rows_contents = input_dfs.values.tolist()
# 写入数据库
self.cursor.executemany(insert_sql, [tuple(row) for row in rows_contents])
self.db.commit()
self.signal_input_success.emit()
信号槽函数
def __signal_input_success(self):
QMessageBox.warning(self, '提示', '导入成功')
app.restoreOverrideCursor() # 恢复鼠标加载状态
self.p2_btn_ipt.setDisabled(False) # 恢复按钮可点击状态
self.display(self.p2_tb, self.p2_sql, self.p2_header) # 加载数据
def __signal_input_fail(self):
QMessageBox.warning(self, '提示', '导入失败')
app.restoreOverrideCursor()
self.p2_btn_ipt.setDisabled(False)
实现翻页#
当数据过多时,会出现卡顿崩溃的现象,上文定义的显示数据函数display()没有考虑到分页显示,而是全部显示,因此,需要修改display()的代码
(1) 初始化定义每页显示最大记录、总页数、当前页、总记录式、查询记录数
def init_page(self):
self.pageSize = 50 # 一页n条
self.p2_totalPage = 1 # 总页数
self.p2_curPage = 1 # 当前页
self.p2_totalRecord = 0 # 总记录数
self.p2_offset = 0
(2)对按钮绑定点击事件
def pageMenu(self):
self.p2_btn_homePage.clicked.connect(lambda: self.p2_pageController('home'))
self.p2_btn_prePage.clicked.connect(lambda: self.p2_pageController('pre'))
self.p2_btn_nextPage.clicked.connect(lambda: self.p2_pageController('next'))
self.p2_btn_last_page.clicked.connect(lambda: self.p2_pageController('final'))
self.p2_btn_sure.clicked.connect(lambda: self.p2_pageController('sure'))
def p2_pageController(self, lab):
try:
if lab == 'home':
self.p2_lab_curPage.setText('1')
self.p2_curPage = 1
elif lab == 'pre':
if self.p2_curPage == 1:
QMessageBox.information(self, '提示', '已经是第一页了', QMessageBox.Yes)
return
self.p2_curPage -= 1
self.p2_lab_curPage.setText(str(self.p2_curPage))
elif lab == 'next':
if self.p2_curPage == self.p2_totalPage:
QMessageBox.information(self, '提示', '已经是最后一页了', QMessageBox.Yes)
return
self.p2_curPage += 1
self.p2_lab_curPage.setText(str(self.p2_curPage))
elif lab == 'sure':
if self.p2_totalPage < int(self.p2_inp_defPage.text()) or int(self.p2_inp_defPage.text()) < 0:
QMessageBox.information(self, "提示", "跳转页码超出范围", QMessageBox.Yes)
return
self.p2_curPage = self.p2_inp_defPage.text()
self.p2_lab_curPage.setText(str(self.p2_inp_defPage.text()))
elif lab == 'final':
if self.p2_curPage == self.p2_totalPage:
QMessageBox.information(self, "提示", "已经是最后一页了", QMessageBox.Yes)
return
self.p2_curPage = self.p2_totalPage
self.p2_lab_curPage.setText(str(self.p2_totalPage))
self.p2_offset = (self.p2_curPage - 1) * self.pageSize
self.display(self.p2_tb, self.p2_sql, self.p2_header,self.p2_offset)
except Exception as e:
print(e.args)
(3)修改display()
将self.p2_sql修改为
'select * from typhoon_tb limit %s,%s;'
display()传参新增offset,执行查询语句改为
self.cursor.execute(sql, (offset, self.pageSize))
此时能实现一页显示50条数据,但三万条数据只显示了五十条,并且左下角错误显示“共0条”。此时还需要添加一个读取数据库中所有记录数的方法
(4)定义get_totalRecord()获取所有记录数
def get_totalRecord(self,tb_name):
self.cursor.execute(f'select count(ID) from {tb_name}')
res = self.cursor.fetchall()[0][0]
self.db.commit()
self.p2_totalRecord = res
self.p2_lab_totalRecord.setText(str(self.p2_totalRecord))
self.p2_totalPage = math.ceil(self.p2_totalRecord / self.pageSize)
self.p2_lab_totalPage.setText(str(self.p2_totalPage))
(5)在p1_connect_status()调用get_totalRecord()
(6)在__signal_input_success()调用get_totalRecord()
(7)最终效果
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)