Bootstrap

PyQt5 使用 QAbstractTableModel 和 QTableView 组件实现分页效果展示

1.背景

        项目中需要实现一个历史消息分页查看的功能,并能够对消息进行删除的操作。换做是在web网站里面,这是一个非常常规的功能,但是如何在桌面应用程序做到这一点,还需要花点心思去探索一下。不过无论桌面应用程序还是html网页,本质上开发的思路没有任何的区别。需要克服的难点问题就是,如何使用PyQt5 的组件来实现上述的功能。通过查阅QT的的资料文档,找到了两个组件可以实现这样的功能,分别为QAbastractTableModel (这里主要存放的是数据模型对象,当数据模型对象发生改变会触发QTableView对象,基于MVC的开发模式),QTableView负责数据的渲染工作。分页组件自行实现。

2.目标

该功能最终实现的效果图如下, 点击翻页实现数据分页展示效果:

3.实现思路

3.1 UI布局分析

        整个页面使用的是QMainWindow 组件, 整体布局分分为三个模块,模块一,工具按钮;模块二,QTableView用于数据的渲染展示;模块三,自定义翻页组件;很显然这里使用垂直的盒子布局模型,分页将这三个模块依次放入。

        考虑到功能代码的复用,决定将有关数据表格组件单独封装出来,提供给后序功能开发使用,提升开发的效率,同时通过该过程去思考如何去抽象问题,如何去组织代码,力求代码的简洁和高效运行。

3.2 封装自定义数据图表组件

         由于QTableView 和 翻页的组件有较强的相关性,所以这里决定把他们的功能代码单独封装起来,同时定义好事件的信号/槽。并考虑将用户的点击行为,比如说,首页、上一页、跳转之类的按钮点击事件也一起暴露给他的父容器,交给父容器去处处理(比如去远程获取数据,让QTableView重新渲染数据),代码文件为mydatawidget.py,文件中TableModel为数据的模型,通过改变self._data来改变表格组件的数据详细可以参考api文档以及相关的教程。具体代码展示如下:

from PyQt5.QtCore import QAbstractTableModel, Qt
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QTableView, QWidget

from widget.HistoryMsgManage import QHeaderView, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QMessageBox


class TableModel(QAbstractTableModel):
    """
    表格数据模型MVC模式
    """

    def __init__(self, data, HEADERS):
        super(TableModel, self).__init__()
        self._data = data
        self.headers = HEADERS

    def updateData(self, data):
        """
        (自定义)更新数据
        """
        self.beginResetModel()
        self._data = data
        self.endResetModel()

    def data(self, index, role=None):
        if role == Qt.DisplayRole:
            value = self._data[index.row()][index.column()]
            return value

        if role == Qt.DecorationRole:
            pass

    def rowCount(self, parent=None, *args, **kwargs):
        """
        行数
        """
        return len(self._data)

    def columnCount(self, parent=None, *args, **kwargs):
        """
        列数
        """
        return len(self.headers)

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        """
        标题定义
        """
        if role != Qt.DisplayRole:
            return None
        if orientation == Qt.Horizontal:
            return self.headers[section]
        return int(section + 1)


class TableWidget(QWidget):
    """
    数据库表展示组件
    """
    control_signal = pyqtSignal(list)

    def __init__(self, pagetotal):
        super(TableWidget, self).__init__()
        self.total = pagetotal
        self.control_layout = None
        self.__init_ui()

    def __init_ui(self):
        curtable = QTableView()
        curtable.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        curtable.setAlternatingRowColors(True)
        self.table = curtable
        self.__layout = QVBoxLayout()
        self.__layout.addWidget(self.table)
        self.setLayout(self.__layout)
        self.setPageController()

    def updatePageSize(self, pagetotal):
        """
        当进行删除数据,或者添加数据时候,需要跟新一下总页数
        """
        self.curPage.setText("1")  # 设置当前页默认为1
        self.total = pagetotal  # 设置总页数
        self.totalPage.setText("共" + str(self.total) + "页")  # 设置显示的总页数

    def setPageController(self):
        """自定义页码控制器"""
        control_layout = QHBoxLayout()
        homePage = QPushButton("首页")
        prePage = QPushButton("<上一页")
        self.curPage = QLabel("1")
        nextPage = QPushButton("下一页>")
        finalPage = QPushButton("尾页")
        self.totalPage = QLabel("共" + str(self.total) + "页")
        skipLable_0 = QLabel("跳到")
        self.skipPage = QLineEdit()
        self.skipPage.setMaximumWidth(50)
        skipLabel_1 = QLabel("页")
        confirmSkip = QPushButton("确定")
        homePage.clicked.connect(self.__home_page)
        prePage.clicked.connect(self.__pre_page)
        nextPage.clicked.connect(self.__next_page)
        finalPage.clicked.connect(self.__final_page)
        confirmSkip.clicked.connect(self.__confirm_skip)
        control_layout.addStretch(1)
        control_layout.addWidget(homePage)
        control_layout.addWidget(prePage)
        control_layout.addWidget(self.curPage)
        control_layout.addWidget(nextPage)
        control_layout.addWidget(finalPage)
        control_layout.addWidget(self.totalPage)
        control_layout.addWidget(skipLable_0)
        control_layout.addWidget(self.skipPage)
        control_layout.addWidget(skipLabel_1)
        control_layout.addWidget(confirmSkip)
        control_layout.addStretch(1)
        self.__layout.addLayout(control_layout)

    def __home_page(self):
        """点击首页信号"""
        self.control_signal.emit(["home", self.curPage.text()])

    def __pre_page(self):
        """点击上一页信号"""
        self.control_signal.emit(["pre", self.curPage.text()])

    def __next_page(self):
        """点击下一页信号"""
        self.control_signal.emit(["next", self.curPage.text()])

    def __final_page(self):
        """尾页点击信号"""
        self.control_signal.emit(["final", self.curPage.text()])

    def __confirm_skip(self):
        """跳转页码确定"""
        self.control_signal.emit(["confirm", self.skipPage.text()])

    def showTotalPage(self):
        """返回当前总页数"""
        return int(self.totalPage.text()[1:-1])

    def handle_click_ViewLogic(self, signal):
        """
        处理点击逻辑
        """
        if "home" == signal[0]:
            self.curPage.setText("1")
        elif "pre" == signal[0]:
            if 1 == int(signal[1]):
                QMessageBox.information(self, "提示", "已经是第一页了", QMessageBox.Yes)
                return
            self.curPage.setText(str(int(signal[1]) - 1))
        elif "next" == signal[0]:
            if self.total == int(signal[1]):
                QMessageBox.information(self, "提示", "已经是最后一页了", QMessageBox.Yes)
                return
            self.curPage.setText(str(int(signal[1]) + 1))
        elif "final" == signal[0]:
            self.curPage.setText(str(self.total))
        elif "confirm" == signal[0]:
            if signal[1] != '':
                try:
                    if self.total < int(signal[1]) or int(signal[1]) < 0:
                        QMessageBox.information(self, "提示", "跳转页码超出范围", QMessageBox.Yes)
                        return
                    self.curPage.setText(signal[1])
                except Exception as e:
                    pass

3.3 代码实现

          该部分就需要定义我们自己业务功能页面上的内容,将上面封装好的组件放到我们自己的布局里面,同时需要绑定好数据模型和QTable的关系,详细的代码参考如下:

"""
历史消息管理
"""

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import *

from utils.config import BasesConfig
from utils.date_util import MyDateInfoHelper
from utils.myhttp import MyHttpHelper


class HistoryMsgWindow(QMainWindow):
    """
    历史消息窗体
    """

    def __init__(self, ctx):
        super(HistoryMsgWindow, self).__init__()
        # 上下文
        self.ctx = ctx
        # 表头
        self.HEADERS = ['编号', '消息', '地址', '来自', '推送时间', '状态']
        # 表格数据渲染的模型
        self.table_model = None
        # 查询到的数据
        self.data = []
        # 查询到总的页数
        self.pagetotal = 0
        # 分页查询的条件实体
        self.filterobj = {
            "page": 1,
            "pagesize": 10,
            "condition": {
                "workerid": ''
            }
        }

    def set_workerid(self, workerid):
        """
        设置工号
        """
        self.filterobj['condition']['workerid'] = workerid

    def Init_ui(self):
        """
        初始化UI
        """
        # 设置窗体信息
        self.resize(1200, 650)
        self.setWindowTitle("历史消息记录查看")

        # 主组件
        mainWidget = QWidget()
        main_layout = QVBoxLayout()
        main_layout.setSpacing(0)
        mainWidget.setLayout(main_layout)

        # 实例化工具栏
        buttonContainer = QWidget()
        buttonContainer.setLayout(QVBoxLayout())
        self.removeButton = QPushButton('删除选中记录(点击表格的行表头)')
        self.removeButton.setMinimumSize(300, 50)
        self.removeButton.setFont(QFont("Microsoft YaHei"))
        self.removeButton.setStyleSheet("background-color:orange;color:#fff;border:none;border-radius:4px;")
        buttonContainer.layout().addWidget(self.removeButton)
        self.removeButton.clicked.connect(self.removeSelected)
        main_layout.addWidget(self.removeButton, 1, alignment=Qt.AlignHCenter)

        # 实例化表格组件
        from widget.mydatawidget import TableWidget, TableModel
        self.table_model = TableModel(self.data, self.HEADERS)  # 创建数据模型
        self.table_widget = TableWidget(self.pagetotal)  # 实例化表格
        self.table_widget.table.setModel(self.table_model)  # 设置模型
        self.table_widget.table.setColumnHidden(0, True)  # 设置第一列隐藏
        self.table_widget.control_signal.connect(self.page_controller)  # 设置点击跳转事件
        main_layout.addWidget(self.table_widget, 10)

        # 设置中央组件
        self.setCentralWidget(mainWidget)

    def page_controller(self, signal):
        self.table_widget.handle_click_ViewLogic(signal)  # 处理页面展示逻辑
        self.changeTableContent()  # 改变表格内容

    def changeTableContent(self):
        """根据当前页改变表格的内容"""
        cur_page = self.table_widget.curPage.text()
        self.filterobj['page'] = int(cur_page)
        # 重新抓取数据
        self.fetchData()
        # 渲染数据
        self.table_model.updateData(self.data)

    def fetchData(self):
        """
        从服务器抓取数据
        """
        url = BasesConfig.REMOTE_URL_BASE + BasesConfig.API_V + "/fetch_usrreadedmsg"
        result = MyHttpHelper.http_post_json(url, self.filterobj, is_token=True)
        # 解析数据
        history = self.data
        try:
            if result['re_code'] == '0':
                self.data = []
                # 如果请求成功
                pagedInfo = result['pagedInfo']
                self.pagetotal = pagedInfo['pages']  # 总的页数
                datalist = result['pagedInfo']['data']
                for v in datalist:
                    elem = []
                    elem.append(v['id'])
                    elem.append(v['msg_title'])
                    elem.append(v['msg_url'])
                    elem.append(v['sys_name'])
                    elem.append(MyDateInfoHelper.fmtdt_str(v['msg_push_time'], "%Y-%m-%d %H:%M"))
                    elem.append(v['msg_status'])
                    self.data.append(elem)
        except Exception as e:
            self.data = history
            pass

    def removeSelected(self):
        """
        删除选中记录
        """
        cur_index = self.table_widget.table.currentIndex().row()
        if cur_index != -1:
            cur_data = self.data[cur_index]
            msgid = cur_data[0]
            msg_info = cur_data[1]
            button = QMessageBox.warning(self, "提示", "是否确认删除%s?" % (msg_info),
                                         QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)
            if button == QMessageBox.Ok:
                # remove_msg
                url = BasesConfig.REMOTE_URL_BASE + BasesConfig.API_V + "/remove_msg?msgid=" + str(msgid)
                result = MyHttpHelper.http_post_json(url, None, is_token=True)
                if result['re_code'] == '0':
                    QMessageBox.information(self, "提示", "删除记录成功!", QMessageBox.Yes)
                    # 当删除完成之后,需要从第一页开始渲染
                    self.filterobj['page'] = 1  # 设置第一页
                    self.fetchData()  # 重新抓取数据
                    self.table_widget.updatePageSize(self.pagetotal)  # 重新渲染分页组件
                    self.table_model.updateData(self.data)  # 更新模型数据
                else:
                    QMessageBox.warning(self, "提示", "删除记录失败,请稍后重试!", QMessageBox.Yes)

4. 总结

        本文最终实现了通过使用PyQt5的组件来实现数据表格分页展示的功能。具体的实现思路和web端开发大体一致,难点在于如何自己去探索pyqt的组件的用法,以及去通过实践将功能点融入到自己的项目,最终实现自己需求。

;