그래프 갤러리

막대형, 분산형 및 표면 그래프 갤러리입니다.

그래프 갤러리는 세 가지 그래프 유형과 일부 특수 기능을 모두 보여줍니다. 그래프는 애플리케이션에 자체 탭이 있습니다.

예제 실행하기

에서 예제를 실행하려면 Qt Creator에서 Welcome 모드를 열고 Examples 에서 예제를 선택합니다. 자세한 내용은 예제 작성 및 실행하기를 참조하세요.

막대 그래프

Bar Graph 탭에서 Q3DBars 을 사용하여 3D 막대 그래프를 만들고 위젯을 사용하여 다양한 막대 그래프 품질을 조정합니다. 이 예제에서는 그 방법을 보여 줍니다:

  • Q3DBars 및 일부 위젯을 사용하여 애플리케이션 만들기
  • QBar3DSeriesQBarDataProxy 을 사용하여 그래프에 데이터 설정하기
  • 위젯 컨트롤을 사용하여 일부 그래프 및 계열 속성 조정하기
  • 축 레이블을 클릭하여 행 또는 열 선택하기
  • 함께 사용할 사용자 지정 프록시 만들기 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()을 사용하여 작은 자동 회전 각도를 부여합니다. 이렇게 하면 극단적인 카메라 각도에서 축 레이블의 가독성이 향상됩니다.

다음으로 계열의 시각적 속성을 초기화합니다. 두 번째 계열은 처음에는 보이지 않습니다:

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;

씬 및 카메라 사용에 대한 자세한 내용은 Q3DSceneQ3DCamera 을 참조하십시오.

그래프에 데이터 추가하기

생성자 마지막에 데이터를 설정하는 메서드를 호출합니다:

resetTemperatureData();

이 메서드는 두 시리즈의 프록시에 데이터를 추가합니다:

// 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 의 메서드에 연결합니다:

QObject::connect(rotationSliderX, &QSlider::valueChanged, modifier, &GraphModifier::rotateX);

GraphModifier 에 신호 연결을 위한 슬롯을 만듭니다. 카메라는 장면 객체를 통해 제어됩니다. 이번에는 미리 설정된 카메라 각도를 지정하는 대신 중심점을 중심으로 궤도를 따라 실제 카메라 위치를 지정합니다:

void GraphModifier::rotateX(int rotation)
{
    m_xRotation = rotation;
    m_graph->scene()->activeCamera()->setCameraPosition(m_xRotation, m_yRotation);
}

이제 슬라이더를 사용하여 그래프를 회전할 수 있습니다.

세로 레이아웃에 더 많은 위젯을 추가하여 제어할 수 있습니다:

  • 그래프 회전
  • 라벨 스타일
  • 카메라 사전 설정
  • 배경 가시성
  • 그리드 가시성
  • 막대 음영 부드러움
  • 두 번째 막대 시리즈의 가시성
  • 값 축 방향
  • 축 제목 가시성 및 회전
  • 표시할 데이터 범위
  • 막대 스타일
  • 선택 모드
  • 테마
  • 그림자 품질
  • 글꼴
  • 글꼴 크기
  • 축 레이블 회전
  • 데이터 모드

Custom Proxy Data 데이터 모드에서는 일부 위젯 컨트롤이 의도적으로 비활성화됩니다.

축 레이블을 클릭하여 행 또는 열 선택하기

축 레이블별 선택은 막대 그래프의 기본 기능입니다. 예를 들어 다음과 같은 방법으로 축 레이블을 클릭하여 행을 선택할 수 있습니다:

  1. 선택 모드를 다음과 같이 변경합니다. SelectionRow
  2. 연도 레이블을 클릭합니다.
  3. 클릭한 연도가 있는 행이 선택됩니다.

SelectionRow 또는 SelectionColumn 플래그가 설정되어 있는 경우 SelectionSliceSelectionItem 플래그에서도 동일한 방법이 작동합니다.

선택 항목으로 확대하기

카메라 대상을 조정하는 예로 버튼을 눌러 선택 영역으로 확대하는 애니메이션을 구현해 보겠습니다. 애니메이션 초기화는 생성자에서 수행됩니다:

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 에서 제공하는 항목 모델 기반 프록시와 유사하며, 데이터를 해석하기 위해 매핑이 필요합니다.

변형 데이터 세트

데이터 항목을 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

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

RainfallData 클래스의 사용자 정의 프록시를 사용하여 QBar3DSeries 의 설정을 처리합니다:

m_proxy = new VariantBarDataProxy;
m_series = new QBar3DSeries(m_proxy);

addDataSet() 메서드에서 변형 데이터 세트를 채웁니다:

void RainfallData::addDataSet() { // 새 변형 데이터 세트 및 데이터 항목 목록 생성m_dataSet = new VariantDataSet; auto *itemList = new VariantDataItemList; // 데이터 파일에서 데이터 항목 목록으로 데이터 읽기   QFile dataFile(":/data/raindata.txt"); if (dataFile.open(QIODevice::읽기 전용 | QIODevice::Text)) { QTextStream stream(&dataFile); while (!stream.atEnd()) { { QString line = stream.readLine(); if (line.startsWith('#')) // 주석은 계속  무시; const auto strList = QStringView{line}.split(',', Qt::SkipEmptyParts); // 각 줄에는 세 개의 데이터 항목이 있습니다: 연도, 월, 강우량 값 if (strList.size() < 3) {.                qWarning() << "Invalid row read from data:" << line;
               계속; } // 연도와 월을 문자열로, 강우량 값을 더블로 // 변형 데이터 항목에 저장하고 해당 항목을 항목 목록에 추가합니다.
           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 분산 그래프를 만듭니다. 이 예는 방법을 보여줍니다:

기본 애플리케이션 생성에 대해서는 막대 그래프를 참조하세요.

스캐터 그래프 설정하기

먼저 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 을 설정하고 데이터를 설정합니다.
  • 3D 높이 맵을 표시하려면 QHeightMapSurfaceDataProxy 을 사용합니다.
  • 지형 데이터를 사용하여 3D 높이 맵을 만듭니다.
  • 그래프를 연구하기 위해 세 가지 선택 모드를 사용합니다.
  • 축 범위를 사용하여 그래프의 선택된 부분을 표시합니다.
  • 사용자 지정 표면 그라데이션을 설정합니다.
  • QCustom3DItemQCustom3DLabel 을 사용하여 사용자 지정 항목과 레이블을 추가합니다.
  • 사용자 지정 입력 핸들러를 사용하여 확대/축소 및 이동을 활성화합니다.
  • 서페이스의 영역을 강조 표시합니다.

기본 애플리케이션 생성에 대해서는 막대 그래프를 참조하세요.

생성된 데이터가 있는 간단한 서피스

먼저 새 QSurfaceDataProxy 을 인스턴스화하여 새 QSurface3DSeries 에 첨부합니다:

m_sqrtSinProxy = new QSurfaceDataProxy();
m_sqrtSinSeries = new QSurface3DSeries(m_sqrtSinProxy);

그런 다음 프록시를 간단한 제곱근과 사인파 데이터로 채웁니다. 새 QSurfaceDataArray 인스턴스를 생성하고 QSurfaceDataRow 요소를 추가합니다. resetArray() 을 호출하여 생성된 QSurfaceDataArrayQSurfaceDataProxy 의 데이터 배열로 설정합니다.

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° N - 40.0° N 및 18.0° E - 24.0° E의 가상의 위치에서 축에 맵을 배치하는 데 사용됩니다.

// 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 이라는 제품을 제공합니다. 지형 데이터는 리바이 폴드에서 가져온 것입니다. 데이터의 정확도가 필요 이상으로 높기 때문에 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 에는 시리즈 간에 전환할 수 있는 세 개의 라디오 버튼이 있습니다.

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 에서 지원하는 세 가지 선택 모드는 라디오 버튼과 함께 사용할 수 있습니다. 선택한 모드를 활성화하거나 지우려면 다음 인라인 메서드를 추가합니다:

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::SelectionSliceQAbstract3DGraph::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 데이터 세트에서는 두 개의 푸시 버튼으로 사용자 지정 표면 그라데이션을 사용할 수 있습니다. 원하는 색상이 설정된 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 데이터 세트를 사용하면 사용자 정의 항목이 그래프에 삽입되고 체크박스를 사용하여 켜거나 끌 수 있습니다. 두 개의 위쪽 레이어에 대한 시스루, 아래쪽 레이어에 대한 하이라이트 등 다른 체크박스 세트를 사용하여 다른 시각적 품질도 제어할 수 있습니다.

작은 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.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);
}
하이라이트 시리즈에 그라데이션 적용하기

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(Q3DTheme::ColorStyleRangeGradient);
}

예제 내용

예제 프로젝트 @ 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.