第10章 Data Visualization
Data Visualization是Qt提供的用于数据三维显示的模块。在Qt 5.7以前只有商业版才有此模块,而从Qt 5.7开始此模块在社区版本里也可以免费使用了。Data Visualization用于数据的三维显示,包括三维柱状图、三维空间散点、三维曲面等。Data Visualization与Qt Charts类似,也是基于Qt的图形视图架构。Data Visualization的功能无法和一些专业的三维显示类库相提并论,但是对于一些简单的三维数据显示是比较实用的,例如一些科学计算结果的三维显示。
本章介绍Data Visualization模块的一些主要功能的使用,主要是三维柱状图、三维空间散点和三维曲面的显示。
10.1 Data Visualization模块概述
Data Visualization的三维显示功能主要由3种三维图形类来实现,分别是三维柱状图类Q3DBars,三维空间散点类Q3DScatter,三维曲面类Q3DSurface。这3个类的父类是QAbstract3 DGraph,是从QWindow继承而来的,继承关系如原书的图10-1所示。
Data Visualization与Qt Charts类似,都基于Qt的图形视图结构,所以一个三维图形也是由图表、序列、坐标轴等元素组成的。Q3DBars、Q3DScatter、Q3DSurface相当于Qt Charts中的QChart,而每一种三维图形对应一种三维序列,Data Visualization中的3种序列类见原书的图10-2。
一种序列类只能用于某种三维图形类,如QBar3DSeries只能用作三维柱状图Q3DBars的序列,而不能作为三维散点图Q3DScatter的序列。在一个图中可以有多个同类型的序列,如三维曲面图Q3DSurface中可以有多个QSurface3DSeries序列,用于显示不同的曲面。
与QChart有坐标轴类一样,三维图形也有坐标轴类。有两种三维坐标轴类,QValue3DAxis用于数值型坐标轴,QCategory3DAxis用于文字型坐标轴,它们都继承自QAbstract3DAxis(如原书的图10-3所示)。
Data Visualization模块中有数据代理(data proxy)类,数据代理类就是与序列对应,用于存储序列的数据的类。因为三维图形类型不一样,存储数据的结构也不一样,例如三维散点序列QScatter3DSeries存储的是一些三维数据点的坐标,只需要用一维数组或列表就可以存储这些数据,而QSurface3DSeries序列存储的数据点在水平面上是均匀网格分布的,需要二维数组才可以存储相应的数据。为此,对于每一种序列,都有一个数据代理类,它们都继承自QAbstractDataProxy,每个数据代理类还有一个基于项数据模型的数据代理子类(如原书的图10-4所示)。
对于三维曲面序列QSurface3DSeries,还有一个专门用于显示地图高程数据的数据代理类QHeightMapSurfaceDataProxy,可以将一个图片表示的高程数据显示为三维曲面。用户也可以根据需要从QAbstractDataProxy继承,定义自己的数据代理类。
要在项目中使用Data Visualization模块,需要在项目配置文件中添加下面一行语句:
Qt += datavisualization
在使用Data Visualization模块中的类的头文件或源程序文件中,还需要加入下面两行语句。如果只需使用模块中的部分类,可以单独包含某些类。
#include <QtDataVisualization>
using namespace QtDataVisualization;
10.2 三维柱状图
10.2.1 实例功能
通过一个三维柱状图的实例来说明使用Data Visualization的类绘制三维图形的基本原理。实例samp10_1使用Q3DBars绘制一个三维柱状图,并在界面上对其一些常见属性和操作进行控制,程序运行效果如原书的图10-5所示。
实例samp10_1运行时,右侧是用Q3DBars绘制的三维柱状图;左侧是对三维柱状图的一些操作和属性设置,包括预设视角的选择,水平、垂直方向的旋转以及缩放,属性设置包括图表主题、选择方式、棒图项的样式,以及一些显示效果的设置。
无需额外编程或设置,程序运行时在图表单击左键可以选择图表中的某个项、某个列或行(与选择方式的设置有关),按住鼠标右键时上下、左右移动可以进行水平和垂直方向的旋转,鼠标滚轮可以进行缩放。
10.2.2 主窗口设计
实例samp10_1是主窗口继承自QMainWindow的应用程序,可视化创建主窗体。
由于三维图形类Q3DBars、Q3DScatter和Q3DSurface都是从QWindow继承而来的(见图10-1),不能简单使用QWidget组件作为Q3DBars组件的容器,也就是不能在主窗口上放置一个QWidget组件,然后作为Q3DBars组件的容器,而是需要使用QWidget::createWindowContainer()动态创建QWidget作为Q3DBars的容器。
在UI设计器中设计主窗体的界面如原书的图10-6所示,只设计用于三维图表属性控制的界面组件,用一个QGroupBox组件容纳所有这些三维属性控制界面组件,将这个QGroupBox组件命名为groupBox。为groupBox内的组件设计好布局,删除主窗口的工具栏、菜单栏和状态栏。窗口右侧区域是空白,是为动态创建Q3DBars的QWidget容器预留的空间。
主窗口类的头文件mainwindow.h中的定义如下:
#include <QMainWindow>
#include <QtDataVisualization>
using namespace QtDataVisualization;
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QWidget *graphContainer; //图表的容器
Q3DBars *graph3D; //图表
QBar3DSeries *series; //图表的序列
void iniGraph3D(); //创建图表
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
Ui::MainWindow *ui;
};
在MainWindow类中,定义了私有变量graphContainer作为三维图表的容器,它在MainWindow的构造函数中创建。还定义三维图表和序列的私有变量graph3D和series,是为了在程序里便于直接引用。
iniGraph3D()函数用于初始化创建图表,在构造函数里调用。
10.2.3 三维柱状图的创建
在主窗口的构造函数里创建界面和三维图表,MainWindow的构造函数代码如下:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
ui->setupUi(this);
iniGraph3D(); //创建三维图表
QSplitter *splitter=new QSplitter(Qt::Horizontal);
splitter->addWidget(ui->groupBox); //左侧控制面板
splitter->addWidget(graphContainer); //右侧图表
this->setCentralWidget(splitter);
}
构造函数里首先调用iniGraph3D()创建三维图表及其容器graphContainer,然后创建一个分隔条组件splitter,将可视化设计的控制面板groupBox与动态创建的图表容器graphContainer添加到分隔条里,作为主窗口的中心组件。
这样在运行时就获得原书的图10-1的界面,界面具体自动填充效果,并且可以分割左侧的控制面板和右侧的图表。
iniGraph3D()函数创建图表,其实现代码如下:
void MainWindow::iniGraph3D()
{//创建图表
graph3D = new Q3DBars();
graphContainer = QWidget::createWindowContainer(graph3D);
//创建坐标轴
QStringList rowLabs;
QStringList colLabs;
rowLabs << "row1" << "row2"<<"row3";
colLabs << "col1" << "col2" << "col3" << "col4"<<"col5";
QValue3DAxis *axisV=new QValue3DAxis; //数值坐标轴
axisV->setTitle("value");
axisV->setTitleVisible(true);
QCategory3DAxis *axisRow=new QCategory3DAxis; //行坐标轴
axisRow->setTitle("row axis");
axisRow->setLabels(rowLabs);
axisRow->setTitleVisible(true);
QCategory3DAxis *axisCol=new QCategory3DAxis; //列坐标轴
axisCol->setTitle("column axis");
axisCol->setLabels(colLabs);
axisCol->setTitleVisible(true);
graph3D->setValueAxis(axisV);
graph3D->setRowAxis(axisRow);
graph3D->setColumnAxis(axisCol);
//创建序列
series = new QBar3DSeries;
series->setMesh(QAbstract3DSeries::MeshCylinder); //棒图形状
series->setItemLabelFormat("(@rowLabel,@colLabel): %.1f"); //标签格式
graph3D->addSeries(series);
//添加数据
QBarDataArray *dataSet = new QBarDataArray; //数据数组
dataSet->reserve(rowLabs.count());
QBarDataRow *dataRow = new QBarDataRow;
*dataRow << 1 << 2<< 3<< 4<<5; //第1行数据,有5列
dataSet->append(dataRow);
QBarDataRow *dataRow2 = new QBarDataRow;
*dataRow2 << 5<< 5<< 5<< 5<<5; //第2行数据,有5列
dataSet->append(dataRow2);
QBarDataRow *dataRow3 = new QBarDataRow;
*dataRow3 << 1<< 5<< 9<< 5<<1; //第3行数据,有5列
dataSet->append(dataRow3);
series->dataProxy()->resetArray(dataSet);
}
程序首先创建Q3DBars类的实例graph3D,然后创建一个QWidget实例graphContainer作为其容器。创建graphContainer时使用了QWidget的静态函数createWindowContainer(),这样创建的QWidget实例才可以作为QWindow及其子类组件的容器。
三维柱状图的水平面的两个轴采用QCategory3DAxis类型的坐标轴,是文字型标签坐标轴,分别为其设置轴标题和轴标签。垂直方向表示项的数值大小,所以使用QValue3DAxis类型的坐标轴。创建坐标轴后,分别设置为柱状图行坐标轴、列坐标轴和数值坐标轴,即:
graph3D->setValueAxis(axisV);
graph3D->setRowAxis(axisRow);
graph3D->setColumnAxis(axisCol);
与Q3DBars对应使用的序列是QBar3DSeries类,创建序列实例对象series,设置棒图性质和标签格式后添加到图表。
然后创建QBarDataArray数组dataSet,其元素个数等于三维柱状图的行数,也就是dataSet的每个元素是一个行的数据。
一个行的数据是QBarDataRow类型,例如,创建第1行的数据并添加到柱状图的数据数组里的代码如下:
QBarDataRow *dataRow = new QBarDataRow;
*dataRow << 1 << 2<< 3<< 4<<5; //第1行数据,有5列
dataSet->append(dataRow);
实际上,QBarDataRow和QBarDataArray都不是类,而是类型定义,在Qt的源代码中,它们的定义如下:
typedef QVector<QBarDataItem> QBarDataRow;
typedef QList<QBarDataRow *> QBarDataArray;
所以,QBarDataRow是QBarDataItem的向量,QBarDataArray是QBarDataRow的列表。数据的基本元素是QBarDataItem,也就是柱状图中的一个项,QBarDataItem只有value和rotation两个属性,用于存储项的数值和旋转角度。
在原书的图10-5中,柱状图的数据有3行5列,添加数据时,每次添加一行的数据,但是属于一个序列。
设置完数据后,为序列的数据代理设置数据,语句如下:
series->dataProxy()->resetArray(dataSet);
程序里没有为序列先创建QBarDataProxy类型的数据代理,然后再添加到序列里。调用series->dataProxy()使用内部缺省的数据代理,resetArray(dataSet)将清除数据代理原有的数据,并使用dataSet作为数据源。
10.2.4 三维柱状图属性设置
原书的图10-5左侧的控制面板用于图表的操作和一些常用属性的设置。
1.预设视角
3种三维图表的父类QAbstract3DGraph有一个函数scene()可以获得图表的场景,是一个Q3DScene类。Q3DScene是三维图的场景,包含一个相机(camera)和一个光源,还提供一个3D主视口(viewport)和两个2D副视口(subviewport),其中次级2D副视口用于显示二维切片图。
场景的相机位置就是我们看图表的视角,当旋转一个图表时,实际上就是相机位置的变化,Qt为相机提供了一些预设的视角,由枚举类型Q3DCamera::CameraPreset定义,它有二十多种取值,其中几种是:
-
Q3DCamera::CameraPresetFrontLow,前下方;
-
Q3DCamera::CameraPresetFront,正前方;
-
Q3DCamera::CameraPresetFrontHigh,前上方;
-
Q3DCamera::CameraPresetLeft,左侧。
在控制面板的“预设视角”的下拉列表框中列出了所有视角取值,为currentIndexChanged()信号编写槽函数代码如下:
void MainWindow::on_comboCamera_currentIndexChanged(int index)
{ //变换视角
Q3DCamera::CameraPreset cameraPos=Q3DCamera::CameraPreset(index);
graph3D->scene()->activeCamera()->setCameraPreset(cameraPos);
}
通过graph3D->scene()->activeCamera()获取图表视图当前的相机,是一个Q3DCamera类型的对象,然后调用其setCameraPreset()函数设置预设的视角。因为列表框中的选项是完全按照枚举类型Q3DCamera::CameraPreset的取值顺序排列的,所以,只需将其序号转换为Q3DCamera::Camera Preset类型即可。
2.旋转和缩放
在程序运行时,在图表上滚动鼠标滚轮就可以缩放,按住鼠标右键上下左右移动可以对图表进行三维旋转。图表的旋转和缩放是通过改变图表场景的相机的位置和缩放系数来实现的。
控制面板上有3个QSlider组件分别用于水平旋转、垂直旋转和缩放。3个QSlider组件的valueChanged(int value)信号的响应代码相同,代码如下:
void MainWindow::on_sliderH_valueChanged(int value)
{
Q_UNUSED(value);
int xRot =ui->sliderH->value();//水平
int yRot=ui->sliderV->value(); //垂直
int zoom=ui->sliderZoom->value(); //缩放
graph3D->scene()->activeCamera()->setCameraPosition(xRot, yRot,zoom);
}
Q3DCamera类的setCameraPosition()函数用于设置相机的位置和缩放系数,setCameraPosition()函数的原型定义是:
void Q3DCamera::setCameraPosition(float horizontal, float vertical, float zoom = 100.0f)
其中,horizontal是水平旋转角度,在-180°至+180°之间取值,所以sliderH的取值范围设置为-180至+180;vertical是垂直方向旋转角度,在0至90°之间取值,所以sliderV的取值范围设置为0至90;zoom是缩放系数,缺省值为100,表示无缩放,sliderZoom的取值范围设置为10至500,小于100是缩小,大于100是放大。
setCameraPosition()函数同时定义了相机的位置和缩放,所以3个QSlider组件的valueChanged(int value)信号响应代码相同。
3.主题
与QChart一样,QAbstract3DGraph类可以设置主题,主题定义了图表的各种颜色和外观设置。通过activeTheme()可以获取图表当前设置的主题,通过setActiveTheme()函数可以设置新的主题。
用于三维图表的主题类是Q3DTheme,其type()属性表示主题类型。主题类型是一个枚举类型Q3DTheme::Theme,有多种取值,与QChart的主题的枚举类型的取值类似。
控制面板上的“主题”下拉列表框中列出了所有可选的主题,为其currentIndexChanged(int index)信号编写响应槽函数,代码如下:
void MainWindow::on_cBoxTheme_currentIndexChanged(int index)
{//设置主题
Q3DTheme *currentTheme = graph3D->activeTheme();
currentTheme->setType(Q3DTheme::Theme(index));
}
图表的一些显示属性的设置是通过设置activeTheme()主题的属性来实现的,例如控制面板上的“显示背景”复选框的代码如下:
void MainWindow::on_chkBoxBackground_clicked(bool checked)
{//图表的背景
graph3D->activeTheme()->setBackgroundEnabled(checked);
}
“显示背景网格”复选框的代码如下:
void MainWindow::on_chkBoxGrid_clicked(bool checked)
{ //图表的网格
graph3D->activeTheme()->setGridEnabled(checked);
}
“坐标轴标签背景可见”的复选框的代码如下:
void MainWindow::on_chkBoxAxisBackground_clicked(bool checked)
{//轴标签背景
graph3D->activeTheme()->setLabelBackgroundEnabled(checked);
}
4.选择模式
“选择模式”指的是鼠标在图表上单击时,项被选择的模式,缺省为选择一个项。QAbstract3DGraph类的setSelectionMode()函数用于设置选择模式,选择模式是枚举类型QAbstract3DGraph::SelectionFlag,其取值常量见表10-1。
表10-1 QAbstract3DGraph::SelectionFlag枚举类型取值
| SelectionNone | 不允许选择 |
| SelectionItem | 选择并且高亮度显示一个项 |
| SelectionRow | 选择并且高亮度显示一行 |
| SelectionItemAndRow | 选择一个项和一行,用不同颜色高亮显示 |
| SelectionColumn | 选择并且高亮度显示一列 |
| SelectionItemAndColumn | 选择一个项和一列,用不同颜色高亮显示 |
| SelectionRowAndColumn | 选择交叉的一行和一列 |
| SelectionItemRowAndColumn | 选择交叉的一行和一列,用不同颜色高亮显示 |
| SelectionSlice | 切片选择,需要与SelectionRow或SelectionColumn结合使用 |
| SelectionMultiSeries | 选中同一个位置处的多个序列的项 |
控制面板的“选择模式”下拉列表框中列出了除SelectionMultiSeries之外的其他所有选择模式,其currentIndexChanged(int index)信号的槽函数代码如下:
void MainWindow::on_cBoxSelectionMode_currentIndexChanged(int index)
{//选择模式
graph3D->setSelectionMode(QAbstract3DGraph::SelectionFlags(index));
}
5.序列相关的设置
Q3DBars只能显示QBar3DSeries序列,QBar3DSeries、QScatter3Dseries和QSurface3DSeries都继承自QAbstract3DSeries(见原书的图10-2)。
序列的设置主要是对其一些显示属性的设置,QAbstract3DSeries类的主要函数见表10-2(省略了函数参数中的const关键字)。一个设置函数一般对应一个读取函数,如setBaseColor()用于设置序列基本颜色,对应的读取序列基本颜色的函数是baseColor(),表中仅列出设置函数。
表10-2 QAbstract3DSeries类的主要功能函数
| void setBaseColor(QColor &color) | 设置序列基本颜色 |
| void setBaseGradient(QLinearGradient &gradient) | 设置序列渐变色 |
| void setColorStyle(Q3DTheme::ColorStyle style) | 设置序列颜色类型 |
| void setMesh(Mesh mesh) | 设置棒图项的样式,参数是QAbstract3DSeries::Mesh枚举类型,定义棒图是棱柱、圆柱、圆锥形等 |
| void setMeshSmooth(bool enable) | 设置棒图是否有光滑效果 |
| void setMeshRotation(QQuaternion &rotation) | 设置所有项的旋转角度 |
| void setItemLabelVisible(bool visible) | 设置项的标签是否可见,若为True,则选中一个项时会显示其标签 |
| void setName(QString &name) | 设置序列的名称,标记符号“@seriesName”可用于项的标签格式定义中 |
| void setVisible(bool visible) | 设置序列是否可见 |
| void setItemLabelFormat(QString &format) | 设置选中项的标签文字格式 |
窗口控制面板中的“棒图样式”下拉列表框列出了QAbstract3DSeries::Mesh枚举类型的各种取值,用于设置棒图的样式,如:
-
QAbstract3DSeries::MeshBar,棱柱;
-
QAbstract3DSeries::MeshCylinder,圆柱;
-
QAbstract3DSeries::MeshSphere,椭圆。
还有一些其他取值,其详细描述见Qt的帮助文件。
“棒图样式”下拉列表框的currentIndexChanged(int index)信号的槽函数代码如下:
void MainWindow::on_cBoxBarStyle_currentIndexChanged(int index)
{ //棒图的样式
QAbstract3DSeries::Mesh aMesh;
aMesh=QAbstract3DSeries::Mesh(index+1);
series->setMesh(aMesh);
}
“项的标签可见”复选框的响应代码如下:
void MainWindow::on_chkBoxItemLabel_clicked(bool checked)
{ //项的标签是否可见
series->setItemLabelFormat("value at (@rowLabel,@colLabel): %.1f");
series->setItemLabelVisible(checked);
}
这里使用了setItemLabelFormat()函数设置项的标签显示文字的格式,在格式设置中,可以使用一些标记符号,见表10-3。
表10-3 QBar3DSeries的setItemLabelFormat()函数可用的标记符号
| @rowTitle | 行坐标轴的标题 |
| @colTitle | 列坐标轴的标题 |
| @valueTitle | 数值坐标轴的标题 |
| @rowIdx | 可见的行索引号 |
| @colIdx | 可见的列索引号 |
| @rowLabel | 项所在的行坐标的文字标签 |
| @colLabel | 项所在的列坐标的文字标签 |
| @valueLabel | 项的数值,显示格式与QValue3DAxis::labelFormat 格式相同 |
| @seriesName | 序列名称 |
| % | 指定的数值显示格式,格式规则与QValue3DAxis::labelFormat相同 |
6.坐标轴反向
在原书的图10-5的三维柱状图中,垂直方向是数值坐标轴,缺省的方向是向上为正。如果实际情况需要数值坐标轴反向,可以调用QValue3DAxis::setReversed()函数将坐标轴设置为反向。控制面板上的“数值坐标轴反向”复选框的槽函数代码如下:
void MainWindow::on_chkBoxReverse_clicked(bool checked)
{//数值轴反向
graph3D->valueAxis()->setReversed(checked);
}
10.3 三维散点图
10.3.1 绘制三维散点图
绘制三维散点图采用Q3DScatter类,绘图序列是QScatter3DSeries类。实例samp10_2演示如何绘制三维散点图,实例运行界面如原书的图10-7所示,绘制了一个“墨西哥草帽”的散点图。
程序的界面与实例samp10_1类似,左侧是控制面板,右侧是绘图区,项目的创建和界面元素的布局也相同。
主窗口类MainWindow的主要定义如下(省略了界面组件的槽函数定义):
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QWidget *graphContainer;//图表的容器
Q3DScatter *graph3D; //散点图
QScatter3DSeries *series; //散点序列
void iniGraph3D(); //初始化绘图
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
Ui::MainWindow *ui;
};
在其中定义了绘制三维散点图的Q3DScatter类图表对象graph3D,以及绘图序列QScatter3 DSeries类的散点序列series。
iniGraph3D()函数用于图表的创建,在构造函数里完成图表的创建。下面是主窗口构造函数和iniGraph3D()函数的代码。
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
iniGraph3D();
QSplitter *splitter=new QSplitter(Qt::Horizontal);
splitter->addWidget(ui->groupBox);
splitter->addWidget(graphContainer);
this->setCentralWidget(splitter);
}
void MainWindow::iniGraph3D()
{
graph3D = new Q3DScatter();
graphContainer = QWidget::createWindowContainer(graph3D);
QScatterDataProxy *proxy = new QScatterDataProxy(); //数据代理
series = new QScatter3DSeries(proxy); //创建序列
series->setItemLabelFormat("(@xLabel @yLabel @zLabel)");
series->setMeshSmooth(true);
graph3D->addSeries(series);
//创建坐标轴
graph3D->axisX()->setTitle("axis X");
graph3D->axisX()->setTitleVisible(true);
graph3D->axisY()->setTitle("axis Y");
graph3D->axisY()->setTitleVisible(true);
graph3D->axisZ()->setTitle("axis Z");
graph3D->axisZ()->setTitleVisible(true);
graph3D->activeTheme()->setLabelBackgroundEnabled(false);
series->setMesh(QAbstract3DSeries::MeshSphere); //数据点为圆球
series->setItemSize(0.2);//取值范围 0~1
int N=41;
int itemCount=N*N;
QScatterDataArray *dataArray = new QScatterDataArray();
dataArray->resize(itemCount);
QScatterDataItem *ptrToDataArray = &dataArray->first();
//墨西哥草帽,-10:0.5:10, N=41
float x,y,z;
int i,j;
x=-10;
for (i=1 ; i <=N; i++)
{
y=-10;
for ( j =1; j <=N; j++)
{
z=qSqrt(x*x+y*y);
if (z!=0)
z=10*qSin(z)/z;
else
z=10;
ptrToDataArray->setPosition(QVector3D(x,z,y));
ptrToDataArray++;
y+=0.5;
}
x+=0.5;
}
series->dataProxy()->resetArray(dataArray);
}
在iniGraph3D()函数中,程序显式地创建了数据代理组件,并在序列创建时将此数据代理传递给序列。
QScatterDataProxy *proxy = new QScatterDataProxy(); //数据代理
series = new QScatter3DSeries(proxy); //创建序列
每一种序列都有其对应的数据代理类,与散点序列对应的数据代理类是QScatterDataProxy。
散点序列的每一个点都是一个QScatterDataItem类,它存储一个散点的三维坐标和旋转角度信息。QScatterDataArray是QScatterDataItem类的向量的类型定义,定义如下:
QVector<QScatterDataItem> QScatterDataArray;
所以,QScatterDataArray实际就是一个一维数组,数组的元素是QScatterDataItem对象。创建QScatterDataArray对象dataArray,并预设其大小。
QScatterDataArray *dataArray = new QScatterDataArray();
dataArray->resize(itemCount);
程序中根据数学公式计算“墨西哥草帽”每个散点的坐标,并且保存到数组dataArray里,最后将此数组设置为数据代理的数据内容,即:
series->dataProxy()->resetArray(dataArray);
注意:在此实例中,“墨西哥草帽”的数据点在水平面上是均匀分布的,所以相当于在水平面上做了网格划分,每个网格里面都有一个数据点。但是散点图并不要求数据点规则分布,它只是根据每个散点的三维坐标和旋转方向绘图。
10.3.2 三维坐标轴的方向
由于三维散点图是根据数据点的三维坐标绘图,所以3个坐标轴都是QValue3DAxis类型的坐标轴,在iniGraph3D()函数中,为3个坐标轴标注了轴标题,在绘制出来的三维图形中会发现,从正前方看所绘制的三维图时,3个坐标轴的正向分布如原书的图10-8所示。
在此坐标系中,东西方向为X轴,南北方向为Z轴,且向北方向为正,垂直方向为Y轴,向上为正。所以,在iniGraph3D()函数中,计算墨西哥草帽的散点坐标时采用的是笛卡尔坐标,水平面是X和Y,垂直方向是Z,所以计算出的(x, y, z)坐标添加作为散点的坐标时,使用的语句如下:
ptrToDataArray->setPosition(QVector3D(x,z,y));
10.3.3 散点形状与大小
散点形状使用QAbstract3DSeries类的setMesh()函数进行设置,设置的参数类型是枚举类型QAbstract3DSeries::Mesh。这个函数在柱状图的设置中,是设置每个项的形状;在散点图中用于设置每个散点的形状。
散点的大小由QScatter3DSeries类的函数setItemSize(float size)进行设置,参数size是0至1之间的小数,表示散点的相对大小,图表会根据数值自动调整散点的大小。
控制面板上其他的设置与柱状图实例samp10_1中的一样,这里不再详述。
10.4 三维曲面绘图
10.4.1 三维曲面图
1.绘制三维曲面图
绘制三维曲面使用Q3DSurface图表类和QSurface3DSeries序列,根据使用的数据代理类的不同,可以绘制两种三维曲面图。
-
QSurfaceDataProxy数据代理类根据空间点的三维坐标绘制曲面,如一般的三维函数曲面。
-
QHeightMapSurfaceDataProxy数据代理类根据一个图片的数据绘制三维曲面,典型的如三维地形图。
原书的图10-9是实例samp10_3使用QSurfaceDataProxy数据代理绘制普通三维曲面图的运行界面,左侧的控制面板的控制功能与前述实例基本相同,只是增加了3个颜色设置的按钮。
下面是主窗口类的定义的主要部分(省略了界面组件的槽函数定义):
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QWidget *graphContainer;
Q3DSurface *graph3D; //三维图表
QSurface3DSeries *series; //序列
QSurfaceDataProxy *proxy; //数据代理
void iniGraph3D();
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
Ui::MainWindow *ui;
};
MainWindow类定义了Q3DSurface类的变量graph3D,QSurface3DSeries类的序列变量series,QSurfaceDataProxy类的数据代理变量proxy。iniGraph3D()函数用于图表初始化绘制,在构造函数里调用此函数。
下面是MainWindow类的构造函数,在构造函数里为控制面板上的两个设置曲面渐变颜色的按钮绘制了渐变图片。QLinearGradient类的使用见8.1节,这里不再赘述。
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
ui->setupUi(this);
//渐变颜色按钮
QLinearGradient grBtoY(0, 0, 100, 0); //线性渐变
grBtoY.setColorAt(1.0, Qt::black);
grBtoY.setColorAt(0.67, Qt::blue);
grBtoY.setColorAt(0.33, Qt::red);
grBtoY.setColorAt(0.0, Qt::yellow);
QPixmap pm(160, 20); //图片
QPainter pmp(&pm);
pmp.setBrush(QBrush(grBtoY)); //渐变颜色1
pmp.setPen(Qt::NoPen);
pmp.drawRect(0, 0, 160, 20);
ui->btnGrad1->setIcon(QIcon(pm)); //渐变颜色按钮1
ui->btnGrad1->setIconSize(QSize(160, 20));
QLinearGradient grGtoR(0, 0, 100, 0); //渐变颜色2
grGtoR.setColorAt(1.0, Qt::darkGreen);
grGtoR.setColorAt(0.5, Qt::yellow);
grGtoR.setColorAt(0.2, Qt::red);
grGtoR.setColorAt(0.0, Qt::darkRed);
pmp.setBrush(QBrush(grGtoR));
pmp.drawRect(0, 0, 160, 20);
ui->btnGrad2->setIcon(QIcon(pm)); //渐变颜色按钮2
ui->btnGrad2->setIconSize(QSize(160, 20));
iniGraph3D();
QSplitter *splitter=new QSplitter(Qt::Horizontal);
splitter->addWidget(ui->groupBox);
splitter->addWidget(graphContainer);
this->setCentralWidget(splitter);
}
构造函数里调用iniGraph3D()函数进行图形绘制,下面是iniGraph3D()函数的完整代码。
void MainWindow::iniGraph3D()
{
graph3D = new Q3DSurface();
graphContainer = QWidget::createWindowContainer(graph3D);
//创建坐标轴
QValue3DAxis *axisX=new QValue3DAxis;
axisX->setTitle("Axis X");
axisX->setTitleVisible(true);
axisX->setRange(-11,11);
graph3D->setAxisX(axisX);
QValue3DAxis *axisY=new QValue3DAxis;
axisY->setTitle("Axis Y");
axisY->setTitleVisible(true);
axisY->setAutoAdjustRange(true);
graph3D->setAxisY(axisY);
QValue3DAxis *axisZ=new QValue3DAxis;
axisZ->setTitle("Axis Z");
axisZ->setTitleVisible(true);
axisZ->setRange(-11,11);
graph3D->setAxisZ(axisZ);
//创建数据代理
proxy = new QSurfaceDataProxy();
series = new QSurface3DSeries(proxy);
graph3D->addSeries(series);
series->setItemLabelFormat("(@xLabel @yLabel @zLabel)");
series->setMeshSmooth(true);
graph3D->activeTheme()->setLabelBackgroundEnabled(false);
series->setDrawMode(QSurface3DSeries::DrawSurfaceAndWireframe);
//创建数据, 墨西哥草帽
int N=41;//-10:0.5:10, N个数据点
QSurfaceDataArray *dataArray = new QSurfaceDataArray; //数组
dataArray->reserve(N);
float x=-10,y,z;
int i,j;
for (i =1 ; i <=N; i++)
{
QSurfaceDataRow *newRow = new QSurfaceDataRow(N); //一行的数据
y=-10;
int index=0;
for (j = 1; j <=N; j++)
{
z=qSqrt(x*x+y*y);
if (z!=0)
z=10*qSin(z)/z;
else
z=10;
(*newRow)[index++].setPosition(QVector3D(x, z, y));
y=y+0.5;
}
x=x+0.5;
*dataArray << newRow;
}
proxy->resetArray(dataArray);
}
代码首先创建三维图表graph3D,并为其创建窗口容器,然后创建了3个QValue3DAxis类型的坐标轴对象,设置基本属性后作为图表的相应坐标轴。
程序创建QSurfaceDataProxy类型的数据代理变量proxy,并在创建序列时作为参数传递给它,然后将序列添加到图表。
下面的代码对序列做了一些属性的设置,特别设置了绘图模式为曲面加线网的模式,即:
series->setDrawMode(QSurface3DSeries::DrawSurfaceAndWireframe);
然后是创建数据,采用与实例samp10_2相同的“墨西哥草帽”函数生成数据点。QsurfaceData Proxy数据代理采用两层一维数组的模式存储三维曲面的数据,与三维柱状图的数据代理类存储数据的模式类似。
这里创建一个QSurfaceDataArray类型的向量dataArray,它的元素个数等于行数,每一行又是一个QSurfaceDataRow类型的列表,用于存储一行中多列的数据。QSurfaceDataArray和QSurface DataRow是类型定义,其定义为:
typedef QVector<QSurfaceDataItem> QSurfaceDataRow;
typedef QList<QSurfaceDataRow *> QSurfaceDataArray;
每个基本的数据点都是QSurfaceDataItem类,有x、y、z 3个坐标,通过setPosition(const QVector3D &pos)函数可以设置一个点的三维坐标。同样,Q3DSurface的三维坐标系是原书的图10-8的样式,与在数学中常见的坐标轴的名称不一致,在设置数据点时需要注意。
2.曲面样式
QSurface3DSeries的函数setDrawMode(DrawFlags mode)用于设置曲面样式,参数是枚举类型QSurface3DSeries::DrawFlag的组合,取值有以下几种:
-
QSurface3DSeries::DrawWireframe,只绘制线网;
-
QSurface3DSeries::DrawSurface,只绘制曲面;
-
QSurface3DSeries::DrawSurfaceAndWireframe,绘制线网和曲面。
原书的图10-9是设置为QSurface3DSeries::DrawSurfaceAndWireframe时的绘图效果。
3.曲面的单一颜色设置
在改变图表的主题时,曲面的颜色会根据主题的设置而改变,也可以单独改变曲面的颜色。控制面板中的“曲面颜色”按钮用于设置曲面颜色,代码如下:
void MainWindow::on_btnBarColor_clicked()
{ //单一曲面颜色
QColor color=series->baseColor();
color=QColorDialog::getColor(color);
if (color.isValid())
{
series->setBaseColor(color);
series->setColorStyle(Q3DTheme::ColorStyleUniform);
}
}
对序列调用setBaseColor()函数设置颜色,并且调用setColorStyle()函数将序列颜色样式设置为单一颜色。setColorStyle()设置的参数是枚举类型Q3DTheme::ColorStyle,其取值有以下几种:
-
Q3DTheme::ColorStyleUniform,单一颜色;
-
Q3DTheme::ColorStyleObjectGradient,不考虑对象高度,只根据对象渐变;
-
Q3DTheme::ColorStyleRangeGradient,根据对象高度渐变。
4.曲面的渐变颜色设置
曲面还可以设置为渐变颜色,例如Q3DTheme::ColorStyleRangeGradient渐变类型,即根据数据点的高度赋予不同的颜色。控制面板中“渐变色1”按钮的响应代码如下:
void MainWindow::on_btnGrad1_clicked()
{//渐变颜色
QLinearGradient gr;
gr.setColorAt(0.0, Qt::black);
gr.setColorAt(0.33, Qt::blue);
gr.setColorAt(0.67, Qt::red);
gr.setColorAt(1.0, Qt::yellow);
series->setBaseGradient(gr);
series->setColorStyle(Q3DTheme::ColorStyleRangeGradient);
}
程序定义一个QLinearGradient线性渐变类的变量,定义了渐变颜色属性后,调用序列的setBaseGradient()函数设置渐变颜色,并设置颜色样式为Q3DTheme::ColorStyleRangeGradient。
10.4.2 三维地形图
1.绘制三维地形图
当为QSurface3DSeries序列使用QHeightMapSurfaceDataProxy数据代理时,数据代理可以从一个含有高程数据的图片中读入数据,绘制地形图。原书的图10-10是实例samp10_4运行界面,右侧的三维曲面是一个地形图。
实例samp10_4的程序结构与实例samp10_3相同,只是数据代理类定义为QHeightMapSurface DataProxy类,下面是MainWindow类的定义部分。
class MainWindow : public QMainWindow
{ Q_OBJECT
private:
QWidget *graphContainer;
Q3DSurface *graph3D; //三维图表
QSurface3DSeries *series; //序列
QHeightMapSurfaceDataProxy *proxy; //数据代理
void iniGraph3D();
public:
explicit MainWindow(QWidget *parent = 0);
};
iniGraph3D()函数实现绘图,其完整代码如下:
void MainWindow::iniGraph3D()
{
graph3D = new Q3DSurface();
graphContainer = QWidget::createWindowContainer(graph3D);
//创建坐标轴
QValue3DAxis *axisX=new QValue3DAxis; //X,东西
axisX->setTitle("东–西");
axisX->setTitleVisible(true);
axisX->setLabelFormat("%.1f 米");
axisX->setRange(-5000,5000);
graph3D->setAxisX(axisX);
QValue3DAxis *axisY=new QValue3DAxis; //Y,深度
axisY->setTitle("深度");
axisY->setTitleVisible(true);
axisY->setAutoAdjustRange(true);
graph3D->setAxisY(axisY);
QValue3DAxis *axisZ=new QValue3DAxis; //Z, 南北
axisZ->setTitle("南–北");
axisZ->setLabelFormat("%.1f 米");
axisZ->setTitleVisible(true);
axisZ->setRange(-5000,5000);
graph3D->setAxisZ(axisZ);
graph3D->activeTheme()->setLabelBackgroundEnabled(false);
//创建数据代理
QImage heightMapImage(":/map/sea.png");
proxy = new QHeightMapSurfaceDataProxy(heightMapImage);
proxy->setValueRanges(-5000, 5000, -5000, 5000);
series = new QSurface3DSeries(proxy);
series->setItemLabelFormat("(@xLabel, @zLabel): @yLabel");
series->setFlatShadingEnabled(false);
series->setMeshSmooth(true);
series->setDrawMode(QSurface3DSeries::DrawSurface);
graph3D->addSeries(series);
}
在项目中创建资源文件,并且导入一个高程图片文件sea.png,原始图片如原书的图10-11所示,图片的各个点的颜色与高程有关。
该图片用于创建QHeightMapSurfaceDataProxy数据代理,并且导入图片的数据。
QImage heightMapImage(":/map/sea.png");
proxy = new QHeightMapSurfaceDataProxy(heightMapImage);
proxy->setValueRanges(-5000, 5000, -5000, 5000);
QHeightMapSurfaceDataProxy支持多种图片文件格式,可以是彩色图也可以是灰度图。由于图片信息不包含平面坐标范围,所以需要使用setValueRanges()设置图片数据的平面坐标范围,其函数原型如下:
void setValueRanges(float minX, float maxX, float minZ, float maxZ)
高程数据从图片中读取,如果是灰度图,就将像素的颜色值作为高度,如果是彩色图,就以红、绿、蓝3种颜色的平均值作为高度值。使用灰度图处理速度更快,所以,最好使用QImage::Format_RGB32格式的32位灰度图片。
2.切片选择模式
使用QAbstract3DGraph类的setSelectionMode(SelectionFlags mode)设置选择模式时,可以设置为切片选择模式。控制面板上的“选择模式”组合选择框提供了None、Item、Row Slice和Column Slice 4种选择模式,组合选择框的响应代码如下:
void MainWindow::on_cBoxSelectionMode_currentIndexChanged(int index)
{//选择模式
switch(index)
{ case 0: //none
graph3D->setSelectionMode(QAbstract3DGraph::SelectionNone);
break;
case 1: //Item
graph3D->setSelectionMode(QAbstract3DGraph::SelectionItem);
break;
case 2: //Row Slice
graph3D->setSelectionMode(QAbstract3DGraph::SelectionItemAndRow
| QAbstract3DGraph::SelectionSlice);
break;
case 3: //Column Slice
graph3D->setSelectionMode(QAbstract3DGraph::SelectionItemAndColumn
| QAbstract3DGraph::SelectionSlice);
break;
}
}
当设置为Row Slice或Column Slice选择模式时,在三维图上单击一个位置后,图表会自动切换到切片显示模式,如原书的图10-12所示。
在此状态下,中间显示选择的项所在行或列的切片图,左上角显示缩小的三维图。在左上角的三维图上再单击一下,就会恢复原始的显示状态。
在Q3DSurface图中,可以显示多个曲面或地形图,还可以通过addCustomItem(QCustom3DItem *item)函数添加自定义三维对象,QCustom3DItem类对象可以是由其他三维建模软件建立的三维对象,由此可以显示较复杂的三维场景。


