グラフギャラリー

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

グラフ・ギャラリーは、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() メソッドで variant データセットを投入します:
    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();
        }
        ...
  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);

他のサーフェスレイヤーも同じように、ハイトマップ画像を使ってプロキシと系列を作成して追加します。-

地形図データ

地形データはフィンランド国土調査局(National Land Survey of Finland)から入手した。これは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

©2024 The Qt Company Ltd. 本文書に含まれる文書の著作権は、それぞれの所有者に帰属します。 ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。