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的组件的用法,以及去通过实践将功能点融入到自己的项目,最终实现自己需求。