グラフギャラリー

棒グラフ、散布図、面グラフのギャラリー。

グラフ・ギャラリーは、3つのグラフ・タイプすべてと、それらの特別な機能のいくつかを示しています。グラフはアプリケーション内にそれぞれのタブを持っています。

例の実行

からサンプルを実行するには Qt Creatorからサンプルを実行するには、Welcome モードを開き、Examples からサンプルを選択します。詳しくは、Building and Running an Exampleをご覧ください。

棒グラフ

Bar Graph タブで、Q3DBarWidgetItem を使って3D棒グラフを作成し、様々な棒グラフの品質を調整するためにウィジェットの使用を組み合わせます。例では、以下の方法を示します:

  • Q3DBarWidgetItemといくつかのコントロールウィジェットでアプリケーションを作成する。
  • QBar3DSeriesQBarDataProxy を使って、グラフにデータをセットする。
  • ウィジェットコントロールを使って、グラフと系列のプロパティを調整する。
  • 軸ラベルをクリックして行や列を選択する
  • Q3DBarWidgetItemで使用するカスタムプロキシーの作成

グラフとのインタラクションについては、このページを参照してください。

アプリケーションの作成
  1. bargraph.cpp で、QQuickWidgetQ3DBarsWidgetItem をインスタンス化し、QQuickWidget インスタンスをQ3DBarsWidgetItem のウィジェットとして設定します:
    m_quickWidget = new QQuickWidget();
    m_barGraph = new Q3DBarsWidgetItem(this);
    m_barGraph->setWidget(m_quickWidget);
  2. コンテナ・ウィジェット、水平および垂直レイアウトを作成します。グラフと垂直レイアウトを水平レイアウトに追加します:
    m_container = new QWidget();
    auto *hLayout = new QHBoxLayout(m_container);
    QSize screenSize = m_quickWidget->screen()->size();
    m_quickWidget->setMinimumSize(QSize(screenSize.width() / 2, screenSize.height() / 1.75));
    m_quickWidget->setMaximumSize(screenSize);
    m_quickWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    m_quickWidget->setFocusPolicy(Qt::StrongFocus);
    hLayout->addWidget(m_quickWidget, 1);
    
    auto *vLayout = new QVBoxLayout();
    hLayout->addLayout(vLayout);
  3. データの追加やグラフとのその他のインタラクションを処理する別のクラスを作成する:
    m_modifier = new GraphModifier(m_barGraph, this);
棒グラフの設定
  1. GraphModifier クラスのコンストラクタでグラフを設定する:
    GraphModifier::GraphModifier(Q3DBarsWidgetItem *bargraph, QObject *parent)
        : QObject(parent)
        , m_graph(bargraph)
  2. 軸と系列をメンバ変数に作成し、それらの変更をサポートする:
    , m_temperatureAxis(new QValue3DAxis)
    , m_yearAxis(new QCategory3DAxis)
    , m_monthAxis(new QCategory3DAxis)
    , m_primarySeries(new QBar3DSeries)
    , m_secondarySeries(new QBar3DSeries)
  3. グラフの見た目を設定する:
    m_graph->setShadowQuality(QtGraphs3D::ShadowQuality::SoftMedium);
    m_graph->setMultiSeriesUniform(true);
    // These are set through the active theme
    m_graph->activeTheme()->setPlotAreaBackgroundVisible(false);
    m_graph->activeTheme()->setLabelFont(QFont("Times New Roman", m_fontSize));
    m_graph->activeTheme()->setLabelBackgroundVisible(true);
  4. 軸を設定し、グラフのアクティブ軸にする:
    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->setLabelAutoAngle(30.0f);
    m_temperatureAxis->setTitleVisible(true);
    
    m_yearAxis->setTitle("Year");
    m_yearAxis->setLabelAutoAngle(30.0f);
    m_yearAxis->setTitleVisible(true);
    
    m_monthAxis->setTitle("Month");
    m_monthAxis->setLabelAutoAngle(30.0f);
    m_monthAxis->setTitleVisible(true);
    
    m_graph->setValueAxis(m_temperatureAxis);
    m_graph->setRowAxis(m_yearAxis);
    m_graph->setColumnAxis(m_monthAxis);
  5. 軸ラベルに小さな自動回転角度を与える:
    m_yearAxis->setLabelAutoAngle(30.0f);

    これは、軸ラベルの向きをわずかにカメラ側にするためで、これにより極端なカメラアングルでの軸ラベルの読みやすさが向上する。

  6. 系列の視覚的プロパティを初期化する。2番目の系列は最初は表示されないことに注意してください:
    m_primarySeries->setItemLabelFormat(u"Oulu - @colLabel @rowLabel: @valueLabel"_s);
    m_primarySeries->setMesh(QAbstract3DSeries::Mesh::BevelBar);
    m_primarySeries->setMeshSmooth(false);
    
    m_secondarySeries->setItemLabelFormat(u"Helsinki - @colLabel @rowLabel: @valueLabel"_s);
    m_secondarySeries->setMesh(QAbstract3DSeries::Mesh::BevelBar);
    m_secondarySeries->setMeshSmooth(false);
    m_secondarySeries->setVisible(false);
  7. 系列をグラフに追加する:
    m_graph->addSeries(m_primarySeries);
    m_graph->addSeries(m_secondarySeries);
  8. UIのカメラ・アングル変更ボタンが様々なカメラ・アングルを循環させるのと同じメソッドを呼び出して、カメラ・アングルを設定します:
    changePresetCamera();
  9. 新しいカメラプリセットがグラフに設定されます:
    static int preset = int(QtGraphs3D::CameraPreset::Front);
    
    m_graph->setCameraPreset((QtGraphs3D::CameraPreset) preset);
    
    if (++preset > int(QtGraphs3D::CameraPreset::DirectlyBelow))
        preset = int(QtGraphs3D::CameraPreset::FrontLow);
グラフへのデータの追加

コンストラクタの最後に、データを設定するメソッドを呼び出します:

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
...
QBarDataArray dataSet;
QBarDataArray dataSet2;

dataSet.reserve(m_years.size());
for (qsizetype year = 0; year < m_years.size(); ++year) {
    // Create a data row
    QBarDataRow dataRow(m_months.size());
    QBarDataRow dataRow2(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 にウィジェットを追加します。

  1. スライダーを追加します:
    auto *rotationSliderX = new QSlider(Qt::Horizontal, m_container);
    rotationSliderX->setTickInterval(30);
    rotationSliderX->setTickPosition(QSlider::TicksBelow);
    rotationSliderX->setMinimum(-180);
    rotationSliderX->setValue(0);
    rotationSliderX->setMaximum(180);
  2. マウスやタッチではなく、スライダーを使ってグラフを回転させる。垂直レイアウトに追加します:
    vLayout->addWidget(new QLabel(u"Rotate horizontally"_s));
    vLayout->addWidget(rotationSliderX, 0, Qt::AlignTop);
  3. GraphModifier のメソッドに接続:
    QObject::connect(rotationSliderX, &QSlider::valueChanged, m_modifier, &GraphModifier::rotateX);
  4. GraphModifier 、信号接続用のスロットを作成する。プリセットのカメラアングルを指定する代わりに、中心点を中心とした軌道に沿った実際のカメラ位置を指定する:
    void GraphModifier::rotateX(int angle)
    {
        m_xRotation = angle;
        m_graph->setCameraPosition(m_xRotation, m_yRotation);
    }

スライダーでグラフを回転できるように。

垂直レイアウトにウィジェットを追加してコントロールできるようにする:

  • グラフの回転
  • ラベルスタイル
  • カメラのプリセット
  • 背景の可視性
  • グリッドの可視性
  • バーの陰影の滑らかさ
  • 2本目のバーの可視性
  • 値軸の方向
  • 軸タイトルの可視性と回転
  • 表示するデータ範囲
  • バーのスタイル
  • 選択モード
  • テーマ
  • 影の質
  • フォント
  • フォントサイズ
  • 軸ラベル回転
  • データモード

Custom Proxy Data データ・モードでは、いくつかのウィジェット・コントロールが意図的に無効になります。

軸ラベルのクリックによる行または列の選択

軸ラベルによる選択は、棒グラフのデフォルト機能です。例えば、以下のように軸ラベルをクリックして行を選択できる:

  1. 選択モードをRow
  2. 年ラベルをクリック
  3. クリックした年の行が選択される。

RowColumn のどちらかが設定されていれば、SliceItem のフラグでも同じ方法が使える。

選択範囲へのズーム

カメラのターゲットを調整する例として、ボタンを押すことで選択範囲にズームするアニメーションを実装します。アニメーションの初期化はコンストラクタで行います:

m_defaultAngleX = m_graph->cameraXRotation();
m_defaultAngleY = m_graph->cameraYRotation();
m_defaultZoom = m_graph->cameraZoomLevel();
m_defaultTarget = m_graph->cameraTargetPosition();

m_animationCameraX.setTargetObject(m_graph);
m_animationCameraY.setTargetObject(m_graph);
m_animationCameraZoom.setTargetObject(m_graph);
m_animationCameraTarget.setTargetObject(m_graph);

m_animationCameraX.setPropertyName("cameraXRotation");
m_animationCameraY.setPropertyName("cameraYRotation");
m_animationCameraZoom.setPropertyName("cameraZoomLevel");
m_animationCameraTarget.setPropertyName("cameraTargetPosition");

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) に正規化された値をとります。cameraTargetPosition

選択されたバーが軸に対してどの位置にあるかを把握し、それを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 の値はその月の降雨量を表す。

カスタムプロキシは、QtGraphs が提供するアイテムモデルベースのプロキシ、QItemModelBarDataProxy に似ており、データを解釈するためにマッピングを必要とする。

データセットの実装

データ項目を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();
データ・プロキシを実装する。

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();
        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
    QBarDataArray newProxyArray;
    for (const QString &rowKey : rowList) {
        QBarDataRow newProxyRow(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);
}
データマッパーの実装

VariantDataSet のデータ項目インデックスとQBarDataArray の行、列、値の間のマッピング情報をVariantBarDataMapping に格納する。解決されたデータに含まれる行と列のリストが含まれる:

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
  1. RainfallData クラスのカスタムプロキシで のセットアップを処理します: QBar3DSeries
    m_proxy = new VariantBarDataProxy;
    m_series = new QBar3DSeries(m_proxy);
  2. メソッドでバリアントデータセットを投入します:addDataSet()
    voidRainfallData::addDataSet() {// 新しいバリアントデータセットとデータアイテムリストを作成m_dataSet= newVariantDataSet;auto *itemList = newVariantDataItemList;// データファイルからデータアイテムリストにデータを読み込む   QFiledataFile(":/data/raindata.txt");if(dataFile.open(QIODevice::読み取り専用QIODevice::Text)) { ストリーム(&dataFile)        QTextStreamstream(&dataFile);while(!stream.atEnd()) { 行を読み込む。            QStringline=stream.readLine();if(line.startsWith('#'))// コメントを無視 continue;const autostrList=QStringView{line}.split(','Qt::SkipEmptyParts);// 各行には3つのデータ項目がある:年、月、雨量値 if(strList.size()< 3) { // 各行には3つのデータ項目があります。                qWarning() << "Invalid row read from data:" << line;
                   continue; }// 年と月を文字列として、降雨値を double // として variantDataItem に 格納 し、その項目を項目リストに追加する。
               auto *newItem = newVariantDataItem;for(inti= 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();
        } ...
  3. カスタムプロキシ関数を使用して、データセットをシリーズに追加し、マッピングを設定します:
    // 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);
  4. 最後に、作成されたシリーズを表示するために取得する関数を追加します:
    QBar3DSeries *customSeries() { return m_series; }

散布図

Scatter Graph タブで、Q3DScatterWidgetItem を使って3D散布図を作成する。この例では、次の方法を示している:

基本的なアプリケーションの作成については、棒グラフを参照してください。

散布図グラフの設定
  1. ScatterDataModifier のコンストラクタで、グラフの見た目を設定します:
    m_graph->setShadowQuality(QtGraphs3D::ShadowQuality::SoftHigh);
    m_graph->setCameraPreset(QtGraphs3D::CameraPreset::Front);
    m_graph->setCameraZoomLevel(80.f);
    // These are set through active theme
    m_graph->activeTheme()->setTheme(QGraphsTheme::Theme::MixSeries);
    m_graph->activeTheme()->setColorScheme(QGraphsTheme::ColorScheme::Dark);

    これらの設定はどれも必須ではないが、グラフのデフォルトを上書きするためのものである。プリセットのデフォルトで見た目を観察するには、上のブロックをコメントアウトすればよい。

  2. 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);
散布図データの追加
  1. ScatterDataModifier コンストラクタで、グラフにデータを追加します:
    addData();
  2. 実際のデータ追加はaddData() メソッドで行います。まず、軸を設定します:
    m_graph->axisX()->setTitle("X");
    m_graph->axisY()->setTitle("Y");
    m_graph->axisZ()->setTitle("Z");

    これは、ScatterDataModifier のコンストラクタでも行うことができます。 ここで行うことで、コンストラクタがよりシンプルになり、軸の設定がデータの近くに保たれます。

  3. データ配列を作成し、それにデータを入れます:
    QScatterDataArray dataArray;
    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));
        }
    }
  4. 最後に、プロキシに渡したデータを使い始めるように指示します:
    m_graph->seriesList().at(0)->dataProxy()->resetArray(dataArray);

これで、グラフにデータが入り、使用できるようになりました。ウィジェットを追加してグラフを制御する方法については、ウィジェットを使ってグラフを制御するを参照してください。

デフォルトの入力処理を置き換える

デフォルトの入力処理メカニズムを置き換えるには、Q3DScatterWidgetItem の新しい入力ハンドラを設定します:

connect(m_graph,
        &Q3DGraphsWidgetItem::selectedElementChanged,
        this,
        &ScatterDataModifier::handleElementSelected);
connect(m_graph, &Q3DGraphsWidgetItem::dragged, this, &ScatterDataModifier::handleAxisDragging);
m_graph->setDragButton(Qt::LeftButton);
マウスイベント処理の拡張

新しいdrag イベントハンドラを実装します。これは、軸ドラッグ計算のためのマウス移動距離を提供します(詳細は、軸ドラッグの実装を参照してください):

connect(m_graph,
        &Q3DGraphsWidgetItem::selectedElementChanged,
        this,
        &ScatterDataModifier::handleElementSelected);
connect(m_graph, &Q3DGraphsWidgetItem::dragged, this, &ScatterDataModifier::handleAxisDragging);
m_graph->setDragButton(Qt::LeftButton);
軸ドラッグの実装
  1. グラフからの選択シグナルのリッスンを開始する。コンストラクタでそれを行い、handleElementSelected メソッドに接続する:
    connect(m_graph,
            &Q3DGraphsWidgetItem::selectedElementChanged,
            this,
            &ScatterDataModifier::handleElementSelected);
    connect(m_graph, &Q3DGraphsWidgetItem::dragged, this, &ScatterDataModifier::handleAxisDragging);
    m_graph->setDragButton(Qt::LeftButton);
  2. handleElementSelected で、選択のタイプをチェックし、それに基づいて内部状態を設定する:
    switch (type) {
    case QtGraphs3D::ElementType::AxisXLabel:
        m_state = StateDraggingX;
        break;
    case QtGraphs3D::ElementType::AxisYLabel:
        m_state = StateDraggingY;
        break;
    case QtGraphs3D::ElementType::AxisZLabel:
        m_state = StateDraggingZ;
        break;
    default:
        m_state = StateNormal;
        break;
    }
  3. 実際のドラッグ・ロジックはhandleAxisDragging メソッドに実装され、drag イベントから呼び出されます:
    void ScatterDataModifier::handleAxisDragging(QVector2D delta)
  4. handleAxisDragging では、まずアクティブなカメラからシーンの向きを取得します:
    // Get scene orientation from active camera
    float xRotation = m_graph->cameraXRotation();
    float yRotation = m_graph->cameraYRotation();
  5. オリエンテーションに基づいて、マウス移動方向の修飾子を計算します:
    // 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));
  6. マウスの動きを計算し、カメラのy回転に基づいて修正します:
    // Get the drag amount
    QPoint move = delta.toPoint();
    
    // Flip the effect of y movement if we're viewing from below
    float yMove = (yRotation < 0) ? -move.y() : move.y();
  7. 移動した距離を正しい軸に適用する:
    // Adjust axes
    QValue3DAxis *axis = nullptr;
    switch (m_state) {
    case StateDraggingX:
        axis = m_graph->axisX();
        distance = (move.x() * xMulX - yMove * xMulY) / m_dragSpeedModifier;
        axis->setRange(axis->min() - distance, axis->max() - distance);
        break;
    case StateDraggingZ:
        axis = m_graph->axisZ();
        distance = (move.x() * zMulX + yMove * zMulY) / m_dragSpeedModifier;
        axis->setRange(axis->min() + distance, axis->max() + distance);
        break;
    case StateDraggingY:
        axis = m_graph->axisY();
        distance = move.y() / m_dragSpeedModifier; // No need to use adjusted y move here
        axis->setRange(axis->min() + distance, axis->max() + distance);
        break;
    default:
        break;
    }

サーフェスグラフ

Surface Graph タブで、Q3DSurfaceWidgetItem を使って3Dサーフェスグラフを作成します。この例では、次の方法を示しています:

  • 基本的なQSurfaceDataProxy を設定し、データを設定する。
  • QHeightMapSurfaceDataProxy 、3D高さマップを表示する。
  • 地形データを使用して3D高さマップを作成する。
  • グラフを研究するために3つの異なる選択モードを使用する。
  • 軸範囲を使ってグラフの選択部分を表示する
  • カスタムサーフェスグラデーションを設定する。
  • QCustom3DItemQCustom3DLabel を使ってカスタム項目とラベルを追加する。
  • カスタム入力ハンドラを使用してズームとパンを有効にする。
  • サーフェスのエリアをハイライトする。

基本的なアプリケーションの作成については、棒グラフを参照してください。

生成されたデータによる単純なサーフェス
  1. まず、新しいQSurfaceDataProxy をインスタンス化し、新しいQSurface3DSeries にアタッチする:
    m_sqrtSinProxy = new QSurfaceDataProxy();
    m_sqrtSinSeries = new QSurface3DSeries(m_sqrtSinProxy);
  2. プロキシに単純な矩形波と正弦波のデータを入れる。QSurfaceDataArray インスタンスを作成し、QSurfaceDataRow 要素を追加します。resetArray() を呼び出して、作成したQSurfaceDataArrayQSurfaceDataProxy のデータ配列として設定する。
    QSurfaceDataArray dataArray;
    dataArray.reserve(sampleCountZ);
    for (int i = 0; i < sampleCountZ; ++i) {
        QSurfaceDataRow newRow;
        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);
マルチシリーズ高さマップデータ

高さデータを含むQImageQHeightMapSurfaceDataProxy をインスタンス化し、高さマップを作成する。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

  1. エンコーディング乗数を定義する:
    // Value used to encode height data as RGB value on PNG file
    const float packingFactor = 11983.f;
  2. 実際のデコードを行う:
    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);
    
    QSurfaceDataArray dataArray;
    dataArray.reserve(imageHeight);
    for (int i = 0; i < imageHeight; ++i) {
        int p = i * widthBits;
        float z = height - float(i) * stepZ;
        QSurfaceDataRow newRow;
        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 では、単純に生成された系列が有効になります。

  1. サーフェスのグリッドを有効にしたり、フラット・シェーディング・モードを選択するなど、装飾的な機能を設定します。
  2. 軸ラベルのフォーマットと値の範囲を定義する。低いカメラアングルでのラベルの読みやすさを向上させるために、ラベルの自動回転を設定する。
  3. 正しい系列がグラフに追加され、他の系列が追加されていないことを確認します。
m_sqrtSinSeries->setDrawMode(QSurface3DSeries::DrawSurfaceAndWireframe);
m_sqrtSinSeries->setShading(QSurface3DSeries::Shading::Flat);

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()->setLabelAutoAngle(30.f);
m_graph->axisY()->setLabelAutoAngle(90.f);
m_graph->axisZ()->setLabelAutoAngle(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->setDragButton(Qt::LeftButton);
QObject::connect(m_graph,
                 &Q3DGraphsWidgetItem::dragged,
                 this,
                 &SurfaceGraphModifier::handleAxisDragging);

QObject::connect(m_graph,
                 &Q3DGraphsWidgetItem::wheel,
                 this,
                 &SurfaceGraphModifier::onWheel);
m_graph->setZoomEnabled(false);

このデータセットのカスタム入力ハンドラについては「カスタム入力ハンドラを使用してズームとパンを有効にする」を参照してください。

選択モード

Q3DSurfaceWidgetItem でサポートされている3つの選択モードは、ラジオボタンで使用できます。選択モードをアクティブにしたりクリアしたりするには、以下のインラインメソッドを追加します:

void toggleModeNone() { m_graph->setSelectionMode(QtGraphs3D::SelectionFlag::None); }
void toggleModeItem() { m_graph->setSelectionMode(QtGraphs3D::SelectionFlag::Item); }
void toggleModeSliceRow()
{
    m_graph->setSelectionMode(QtGraphs3D::SelectionFlag::ItemAndRow
                              | QtGraphs3D::SelectionFlag::Slice
                              | QtGraphs3D::SelectionFlag::MultiSeries);
}
void toggleModeSliceColumn()
{
    m_graph->setSelectionMode(QtGraphs3D::SelectionFlag::ItemAndColumn
                              | QtGraphs3D::SelectionFlag::Slice
                              | QtGraphs3D::SelectionFlag::MultiSeries);
}

グラフのすべての可視系列を同時にスライス選択することをサポートするために、行と列の選択モードにQtGraphs3D::SelectionFlag::SliceQtGraphs3D::SelectionFlag::MultiSeries フラグを追加する。

グラフを研究するための軸範囲

この例には、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::ColorStyle::RangeGradientに変更してください。

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(QGraphsTheme::ColorStyle::RangeGradient);
アプリケーションにカスタムメッシュを追加する

アプリケーションにカスタムメッシュを追加する:

  • cmakeビルドの場合CMakeLists.txt にメッシュファイルを追加する:
    set(graphgallery_resource_files
        ...
        "data/oilrig.mesh"
        "data/pipe.mesh"
        "data/refinery.mesh"
        ...
    )
    
    qt6_add_resources(widgetgraphgallery "widgetgraphgallery"
        PREFIX
            "/"
        FILES
            ${graphgallery_resource_files}
    )
  • qmakeビルドの場合。qrcリソースファイルにメッシュファイルを追加する:
    <RCC>
        <qresource prefix="/">
            ...
            <file>data/refinery.mesh</file>
            <file>data/oilrig.mesh</file>
            <file>data/pipe.mesh</file>
            ...
        </qresource>
    </RCC>
グラフへのカスタム項目の追加

Multiseries Height Map データセットでは、カスタム項目がグラフに挿入され、チェックボックスを使ってオン/オフを切り替えることができます。その他の視覚的な変化も、チェックボックスで制御することができます。

  • 小さなQImage を作成します。カスタムオブジェクトの色として使用する単色で塗りつぶします:
    QImage color = QImage(2, 2, QImage::Format_RGB32);
    color.fill(Qt::red);
  • アイテムの位置を変数で指定します。この位置は、グラフから正しいアイテムを取り除くのに使われます:
    QVector3D positionOne = QVector3D(39.f, 77.f, 19.2f);
  • すべてのパラメータで新しいQCustom3DItem
    auto *item = new QCustom3DItem(":/data/oilrig.mesh",
                                   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_graph->axisX()->setRange(m_axisXMinValue, m_axisXMaxValue);
    break;

ズームについては、wheelEvent をキャッチし、QWheelEvent のデルタ値に従ってX軸とY軸の範囲を調整する。Y軸とXZ平面の縦横比が変わらないようにY軸を調整する。これにより、高さが誇張されたグラフになるのを防ぐことができます:

void SurfaceGraphModifier::onWheel(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_graph->axisX()->setRange(m_axisXMinValue, m_axisXMaxValue);
    m_graph->axisY()->setRange(100.f, y);
    m_graph->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->dataArray().at(0).size();
    m_srcHeight = m_topographicSeries->dataArray().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.x() - halfWidth;
    if (startX < 0)
        startX = 0;
    int endX = position.x() + halfWidth;
    if (endX > (m_srcWidth - 1))
        endX = m_srcWidth - 1;
    int startZ = position.y() - halfHeight;
    if (startZ < 0)
        startZ = 0;
    int endZ = position.y() + halfHeight;
    if (endZ > (m_srcHeight - 1))
        endZ = m_srcHeight - 1;

    const QSurfaceDataArray &srcArray = m_topographicSeries->dataArray();

    QSurfaceDataArray dataArray;
    dataArray.reserve(endZ - startZ);
    for (int i = startZ; i < endZ; ++i) {
        QSurfaceDataRow newRow;
        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() + m_heightAdjustment);
            newRow.append(QSurfaceDataItem(pos));
        }
        dataArray.append(newRow);
    }

    dataProxy()->resetArray(dataArray);
    setVisible(true);
}
ハイライト・シリーズへのグラデーション

HighlightSeriesQSurface3DSeries であるため、系列が持ちうるすべての装飾方法が利用可能である。この例では、標高を強調するためにグラデーションを追加します。適切なグラデーションのスタイルは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(QGraphsTheme::ColorStyle::RangeGradient);

    handleZoomChange(ratio);
}

コンテンツ例

プロジェクト例 @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.