本代码原来是PySide6官网的一个示例程序,我对其进行的详细的注释,同时增加了一个功能:加载显示cass的地形图坐标数据示例,示例可显示以下几种三维图形
程序运行界面如下:
代码如下:
# -*- coding: utf-8 -*-
from __future__ import annotations
import sys,time,copy,os,random
import numpy as np
from matplotlib.backends.backend_qtagg import FigureCanvas
from matplotlib.figure import Figure
from mpl_toolkits.mplot3d import axes3d
from PySide6.QtCore import Qt, Slot
from PySide6.QtGui import QAction, QKeySequence
from PySide6.QtWidgets import (QApplication, QComboBox, QHBoxLayout,
QHeaderView, QLabel, QMainWindow, QSlider,
QTableWidget, QTableWidgetItem, QVBoxLayout,
QWidget)
"""这是一个使用3Dmatplotlib plot 在Qt Widgets上显示三维图形的示例
原版为PySide6官网上的一示例代码,对其进行了详细注解,同时扩展增加了以下一组示例代码
1、从外部导入地形图数据(cass地形测绘数据格式)并进行显示地形三角网
"""
class ApplicationWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.column_names = ["X向", "Y向", "Z向"]
#定义中心窗口,并居中显示
self._main = QWidget()
self.setCentralWidget(self._main)
self.setWindowTitle('PySide6+matplotlib绘制三维图示例')
#定义菜单栏
self.menu = self.menuBar()
self.menu_file = self.menu.addMenu("文件")
exit = QAction("离开", self, triggered=qApp.quit) # noqa: F821
self.menu_file.addAction(exit)
self.menu_about = self.menu.addMenu("关于")
about = QAction("关于Qt", self, shortcut=QKeySequence(QKeySequence.HelpContents),
triggered=QApplication.aboutQt) # noqa: F821
self.menu_about.addAction(about)
#窗体左侧轮廓:为一视图和标签及滑条
self.fig = Figure(figsize=(5, 3))
self.canvas = FigureCanvas(self.fig)
#左侧滑条控件
min = 0
max = 360
self.slider_azim = QSlider(minimum=min, maximum=max, orientation=Qt.Orientation.Horizontal)
self.slider_elev = QSlider(minimum=min, maximum=max, orientation=Qt.Orientation.Horizontal)
self.slider_azim_layout = QHBoxLayout()
self.slider_azim_layout.addWidget(QLabel(f"{min}"))
self.slider_azim_layout.addWidget(self.slider_azim)
self.slider_azim_layout.addWidget(QLabel(f"{max}"))
self.slider_elev_layout = QHBoxLayout()
self.slider_elev_layout.addWidget(QLabel(f"{min}"))
self.slider_elev_layout.addWidget(self.slider_elev)
self.slider_elev_layout.addWidget(QLabel(f"{max}"))
#窗体右侧为一表格控件
self.table = QTableWidget()
header = self.table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
#窗体右侧顶部为一组合框控件
self.combo = QComboBox()
self.combo.addItems(["网格", "表面", "三角网表面", "球面","导入cass地形图","导入散点模型"])
#设置窗体右侧布局
rlayout = QVBoxLayout()
rlayout.setContentsMargins(1, 1, 1, 1)
rlayout.addWidget(QLabel("Plot类:"))
rlayout.addWidget(self.combo)
rlayout.addWidget(self.table)
#设置窗体左侧布局
llayout = QVBoxLayout()
rlayout.setContentsMargins(1, 1, 1, 1)
llayout.addWidget(self.canvas, 88)
llayout.addWidget(QLabel("方位角:"), 1)
llayout.addLayout(self.slider_azim_layout, 5)
llayout.addWidget(QLabel("高程:"), 1)
llayout.addLayout(self.slider_elev_layout, 5)
#窗体总布局采用竖向布局
layout = QHBoxLayout(self._main)
layout.addLayout(llayout, 70) #左侧布局占70%
layout.addLayout(rlayout, 30) #右侧布局点30%
#定义信号槽绑定
self.combo.currentTextChanged.connect(self.combo_option) #组合框选择项发生变化时
self.slider_azim.valueChanged.connect(self.rotate_azim) #方位角(XY平面)滑条控件值发生变化时
self.slider_elev.valueChanged.connect(self.rotate_elev) #方位角(Z向)滑条控件值发生变化时
#初始化视图显示
self.plot_wire() #默认加载
self._ax.view_init(30, 30)
self.slider_azim.setValue(30)
self.slider_elev.setValue(30)
self.fig.canvas.mpl_connect("button_release_event", self.on_click) #左侧画布视图单击后鼠标释放时信号
#槽函数: 单击Matplotlib视时发出的信号
def on_click(self, event):
azim, elev = self._ax.azim, self._ax.elev
self.slider_azim.setValue(azim + 180)
self.slider_elev.setValue(elev + 180)
# 设置表格中的数据:传入XYZ数据(数据是一个np一维数组)
def set_table_dataS(self, xS, yS, zS):
for i in range(len(xS)):
self.table.setItem(i, 0, QTableWidgetItem(f"{xS[i]:.2f}"))
self.table.setItem(i, 1, QTableWidgetItem(f"{yS[i]:.2f}"))
self.table.setItem(i, 2, QTableWidgetItem(f"{zS[i]:.2f}"))
#重绘视图和表格时,当前的XYZ数据集
def set_canvas_table_configuration(self, row_count, dataS):
self.fig.set_canvas(self.canvas)
self._ax = self.canvas.figure.add_subplot(projection="3d")
self._ax.set_xlabel(self.column_names[0]) #表格的0例数据全部导入到视图的的x
self._ax.set_ylabel(self.column_names[1]) #表格的0例数据全部导入到视图的的y
self._ax.set_zlabel(self.column_names[2]) #表格的0例数据全部导入到视图的的z
self.table.setRowCount(row_count)
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(self.column_names)
self.set_table_dataS(dataS[0], dataS[1], dataS[2])
# Plot 绘制样式:
#视图和表格中数据为四边形网(无表面)
def plot_wire(self):
#产生示例数据: np二维数组->(200,200)
self.X, self.Y, self.Z = axes3d.get_test_data(0.03) #从axes3d类中得到示例数据
self.set_canvas_table_configuration(len(self.X[0]), (self.X[0], self.Y[0], self.Z[0]))
self._ax.plot_wireframe(self.X, self.Y, self.Z, rstride=10, cstride=10, cmap="viridis")
self.canvas.draw()
#视图和表格中数据为漏斗型表面
def plot_surface(self):
#产生示例数据: np二维数组->(30,30)
self.X, self.Y = np.meshgrid(np.linspace(-6, 6, 30), np.linspace(-6, 6, 30))
self.Z = np.sin(np.sqrt(self.X ** 2 + self.Y ** 2))
self.set_canvas_table_configuration(len(self.X[0]), (self.X[0], self.Y[0], self.Z[0]))
self._ax.plot_surface(self.X, self.Y, self.Z,
rstride=1, cstride=1, cmap="viridis", edgecolor="none")
self.canvas.draw()
#视图和表格中数据为三角网表面
def plot_triangular_surface(self):
#产生示例数据: np一维数组->(289,)
radii = np.linspace(0.125, 1.0, 8)
angles = np.linspace(0, 2 * np.pi, 36, endpoint=False)[..., np.newaxis]
self.X = np.append(0, (radii * np.cos(angles)).flatten())
self.Y = np.append(0, (radii * np.sin(angles)).flatten())
self.Z = np.sin(-self.X * self.Y)
self.set_canvas_table_configuration(len(self.X), (self.X, self.Y, self.Z))
self._ax.plot_trisurf(self.X, self.Y, self.Z, linewidth=0.2, antialiased=True)
self.canvas.draw()
#视图和表格中数据为球面
def plot_sphere(self):
#产生示例数据: np二维数组->(100,100)
u = np.linspace(0, 2 * np.pi, 100)
v = np.linspace(0, np.pi, 100)
self.X = 10 * np.outer(np.cos(u), np.sin(v))
self.Y = 10 * np.outer(np.sin(u), np.sin(v))
self.Z = 9 * np.outer(np.ones(np.size(u)), np.cos(v))
self.set_canvas_table_configuration(len(self.X), (self.X[0], self.Y[0], self.Z[0])) #按当前球面的数据重新填写表格
self._ax.plot_surface(self.X, self.Y, self.Z)
self.canvas.draw()
#--------------------------------------------------------------------------------------------------------------------------------------------------------------
#自定义数据1:视图和表格中数据为从外部文件导入地形图数据: 本例导入CASS的抄测地形坐标文件
def plot_loadMapDatas(self):
#导入的示例数据: np二维数组->(526,5)
self.loadMapData('cass格式地形95个点.DAT')
self.cassXyz=self.cass_Datas[:,2:5]
self.X = self.cass_Datas[:,2:3].reshape(int(self.cass_Datas[:,2:3].size))
self.Y = self.cass_Datas[:,3:4].reshape(int(self.cass_Datas[:,3:4].size))
self.Z = self.cass_Datas[:,4:5].reshape(int(self.cass_Datas[:,4:5].size))
self.set_canvas_table_configuration(len(self.X), (self.X, self.Y, self.Z))
self._ax.plot_trisurf(self.X, self.Y, self.Z, linewidth=0.2, antialiased=True)
self.canvas.draw()
#自定义数据2:视图和表格中数据为从外部文件导入散点模形
def plot_load3DSapeDatas(self):
#产生示例数据: np二维数组->(n,8)
pass
self.canvas.draw()
#从外部地形数据文件导入全部点的坐标数据,示例采用CASS的dat格式数据 二维数组(n,5):如:1,,86240.8790,87568.3282,252.6600
#导入cass软件用的dat测绘数据
def loadMapData(self,mapDataFile,type=0):
s=''
lstRowData=[]
lstData=[]
index=0
#以下四个变量用于测试坐标点文件中是否存在差距较大的数,以提醒修改,如不修改,可能偏差点会对计算结果影响较大
minXYZ=[1000000,1000000,1000000]
maxXYZ=[0,0,0]
self.minXYZid=[0,0,0]
self.maxXYZid=[0,0,0]
if(os.path.exists(mapDataFile)):
rf = open(mapDataFile,'r',encoding='utf-8')
lindS=''
for lineS in rf.readlines():
lstRowData.clear()
s=lineS.strip()
s=s.replace("\n","") #将从文件中读出的\n删除,此语句可能会报异常
s=s.replace("\r","") #将从文件中读出的\r删除,此语句可能会报异常
if(s==''):continue #去空行
lstRowData = s.split(',')
if(len(lstRowData)!=5):continue #去格式不对的行(要求每行5个数据,4个逗号)
lstRowData[0]=index #重新为数据编号
if(len(str(lstRowData[1]))!=0):
lstRowData[1]=1 #对第二个参数不为空时,改其值为1,否则为0
else:
lstRowData[1]=0
lstRowData[2]=float(lstRowData[2]) #dat文件中的坐标x坐标
lstRowData[3]=float(lstRowData[3]) #dat文件中的坐标y坐标
lstRowData[4]=float(lstRowData[4]) #dat文件中的坐标z坐标
lstData.append(copy.deepcopy(lstRowData))
#以下对导入的点作最大最小值记录
if(minXYZ[0]>lstRowData[2]):
minXYZ[0]=lstRowData[2]
self.minXYZid[0]=index
if(minXYZ[1]>lstRowData[3]):
minXYZ[1]=lstRowData[3]
self.minXYZid[1]=index
if(minXYZ[2]>lstRowData[4]):
minXYZ[2]=lstRowData[4]
self.minXYZid[2]=index
if(maxXYZ[0]<lstRowData[2]):
maxXYZ[0]=lstRowData[2]
self.maxXYZid[0]=index
if(maxXYZ[1]<lstRowData[3]):
maxXYZ[1]=lstRowData[3]
self.maxXYZid[1]=index
if(maxXYZ[2]<lstRowData[4]):
maxXYZ[2]=lstRowData[4]
self.maxXYZid[2]=index
index+=1
rf.close
print(f'\n minXYZid={self.minXYZid}\n maxXYZid={self.maxXYZid}\n')
self.pointCount=len(lstData) #记录本数据文件的总坐标点数
self.cass_Datas=np.array(lstData) #保存全部的np原始数据
#开始处理原始数据
print(f'导入cass测绘数据文件"{mapDataFile}"成功!')
return True
return False
#-------------------------------------------------------------------------------------------------------------------------------------------------------------
#槽函数:组合框选择项发生变化时
@Slot()
def combo_option(self, text):
if text == "网格":
self.plot_wire()
elif text == "表面":
self.plot_surface()
elif text == "三角网表面":
self.plot_triangular_surface()
elif text == "球面":
self.plot_sphere()
elif text == "导入cass地形图":
self.plot_loadMapDatas()
elif text == "导入散点模型":
pass #暂未扩展
#方位角(XY平面)滑条控件值发生变化时槽函数
@Slot()
def rotate_azim(self, value):
self._ax.view_init(self._ax.elev, value)
self.fig.set_canvas(self.canvas)
self.canvas.draw()
#方位角(Z向)滑条控件值发生变化时槽函数
@Slot()
def rotate_elev(self, value):
self._ax.view_init(value, self._ax.azim)
self.fig.set_canvas(self.canvas)
self.canvas.draw()
if __name__ == "__main__":
app = QApplication(sys.argv)
w = ApplicationWindow()
w.setFixedSize(1280, 720)
w.show()
app.exec()
扩展导入的示例cass地形坐标文件内容如下,copy出来后粘贴到记事本另存文件为“cass格式地形95个点.DAT”,将此文件同代码模块文件放在一个目录里即可
1,,8535.0620,20831.2885,384.9640
2,,8564.7031,20818.7979,386.8800
3,,8566.4578,20824.8254,386.8800
4,,8580.2215,20819.2465,388.0300
5,,8590.5382,20815.9465,388.7660
6,,8525.9910,20830.1208,377.0900
7,,8502.7606,20839.6356,383.2100
8,,8537.2997,20855.6420,385.0000
9,,8537.6544,20857.5221,385.0000
10,,8531.2665,20856.1611,385.3500
11,,8534.0532,20857.8461,385.3500
12,,8533.6219,20853.4993,385.3500
13,,8595.2543,20819.8627,391.3100
14,,8558.8369,20813.6480,387.1910
15,,8576.1126,20813.9809,388.4000
16,,8582.0086,20813.3326,388.7660
17,,8574.6472,20807.6877,388.2800
18,,8578.2663,20799.6209,387.8800
19,,8581.3061,20806.9918,388.7000
20,,8544.7344,20858.6430,384.0600
21,,8546.2683,20821.1758,385.8000
22,,8550.3724,20833.8913,385.5660
23,,8544.0034,20833.0925,385.4460
24,,8597.5370,20808.7561,390.1870
25,,8588.3263,20809.5540,389.2140
26,,8581.5961,20810.1927,388.7600
27,,8575.4039,20810.7972,388.3400
28,,8569.2116,20813.0327,387.9020
29,,8560.1202,20816.6382,387.2510
30,,8548.2657,20828.3883,385.8600
31,,8547.2163,20833.4220,385.5060
32,,8546.3340,20840.2102,385.0240
33,,8545.5363,20848.8394,384.7000
34,,8553.8038,20819.7866,386.7750
35,,8550.7684,20823.7166,386.2300
36,,8544.1572,20863.7326,383.6830
37,,8543.6784,20868.8701,383.3000
38,,8529.1556,20863.1078,383.3000
39,,8533.9251,20864.9365,383.3000
40,,8538.7670,20866.7411,383.3000
41,,8548.0837,20870.6865,383.3000
42,,8540.6612,20862.3642,383.6830
43,,8536.5205,20861.3426,383.6830
44,,8531.3321,20860.6948,383.6830
45,,8541.7397,20859.0886,384.0600
46,,8538.8243,20859.4973,384.0600
47,,8528.7124,20850.9021,384.1650
48,,8521.5594,20849.2209,384.1650
49,,8511.9518,20846.3489,384.1650
50,,8531.5137,20846.2046,384.6400
51,,8537.3263,20847.9737,384.6400
52,,8533.9232,20839.3649,384.9640
53,,8540.2409,20839.6848,384.9640
54,,8540.3997,20832.9546,385.4460
55,,8526.7023,20856.3875,384.0600
56,,8520.6090,20855.1941,384.0600
57,,8512.6042,20852.9863,384.0600
58,,8503.9422,20850.4801,384.0600
59,,8499.1817,20849.5675,384.0600
60,,8527.0635,20859.6211,383.6830
61,,8520.2142,20858.3600,383.6830
62,,8511.2339,20856.1466,383.6830
63,,8503.0271,20854.0863,383.6830
64,,8495.8691,20853.3912,383.6830
65,,8585.2618,20790.1975,391.2700
66,,8583.4519,20792.3716,389.6900
67,,8526.2071,20833.0721,377.0900
68,,8511.7583,20834.1661,377.2200
69,,8501.9803,20833.0331,377.2200
70,,8485.7633,20837.8191,377.2800
71,,8480.4063,20841.6261,377.4100
72,,8483.0683,20846.3021,378.1000
73,,8596.9664,20786.3285,391.8000
74,,8587.9307,20791.0655,390.0370
75,,8586.8727,20802.9864,389.1540
76,,8596.9759,20800.4602,390.0300
77,,8565.5924,20800.8019,386.6600
78,,8521.5125,20838.1220,380.1400
79,,8487.6261,20847.4016,378.7800
80,,8497.7541,20845.0826,378.7100
81,,8494.2471,20850.5446,381.6400
82,,8514.0502,20841.4980,383.2100
83,,8524.2421,20843.4686,384.2000
84,,8541.1481,20826.6746,384.7300
85,,8561.0611,20806.2766,386.6300
86,,8547.6462,20816.0442,386.0600
87,,8557.0848,20809.5790,387.1910
88,,8566.6440,20805.8867,387.8420
89,,8580.9124,20803.9858,388.7000
90,,8558.3059,20823.0354,386.8800
91,,8570.3499,20819.9284,388.0300
92,,8587.7689,20820.1814,389.6800
93,,8571.6399,20826.1884,388.0000
94,,8556.9299,20831.2744,386.4100
95,,8550.9689,20844.2314,385.2700