一、项目介绍
本项目将通过PySide6构建一个可以显示数据折线图的可视化程序,其中,数据来源时美国地质调查局(US Geological Survey)上公开的一小时地震震级数据。
可以通过链接进行下载。
二、实现步骤
本项目的实现步骤可以概括为:
- 读取数据
- 数据处理
- 创建主窗口
- 添加控件
- 绘制图形并显示、
预期结果如下图所示:
三、实现
1️⃣ 读取数据
这里我们借助Pandas
对CSV文件进行读取。argparse
模块主要用于参数控制,具体可见这篇文章。
创建一个新文档main.py
,接下来都是一些简单的操作,就不做赘述了。
import argparse
import pandas as pd
def read_data(file):
return pd.read_csv(file)
if __name__ == '__main__':
options=argparse.ArgumentParser()
options.add_argument("-f","--file",type=str,required=True)
args=options.parse_args()
data=read_data(args.file)
print(data)
我们可以通过终端输入python main.py -f "YourPath"
来查看数据读取情况。
2️⃣ 数据清洗
我们在这部,需要将数据中的日期转换为Qt类型,并且确保数据的完整性、准确性。
值得注意的是,数据中的日期是UTC标准(如: 2018-12-11T21:14:44,682Z),我们可以比较容易地转换为QDateTime类型。
这个QtDateTime位于QtCore模块,从QtCore中将其导入:
from PySide6.QtCore import QDateTime,QTimeZone
接着,通过QtDateTime().fromString( time , format )
进行转换:
def transform_date(utc,timezone=None):
# 日期转换
utc_fmt="yyyy-MM-ddTHH:mm:ss.zzzZ"
new_date=QDateTime().fromString(utc,utc_fmt)
if timezone:
new_date.setTimeZone(timezone)
return new_date
我们对读取数据方法进行一定的修改,首先是移除错误的震级数据:
data=pd.read_csv(file)
data=data.drop(data[data['mag']<0].index)
magnitudes=data['mag']
然后,设置本地的时区:
# 时区设置
timezone = QTimeZone(QTimeZone.systemTimeZone()) # "Asia/Shanghai"
# 时间转换
times=data['time'].apply(lambda x:transform_date(x,timezone))
return times,magnitudes
虽然不该在这篇文档中提,但还是提一嘴:
Pandas快速对某一类操作:
Series.apply(lambda x:func(x)) # apply(func)但是如果要输入参数,可以这样写: .apply(lambda x:func(x))
好了,此时我们的read_data
方法应该长这样:
def read_data(file):
# 读取数据
data=pd.read_csv(file)
# 处理震级
data=data.drop(data[data['mag']<0].index)
magnitudes=data['mag']
# 时区设置
timezone = QTimeZone(QTimeZone.systemTimeZone()) # "Asia/Shanghai"
# 时间转换
times=data['time'].apply(lambda x:transform_date(x,timezone))
return times,magnitudes
3️⃣ 创建主窗体
好啦,终于要进入我们的核心啦,这一步我们将创建一个PySide主窗口。
下图是QMainWindow的布局。
![在
在本项目中,我们需要一个“文件”菜单,用来打开文件对话框,和一个“退出”菜单。应用程序启动时,应该要自动加载状态栏。
我们新建一个文件,叫做MainWindow.py
。
from PySide6.QtCore import Slot
from PySide6.QtGui import QAction,QKeySequence
from PySide6.QtWidgets import QMainWindow
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
pass
让我们的窗体控件继承自QMainWindow。
然后是菜单栏的设置,直接选择获取self.menuBar()
即可,通过addMenu(Str)
的方式,可以添加选项。
# 设置菜单栏
self.menu=self.menuBar()
# 添加 文件 菜单项
self.file_menu=self.menu.addMenu("文件")
然后我们为菜单栏添加一个退出事件。这个事件可以通过QAction
来直接绑定。QAction("Exit",self)
表示退出本窗体。
# 退出事件
exit_action=QAction("Exit",self) # 设置名字
exit_action.setShortcut(QKeySequence.Quit) # 设置快捷键
exit_action.triggered.connect(self.close) # 该事件直接与close函数关联
self.file_menu.addAction(exit_action)
再添加状态栏
# 状态栏
self.status=self.statusBar()
self.status.showMessage("Data loaded and plotted")
以及设置窗口尺寸,直接通过self.screen().availableGeometry()
方法获取当前可用的窗口大小,我们将主窗体的大小设置为可用窗口大小的(0.8,0.7)。
# 窗口尺寸
geometry=self.screen().availableGeometry()
self.setFixedSize(geometry.width()*0.8,geometry.height()*0.7)
4️⃣ 添加控件
现在,我们需要添加一个表视图,用来显示数据。
我们可以建一个QTableView对象,并将其放置在QHBoxLayout中,并将其作为小部件传递给我们的主窗体。
值得注意的是,QTableView需要一个模型来显示信息。在这种情况下,可以使用QAbstractTableModel实例。
要子类化QAbstractTable,必须重新实现它的抽象方法rowCount()、columnCount()和data()。通过这种方式,可以确保正确地处理数据。此外,重新实现headerData()方法以向视图提供头部信息。
我们再新建一个文件,就叫做TableModel.py
好了。
这里我们实现了三个抽象方法。
from PySide6.QtCore import Qt,QAbstractTableModel,QModelIndex
from PySide6.QtGui import QColor
class CustomTableModel(QAbstractTableModel):
def __init__(self,data=None):
super(CustomTableModel, self).__init__()
self.load_data(data)
def load_data(self,data):
# 获取UTC日期
self.input_dates=data[0].values
# 获取震级
self.input_magnitudes=data[1].values
self.column_count=2
self.row_count=len(self.input_dates)
def rowCount(self, parent=QModelIndex()):
# parent需要获取Qt模型索引
return self.row_count
def columnCount(self, parent=QModelIndex()):
# parent需要获取Qt模型索引
return self.column_count
def headerData(self,section,orientation,role):
# 头文件信息
if role!=Qt.DisplayRole:
return None
if orientation==Qt.Horizontal:
return ("Date","Magnitude")[section]
else:
return f"{section}"
def data(self,index,role=Qt.DisplayRole):
# 需要实现的抽象方法
# index是索引位置,role是当前状态
column=index.column()
row=index.row()
if role==Qt.DisplayRole:
# 如果当前在显示
if column==0:
# 表示当前的位置为: row,0 理应返回date数据
# 返回str格式的数据,除了时区
date=self.input_dates[row].toPython()
return str(date)[:-3]
elif column==1:
# 返回震级数据
magnitude=self.input_magnitudes[row]
return f"{magnitude:.2f}"
elif role==Qt.BackgroundRole:
# 在背后的话就返回一个颜色
return QColor(Qt.white)
elif role==Qt.TextAlignmentRole:
return Qt.AlignRight
return None
接着就可以再构建我们自己的小控件啦,新建一个文件,叫做TableWidge
,将我们的模型导入:
from PySide6.QtWidgets import (QHBoxLayout,QHeaderView,QSizePolicy,QTableView,QWidget)
from Q003_TableModel import CustomTableModel # 我这里是写作Q003,去掉就行,换成上一个文件的名字
class Widget(QWidget):
def __init__(self,data):
super(Widget, self).__init__()
# 获取TableModel
self.model=CustomTableModel(data) # 基于TableModel的数据读取
# 创建表视图
self.table_view=QTableView()
self.table_view.setModel(self.model) # 设置模型
# 设置表视图的标头
self.horizontal_header=self.table_view.horizontalHeader()
self.vertical_header=self.table_view.verticalHeader()
self.horizontal_header.setSectionResizeMode(
QHeaderView.ResizeToContents
)# 依据内容自动设置尺寸
self.vertical_header.setSectionResizeMode(
QHeaderView.ResizeToContents
)
self.horizontal_header.setStretchLastSection(True) # 拉伸部件
# 设置布局
self.main_layout=QHBoxLayout()
size=QSizePolicy(QSizePolicy.Preferred,QSizePolicy.Preferred) # 默认sizeHint()为最优尺寸的策略
# 水平布局
size.setHorizontalStretch(1) # 设置水平拉伸因子为1
self.table_view.setSizePolicy(size) # 对组件应用尺寸策略
self.main_layout.addWidget(self.table_view) # mainlayout是一个水平布局盒子,将我们的组件加进来
# 将布局设置到QWidget中
self.setLayout(self.main_layout) # 最后,将我们的水平盒子设为小组件的布局
QSizePolicy 类是布局属性,描述了水平和垂直大小调整策略,部分参数如下:
参数名 | 作用 |
---|---|
Fixed | size固定为Qwidget.sizeHint() |
Minimum | size不能小于sizeHint()的大小 |
Maximum | size不能大于sizeHint()的大小 |
Preferred | 最佳size为sizeHint() |
Expanding | sizeHint是推荐的size,但尽可能地获取更大的空间 |
Ignored | sizeHint()被忽略,小部件将尽可能获取空间 |
这里我们用到了void setHorizontalStretch\setVerticalStretch (int stretchFactor)
方法,这个方法是用来设置大小策略的水平/垂直拉伸因子的,范围必须在[0,255]。举个栗子,当有两个部件水平相邻时,左边的部件拉伸系数为2,右边的拉深系数为1,那么将确保左边窗口的大小始终是右边的两倍。
好了,现在我们有一个TableView组件啦,将其加入主窗口。在MainWindow.py
文件中添加如下代码:
class MainWindow(QMainWindow):
def __init__(self,widget):
super(MainWindow, self).__init__()
self.setCentralWidget(widget)
在main.py
中添加:
from resource.OtherSupportPyFile.Q003_DataProcess import MainWindow
# 注意写成自己的路径和文件名
from resource.OtherSupportPyFile.Q003_Table import Widget
# 注意写成自己的路径和文件名
import sys
from PySide6.QtWidgets import QApplication
if __name__ == '__main__':
options=argparse.ArgumentParser()
options.add_argument("-f","--file",type=str,required=True)
args=options.parse_args()
data=read_data(args.file)
# 创建应用程序
app=QApplication()
widget=Widget(data)
window=MainWindow(widget)
window.show()
sys.exit(app.exec())
好了,最后的结果如下所示:
5️⃣ 添加并绘制图
有了表后,就要添加图了!我们在之前的TableWidge.py
上修改,基于Pyside6.QCharts
绘制图形。
首先是导入模块:
from PySide6.QtWidgets import (QHBoxLayout,QHeaderView,QSizePolicy,QTableView,QWidget)
from .Q003_TableModel import CustomTableModel
from PySide6.QtCharts import QChart,QChartView,QLineSeries,QDateTimeAxis,QValueAxis
from PySide6.QtGui import QPainter
from PySide6.QtCore import QDateTime,Qt
添加创建图的方法
# 创建QChart对象
self.chat=QChart()
self.chat.setAnimationOptions(QChart.AllAnimations) # 设置移动动画
# 创建表视图
self.chat_view=QChartView(self.chat)
self.chat_view.setRenderHint(QPainter.Antialiasing) # 抗锯齿
将图设置为右边,且大小是表的四倍
# 右布局
size.setHorizontalStretch(4)
self.chat_view.setSizePolicy(size)
self.main_layout.addWidget(self.chat_view)
我们创建一个添加序列的方法,用来为Chart读取数据~
def add_series(self,name):
# 创建线序列QLineSeries
self.series=QLineSeries()
self.series.setName(name)
# 填充QLineSeries
for i in range(self.model.row_count):
t=self.model.index(i,0).data()
data_fmt="yyyy-MM-dd HH:mm:ss.zzz"
x=QDateTime().fromString(t,data_fmt).toSecsSinceEpoch() # 转化为时间戳
y=float(self.model.index(i,1).data())
if x>0 and y>0:
self.series.append(x,y)
self.chat.addSeries(self.series)
# 设置图样式
# 设置x坐标
self.axis_x=QDateTimeAxis()
self.axis_x.setTickCount(10) # 设置间隔
# self.axis_x.setFormat("dd.MM (h:mm)") # 设置时间显示
self.axis_x.setFormat("MM.dd") # 设置时间显示
self.axis_x.setTitleText("Date")
self.chat.addAxis(self.axis_x,Qt.AlignBottom) # 在表格中加入坐标,位置为底部
self.series.attachAxis(self.axis_x) # 自动让QLineSeries贴附
# 设置y坐标
self.axis_y=QValueAxis()
self.axis_y.setTickCount(10)
self.axis_y.setLabelFormat("%.2f")
self.axis_y.setTitleText("Magnitude")
self.chat.addAxis(self.axis_y,Qt.AlignLeft)
self.series.attachAxis(self.axis_y)
# 从Chart上获取颜色,并在QTableView上使用
color_name=self.series.pen().color().name()
self.model.color=f"{color_name}"
将其在初始化方法中绑定:
self.add_series("Magnitude")
最终的结果如下!
完整代码附上:
1️⃣Q003_TableModel.py
from PySide6.QtCore import Qt,QAbstractTableModel,QModelIndex
from PySide6.QtGui import QColor
class CustomTableModel(QAbstractTableModel):
def __init__(self,data=None):
super(CustomTableModel, self).__init__()
self.load_data(data)
def load_data(self,data):
# 获取UTC日期
self.input_dates=data[0].values
# 获取震级
self.input_magnitudes=data[1].values
self.column_count=2
self.row_count=len(self.input_dates)
def rowCount(self, parent=QModelIndex()):
# parent需要获取Qt模型索引
return self.row_count
def columnCount(self, parent=QModelIndex()):
# parent需要获取Qt模型索引
return self.column_count
def headerData(self,section,orientation,role):
# 头文件信息
if role!=Qt.DisplayRole:
return None
if orientation==Qt.Horizontal:
return ("Date","Magnitude")[section]
else:
return f"{section}"
def data(self,index,role=Qt.DisplayRole):
# 需要实现的抽象方法
# index是索引位置,role是当前状态
column=index.column()
row=index.row()
if role==Qt.DisplayRole:
# 如果当前在显示
if column==0:
# 表示当前的位置为: row,0 理应返回date数据
# 返回str格式的数据,除了时区
date=self.input_dates[row].toPython()
return str(date)[:-3]
elif column==1:
# 返回震级数据
magnitude=self.input_magnitudes[row]
return f"{magnitude:.2f}"
elif role==Qt.BackgroundRole:
# 在背后的话就返回一个颜色
return QColor(Qt.white)
elif role==Qt.TextAlignmentRole:
return Qt.AlignRight
return None
2️⃣ Q003_DataProcess.py
from PySide6.QtGui import QAction,QKeySequence
from PySide6.QtWidgets import QMainWindow
class MainWindow(QMainWindow):
def __init__(self,widget):
super(MainWindow, self).__init__()
self.setWindowTitle("Earquakes Information")
self.setCentralWidget(widget)
# 设置菜单栏
self.menu=self.menuBar()
# 添加 文件 菜单项
self.file_menu=self.menu.addMenu("文件")
# 退出事件
exit_action = QAction("Exit", self) # 设置名字
exit_action.setShortcut(QKeySequence.Quit) # 设置快捷键
exit_action.triggered.connect(self.close) # 该事件直接与close函数关联
self.file_menu.addAction(exit_action)
# 状态栏
self.status=self.statusBar()
self.status.showMessage("Data loaded and plotted")
# 窗口尺寸
geometry=self.screen().availableGeometry()
self.setFixedSize(geometry.width()*0.8,geometry.height()*0.7)
3️⃣ Q003_Table.py
from PySide6.QtWidgets import (QHBoxLayout,QHeaderView,QSizePolicy,QTableView,QWidget)
from .Q003_TableModel import CustomTableModel
from PySide6.QtCharts import QChart,QChartView,QLineSeries,QDateTimeAxis,QValueAxis
from PySide6.QtGui import QPainter
from PySide6.QtCore import QDateTime,Qt
class Widget(QWidget):
def __init__(self,data):
super(Widget, self).__init__()
# 获取TableModel
self.model=CustomTableModel(data) # 基于TableModel的数据读取
# 创建表视图
self.table_view=QTableView()
self.table_view.setModel(self.model) # 设置模型
# 设置表视图的标头
self.horizontal_header=self.table_view.horizontalHeader()
self.vertical_header=self.table_view.verticalHeader()
self.horizontal_header.setSectionResizeMode(
QHeaderView.ResizeToContents
)# 依据内容自动设置尺寸
self.vertical_header.setSectionResizeMode(
QHeaderView.ResizeToContents
)
self.horizontal_header.setStretchLastSection(True) # 拉伸部件
# 创建QChart对象
self.chat=QChart()
self.chat.setAnimationOptions(QChart.AllAnimations) # 设置移动动画
self.add_series("Magnitude")
# 创建表视图
self.chat_view=QChartView(self.chat)
self.chat_view.setRenderHint(QPainter.Antialiasing) # 抗锯齿
# 设置布局
self.main_layout=QHBoxLayout()
size=QSizePolicy(QSizePolicy.Preferred,QSizePolicy.Preferred) # 默认sizeHint()为最优尺寸的策略
# 左布局
size.setHorizontalStretch(1) # 设置水平拉伸因子为1
self.table_view.setSizePolicy(size) # 对组件应用尺寸策略
self.main_layout.addWidget(self.table_view) # mainlayout是一个水平布局盒子,将我们的组件加进来
# 右布局
size.setHorizontalStretch(4)
self.chat_view.setSizePolicy(size)
self.main_layout.addWidget(self.chat_view)
# 将布局设置到QWidget中
self.setLayout(self.main_layout) # 最后,将我们的水平盒子设为小组件的布局
def add_series(self,name):
# 创建线序列QLineSeries
self.series=QLineSeries()
self.series.setName(name)
# 填充QLineSeries
for i in range(self.model.row_count):
t=self.model.index(i,0).data()
data_fmt="yyyy-MM-dd HH:mm:ss.zzz"
x=QDateTime().fromString(t,data_fmt).toSecsSinceEpoch() # 转化为时间戳
y=float(self.model.index(i,1).data())
if x>0 and y>0:
self.series.append(x,y)
self.chat.addSeries(self.series)
# 设置图样式
# 设置x坐标
self.axis_x=QDateTimeAxis()
self.axis_x.setTickCount(10) # 设置间隔
# self.axis_x.setFormat("dd.MM (h:mm)") # 设置时间显示
self.axis_x.setFormat("MM.dd") # 设置时间显示
self.axis_x.setTitleText("Date")
self.chat.addAxis(self.axis_x,Qt.AlignBottom) # 在表格中加入坐标,位置为底部
self.series.attachAxis(self.axis_x) # 自动让QLineSeries贴附
# 设置y坐标
self.axis_y=QValueAxis()
self.axis_y.setTickCount(10)
self.axis_y.setLabelFormat("%.2f")
self.axis_y.setTitleText("Magnitude")
self.chat.addAxis(self.axis_y,Qt.AlignLeft)
self.series.attachAxis(self.axis_y)
# 从Chart上获取颜色,并在QTableView上使用
color_name=self.series.pen().color().name()
self.model.color=f"{color_name}"
4️⃣ Main.py
import argparse
import pandas as pd
from PySide6.QtCore import QDateTime,QTimeZone
from PySide6.QtWidgets import QApplication
from resource.OtherSupportPyFile.Q003_DataProcess import MainWindow
from resource.OtherSupportPyFile.Q003_Table import Widget
import sys
def transform_date(utc,timezone=None):
# 日期转换
utc_fmt="yyyy-MM-ddTHH:mm:ss.zzzZ"
new_date=QDateTime().fromString(utc,utc_fmt)
if timezone:
new_date.setTimeZone(timezone)
return new_date
def read_data(file):
# 读取数据
data=pd.read_csv(file)
# 处理震级
data=data.drop(data[data['mag']<0].index)
magnitudes=data['mag']
# 时区设置
timezone = QTimeZone(QTimeZone.systemTimeZone()) # "Asia/Shanghai"
# 时间转换
times=data['time'].apply(lambda x:transform_date(x,timezone))
return times,magnitudes
if __name__ == '__main__':
options=argparse.ArgumentParser()
options.add_argument("-f","--file",type=str,required=True)
args=options.parse_args()
data=read_data(args.file)
# 创建应用程序
app=QApplication()
widget=Widget(data)
window=MainWindow(widget)
window.show()
sys.exit(app.exec())