一、项目介绍
本项目将通过PySide6构建一个可以显示数据折线图的可视化程序,其中,数据来源时美国地质调查局(US Geological Survey)上公开的一小时地震震级数据。
可以通过链接 进行下载。
二、实现步骤
本项目的实现步骤可以概括为:
读取数据
数据处理
创建主窗口
添加控件
绘制图形并显示、
预期结果如下图所示:
三、实现
1️⃣ 读取数据
这里我们借助Pandas对CSV文件进行读取。argparse模块主要用于参数控制,具体可见这篇文章 。
创建一个新文档main.py,接下来都是一些简单的操作,就不做赘述了。
1 2 3 4 5 6 7 8 9 10 11 12 import argparseimport pandas as pddef 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中将其导入:
1 from PySide6.QtCore import QDateTime,QTimeZone
接着,通过QtDateTime().fromString( time , format )进行转换:
1 2 3 4 5 6 7 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
我们对读取数据方法进行一定的修改,首先是移除错误的震级数据:
1 2 3 data=pd.read_csv(file) data=data.drop(data[data['mag' ]<0 ].index) magnitudes=data['mag' ]
然后,设置本地的时区:
1 2 3 4 5 timezone = QTimeZone(QTimeZone.systemTimeZone()) times=data['time' ].apply(lambda x:transform_date(x,timezone)) return times,magnitudes
虽然不该在这篇文档中提,但还是提一嘴:
Pandas快速对某一类操作:
1 Series.apply(lambda x:func(x))
好了,此时我们的read_data方法应该长这样:
1 2 3 4 5 6 7 8 9 10 11 def read_data (file ): data=pd.read_csv(file) data=data.drop(data[data['mag' ]<0 ].index) magnitudes=data['mag' ] timezone = QTimeZone(QTimeZone.systemTimeZone()) times=data['time' ].apply(lambda x:transform_date(x,timezone)) return times,magnitudes
3️⃣ 创建主窗体
好啦,终于要进入我们的核心啦,这一步我们将创建一个PySide主窗口。
下图是QMainWindow的布局。
在本项目中,我们需要一个“文件”菜单,用来打开文件对话框,和一个“退出”菜单。应用程序启动时,应该要自动加载状态栏。
我们新建一个文件,叫做MainWindow.py。
1 2 3 4 5 6 7 8 from PySide6.QtCore import Slotfrom PySide6.QtGui import QAction,QKeySequencefrom PySide6.QtWidgets import QMainWindowclass MainWindow (QMainWindow ): def __init__ (self ): super (MainWindow, self).__init__() pass
让我们的窗体控件继承自QMainWindow。
然后是菜单栏的设置,直接选择获取self.menuBar()即可,通过addMenu(Str)的方式,可以添加选项。
1 2 3 4 self.menu=self.menuBar() self.file_menu=self.menu.addMenu("文件" )
然后我们为菜单栏添加一个退出事件。这个事件可以通过QAction来直接绑定。QAction("Exit",self)表示退出本窗体。
1 2 3 4 5 6 exit_action=QAction("Exit" ,self) exit_action.setShortcut(QKeySequence.Quit) exit_action.triggered.connect(self.close) self.file_menu.addAction(exit_action)
再添加状态栏
1 2 3 self.status=self.statusBar() self.status.showMessage("Data loaded and plotted" )
以及设置窗口尺寸,直接通过self.screen().availableGeometry()方法获取当前可用的窗口大小,我们将主窗体的大小设置为可用窗口大小的(0.8,0.7)。
1 2 3 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好了。
这里我们实现了三个抽象方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 from PySide6.QtCore import Qt,QAbstractTableModel,QModelIndexfrom PySide6.QtGui import QColorclass CustomTableModel (QAbstractTableModel ): def __init__ (self,data=None ): super (CustomTableModel, self).__init__() self.load_data(data) def load_data (self,data ): 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( ) ): return self.row_count def columnCount (self, parent=QModelIndex( ) ): 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 ): column=index.column() row=index.row() if role==Qt.DisplayRole: if column==0 : date=self.input_dates[row].toPython() return str (date)[:-3 ] elif column==1 : magnitude=self.input_magnitudes[row] return f"{magnitude:.2 f} " elif role==Qt.BackgroundRole: return QColor(Qt.white) elif role==Qt.TextAlignmentRole: return Qt.AlignRight return None
接着就可以再构建我们自己的小控件啦,新建一个文件,叫做TableWidge,将我们的模型导入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 from PySide6.QtWidgets import (QHBoxLayout,QHeaderView,QSizePolicy,QTableView,QWidget)from Q003_TableModel import CustomTableModel class Widget (QWidget ): def __init__ (self,data ): super (Widget, self).__init__() self.model=CustomTableModel(data) 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) size.setHorizontalStretch(1 ) self.table_view.setSizePolicy(size) self.main_layout.addWidget(self.table_view) 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文件中添加如下代码:
1 2 3 4 class MainWindow (QMainWindow ): def __init__ (self,widget ): super (MainWindow, self).__init__() self.setCentralWidget(widget)
在main.py中添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from resource.OtherSupportPyFile.Q003_DataProcess import MainWindowfrom resource.OtherSupportPyFile.Q003_Table import Widgetimport sys from PySide6.QtWidgets import QApplicationif __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绘制图形。
首先是导入模块:
1 2 3 4 5 from PySide6.QtWidgets import (QHBoxLayout,QHeaderView,QSizePolicy,QTableView,QWidget)from .Q003_TableModel import CustomTableModelfrom PySide6.QtCharts import QChart,QChartView,QLineSeries,QDateTimeAxis,QValueAxisfrom PySide6.QtGui import QPainterfrom PySide6.QtCore import QDateTime,Qt
添加创建图的方法
1 2 3 4 5 6 7 self.chat=QChart() self.chat.setAnimationOptions(QChart.AllAnimations) self.chat_view=QChartView(self.chat) self.chat_view.setRenderHint(QPainter.Antialiasing)
将图设置为右边,且大小是表的四倍
1 2 3 4 size.setHorizontalStretch(4 ) self.chat_view.setSizePolicy(size) self.main_layout.addWidget(self.chat_view)
我们创建一个添加序列的方法,用来为Chart读取数据~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 def add_series (self,name ): self.series=QLineSeries() self.series.setName(name) 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) self.axis_x=QDateTimeAxis() self.axis_x.setTickCount(10 ) 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) 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) color_name=self.series.pen().color().name() self.model.color=f"{color_name} "
将其在初始化方法中绑定:
1 self.add_series("Magnitude" )
最终的结果如下!
完整代码附上:
1️⃣Q003_TableModel.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 from PySide6.QtCore import Qt,QAbstractTableModel,QModelIndexfrom PySide6.QtGui import QColorclass CustomTableModel (QAbstractTableModel ): def __init__ (self,data=None ): super (CustomTableModel, self).__init__() self.load_data(data) def load_data (self,data ): 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( ) ): return self.row_count def columnCount (self, parent=QModelIndex( ) ): 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 ): column=index.column() row=index.row() if role==Qt.DisplayRole: if column==0 : date=self.input_dates[row].toPython() return str (date)[:-3 ] elif column==1 : magnitude=self.input_magnitudes[row] return f"{magnitude:.2 f} " elif role==Qt.BackgroundRole: return QColor(Qt.white) elif role==Qt.TextAlignmentRole: return Qt.AlignRight return None
2️⃣ Q003_DataProcess.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from PySide6.QtGui import QAction,QKeySequencefrom PySide6.QtWidgets import QMainWindowclass 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) 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 from PySide6.QtWidgets import (QHBoxLayout,QHeaderView,QSizePolicy,QTableView,QWidget)from .Q003_TableModel import CustomTableModelfrom PySide6.QtCharts import QChart,QChartView,QLineSeries,QDateTimeAxis,QValueAxisfrom PySide6.QtGui import QPainterfrom PySide6.QtCore import QDateTime,Qtclass Widget (QWidget ): def __init__ (self,data ): super (Widget, self).__init__() self.model=CustomTableModel(data) 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.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) size.setHorizontalStretch(1 ) self.table_view.setSizePolicy(size) self.main_layout.addWidget(self.table_view) size.setHorizontalStretch(4 ) self.chat_view.setSizePolicy(size) self.main_layout.addWidget(self.chat_view) self.setLayout(self.main_layout) def add_series (self,name ): self.series=QLineSeries() self.series.setName(name) 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) self.axis_x=QDateTimeAxis() self.axis_x.setTickCount(10 ) 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) 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) color_name=self.series.pen().color().name() self.model.color=f"{color_name} "
4️⃣ Main.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import argparseimport pandas as pdfrom PySide6.QtCore import QDateTime,QTimeZonefrom PySide6.QtWidgets import QApplicationfrom resource.OtherSupportPyFile.Q003_DataProcess import MainWindowfrom resource.OtherSupportPyFile.Q003_Table import Widgetimport sysdef 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()) 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 ())