그래프 갤러리
막대, 산점도 및 표면 그래프 갤러리.
그래프 갤러리에서는 세 가지 그래프 유형과 그 중 일부의 특수 기능을 보여줍니다. 각 그래프에는 애플리케이션 내에 별도의 탭이 마련되어 있습니다.

예제 실행
다음 경로에서 예제를 실행할 수 있습니다:
- Qt Creator
Welcome 모드를 열고 Examples 에서 예제를 선택하십시오. 자세한 내용은 Qt Creator: 튜토리얼: 빌드 및 실행을 참조하십시오.
- Qt Extension for Visual Studio Code
Command Palette 에서 Qt: Open Qt examples 명령을 실행하고 목록에서 예제를 선택합니다. 자세한 내용은 Qt Extension for Visual Studio Code: 튜토리얼: 빌드 및 실행을 참조하십시오.
막대 그래프
Bar Graph 탭에서 Q3DBars를 사용하여 3D 막대 그래프를 만들고, 위젯을 조합하여 다양한 막대 그래프 특성을 조정해 보세요. 이 예제에서는 다음을 수행하는 방법을 보여줍니다:
- Q3DBars와 몇 가지 위젯을 사용하여 애플리케이션 만들기
- QBar3DSeries 및 QBarDataProxy를 사용하여 그래프에 데이터를 설정하기
- 위젯 컨트롤을 사용하여 그래프 및 시리즈 속성을 조정하는 방법
- 축 레이블을 클릭하여 행 또는 열 선택하기
- Q3DBars와 함께 사용할 사용자 정의 프록시 생성하기
그래프와의 상호 작용에 대한 자세한 내용은 이 페이지를 참조하십시오.
애플리케이션 생성
먼저, ` bargraph.cpp`에서 Q3DBars를 인스턴스화합니다:
m_barsGraph = new Q3DBars();그런 다음 위젯과 가로 및 세로 레이아웃을 생성합니다.
그래프는 QWidget::createWindowContainer()를 사용하여 창 컨테이너에 삽입됩니다. 모든 데이터 시각화 그래프 클래스(Q3DBars, Q3DScatter, Q3DSurface)는 QWindow 를 상속받기 때문에 이 과정이 필수적입니다. 이는 QWindow 를 상속받은 클래스를 위젯으로 사용하는 유일한 방법입니다.
그래프와 수직 레이아웃을 수평 레이아웃에 추가합니다:
m_barsWidget = new QWidget;
auto *hLayout = new QHBoxLayout(m_barsWidget);
m_container = QWidget::createWindowContainer(m_barsGraph, m_barsWidget);
m_barsGraph->resize(minimumGraphSize);
m_container->setMinimumSize(minimumGraphSize);
m_container->setMaximumSize(maximumGraphSize);
m_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_container->setFocusPolicy(Qt::StrongFocus);
hLayout->addWidget(m_container, 1);
auto *vLayout = new QVBoxLayout();
hLayout->addLayout(vLayout);다음으로, 데이터 추가 및 그래프와의 기타 상호 작용을 처리할 또 다른 클래스를 생성합니다:
auto *modifier = new GraphModifier(m_barsGraph, this);막대 그래프 설정
GraphModifier 클래스의 생성자에서 그래프를 설정합니다:
GraphModifier::GraphModifier(Q3DBars *bargraph, QObject *parent) :
QObject(parent),
m_graph(bargraph),먼저, 축과 시계를 멤버 변수로 선언하여 쉽게 변경할 수 있도록 합니다:
m_temperatureAxis(new QValue3DAxis),
m_yearAxis(new QCategory3DAxis),
m_monthAxis(new QCategory3DAxis),
m_primarySeries(new QBar3DSeries),
m_secondarySeries(new QBar3DSeries),
m_celsiusString(u"°C"_s)그런 다음, 그래프의 시각적 속성을 설정합니다:
m_graph->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftMedium);
m_graph->activeTheme()->setBackgroundEnabled(false);
m_graph->activeTheme()->setFont(QFont("Times New Roman", m_fontSize));
m_graph->activeTheme()->setLabelBackgroundEnabled(true);
m_graph->setMultiSeriesUniform(true);축을 설정하고 이를 그래프의 활성 축으로 지정합니다:
m_temperatureAxis->setTitle("Average temperature");
m_temperatureAxis->setSegmentCount(m_segments);
m_temperatureAxis->setSubSegmentCount(m_subSegments);
m_temperatureAxis->setRange(m_minval, m_maxval);
m_temperatureAxis->setLabelFormat(u"%.1f "_s + m_celsiusString);
m_temperatureAxis->setLabelAutoRotation(30.0f);
m_temperatureAxis->setTitleVisible(true);
m_yearAxis->setTitle("Year");
m_yearAxis->setLabelAutoRotation(30.0f);
m_yearAxis->setTitleVisible(true);
m_monthAxis->setTitle("Month");
m_monthAxis->setLabelAutoRotation(30.0f);
m_monthAxis->setTitleVisible(true);
m_graph->setValueAxis(m_temperatureAxis);
m_graph->setRowAxis(m_yearAxis);
m_graph->setColumnAxis(m_monthAxis);setLabelAutoRotation()을 사용하여 축 레이블에 작은 자동 회전 각도를 부여하여 레이블이 카메라를 향해 약간 기울어지도록 합니다. 이렇게 하면 극단적인 카메라 각도에서도 축 레이블의 가독성이 향상됩니다.
다음으로, 시리즈의 시각적 속성을 초기화합니다. 두 번째 시리즈는 처음에는 표시되지 않는다는 점에 유의하십시오:
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();카메라는 그래프의 scene 객체를 통해 제어됩니다:
static int preset = Q3DCamera::CameraPresetFront;
m_graph->scene()->activeCamera()->setCameraPreset((Q3DCamera::CameraPreset)preset);
if (++preset > Q3DCamera::CameraPresetDirectlyBelow)
preset = Q3DCamera::CameraPresetFrontLow;씬과 카메라 사용에 대한 자세한 내용은 Q3DScene 및 Q3DCamera를 참조하십시오.
그래프에 데이터 추가하기
생성자 끝부분에서 데이터를 설정하는 메서드를 호출합니다:
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`의 메서드에 연결하세요:
GraphModifier 에 신호 연결을 위한 슬롯을 생성합니다. 카메라는 scene 객체를 통해 제어됩니다. 이번에는 미리 설정된 카메라 각도를 지정하는 대신, 중심점을 중심으로 한 궤도상의 실제 카메라 위치를 지정합니다:
void GraphModifier::rotateX(int rotation)
{
m_xRotation = rotation;
m_graph->scene()->activeCamera()->setCameraPosition(m_xRotation, m_yRotation);
}이제 슬라이더를 사용하여 그래프를 회전할 수 있습니다.
제어할 위젯을 수직 레이아웃에 더 추가하세요:
- 그래프 회전
- 라벨 스타일
- 카메라 프리셋
- 배경 표시 여부
- 격자 표시 여부
- 막대 음영 부드러움
- 두 번째 막대 시리즈의 표시 여부
- 값 축 방향
- 축 제목 표시 여부 및 회전
- 표시할 데이터 범위
- 막대 스타일
- 선택 모드
- 테마
- 그림자 품질
- 글꼴
- 글꼴 크기
- 축 레이블 회전
- 데이터 모드
Custom Proxy Data 데이터 모드에서는 일부 위젯 컨트롤이 의도적으로 비활성화됩니다.
축 레이블 클릭을 통한 행 또는 열 선택
축 레이블을 통한 선택은 막대 그래프의 기본 기능입니다. 예를 들어, 다음과 같은 방법으로 축 레이블을 클릭하여 행을 선택할 수 있습니다:
- 선택 모드를
SelectionRow - 연도 레이블을 클릭합니다
- 클릭한 연도가 포함된 행이 선택됩니다
SelectionRow 또는 SelectionColumn 중 하나가 설정되어 있는 경우, SelectionSlice 및 SelectionItem 플래그에서도 동일한 방법이 적용됩니다.
선택 영역으로 확대/축소
카메라 대상을 조정하는 예시로, 버튼을 누르면 선택 영역으로 확대하는 애니메이션을 구현해 보겠습니다. 애니메이션 초기화는 생성자에서 수행됩니다:
Q3DCamera *camera = m_graph->scene()->activeCamera();
m_defaultAngleX = camera->xRotation();
m_defaultAngleY = camera->yRotation();
m_defaultZoom = camera->zoomLevel();
m_defaultTarget = camera->target();
m_animationCameraX.setTargetObject(camera);
m_animationCameraY.setTargetObject(camera);
m_animationCameraZoom.setTargetObject(camera);
m_animationCameraTarget.setTargetObject(camera);
m_animationCameraX.setPropertyName("xRotation");
m_animationCameraY.setPropertyName("yRotation");
m_animationCameraZoom.setPropertyName("zoomLevel");
m_animationCameraTarget.setPropertyName("target");
int duration = 1700;
m_animationCameraX.setDuration(duration);
m_animationCameraY.setDuration(duration);
m_animationCameraZoom.setDuration(duration);
m_animationCameraTarget.setDuration(duration);
// The zoom always first zooms out above the graph and then zooms in
qreal zoomOutFraction = 0.3;
m_animationCameraX.setKeyValueAt(zoomOutFraction, QVariant::fromValue(0.0f));
m_animationCameraY.setKeyValueAt(zoomOutFraction, QVariant::fromValue(90.0f));
m_animationCameraZoom.setKeyValueAt(zoomOutFraction, QVariant::fromValue(50.0f));
m_animationCameraTarget.setKeyValueAt(zoomOutFraction,
QVariant::fromValue(QVector3D(0.0f, 0.0f, 0.0f)));GraphModifier::zoomToSelectedBar() 함수에는 확대/축소 기능이 포함되어 있습니다. QPropertyAnimation 은 m_animationCameraTarget 을 대상으로 하며, 은 Q3DCamera::target 속성을 대상으로 하는데, 이 속성은 (-1, 1) 범위로 정규화된 값을 취합니다.
선택된 막대가 축에 대해 어디에 위치하는지 파악하고, 이를 m_animationCameraTarget 의 종료 값으로 사용합니다:
QVector3D endTarget;
float xMin = m_graph->columnAxis()->min();
float xRange = m_graph->columnAxis()->max() - xMin;
float zMin = m_graph->rowAxis()->min();
float zRange = m_graph->rowAxis()->max() - zMin;
endTarget.setX((selectedBar.y() - xMin) / xRange * 2.0f - 1.0f);
endTarget.setZ((selectedBar.x() - zMin) / zRange * 2.0f - 1.0f);
...
m_animationCameraTarget.setEndValue(QVariant::fromValue(endTarget));그런 다음, 애니메이션이 끝날 때 카메라가 항상 그래프의 중심을 대략적으로 향하도록 카메라를 회전시킵니다:
qreal endAngleX = 90.0 - qRadiansToDegrees(qAtan(qreal(endTarget.z() / endTarget.x())));
if (endTarget.x() > 0.0f)
endAngleX -= 180.0f;
float barValue = m_graph->selectedSeries()->dataProxy()->itemAt(selectedBar.x(),
selectedBar.y())->value();
float endAngleY = barValue >= 0.0f ? 30.0f : -30.0f;
if (m_graph->valueAxis()->reversed())
endAngleY *= -1.0f;데이터용 맞춤형 프록시
Custom Proxy Data 데이터 모드를 켜면 사용자 정의 데이터 세트와 해당 프록시가 사용됩니다.
간단하고 유연한 데이터 세트인 ` VariantDataSet`를 정의합니다. 여기서 각 데이터 항목은 변형 목록입니다. 각 항목은 목록 내 인덱스로 식별되는 여러 값을 가질 수 있습니다. 이 경우, 데이터셋은 월별 강우량 데이터를 저장하며, 인덱스 0의 값은 연도, 인덱스 1의 값은 월, 인덱스 2의 값은 해당 월의 강우량입니다.
이 사용자 정의 프록시는 Qt Data Visualization 에서 제공하는 itemmodel 기반 프록시와 유사하며, 데이터를 해석하기 위해서는 매핑이 필요합니다.
VariantDataSet
데이터 항목을 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
VariantBarDataProxy 를 QBarDataProxy의 서브클래스로 만들고, 데이터 세트 및 매핑에 대한 간단한 게터 및 세터 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::ReadOnly | QIODevice::Text)) {
QTextStream stream(&dataFile);
while (!stream.atEnd()) {
QString line = stream.readLine();
if (line.startsWith('#')) // 주석 무시
continue;
const auto strList = QStringView{line}.split(',', Qt::SkipEmptyParts);
// 각 줄에는 연도, 월, 강수량 값의 세 가지 데이터 항목이 있습니다.
if (strList.size() < 3) {
qWarning() << "Invalid row read from data:" << line;
continue;
}
// 연도와 월은 문자열로, 강우량 값은 double 형으로
// VariantDataItem에 저장 하고, 해당 항목을 항목 목록에 추가합니다.
auto *newItem = new VariantDataItem;
for (int i = 0; i < 2;++i)
newItem->append(strList.at(i).trimmed().toString());
newItem->append(strList.at(2).trimmed().toDouble());
itemList->append(newItem);
}
} else {
qWarning() << "Unable to open data file:" << dataFile.fileName();
}
...데이터 세트를 사용자 정의 프록시에 추가하고 매핑을 설정합니다:
// Add items to the data set and set it to the proxy
m_dataSet->addItems(itemList);
m_proxy->setDataSet(m_dataSet);
// Create new mapping for the data and set it to the proxy
m_mapping = new VariantBarDataMapping(0, 1, 2, m_years, m_numericMonths);
m_proxy->setMapping(m_mapping);마지막으로, 생성된 시계열을 가져와 표시할 수 있는 함수를 추가합니다:
QBar3DSeries *customSeries() { return m_series; }산점도
Scatter Graph 탭에서 Q3DScatter를 사용하여 3D 산점도를 생성합니다. 이 예제는 다음을 수행하는 방법을 보여줍니다:
- Q3DScatter 그래프 설정
- QScatterDataProxy를 사용하여 그래프에 데이터 설정하기
- Q3DInputHandler를 상속하여 사용자 정의 입력 핸들러 생성하기
기본적인 애플리케이션 제작 방법은 막대 그래프를 참조하십시오.
산점도 그래프 설정
먼저, ` ScatterDataModifier`의 생성자에서 그래프의 시각적 속성을 설정합니다:
m_graph->activeTheme()->setType(Q3DTheme::ThemeStoneMoss);
m_graph->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftHigh);
m_graph->scene()->activeCamera()->setCameraPreset(Q3DCamera::CameraPresetFront);
m_graph->scene()->activeCamera()->setZoomLevel(80.f);이 중 어느 것도 필수 사항은 아니지만, 그래프의 기본값을 재정의하는 데 사용됩니다. 위의 코드 블록을 주석 처리하여 사전 설정된 기본값으로 어떻게 표시되는지 확인해 볼 수 있습니다.
다음으로, QScatterDataProxy와 관련 QScatter3DSeries를 생성합니다. 시리즈에 대한 사용자 정의 레이블 형식과 메쉬 스무딩을 설정하고 그래프에 추가합니다:
auto *proxy = new QScatterDataProxy;
auto *series = new QScatter3DSeries(proxy);
series->setItemLabelFormat(u"@xTitle: @xLabel @yTitle: @yLabel @zTitle: @zLabel"_s);
series->setMeshSmooth(m_smooth);
m_graph->addSeries(series);산점도 데이터 추가하기
ScatterDataModifier 의 생성자에서 마지막으로 해야 할 일은 그래프에 데이터를 추가하는 것입니다:
addData();실제 데이터 추가 작업은 ` addData() ` 메서드에서 수행됩니다. 먼저, 축을 설정합니다:
m_graph->axisX()->setTitle("X");
m_graph->axisY()->setTitle("Y");
m_graph->axisZ()->setTitle("Z");ScatterDataModifier 의 생성자에서도 이 작업을 수행할 수 있습니다. 여기에서 설정하면 생성자를 더 간결하게 유지할 수 있으며, 축 설정을 데이터 근처에 배치할 수 있습니다.
다음으로, 데이터 배열을 생성하고 데이터를 채웁니다:
auto *dataArray = new QScatterDataArray;
dataArray->reserve(m_itemCount);
...
const float limit = qSqrt(m_itemCount) / 2.0f;
for (int i = -limit; i < limit; ++i) {
for (int j = -limit; j < limit; ++j) {
const float x = float(i) + 0.5f;
const float y = qCos(qDegreesToRadians(float(i * j) / m_curveDivider));
const float z = float(j) + 0.5f;
dataArray->append(QScatterDataItem({x, y, z}));
}
}마지막으로, 프록시에게 우리가 제공한 데이터를 사용하도록 지시합니다:
m_graph->seriesList().at(0)->dataProxy()->resetArray(dataArray);이제 그래프에 데이터가 반영되어 사용 준비가 완료되었습니다. 그래프를 제어하기 위해 위젯을 추가하는 방법에 대한 자세한 내용은 ‘위젯을 사용하여 그래프 제어하기’를 참조하십시오.
기본 입력 처리 방식 변경
생성자에서 산점도 그래프 인스턴스에 대한 포인터를 사용하여 ` m_inputHandler `를 초기화하십시오:
m_inputHandler(new AxesInputHandler(scatter))사용자 정의 동작을 구현하는 ` AxesInputHandler`를 `Q3DScatter`의 활성 입력 핸들러로 설정하여 기본 입력 처리 메커니즘을 대체합니다:
// Give ownership of the handler to the graph and make it the active handler
m_graph->setActiveInputHandler(m_inputHandler);입력 핸들러는 그래프의 축에 접근할 수 있어야 하므로, 해당 축을 입력 핸들러에 전달하십시오:
// Give our axes to the input handler
m_inputHandler->setAxes(m_graph->axisX(), m_graph->axisZ(), m_graph->axisY());마우스 이벤트 처리 확장
먼저, 기본 입력 처리의 모든 기능을 유지하면서 그 위에 사용자 정의 기능을 추가하기 위해, 사용자 정의 입력 핸들러를 QAbstract3DInputHandler 대신 Q3DInputHandler에서 상속받으세요:
class AxesInputHandler : public Q3DInputHandler일부 마우스 이벤트를 재구현하여 기본 기능을 확장하기 시작합니다. 먼저 ` mousePressEvent`을 확장합니다. 여기에 마우스 왼쪽 버튼에 대한 ` m_mousePressed ` 플래그를 추가하고, 나머지 기본 기능은 그대로 유지합니다:
void AxesInputHandler::mousePressEvent(QMouseEvent *event, const QPoint &mousePos)
{
Q3DInputHandler::mousePressEvent(event, mousePos);
if (Qt::LeftButton == event->button())
m_mousePressed = true;
}다음으로, ` mouseReleaseEvent `를 수정하여 플래그를 지우고 내부 상태를 초기화하세요:
void AxesInputHandler::mouseReleaseEvent(QMouseEvent *event, const QPoint &mousePos)
{
Q3DInputHandler::mouseReleaseEvent(event, mousePos);
m_mousePressed = false;
m_state = StateNormal;
}그런 다음, ` mouseMoveEvent`를 수정합니다. ` m_mousePressed ` 플래그가 ` true `인지, 그리고 내부 상태가 ` StateNormal` 이외의 상태인지 확인합니다. 그렇다면 마우스 이동 거리 계산을 위한 입력 위치를 설정하고, 축 드래깅 함수를 호출합니다(자세한 내용은 ‘축 드래깅 구현’ 참조):
void AxesInputHandler::mouseMoveEvent(QMouseEvent *event, const QPoint &mousePos)
{
// Check if we're trying to drag axis label
if (m_mousePressed && m_state != StateNormal) {
setPreviousInputPos(inputPosition());
setInputPosition(mousePos);
handleAxisDragging();
} else {
Q3DInputHandler::mouseMoveEvent(event, mousePos);
}
}축 드래그 구현
먼저, 그래프에서 선택 신호를 수신하도록 설정합니다. 생성자에서 이를 수행하고, 해당 신호를 ` handleElementSelected ` 메서드에 연결합니다:
// Connect to the item selection signal from graph
connect(graph, &QAbstract3DGraph::selectedElementChanged, this,
&AxesInputHandler::handleElementSelected);handleElementSelected 에서 선택의 유형을 확인하고, 그에 따라 내부 상태를 설정합니다:
switch (type) {
case QAbstract3DGraph::ElementAxisXLabel:
m_state = StateDraggingX;
break;
case QAbstract3DGraph::ElementAxisYLabel:
m_state = StateDraggingY;
break;
case QAbstract3DGraph::ElementAxisZLabel:
m_state = StateDraggingZ;
break;
default:
m_state = StateNormal;
break;
}실제 드래그 로직은 ` handleAxisDragging ` 메서드에 구현되어 있으며, 필요한 조건이 충족될 경우 ` mouseMoveEvent`에서 이 메서드가 호출됩니다:
// Check if we're trying to drag axis label
if (m_mousePressed && m_state != StateNormal) {handleAxisDragging 에서는 먼저 활성 카메라로부터 씬의 방향을 가져옵니다:
// Get scene orientation from active camera
float xRotation = scene()->activeCamera()->xRotation();
float yRotation = scene()->activeCamera()->yRotation();그런 다음, 이 방향을 바탕으로 마우스 이동 방향에 대한 수정 계수를 계산합니다:
// Calculate directional drag multipliers based on rotation
float xMulX = qCos(qDegreesToRadians(xRotation));
float xMulY = qSin(qDegreesToRadians(xRotation));
float zMulX = qSin(qDegreesToRadians(xRotation));
float zMulY = qCos(qDegreesToRadians(xRotation));그 후, 마우스 이동량을 계산하고, 카메라의 y축 회전 각도에 따라 이를 수정합니다:
// Get the drag amount
QPoint move = inputPosition() - previousInputPos();
// Flip the effect of y movement if we're viewing from below
float yMove = (yRotation < 0) ? -move.y() : move.y();그런 다음, 이동한 거리를 올바른 축에 적용합니다:
// Adjust axes
switch (m_state) {
case StateDraggingX:
distance = (move.x() * xMulX - yMove * xMulY) / m_speedModifier;
m_axisX->setRange(m_axisX->min() - distance, m_axisX->max() - distance);
break;
case StateDraggingZ:
distance = (move.x() * zMulX + yMove * zMulY) / m_speedModifier;
m_axisZ->setRange(m_axisZ->min() + distance, m_axisZ->max() + distance);
break;
case StateDraggingY:
distance = move.y() / m_speedModifier; // No need to use adjusted y move here
m_axisY->setRange(m_axisY->min() + distance, m_axisY->max() + distance);
break;
default:
break;
}마지막으로, 드래그 속도를 설정하는 함수를 추가하세요:
inline void setDragSpeedModifier(float modifier) { m_speedModifier = modifier; }마우스 이동 거리는 화면 좌표계에서 절대값이므로, 이를 축 범위에 맞게 조정해야 하기 때문에 이 기능이 필요합니다. 값이 클수록 드래그 속도가 느려집니다. 이 예제에서는 드래그 속도를 결정할 때 씬의 확대/축소 수준이 고려되지 않으므로, 확대/축소 수준을 변경하면 범위 조정에도 변화가 생기는 것을 확인할 수 있습니다.
또한 축 범위와 카메라 줌 레벨에 따라 수정자를 자동으로 조정할 수도 있습니다.
표면 그래프
Surface Graph 탭에서 Q3DSurface를 사용하여 3D 표면 그래프를 생성합니다. 이 예제에서는 다음을 수행하는 방법을 보여줍니다:
- 기본 QSurfaceDataProxy를 설정하고 데이터를 지정하는 방법.
- QHeightMapSurfaceDataProxy를 사용하여 3D 높이 맵을 표시하는 방법.
- 지형 데이터를 사용하여 3D 높이 맵을 생성하는 방법.
- 그래프를 분석하기 위해 세 가지 선택 모드를 사용하는 방법.
- 축 범위를 사용하여 그래프의 선택된 부분만 표시합니다.
- 사용자 정의 표면 그라데이션을 설정합니다.
- QCustom3DItem 및 QCustom3DLabel을 사용하여 사용자 정의 항목과 레이블을 추가합니다.
- 사용자 정의 입력 핸들러를 사용하여 확대/축소 및 이동 기능을 활성화합니다.
- 표면의 특정 영역을 강조 표시하십시오.
기본적인 애플리케이션 제작 방법은 막대 그래프를 참조하십시오.
생성된 데이터가 포함된 간단한 서피스
먼저, 새로운 QSurfaceDataProxy 인스턴스를 생성하고 이를 새로운 QSurface3DSeries에 연결합니다:
m_sqrtSinProxy = new QSurfaceDataProxy();
m_sqrtSinSeries = new QSurface3DSeries(m_sqrtSinProxy);그런 다음, 프록시에 간단한 제곱근 및 사인파 데이터를 채웁니다. 새로운 QSurfaceDataArray 인스턴스를 생성하고, 여기에 QSurfaceDataRow 요소를 추가합니다. resetArray() 를 호출하여 생성된 QSurfaceDataArray 를 QSurfaceDataProxy의 데이터 배열로 설정합니다.
auto *dataArray = new QSurfaceDataArray;
dataArray->reserve(sampleCountZ);
for (int i = 0 ; i < sampleCountZ ; ++i) {
auto *newRow = new QSurfaceDataRow;
newRow->reserve(sampleCountX);
// Keep values within range bounds, since just adding step can cause minor drift due
// to the rounding errors.
float z = qMin(sampleMax, (i * stepZ + sampleMin));
for (int j = 0; j < sampleCountX; ++j) {
float x = qMin(sampleMax, (j * stepX + sampleMin));
float R = qSqrt(z * z + x * x) + 0.01f;
float y = (qSin(R) / R + 0.24f) * 1.61f;
newRow->append(QSurfaceDataItem({x, y, z}));
}
dataArray->append(newRow);
}
m_sqrtSinProxy->resetArray(dataArray);다중 시리즈 높이 맵 데이터
높이 데이터를 포함하는 ` QImage `를 사용하여 `QHeightMapSurfaceDataProxy`를 인스턴스화하여 높이 맵을 생성합니다. ` QHeightMapSurfaceDataProxy::setValueRanges()`를 사용하여 맵의 값 범위를 정의합니다. 이 예제에서 맵은 북위 34.0° ~ 40.0°, 동경 18.0° ~ 24.0°의 가상 위치에 해당합니다. 이 값들은 맵을 좌표축상에 배치하는 데 사용됩니다.
// Create the first surface layer
QImage heightMapImageOne(":/data/layer_1.png");
m_heightMapProxyOne = new QHeightMapSurfaceDataProxy(heightMapImageOne);
m_heightMapSeriesOne = new QSurface3DSeries(m_heightMapProxyOne);
m_heightMapSeriesOne->setItemLabelFormat(u"(@xLabel, @zLabel): @yLabel"_s);
m_heightMapProxyOne->setValueRanges(34.f, 40.f, 18.f, 24.f);높이 지도 이미지를 사용하여 프록시와 시계를 생성하는 방식으로 다른 표면 레이어도 동일한 방법으로 추가하십시오.
지형도 데이터
지형 데이터는 핀란드 국토조사국(National Land Survey of Finland)에서 제공받았습니다. 이 기관은 본 예제에 적합한 ‘ Elevation Model 2 m ’라는 제품을 제공합니다. 지형 데이터는 레비(Levi) 산악 지대의 것입니다. 데이터의 정확도는 필요한 수준을 훨씬 상회하므로, 압축되어 PNG 파일로 인코딩되었습니다. 원본 ASCII 데이터의 높이 값은 곱셈 계수를 사용하여 RGB 형식으로 인코딩되며, 이에 대한 내용은 뒤이어 소개될 코드 예제에서 확인할 수 있습니다. 이 곱셈 계수는 핀란드에서 가장 높은 지점의 24비트 최대값을 나누어 계산됩니다.
QHeightMapSurfaceDataProxy는 1바이트 값만 변환합니다. 핀란드 국토조사국(National Land Survey of Finland)에서 제공한 데이터의 높은 정확도를 활용하려면, 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::SelectionSlice ` 및 ` QAbstract3DGraph::SelectionMultiSeries ` 플래그를 추가하십시오.
그래프 분석을 위한 축 범위
이 예제에는 X축과 Z축의 최소값 및 최대값을 조정하기 위한 네 개의 슬라이더 컨트롤이 있습니다. 프록시를 선택하면 이러한 슬라이더가 현재 데이터 세트의 축 범위에 맞춰 조정됩니다:
// 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 를 생성합니다. 사용자 정의 객체의 색상으로 사용할 단일 색상으로 채웁니다:
그런 다음, 변수에 항목의 위치를 지정합니다. 이 위치를 사용하여 그래프에서 해당 항목을 제거할 수 있습니다:
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;확대/축소(zooming)의 경우, ` wheelEvent ` 이벤트를 처리하고 ` QWheelEvent`의 델타 값에 따라 X 및 Y 축 범위를 조정합니다. Y 축과 XZ 평면 간의 종횡비가 동일하게 유지되도록 Y 축을 조정합니다. 이렇게 하면 그래프의 높이가 과장되어 표시되는 것을 방지할 수 있습니다:
void CustomInputHandler::wheelEvent(QWheelEvent *event)
{
float delta = float(event->angleDelta().y());
m_axisXMinValue += delta;
m_axisXMaxValue -= delta;
m_axisZMinValue += delta;
m_axisZMaxValue -= delta;
checkConstraints();
float y = (m_axisXMaxValue - m_axisXMinValue) * m_aspectRatio;
m_axisX->setRange(m_axisXMinValue, m_axisXMaxValue);
m_axisY->setRange(100.f, y);
m_axisZ->setRange(m_axisZMinValue, m_axisZMaxValue);
}다음으로, 표면과 너무 가까워지거나 멀어지지 않도록 확대/축소 수준에 제한을 추가합니다. 예를 들어, X축 값이 허용 한계를 밑돌 경우, 즉 확대/축소가 너무 멀리 진행될 경우, 해당 값은 허용되는 최소값으로 설정됩니다. 범위가 최소값 아래로 내려갈 경우, 범위가 한계 내에 머물도록 축의 양 끝을 조정합니다:
if (m_axisXMinValue < m_areaMinValue)
m_axisXMinValue = m_areaMinValue;
if (m_axisXMaxValue > m_areaMaxValue)
m_axisXMaxValue = m_areaMaxValue;
// Don't allow too much zoom in
if ((m_axisXMaxValue - m_axisXMinValue) < m_axisXMinRange) {
float adjust = (m_axisXMinRange - (m_axisXMaxValue - m_axisXMinValue)) / 2.f;
m_axisXMinValue -= adjust;
m_axisXMaxValue += adjust;
}표면의 특정 영역 강조 표시
표면에 강조 표시를 구현하려면 시계열의 복사본을 생성하고 y 값에 약간의 오프셋을 추가합니다. 이 예제에서 ` HighlightSeries ` 클래스는 ` handlePositionChange ` 메서드 내에서 복사본 생성을 구현합니다.
먼저, ` HighlightSeries `에 원본 시퀀스에 대한 포인터를 전달한 다음, ` QSurface3DSeries::selectedPointChanged ` 신호를 수신 대기하도록 설정합니다:
void HighlightSeries::setTopographicSeries(TopographicSeries *series)
{
m_topographicSeries = series;
m_srcWidth = m_topographicSeries->dataProxy()->array()->at(0)->size();
m_srcHeight = m_topographicSeries->dataProxy()->array()->size();
QObject::connect(m_topographicSeries, &QSurface3DSeries::selectedPointChanged,
this, &HighlightSeries::handlePositionChange);
}신호가 트리거되면 위치가 유효한지 확인합니다. 그런 다음 복사된 영역의 범위를 계산하고, 해당 범위가 경계 내에 있는지 확인합니다. 마지막으로, 하이라이트 시리얼의 데이터 배열을 지형 시리얼의 데이터 배열에서 가져온 범위로 채웁니다:
void HighlightSeries::handlePositionChange(const QPoint &position)
{
m_position = position;
if (position == invalidSelectionPosition()) {
setVisible(false);
return;
}
int halfWidth = m_width / 2;
int halfHeight = m_height / 2;
int startX = position.y() - halfWidth;
if (startX < 0 )
startX = 0;
int endX = position.y() + halfWidth;
if (endX > (m_srcWidth - 1))
endX = m_srcWidth - 1;
int startZ = position.x() - halfHeight;
if (startZ < 0 )
startZ = 0;
int endZ = position.x() + halfHeight;
if (endZ > (m_srcHeight - 1))
endZ = m_srcHeight - 1;
QSurfaceDataProxy *srcProxy = m_topographicSeries->dataProxy();
const QSurfaceDataArray &srcArray = *srcProxy->array();
auto *dataArray = new QSurfaceDataArray;
dataArray->reserve(endZ - startZ);
for (int i = startZ; i < endZ; ++i) {
auto *newRow = new QSurfaceDataRow;
newRow->reserve(endX - startX);
QSurfaceDataRow *srcRow = srcArray.at(i);
for (int j = startX; j < endX; ++j) {
QVector3D pos = srcRow->at(j).position();
pos.setY(pos.y() + 0.1f);
newRow->append(QSurfaceDataItem(pos));
}
dataArray->append(newRow);
}
dataProxy()->resetArray(dataArray);
setVisible(true);
}‘하이라이트’ 시리즈를 위한 그라데이션
HighlightSeries 는 QSurface3DSeries이므로, 시리즈가 가질 수 있는 모든 장식 메서드를 사용할 수 있습니다. 이 예제에서는 고도를 강조하기 위해 그라데이션을 추가합니다. 적합한 그라데이션 스타일은 Y축의 범위에 따라 달라지며, 확대/축소 시 범위가 변경되므로 범위가 변함에 따라 그라데이션 색상 위치도 조정되어야 합니다. 이를 위해 그라데이션 색상 위치에 비례 값을 정의합니다:
const float darkRedPos = 1.f;
const float redPos = 0.8f;
const float yellowPos = 0.6f;
const float greenPos = 0.4f;
const float darkGreenPos = 0.2f;그라데이션 수정은 handleGradientChange 메서드에서 수행되므로, Y축의 변화에 반응하도록 연결합니다:
QObject::connect(m_graph->axisY(), &QValue3DAxis::maxChanged,
m_highlight, &HighlightSeries::handleGradientChange);Y축의 최대값이 변경되면 새로운 그라데이션 색상 위치를 계산합니다:
void HighlightSeries::handleGradientChange(float value)
{
float ratio = m_minHeight / value;
QLinearGradient gr;
gr.setColorAt(0.f, Qt::black);
gr.setColorAt(darkGreenPos * ratio, Qt::darkGreen);
gr.setColorAt(greenPos * ratio, Qt::green);
gr.setColorAt(yellowPos * ratio, Qt::yellow);
gr.setColorAt(redPos * ratio, Qt::red);
gr.setColorAt(darkRedPos * ratio, Qt::darkRed);
setBaseGradient(gr);
setColorStyle(Q3DTheme::ColorStyleRangeGradient);
}예제 내용
© 2026 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.