【PyQT】实现表格增删改查(无qss)

项目说明#

项目描述#

以爬取的微博数据为例,使用pyqt开发一个队数据进行增删改查的界面
image

需求分析#

自动连接数据库
excel文件导入数据库中
把数据库的数据导出为Excel文件
可以在界面增加、修改、删除数据、
通过下拉框选择值查询数据

实现步骤#

安装qt designer插件#

教程:https://blog.csdn.net/qq_32892383/article/details/108867482?spm=1001.2014.3001.5506

设计ui界面#

2.1 初始化设置页面(数据库连接页面)
image
(1)鼠标移至上方,点击空白处,选择栅格布局,点击stackedWidget,选择栅格布局,
image

(2)在上下分别加入垫片,并将右侧文本框拖入红框内
image

(3)调整比例
image

2.2 设置数据表界面
该界面由四部分组成:导航栏、操作模块、数据预览模块、翻页模块
包括数据表、查询选择框、导入、导出、增加、删除、全部删除、修改、批量修改、查询、清楚查询、保存、刷新按钮,以及翻页功能
(1) 界面排版
image
(2)对翻页模块选择栅格布局、操作模块的按钮水平布局、查询选择模块栅格布局
image
(3)点击stackedWidget,对整体栅格布局
image

(4)预览界面
通过布局可以解决窗口拖动界面比例不变的问题,窗口拖动大小时,界面比例也随之变化。
image

2.3 最终界面预览
image

image

导航栏-选项卡切换#

  1. 将设计好的MainWindow.ui通过PyUic转换为py文件
  2. 在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_())
  1. 实现标签页切换
# 定义标签名列表
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()))

预览效果:
image

标签页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)

如图所示,基本功能已实现
image

(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)  # 设置列标签

image

由于考虑到需要勾选部分数据进行操作,所以第一列需要实现一个勾选框。这里引入一个自定义表头,重新定义初始化方法如下:

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)  # 设置列标签

image

(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组成
image
(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)

(6) 效果预览
image

实现翻页#

当数据过多时,会出现卡顿崩溃的现象,上文定义的显示数据函数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))

预览效果:
image

此时能实现一页显示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)最终效果
image

实现查询#

作者:Gim

出处:https://www.cnblogs.com/Gimm/p/18262439

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   踩坑大王  阅读(307)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示