グラフギャラリー
棒グラフ、散布図、面グラフのギャラリー。
グラフ・ギャラリーは、3つのグラフ・タイプすべてと、それらの特別な機能のいくつかを示しています。グラフはアプリケーション内にそれぞれのタブを持っています。
サンプルを実行する
Qt Creator からサンプルを実行するには、Welcome モードを開き、Examples からサンプルを選択します。詳細については、Building and Running an Example を参照してください。
棒グラフ
Bar Graph タブで、Q3DBars を使用して 3D 棒グラフを作成し、ウィジェットの使用を組み合わせて、棒グラフの様々な品質を調整します。この例では、次の方法を示します:
- Q3DBars といくつかのウィジェットでアプリケーションを作成します。
- QBar3DSeries とQBarDataProxy を使って、グラフにデータを設定する。
- ウィジェット・コントロールを使って、グラフと系列のプロパティを調整する。
- 軸ラベルをクリックして行または列を選択する
- で使用するカスタムプロキシを作成する。Q3DBars
グラフとの対話については、このページを参照。
アプリケーションの作成
まず、bargraph.cpp
で、Q3DBars をインスタンス化します:
m_barsGraph = new Q3DBars();
次に、ウィジェットを作成し、水平および垂直レイアウトを作成します。
グラフは、QWidget::createWindowContainer()を使ってウィンドウ・コンテナに埋め込まれる。これは、すべてのデータ可視化グラフ・クラス (Q3DBars,Q3DScatter,Q3DSurface) がQWindow を継承しているためです。これは、QWindow を継承するクラスをウィジェットとして使用する唯一の方法です。
グラフと垂直レイアウトを水平レイアウトに追加します:
m_barsWidget = new QWidget; auto *hLayout = new QHBoxLayout(m_barsWidget); m_container = QWidget::createWindowContainer(m_barsGraph, m_barsWidget); m_barsGraph->resize(minimumGraphSize); m_container->setMinimumSize(minimumGraphSize); m_container->setMaximumSize(maximumGraphSize); m_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_container->setFocusPolicy(Qt::StrongFocus); hLayout->addWidget(m_container, 1); auto *vLayout = new QVBoxLayout(); hLayout->addLayout(vLayout);
次に、データの追加やグラフとのその他のインタラクションを処理する別のクラスを作成します:
auto *modifier = new GraphModifier(m_barsGraph, this);
棒グラフのセットアップ
GraphModifier
クラスのコンストラクタでグラフを設定する:
GraphModifier::GraphModifier(Q3DBars *bargraph, QObject *parent) : QObject(parent), m_graph(bargraph),
まず、軸と系列をメンバ変数に作成し、簡単に変更できるようにする:
m_temperatureAxis(new QValue3DAxis), m_yearAxis(new QCategory3DAxis), m_monthAxis(new QCategory3DAxis), m_primarySeries(new QBar3DSeries), m_secondarySeries(new QBar3DSeries), m_celsiusString(u"°C"_s)
次に、グラフの視覚的性質を設定します:
m_graph->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftMedium); m_graph->activeTheme()->setBackgroundEnabled(false); m_graph->activeTheme()->setFont(QFont("Times New Roman", m_fontSize)); m_graph->activeTheme()->setLabelBackgroundEnabled(true); m_graph->setMultiSeriesUniform(true);
軸を設定し、グラフのアクティブ軸にする:
m_temperatureAxis->setTitle("Average temperature"); m_temperatureAxis->setSegmentCount(m_segments); m_temperatureAxis->setSubSegmentCount(m_subSegments); m_temperatureAxis->setRange(m_minval, m_maxval); m_temperatureAxis->setLabelFormat(u"%.1f "_s + m_celsiusString); m_temperatureAxis->setLabelAutoRotation(30.0f); m_temperatureAxis->setTitleVisible(true); m_yearAxis->setTitle("Year"); m_yearAxis->setLabelAutoRotation(30.0f); m_yearAxis->setTitleVisible(true); m_monthAxis->setTitle("Month"); m_monthAxis->setLabelAutoRotation(30.0f); m_monthAxis->setTitleVisible(true); m_graph->setValueAxis(m_temperatureAxis); m_graph->setRowAxis(m_yearAxis); m_graph->setColumnAxis(m_monthAxis);
setLabelAutoRotation()を使って軸ラベルに小さな自動回転角度を与え、カメラの方を少し向くようにする。これにより、極端なカメラ・アングルでの軸ラベルの読みやすさが向上します。
次に、系列の視覚的プロパティを初期化します。2番目の系列は最初は表示されないことに注意してください:
m_primarySeries->setItemLabelFormat(u"Oulu - @colLabel @rowLabel: @valueLabel"_s); m_primarySeries->setMesh(QAbstract3DSeries::MeshBevelBar); m_primarySeries->setMeshSmooth(false); m_secondarySeries->setItemLabelFormat(u"Helsinki - @colLabel @rowLabel: @valueLabel"_s); m_secondarySeries->setMesh(QAbstract3DSeries::MeshBevelBar); m_secondarySeries->setMeshSmooth(false); m_secondarySeries->setVisible(false);
系列をグラフに追加します:
m_graph->addSeries(m_primarySeries); m_graph->addSeries(m_secondarySeries);
最後に、UIのカメラ・アングル変更ボタンが様々なカメラ・アングルを循環するために使用するのと同じメソッドを呼び出して、カメラ・アングルを設定します:
changePresetCamera();
カメラはグラフのシーン・オブジェクトで制御されます:
static int preset = Q3DCamera::CameraPresetFront; m_graph->scene()->activeCamera()->setCameraPreset((Q3DCamera::CameraPreset)preset); if (++preset > Q3DCamera::CameraPresetDirectlyBelow) preset = Q3DCamera::CameraPresetFrontLow;
シーンとカメラの使用についての詳細は、Q3DScene とQ3DCamera を参照。
グラフへのデータ追加
コンストラクタの最後に、データをセットアップするメソッドを呼び出します:
resetTemperatureData();
このメソッドは、2つのシリーズのプロキシにデータを追加します:
// Set up data static const float tempOulu[8][12] = { {-7.4f, -2.4f, 0.0f, 3.0f, 8.2f, 11.6f, 14.7f, 15.4f, 11.4f, 4.2f, 2.1f, -2.3f}, // 2015 {-13.4f, -3.9f, -1.8f, 3.1f, 10.6f, 13.7f, 17.8f, 13.6f, 10.7f, 3.5f, -3.1f, -4.2f}, // 2016 ... auto *dataSet = new QBarDataArray; auto *dataSet2 = new QBarDataArray; dataSet->reserve(m_years.size()); for (qsizetype year = 0; year < m_years.size(); ++year) { // Create a data row auto *dataRow = new QBarDataRow(m_months.size()); auto *dataRow2 = new QBarDataRow(m_months.size()); for (qsizetype month = 0; month < m_months.size(); ++month) { // Add data to the row (*dataRow)[month].setValue(tempOulu[year][month]); (*dataRow2)[month].setValue(tempHelsinki[year][month]); } // Add the row to the set dataSet->append(dataRow); dataSet2->append(dataRow2); } // Add data to the data proxy (the data proxy assumes ownership of it) m_primarySeries->dataProxy()->resetArray(dataSet, m_years, m_months); m_secondarySeries->dataProxy()->resetArray(dataSet2, m_years, m_months);
ウィジェットを使ってグラフを制御する
続けて、bargraph.cpp
にウィジェットを追加します。スライダーを追加します:
auto *rotationSliderX = new QSlider(Qt::Horizontal, m_barsWidget); rotationSliderX->setTickInterval(30); rotationSliderX->setTickPosition(QSlider::TicksBelow); rotationSliderX->setMinimum(-180); rotationSliderX->setValue(0); rotationSliderX->setMaximum(180);
マウスやタッチを使う代わりに、スライダーを使ってグラフを回転させる。垂直レイアウトに追加します:
vLayout->addWidget(new QLabel(u"Rotate horizontally"_s)); vLayout->addWidget(rotationSliderX, 0, Qt::AlignTop);
そして、GraphModifier
のメソッドに接続する:
GraphModifier
、信号接続用のスロットを作成します。カメラはシーンオブジェクトでコントロールします。今回は、プリセットのカメラアングルを指定する代わりに、中心点を中心とした軌道に沿った実際のカメラ位置を指定します:
void GraphModifier::rotateX(int rotation) { m_xRotation = rotation; m_graph->scene()->activeCamera()->setCameraPosition(m_xRotation, m_yRotation); }
スライダーを使ってグラフを回転できるようになりました。
垂直レイアウトにさらにウィジェットを追加してコントロールします:
- グラフの回転
- ラベルスタイル
- カメラのプリセット
- 背景の可視性
- グリッドの可視性
- バーの陰影の滑らかさ
- 2本目のバーの可視性
- 値軸の方向
- 軸タイトルの可視性と回転
- 表示するデータ範囲
- バーのスタイル
- 選択モード
- テーマ
- 影の質
- フォント
- フォントサイズ
- 軸ラベル回転
- データモード
Custom Proxy Data データ・モードでは、いくつかのウィジェット・コントロールが意図的に無効になります。
軸ラベルのクリックによる行または列の選択
軸ラベルによる選択は、棒グラフのデフォルト機能です。例として、以下の方法で軸ラベルをクリックして行を選択できる:
- 選択モードを
SelectionRow
- 年ラベルをクリック
- クリックした年の行が選択される。
SelectionRow
、SelectionColumn
のどちらかが設定されていれば、SelectionSlice
、SelectionItem
のフラグでも同じ方法が使える。
選択範囲へのズーム
カメラのターゲットを調整する例として、ボタンを押すことで選択範囲にズームするアニメーションを実装します。アニメーションの初期化はコンストラクタで行います:
Q3DCamera *camera = m_graph->scene()->activeCamera(); m_defaultAngleX = camera->xRotation(); m_defaultAngleY = camera->yRotation(); m_defaultZoom = camera->zoomLevel(); m_defaultTarget = camera->target(); m_animationCameraX.setTargetObject(camera); m_animationCameraY.setTargetObject(camera); m_animationCameraZoom.setTargetObject(camera); m_animationCameraTarget.setTargetObject(camera); m_animationCameraX.setPropertyName("xRotation"); m_animationCameraY.setPropertyName("yRotation"); m_animationCameraZoom.setPropertyName("zoomLevel"); m_animationCameraTarget.setPropertyName("target"); int duration = 1700; m_animationCameraX.setDuration(duration); m_animationCameraY.setDuration(duration); m_animationCameraZoom.setDuration(duration); m_animationCameraTarget.setDuration(duration); // The zoom always first zooms out above the graph and then zooms in qreal zoomOutFraction = 0.3; m_animationCameraX.setKeyValueAt(zoomOutFraction, QVariant::fromValue(0.0f)); m_animationCameraY.setKeyValueAt(zoomOutFraction, QVariant::fromValue(90.0f)); m_animationCameraZoom.setKeyValueAt(zoomOutFraction, QVariant::fromValue(50.0f)); m_animationCameraTarget.setKeyValueAt(zoomOutFraction, QVariant::fromValue(QVector3D(0.0f, 0.0f, 0.0f)));
GraphModifier::zoomToSelectedBar()
QPropertyAnimation m_animationCameraTarget
ターゲット プロパティは、範囲 (-1, 1) に正規化された値をとります。Q3DCamera::target
選択されたバーが軸に対してどの位置にあるかを把握し、それをm_animationCameraTarget
の終了値として使用します:
QVector3D endTarget; float xMin = m_graph->columnAxis()->min(); float xRange = m_graph->columnAxis()->max() - xMin; float zMin = m_graph->rowAxis()->min(); float zRange = m_graph->rowAxis()->max() - zMin; endTarget.setX((selectedBar.y() - xMin) / xRange * 2.0f - 1.0f); endTarget.setZ((selectedBar.x() - zMin) / zRange * 2.0f - 1.0f); ... m_animationCameraTarget.setEndValue(QVariant::fromValue(endTarget));
次に、カメラを回転させ、アニメーションの終了時に常にグラフのほぼ中央を指すようにします:
qreal endAngleX = 90.0 - qRadiansToDegrees(qAtan(qreal(endTarget.z() / endTarget.x()))); if (endTarget.x() > 0.0f) endAngleX -= 180.0f; float barValue = m_graph->selectedSeries()->dataProxy()->itemAt(selectedBar.x(), selectedBar.y())->value(); float endAngleY = barValue >= 0.0f ? 30.0f : -30.0f; if (m_graph->valueAxis()->reversed()) endAngleY *= -1.0f;
データのカスタムプロキシ
Custom Proxy Data データ・モードをオンに切り替えると、カスタム・データセットとそれに対応するプロキシが使用される。
単純なフレキシブル・データセット、VariantDataSet
を定義する。各データ項目はバリアント・リストである。各項目は複数の値を持つことができ、リスト内のインデックスで識別される。この場合、データセットは月ごとの降雨量データを格納しており、インデックス 0 の値は年、インデックス 1 の値は月、インデックス 2 の値はその月の降雨量である。
カスタムプロキシは、Qt Data Visualizationが提供するitemmodelベースのプロキシと似ており、データを解釈するためのマッピングが必要です。
バリアントデータセット
データ項目をQVariantList オブジェクトとして定義します。データセットをクリアし、セットに含まれるデータへの参照をクエリする機能を追加します。また、データが追加されたとき、またはセットがクリアされたときに発行されるシグナルを追加します:
using VariantDataItem = QVariantList; using VariantDataItemList = QList<VariantDataItem *>; ... void clear(); int addItem(VariantDataItem *item); int addItems(VariantDataItemList *itemList); const VariantDataItemList &itemList() const; Q_SIGNALS: void itemsAdded(int index, int count); void dataCleared();
VariantBarDataProxy
QBarDataProxy からVariantBarDataProxy
をサブクラス化し、データセットとマッピングのゲッターとセッターのシンプルな API を提供します:
class VariantBarDataProxy : public QBarDataProxy ... // Doesn't gain ownership of the dataset, but does connect to it to listen for data changes. void setDataSet(VariantDataSet *newSet); VariantDataSet *dataSet(); // Map key (row, column, value) to value index in data item (VariantItem). // Doesn't gain ownership of mapping, but does connect to it to listen for mapping changes. // Modifying mapping that is set to proxy will trigger dataset re-resolving. void setMapping(VariantBarDataMapping *mapping); VariantBarDataMapping *mapping();
プロキシはデータセットとマッピングの変更をリッスンし、変更が検出されるとデータセットを解決します。これは特に効率的な実装ではありません。変更があるとデータセット全体が再解決されるからです。
resolveDataSet()
メソッドでは、バリアントデータの値をマッピングに基づいて行と列にソートする。これは、QItemModelBarDataProxy がマッピングを処理する方法とよく似ていますが、ここでは項目モデルのロールの代わりにリストインデックスを使用する点が異なります。値がソートされたら、QBarDataArray
を生成し、親クラスのresetArray()
メソッドを呼び出します:
void VariantBarDataProxy::resolveDataSet() { // If we have no data or mapping, or the categories are not defined, simply clear the array if (m_dataSet.isNull() || m_mapping.isNull() || !m_mapping->rowCategories().size() || !m_mapping->columnCategories().size()) { resetArray(nullptr); return; } const VariantDataItemList &itemList = m_dataSet->itemList(); int rowIndex = m_mapping->rowIndex(); int columnIndex = m_mapping->columnIndex(); int valueIndex = m_mapping->valueIndex(); const QStringList &rowList = m_mapping->rowCategories(); const QStringList &columnList = m_mapping->columnCategories(); // Sort values into rows and columns using ColumnValueMap = QHash<QString, float>; QHash <QString, ColumnValueMap> itemValueMap; for (const VariantDataItem *item : itemList) { itemValueMap[item->at(rowIndex).toString()][item->at(columnIndex).toString()] = item->at(valueIndex).toReal(); } // Create a new data array in format the parent class understands auto *newProxyArray = new QBarDataArray; for (const QString &rowKey : rowList) { auto *newProxyRow = new QBarDataRow(columnList.size()); for (qsizetype i = 0; i < columnList.size(); ++i) (*newProxyRow)[i].setValue(itemValueMap[rowKey][columnList.at(i)]); newProxyArray->append(newProxyRow); } // Finally, reset the data array in the parent class resetArray(newProxyArray); }
VariantBarDataMapping
VariantBarDataMapping
に、VariantDataSet
データ項目インデックスと、QBarDataArray
の行、列、値との間のマッピング情報を格納します。 これには、解決されたデータに含まれる行と列のリストが含まれます:
Q_PROPERTY(int rowIndex READ rowIndex WRITE setRowIndex NOTIFY rowIndexChanged) Q_PROPERTY(int columnIndex READ columnIndex WRITE setColumnIndex NOTIFY columnIndexChanged) Q_PROPERTY(int valueIndex READ valueIndex WRITE setValueIndex NOTIFY valueIndexChanged) Q_PROPERTY(QStringList rowCategories READ rowCategories WRITE setRowCategories NOTIFY rowCategoriesChanged) Q_PROPERTY(QStringList columnCategories READ columnCategories WRITE setColumnCategories NOTIFY columnCategoriesChanged) ... explicit VariantBarDataMapping(int rowIndex, int columnIndex, int valueIndex, const QStringList &rowCategories, const QStringList &columnCategories); ... void remap(int rowIndex, int columnIndex, int valueIndex, const QStringList &rowCategories, const QStringList &columnCategories); ... void mappingChanged();
VariantBarDataMapping
オブジェクトを使用する主な方法は、コンストラクタでマッピングを与えることですが、remap()
メソッドを使用して、後で個別に、またはまとめて設定することもできます。マッピングが変更されたらシグナルを出す。この結果は、QItemModelBarDataProxy のマッピング機能を簡略化したもので、アイテムモデルの代わりにバリアントリストを扱うようにしたものです。
RainfallData
RainfallData
クラスのカスタムプロキシでQBar3DSeries のセットアップを処理します:
m_proxy = new VariantBarDataProxy; m_series = new QBar3DSeries(m_proxy);
addDataSet()
メソッドでバリアント・データセットを入力します:
void RainfallData::addDataSet() { // Create a new variant data set and data item list m_dataSet = new VariantDataSet; auto *itemList = new VariantDataItemList; // Read data from a data file into the data item list QFile dataFile(":/data/raindata.txt"); if (dataFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream stream(&dataFile); while (!stream.atEnd()) { QString line = stream.readLine(); if (line.startsWith('#')) // Ignore comments continue; const auto strList = QStringView{line}.split(',', Qt::SkipEmptyParts); // Each line has three data items: Year, month, and rainfall value if (strList.size() < 3) { qWarning() << "Invalid row read from data:" << line; continue; } // Store year and month as strings, and rainfall value as double // into a variant data item and add the item to the item list. auto *newItem = new VariantDataItem; for (int i = 0; i < 2; ++i) newItem->append(strList.at(i).trimmed().toString()); newItem->append(strList.at(2).trimmed().toDouble()); itemList->append(newItem); } } else { qWarning() << "Unable to open data file:" << dataFile.fileName(); } ...
カスタムプロキシにデータセットを追加し、マッピングを設定します:
// Add items to the data set and set it to the proxy m_dataSet->addItems(itemList); m_proxy->setDataSet(m_dataSet); // Create new mapping for the data and set it to the proxy m_mapping = new VariantBarDataMapping(0, 1, 2, m_years, m_numericMonths); m_proxy->setMapping(m_mapping);
最後に、作成した系列を表示するための関数を追加します:
QBar3DSeries *customSeries() { return m_series; }
散布図
Scatter Graph タブで、Q3DScatter を使って3D散布図を作成する。この例では、次の方法を示している:
- Q3DScatter グラフのセットアップ
- QScatterDataProxy を使ってグラフにデータを設定する。
- を拡張してカスタム入力ハンドラを作成する。Q3DInputHandler
基本的なアプリケーションの作成については、棒グラフを参照してください。
散布図グラフの設定
まず、ScatterDataModifier
のコンストラクタで、グラフの視覚的性質をいくつか設定します:
m_graph->activeTheme()->setType(Q3DTheme::ThemeStoneMoss); m_graph->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftHigh); m_graph->scene()->activeCamera()->setCameraPreset(Q3DCamera::CameraPresetFront); m_graph->scene()->activeCamera()->setZoomLevel(80.f);
どれも必須ではありませんが、グラフのデフォルトを上書きするために使用します。上のブロックをコメントアウトすることで、プリセットのデフォルトでどのように見えるかを試すことができます。
次に、QScatterDataProxy と関連するQScatter3DSeries を作成します。系列のカスタム・ラベル・フォーマットとメッシュ・スムージングを設定し、グラフに追加する:
auto *proxy = new QScatterDataProxy; auto *series = new QScatter3DSeries(proxy); series->setItemLabelFormat(u"@xTitle: @xLabel @yTitle: @yLabel @zTitle: @zLabel"_s); series->setMeshSmooth(m_smooth); m_graph->addSeries(series);
散布図データの追加
ScatterDataModifier
コンストラクタで行う最後のことは、グラフにデータを追加することです:
addData();
実際のデータ追加はaddData()
メソッドで行います。まず、軸を設定します:
m_graph->axisX()->setTitle("X"); m_graph->axisY()->setTitle("Y"); m_graph->axisZ()->setTitle("Z");
これはScatterDataModifier
のコンストラクタでも行うことができます。 ここで行うことで、コンストラクタがよりシンプルになり、軸の設定がデータの近くに保たれます。
次に、データ配列を作成し、それにデータを入力します:
auto *dataArray = new QScatterDataArray; dataArray->reserve(m_itemCount); ... const float limit = qSqrt(m_itemCount) / 2.0f; for (int i = -limit; i < limit; ++i) { for (int j = -limit; j < limit; ++j) { const float x = float(i) + 0.5f; const float y = qCos(qDegreesToRadians(float(i * j) / m_curveDivider)); const float z = float(j) + 0.5f; dataArray->append(QScatterDataItem({x, y, z})); } }
最後に、プロキシに与えたデータを使い始めるように指示します:
m_graph->seriesList().at(0)->dataProxy()->resetArray(dataArray);
これで、グラフにデータが入り、使用できるようになりました。ウィジェットを追加してグラフを制御する方法については、ウィジェットを使ってグラフを制御するを参照してください。
デフォルトの入力処理を置き換える
コンストラクタでm_inputHandler
を散布図インスタンスへのポインタで初期化します:
m_inputHandler(new AxesInputHandler(scatter))
Q3DScatter のアクティブな入力ハンドラをカスタム動作を実装するAxesInputHandler
に設定することで、デフォルトの入力処理メカニズムを置き換えます:
// Give ownership of the handler to the graph and make it the active handler m_graph->setActiveInputHandler(m_inputHandler);
入力ハンドラーはグラフの軸にアクセスする必要があるので、それを渡します:
// Give our axes to the input handler m_inputHandler->setAxes(m_graph->axisX(), m_graph->axisZ(), m_graph->axisY());
マウスイベント処理の拡張
まず、QAbstract3DInputHandler の代わりにQ3DInputHandler からカスタム入力ハンドラを継承して、デフォルトの入力ハンドリングのすべての機能を維持し、その上にカスタム機能を追加します:
class AxesInputHandler : public Q3DInputHandler
マウスイベントの一部を再実装することで、デフォルトの機能を拡張し始めます。まず、mousePressEvent
を拡張します。これにマウスの左ボタン用のm_mousePressed
フラグを追加し、残りのデフォルトの機能は維持します:
void AxesInputHandler::mousePressEvent(QMouseEvent *event, const QPoint &mousePos) { Q3DInputHandler::mousePressEvent(event, mousePos); if (Qt::LeftButton == event->button()) m_mousePressed = true; }
次に、mouseReleaseEvent
を修正してフラグをクリアし、内部状態をリセットします:
void AxesInputHandler::mouseReleaseEvent(QMouseEvent *event, const QPoint &mousePos) { Q3DInputHandler::mouseReleaseEvent(event, mousePos); m_mousePressed = false; m_state = StateNormal; }
次に、mouseMoveEvent
を修正する。m_mousePressed
フラグがtrue
であり、内部状態がStateNormal
以外であるかどうかをチェックする。もしそうなら、マウス移動距離計算の入力位置を設定し、軸ドラッグ関数を呼び出す(詳細は軸ドラッグの実装を参照):
void AxesInputHandler::mouseMoveEvent(QMouseEvent *event, const QPoint &mousePos) { // Check if we're trying to drag axis label if (m_mousePressed && m_state != StateNormal) { setPreviousInputPos(inputPosition()); setInputPosition(mousePos); handleAxisDragging(); } else { Q3DInputHandler::mouseMoveEvent(event, mousePos); } }
軸ドラッグの実装
まず、グラフからの選択信号のリッスンを開始する。これをコンストラクタで行い、handleElementSelected
メソッドに接続します:
// Connect to the item selection signal from graph connect(graph, &QAbstract3DGraph::selectedElementChanged, this, &AxesInputHandler::handleElementSelected);
handleElementSelected
で、選択のタイプをチェックし、それに基づいて内部状態を設定する:
switch (type) { case QAbstract3DGraph::ElementAxisXLabel: m_state = StateDraggingX; break; case QAbstract3DGraph::ElementAxisYLabel: m_state = StateDraggingY; break; case QAbstract3DGraph::ElementAxisZLabel: m_state = StateDraggingZ; break; default: m_state = StateNormal; break; }
実際のドラッグ・ロジックはhandleAxisDragging
メソッドに実装され、必要な条件が満たされた場合にmouseMoveEvent
から呼び出されます:
// Check if we're trying to drag axis label if (m_mousePressed && m_state != StateNormal) {
handleAxisDragging
では、まずアクティブなカメラからシーンの向きを取得します:
// Get scene orientation from active camera float xRotation = scene()->activeCamera()->xRotation(); float yRotation = scene()->activeCamera()->yRotation();
次に、オリエンテーションに基づいてマウスの移動方向のモディファイアを計算します:
// Calculate directional drag multipliers based on rotation float xMulX = qCos(qDegreesToRadians(xRotation)); float xMulY = qSin(qDegreesToRadians(xRotation)); float zMulX = qSin(qDegreesToRadians(xRotation)); float zMulY = qCos(qDegreesToRadians(xRotation));
その後、マウスの動きを計算し、カメラの y 回転に基づいて修正します:
// Get the drag amount QPoint move = inputPosition() - previousInputPos(); // Flip the effect of y movement if we're viewing from below float yMove = (yRotation < 0) ? -move.y() : move.y();
次に、移動した距離を正しい軸に適用する:
// Adjust axes switch (m_state) { case StateDraggingX: distance = (move.x() * xMulX - yMove * xMulY) / m_speedModifier; m_axisX->setRange(m_axisX->min() - distance, m_axisX->max() - distance); break; case StateDraggingZ: distance = (move.x() * zMulX + yMove * zMulY) / m_speedModifier; m_axisZ->setRange(m_axisZ->min() + distance, m_axisZ->max() + distance); break; case StateDraggingY: distance = move.y() / m_speedModifier; // No need to use adjusted y move here m_axisY->setRange(m_axisY->min() + distance, m_axisY->max() + distance); break; default: break; }
最後に、ドラッグ速度を設定する関数を追加する:
inline void setDragSpeedModifier(float modifier) { m_speedModifier = modifier; }
マウスの移動距離はスクリーン座標での絶対値であり、軸の範囲に合わせて調整する必要があるため、これが必要である。値が大きいほど、ドラッグは遅くなります。この例では、ドラッグ速度を決定する際にシーンのズームレベルは考慮されないので、ズームレベルを変更すると、範囲の調整が変化することに注意してください。
軸の範囲とカメラのズームレベルに基づいて、モディファイアを自動的に調整することもできます。
サーフェスグラフ
Surface Graph タブで、Q3DSurface を使って3Dサーフェスグラフを作成します。この例では次の方法を示します:
- 基本的なQSurfaceDataProxy を設定し、データを設定します。
- QHeightMapSurfaceDataProxy 、3D高さマップを表示する。
- 地形データを使用して3D高さマップを作成する。
- グラフを研究するために3つの異なる選択モードを使用する。
- 軸範囲を使用してグラフの選択部分を表示する。
- カスタムサーフェスグラデーションを設定する。
- QCustom3DItem とQCustom3DLabel を使ってカスタム項目とラベルを追加する。
- カスタム入力ハンドラを使用してズームとパンを有効にする。
- サーフェスの領域をハイライトする。
基本的なアプリケーションの作成については、棒グラフを参照してください。
生成されたデータによる単純なサーフェス
まず、新しいQSurfaceDataProxy をインスタンス化し、新しいQSurface3DSeries にアタッチします:
m_sqrtSinProxy = new QSurfaceDataProxy(); m_sqrtSinSeries = new QSurface3DSeries(m_sqrtSinProxy);
次に、プロキシを単純な矩形波と正弦波のデータで埋めます。新しいQSurfaceDataArray
インスタンスを作成し、QSurfaceDataRow
要素を追加する。resetArray()
を呼び出して、作成したQSurfaceDataArray
をQSurfaceDataProxy のデータ配列として設定する。
auto *dataArray = new QSurfaceDataArray; dataArray->reserve(sampleCountZ); for (int i = 0 ; i < sampleCountZ ; ++i) { auto *newRow = new QSurfaceDataRow; newRow->reserve(sampleCountX); // Keep values within range bounds, since just adding step can cause minor drift due // to the rounding errors. float z = qMin(sampleMax, (i * stepZ + sampleMin)); for (int j = 0; j < sampleCountX; ++j) { float x = qMin(sampleMax, (j * stepX + sampleMin)); float R = qSqrt(z * z + x * x) + 0.01f; float y = (qSin(R) / R + 0.24f) * 1.61f; newRow->append(QSurfaceDataItem({x, y, z})); } dataArray->append(newRow); } m_sqrtSinProxy->resetArray(dataArray);
マルチシリーズ高さマップデータ
高さデータを含むQImage でQHeightMapSurfaceDataProxy をインスタンス化し、高さマップを作成する。QHeightMapSurfaceDataProxy::setValueRanges() を使用して、マップの値域を定義する。この例では、北緯34.0度~北緯40.0度、東経18.0度~東経24.0度の架空の位置からマップを作成しています。
// Create the first surface layer QImage heightMapImageOne(":/data/layer_1.png"); m_heightMapProxyOne = new QHeightMapSurfaceDataProxy(heightMapImageOne); m_heightMapSeriesOne = new QSurface3DSeries(m_heightMapProxyOne); m_heightMapSeriesOne->setItemLabelFormat(u"(@xLabel, @zLabel): @yLabel"_s); m_heightMapProxyOne->setValueRanges(34.f, 40.f, 18.f, 24.f);
他の地表レイヤーも同じように、ハイトマップ画像を使ってプロキシと系列を作成して追加します。
地形図データ
地形データはフィンランド国土調査局から入手した。これはElevation Model 2 m
という製品を提供しており、この例に適している。地形データはLevi fellのものである。データの精度は必要以上に高いため、圧縮してPNGファイルにエンコードした。元のASCIIデータの高さ値は、乗数を使用してRGB形式にエンコードされます。乗数は最大の24ビット値をフィンランドの最高点で割って計算されます。
QHeightMapSurfaceDataProxy は1バイト値のみを変換する。フィンランド国土調査のデータの高い精度を利用するには、PNG ファイルからデータを読み取り、 にデコードします。QSurface3DSeries
まず、エンコード乗数を定義する:
// Value used to encode height data as RGB value on PNG file const float packingFactor = 11983.f;
次に実際のデコードを行う:
QImage heightMapImage(file); uchar *bits = heightMapImage.bits(); int imageHeight = heightMapImage.height(); int imageWidth = heightMapImage.width(); int widthBits = imageWidth * 4; float stepX = width / float(imageWidth); float stepZ = height / float(imageHeight); auto *dataArray = new QSurfaceDataArray; dataArray->reserve(imageHeight); for (int i = 0; i < imageHeight; ++i) { int p = i * widthBits; float z = height - float(i) * stepZ; auto *newRow = new QSurfaceDataRow; newRow->reserve(imageWidth); for (int j = 0; j < imageWidth; ++j) { uchar aa = bits[p + 0]; uchar rr = bits[p + 1]; uchar gg = bits[p + 2]; uint color = uint((gg << 16) + (rr << 8) + aa); float y = float(color) / packingFactor; newRow->append(QSurfaceDataItem({float(j) * stepX, y, z})); p += 4; } dataArray->append(newRow); } dataProxy()->resetArray(dataArray);
これで、プロキシがデータを使えるようになる。
データセットの選択
さまざまなプロキシを示すために、Surface Graph には、系列を切り替えるための3つのラジオボタンがある。
Sqrt & Sin では、単純に生成された系列が有効になっている。最初に、サーフェスのグリッドを有効にしたり、フラット・シェーディング・モードを選択するなど、装飾的な機能を設定します。次に、軸ラベル形式と値範囲を定義する。低いカメラアングルでのラベルの読みやすさを向上させるために、ラベルの自動回転を設定します。最後に、正しい系列がグラフに追加され、他の系列が追加されていないことを確認します:
m_sqrtSinSeries->setDrawMode(QSurface3DSeries::DrawSurfaceAndWireframe); m_sqrtSinSeries->setFlatShadingEnabled(true); m_graph->axisX()->setLabelFormat("%.2f"); m_graph->axisZ()->setLabelFormat("%.2f"); m_graph->axisX()->setRange(sampleMin, sampleMax); m_graph->axisY()->setRange(0.f, 2.f); m_graph->axisZ()->setRange(sampleMin, sampleMax); m_graph->axisX()->setLabelAutoRotation(30.f); m_graph->axisY()->setLabelAutoRotation(90.f); m_graph->axisZ()->setLabelAutoRotation(30.f); m_graph->removeSeries(m_heightMapSeriesOne); m_graph->removeSeries(m_heightMapSeriesTwo); m_graph->removeSeries(m_heightMapSeriesThree); m_graph->removeSeries(m_topography); m_graph->removeSeries(m_highlight); m_graph->addSeries(m_sqrtSinSeries);
Multiseries Height Map では、ハイトマップ系列が有効化され、他は無効化されます。Y軸範囲の自動調整は、ハイトマップ・サーフェスでうまく機能するので、設定されていることを確認してください。
m_graph->axisY()->setAutoAdjustRange(true);
Textured Topography では、地形シリーズが有効化され、他は無効化される。このシリーズのカスタム入力ハンドラを有効にして、そのシリーズ上のエリアをハイライトできるようにします:
m_graph->setActiveInputHandler(m_customInputHandler);
このデータセットのカスタム入力ハンドラについては「カスタム入力ハンドラを使用してズームとパンを有効にする」を参照してください。
選択モード
Q3DSurface でサポートされている3つの選択モードは、ラジオボタンで使用できます。選択モードをアクティブにしたり、クリアしたりするには、以下のインライン・メソッドを追加します:
void toggleModeNone() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionNone); } void toggleModeItem() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItem); } void toggleModeSliceRow() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItemAndRow | QAbstract3DGraph::SelectionSlice | QAbstract3DGraph::SelectionMultiSeries); } void toggleModeSliceColumn() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItemAndColumn | QAbstract3DGraph::SelectionSlice | QAbstract3DGraph::SelectionMultiSeries); }
行と列の選択モードにQAbstract3DGraph::SelectionSlice
とQAbstract3DGraph::SelectionMultiSeries
のフラグを追加し、グラフ内のすべての可視系列を同時にスライス選択できるようにする。
グラフを研究するための軸範囲
この例題には、X軸とZ軸の最小値と最大値を調整するための4つのスライダー・コントロールがある。プロキシを選択するとき、これらのスライダは現在のデータセットの軸範囲に合うように調整される:
// Reset range sliders for Sqrt & Sin m_rangeMinX = sampleMin; m_rangeMinZ = sampleMin; m_stepX = (sampleMax - sampleMin) / float(sampleCountX - 1); m_stepZ = (sampleMax - sampleMin) / float(sampleCountZ - 1); m_axisMinSliderX->setMinimum(0); m_axisMinSliderX->setMaximum(sampleCountX - 2); m_axisMinSliderX->setValue(0); m_axisMaxSliderX->setMinimum(1); m_axisMaxSliderX->setMaximum(sampleCountX - 1); m_axisMaxSliderX->setValue(sampleCountX - 1); m_axisMinSliderZ->setMinimum(0); m_axisMinSliderZ->setMaximum(sampleCountZ - 2); m_axisMinSliderZ->setValue(0); m_axisMaxSliderZ->setMinimum(1); m_axisMaxSliderZ->setMaximum(sampleCountZ - 1); m_axisMaxSliderZ->setValue(sampleCountZ - 1);
ウィジェット・コントロールからグラフにX範囲を設定するサポートを追加する:
void SurfaceGraphModifier::setAxisXRange(float min, float max) { m_graph->axisX()->setRange(min, max); }
同じ方法でZ範囲のサポートを追加。
カスタムサーフェスグラデーション
Sqrt & Sin データセットでは、2つのプッシュボタンでカスタムサーフェスグラデーションを使用することができます。QLinearGradient でグラデーションを定義し、希望の色を設定します。また、グラデーションを使用するには、カラースタイルをQ3DTheme::ColorStyleRangeGradient に変更します。
QLinearGradient gr; gr.setColorAt(0.f, Qt::black); gr.setColorAt(0.33f, Qt::blue); gr.setColorAt(0.67f, Qt::red); gr.setColorAt(1.f, Qt::yellow); m_sqrtSinSeries->setBaseGradient(gr); m_sqrtSinSeries->setColorStyle(Q3DTheme::ColorStyleRangeGradient);
アプリケーションへのカスタムメッシュの追加
cmakeビルドのためにメッシュファイルをCMakeLists.txt
:
set(graphgallery_resource_files ... "data/oilrig.obj" "data/pipe.obj" "data/refinery.obj" ... ) qt6_add_resources(graphgallery "graphgallery" PREFIX "/" FILES ${graphgallery_resource_files} )
また、qmakeで使用するためにqrcリソースファイルに追加する:
<RCC> <qresource prefix="/"> ... <file>data/refinery.obj</file> <file>data/oilrig.obj</file> <file>data/pipe.obj</file> ... </qresource> </RCC>
グラフへのカスタム項目の追加
Multiseries Height Map データセットでは、カスタム項目がグラフに挿入され、チェックボックスを使ってオン・オフを切り替えることができる。また、上の2つのレイヤーのシースルーや、下のレイヤーのハイライトなど、別のチェックボックスを使って視覚的な品質をコントロールすることもできます。
まず小さなQImage を作成します。カスタムオブジェクトの色として使用する単色で塗りつぶします:
次に、アイテムの位置を変数で指定します。この位置は、グラフから正しい項目を取り除くのに使われます:
QVector3D positionOne = QVector3D(39.f, 77.f, 19.2f);
次に、すべてのパラメータを指定して新しいQCustom3DItem :
auto *item = new QCustom3DItem(":/data/oilrig.obj", positionOne, QVector3D(0.025f, 0.025f, 0.025f), QQuaternion::fromAxisAndAngle(0.f, 1.f, 0.f, 45.f), color);
最後に、項目をグラフに追加する:
m_graph->addCustomItem(item);
グラフへのカスタム・ラベルの追加
カスタム・ラベルの追加は、カスタム・アイテムの追加とよく似ている。ラベルにはカスタムメッシュは必要なく、QCustom3DLabel インスタンスだけが必要です:
auto *label = new QCustom3DLabel(); label->setText("Oil Rig One"); label->setPosition(positionOneLabel); label->setScaling(QVector3D(1.f, 1.f, 1.f)); m_graph->addCustomItem(label);
グラフからカスタムアイテムを削除する
グラフから特定のアイテムを削除するには、アイテムの位置を指定してremoveCustomItemAt()
を呼び出します:
m_graph->removeCustomItemAt(positionOne);
注: グラフからカスタム・アイテムを削除すると、そのオブジェクトも削除されます。アイテムを保持したい場合は、代わりにreleaseCustomItem()
メソッドを使用してください。
サーフェス系列へのテクスチャ
Textured Topography データセットで、地形高さマップで使用するマップテクスチャを作成します。
QSurface3DSeries::setTextureFile() を使用して、サーフェス上のテクスチャとして使用する画像を設定します。テクスチャを設定するかどうかを制御するチェックボックスと、チェックボックスの状態に反応するハンドラを追加します:
void SurfaceGraphModifier::toggleSurfaceTexture(bool enable) { if (enable) m_topography->setTextureFile(":/data/maptexture.jpg"); else m_topography->setTextureFile(""); }
この例の画像はJPGファイルから読み込まれます。このメソッドで空のファイルを設定すると、テクスチャはクリアされ、サーフェスはテーマからのグラデーションまたは色を使用します。
カスタム入力ハンドラを使用してズームとパンを有効にする
Textured Topography データセットで、グラフ上の選択をハイライトし、グラフをパンできるようにするカスタム入力ハンドラを作成する。
パンニングの実装は「軸ドラッグの実装」で示したものと似ている。違いは、この例ではX軸とZ軸だけを追い、グラフの外側のサーフェスのドラッグを許可しないことです。ドラッグを制限するには、軸の限界に従い、グラフの外に出る場合は何もしない:
case StateDraggingX: distance = (move.x() * xMulX - move.y() * xMulY) * m_speedModifier; m_axisXMinValue -= distance; m_axisXMaxValue -= distance; if (m_axisXMinValue < m_areaMinValue) { float dist = m_axisXMaxValue - m_axisXMinValue; m_axisXMinValue = m_areaMinValue; m_axisXMaxValue = m_axisXMinValue + dist; } if (m_axisXMaxValue > m_areaMaxValue) { float dist = m_axisXMaxValue - m_axisXMinValue; m_axisXMaxValue = m_areaMaxValue; m_axisXMinValue = m_axisXMaxValue - dist; } m_axisX->setRange(m_axisXMinValue, m_axisXMaxValue); break;
ズームについては、wheelEvent
をキャッチし、QWheelEvent のデルタ値に従ってX軸とY軸の範囲を調整する。Y軸とXZ平面の縦横比が変わらないようにY軸を調整する。これにより、高さが誇張されたグラフになるのを防ぐことができます:
void CustomInputHandler::wheelEvent(QWheelEvent *event) { float delta = float(event->angleDelta().y()); m_axisXMinValue += delta; m_axisXMaxValue -= delta; m_axisZMinValue += delta; m_axisZMaxValue -= delta; checkConstraints(); float y = (m_axisXMaxValue - m_axisXMinValue) * m_aspectRatio; m_axisX->setRange(m_axisXMinValue, m_axisXMaxValue); m_axisY->setRange(100.f, y); m_axisZ->setRange(m_axisZMinValue, m_axisZMaxValue); }
次に、ズーム・レベルに制限を加え、表面に近づきすぎたり、表面から離れすぎたりしないようにする。例えば、X軸の値が許容限度を下回る場合、つまりズームが遠くなりすぎる場合、値は許容最小値に設定される。範囲が範囲の最小値以下になりそうな場合は、範囲が限界にとどまるように軸の両端が調整される:
if (m_axisXMinValue < m_areaMinValue) m_axisXMinValue = m_areaMinValue; if (m_axisXMaxValue > m_areaMaxValue) m_axisXMaxValue = m_areaMaxValue; // Don't allow too much zoom in if ((m_axisXMaxValue - m_axisXMinValue) < m_axisXMinRange) { float adjust = (m_axisXMinRange - (m_axisXMaxValue - m_axisXMinValue)) / 2.f; m_axisXMinValue -= adjust; m_axisXMaxValue += adjust; }
サーフェスの領域をハイライトする
サーフェス上に表示されるハイライトを実装するには、系列のコピーを作成し、y値にオフセットを追加します。この例では、クラスHighlightSeries
、そのhandlePositionChange
メソッドでコピーの作成を実装している。
まず、HighlightSeries
に元の系列へのポインタを渡し、QSurface3DSeries::selectedPointChanged シグナルのリッスンを開始する:
void HighlightSeries::setTopographicSeries(TopographicSeries *series) { m_topographicSeries = series; m_srcWidth = m_topographicSeries->dataProxy()->array()->at(0)->size(); m_srcHeight = m_topographicSeries->dataProxy()->array()->size(); QObject::connect(m_topographicSeries, &QSurface3DSeries::selectedPointChanged, this, &HighlightSeries::handlePositionChange); }
シグナルがトリガーしたら、ポジションが有効であることをチェックする。次に、コピーされた領域の範囲を計算し、その範囲内に収まっていることをチェックする。最後に、ハイライトシリーズのデータ配列を、トポグラフィシリーズのデータ配列の範囲で埋める:
void HighlightSeries::handlePositionChange(const QPoint &position) { m_position = position; if (position == invalidSelectionPosition()) { setVisible(false); return; } int halfWidth = m_width / 2; int halfHeight = m_height / 2; int startX = position.y() - halfWidth; if (startX < 0 ) startX = 0; int endX = position.y() + halfWidth; if (endX > (m_srcWidth - 1)) endX = m_srcWidth - 1; int startZ = position.x() - halfHeight; if (startZ < 0 ) startZ = 0; int endZ = position.x() + halfHeight; if (endZ > (m_srcHeight - 1)) endZ = m_srcHeight - 1; QSurfaceDataProxy *srcProxy = m_topographicSeries->dataProxy(); const QSurfaceDataArray &srcArray = *srcProxy->array(); auto *dataArray = new QSurfaceDataArray; dataArray->reserve(endZ - startZ); for (int i = startZ; i < endZ; ++i) { auto *newRow = new QSurfaceDataRow; newRow->reserve(endX - startX); QSurfaceDataRow *srcRow = srcArray.at(i); for (int j = startX; j < endX; ++j) { QVector3D pos = srcRow->at(j).position(); pos.setY(pos.y() + 0.1f); newRow->append(QSurfaceDataItem(pos)); } dataArray->append(newRow); } dataProxy()->resetArray(dataArray); setVisible(true); }
ハイライト系列へのグラデーション
HighlightSeries
はQSurface3DSeries であるため、系列が持ちうるすべての装飾メソッドが利用可能です。この例では、標高を強調するためにグラデーションを追加します。適切なグラデーションのスタイルはY軸の範囲に依存し、ズームするときに範囲を変更するので、グラデーションの色の位置は範囲の変更に応じて調整する必要があります。グラデーションカラーの位置に比例値を定義することでこれを行います:
const float darkRedPos = 1.f; const float redPos = 0.8f; const float yellowPos = 0.6f; const float greenPos = 0.4f; const float darkGreenPos = 0.2f;
グラデーションの修正はhandleGradientChange
メソッドで行われるので、Y軸の変化に反応するように接続する:
QObject::connect(m_graph->axisY(), &QValue3DAxis::maxChanged, m_highlight, &HighlightSeries::handleGradientChange);
Y軸の最大値の変化が起きたら、新しいグラデーションカラー位置を計算する:
void HighlightSeries::handleGradientChange(float value) { float ratio = m_minHeight / value; QLinearGradient gr; gr.setColorAt(0.f, Qt::black); gr.setColorAt(darkGreenPos * ratio, Qt::darkGreen); gr.setColorAt(greenPos * ratio, Qt::green); gr.setColorAt(yellowPos * ratio, Qt::yellow); gr.setColorAt(redPos * ratio, Qt::red); gr.setColorAt(darkRedPos * ratio, Qt::darkRed); setBaseGradient(gr); setColorStyle(Q3DTheme::ColorStyleRangeGradient); }
コンテンツ例
©2024 The Qt Company Ltd. 本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。