diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index c4b9efe..0816cd0 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(geopro_desktop WIN32 panels/chart/ColorMapService.cpp panels/chart/ColorBarWidget.cpp panels/chart/ScatterPlotItem.cpp + panels/chart/LivePanner.cpp panels/AnomalyTablePanel.cpp panels/DatasetDetailPage.cpp panels/DatasetDetailPanel.cpp diff --git a/src/app/panels/chart/LivePanner.cpp b/src/app/panels/chart/LivePanner.cpp new file mode 100644 index 0000000..6e633f1 --- /dev/null +++ b/src/app/panels/chart/LivePanner.cpp @@ -0,0 +1,61 @@ +#include "panels/chart/LivePanner.hpp" + +#include +#include +#include +#include + +namespace geopro::app { + +LivePanner::LivePanner(QwtPlot* plot, int xAxis, int yAxis, QObject* parent) + : QObject(parent), plot_(plot), xAxis_(xAxis), yAxis_(yAxis) { + if (plot_ && plot_->canvas()) plot_->canvas()->installEventFilter(this); +} + +bool LivePanner::eventFilter(QObject* obj, QEvent* ev) { + if (!plot_ || obj != plot_->canvas()) return QObject::eventFilter(obj, ev); + + switch (ev->type()) { + case QEvent::MouseButtonPress: { + auto* me = static_cast(ev); + if (me->button() == Qt::LeftButton) { + panning_ = true; + startPos_ = me->pos(); + x0Min_ = plot_->axisScaleDiv(xAxis_).lowerBound(); + x0Max_ = plot_->axisScaleDiv(xAxis_).upperBound(); + y0Min_ = plot_->axisScaleDiv(yAxis_).lowerBound(); + y0Max_ = plot_->axisScaleDiv(yAxis_).upperBound(); + return true; + } + break; + } + case QEvent::MouseMove: { + if (!panning_) break; + auto* me = static_cast(ev); + const int W = plot_->canvas()->width(); + const int H = plot_->canvas()->height(); + if (W <= 0 || H <= 0) break; + const double dxData = (me->pos().x() - startPos_.x()) * (x0Max_ - x0Min_) / W; + const double dyData = (me->pos().y() - startPos_.y()) * (y0Max_ - y0Min_) / H; + // 拖右(dx>0)→窗口左移(xmin/xmax 减),内容随光标右移; + // 拖下(像素 dy>0)→yLeft 数据向上→窗口上移(ymin/ymax 增),内容随光标下移。 + plot_->setAxisScale(xAxis_, x0Min_ - dxData, x0Max_ - dxData); + plot_->setAxisScale(yAxis_, y0Min_ + dyData, y0Max_ + dyData); + plot_->replot(); + return true; + } + case QEvent::MouseButtonRelease: { + auto* me = static_cast(ev); + if (me->button() == Qt::LeftButton) { + panning_ = false; + return true; + } + break; + } + default: + break; + } + return QObject::eventFilter(obj, ev); +} + +} // namespace geopro::app diff --git a/src/app/panels/chart/LivePanner.hpp b/src/app/panels/chart/LivePanner.hpp new file mode 100644 index 0000000..7ef146b --- /dev/null +++ b/src/app/panels/chart/LivePanner.hpp @@ -0,0 +1,29 @@ +#pragma once +#include +#include + +class QwtPlot; + +namespace geopro::app { + +// 实时拖动平移:左键拖动时连续平移两轴并 replot(坐标轴 + 内容一起实时移动), +// 区别于 QwtPlotPanner 的"像素抓取"(拖动中轴不动、边缘空白、松手才定稿)。 +// 平移为纯平移,不改变纵横比(与 QwtPlotRescaler 锁定的真实比尺兼容)。 +class LivePanner : public QObject { + Q_OBJECT +public: + LivePanner(QwtPlot* plot, int xAxis, int yAxis, QObject* parent = nullptr); + +protected: + bool eventFilter(QObject* obj, QEvent* ev) override; + +private: + QwtPlot* plot_; + int xAxis_; + int yAxis_; + bool panning_ = false; + QPoint startPos_; + double x0Min_ = 0, x0Max_ = 0, y0Min_ = 0, y0Max_ = 0; +}; + +} // namespace geopro::app diff --git a/src/app/panels/chart/RawDataChartView.cpp b/src/app/panels/chart/RawDataChartView.cpp index f6c7609..6a2ab36 100644 --- a/src/app/panels/chart/RawDataChartView.cpp +++ b/src/app/panels/chart/RawDataChartView.cpp @@ -10,11 +10,13 @@ #include #include -#include #include #include +#include #include +#include "panels/chart/LivePanner.hpp" + namespace geopro::app { RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) { @@ -71,6 +73,16 @@ RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) { plot_->setPalette(pal); } + // 横纵网格线(对齐原版浅灰网格)。 + auto* grid = new QwtPlotGrid(); + grid->setMajorPen(QColor(225, 225, 225), 1.0, Qt::SolidLine); + grid->setMinorPen(QColor(240, 240, 240), 1.0, Qt::DotLine); + grid->enableXMin(false); + grid->enableYMin(false); + grid->setXAxis(QwtPlot::xTop); + grid->setYAxis(QwtPlot::yLeft); + grid->attach(plot_); + // 过原点零线(对齐原版 zeroline:x=0 竖线 + y=0 横线 → "四象限"观感)。 auto* zeroX = new QwtPlotMarker(); zeroX->setLineStyle(QwtPlotMarker::VLine); @@ -84,9 +96,8 @@ RawDataChartView::RawDataChartView(QWidget* parent) : QWidget(parent) { zeroY->setLinePen(QColor(180, 180, 180), 1.0); zeroY->attach(plot_); - // 交互:Panner(左键拖动平移)+ Magnifier(滚轮缩放) - auto* panner = new QwtPlotPanner(plot_->canvas()); - panner->setMouseButton(Qt::LeftButton); + // 交互:LivePanner(左键拖动实时平移,坐标轴+内容一起动)+ Magnifier(滚轮缩放) + new LivePanner(plot_, QwtPlot::xTop, QwtPlot::yLeft, this); auto* magnifier = new QwtPlotMagnifier(plot_->canvas()); // 反转滚轮方向:Qwt 默认 wheelFactor=0.9 → 上滚缩小;取倒数使「上滚=放大」(常规习惯)。 diff --git a/src/controller/DatasetDetailController.cpp b/src/controller/DatasetDetailController.cpp index d3e4a48..379f47c 100644 --- a/src/controller/DatasetDetailController.cpp +++ b/src/controller/DatasetDetailController.cpp @@ -19,10 +19,10 @@ void DatasetDetailController::openDataset(const QString& dsId, const QString& dd ChartData d; d.dsId = dsId; d.ddCode = ddCode; + // 仅拉原数据视图所需(scatter + 散点色阶 + 异常,~0.8s)。 + // 网格数据(inversion/rows 服务端网格化 ~4s)推迟到切「网格数据」页时按需加载。 d.scatter = repo_.loadScatter(id); d.scatterScale = repo_.loadScatterColorScale(id); - d.grid = repo_.loadGrid(id); - d.gridScale = repo_.loadColorScale(id); d.anomalies = repo_.loadAnomalies(id); busy_ = false; emit chartReady(d); diff --git a/tests/controller/test_dataset_detail_controller.cpp b/tests/controller/test_dataset_detail_controller.cpp index 1247677..f1b1b01 100644 --- a/tests/controller/test_dataset_detail_controller.cpp +++ b/tests/controller/test_dataset_detail_controller.cpp @@ -7,8 +7,9 @@ namespace { struct StubRepo : data::IDatasetRepository { bool fail = false; std::vector loadStructure() override { return {}; } - core::Grid loadGrid(const std::string&) override { if (fail) throw std::runtime_error("x"); core::Grid g(2,2); g.x={0,1}; g.y={0,1}; return g; } - core::ScatterField loadScatter(const std::string&) override { return {}; } + core::Grid loadGrid(const std::string&) override { core::Grid g(2,2); g.x={0,1}; g.y={0,1}; return g; } + // openDataset 现只拉 scatter/scatterScale/anomalies(网格懒加载),失败路径由 loadScatter 抛出触发。 + core::ScatterField loadScatter(const std::string&) override { if (fail) throw std::runtime_error("x"); return {}; } core::ColorScale loadColorScale(const std::string&) override { return {}; } core::ColorScale loadScatterColorScale(const std::string&) override { return {}; } std::vector loadAnomalies(const std::string&) override { return {}; }