350 lines
15 KiB
C++
350 lines
15 KiB
C++
#include "Curve2DPlotor.h"
|
|
#include "qcustomplot.h"
|
|
#include "ElaMessageBar.h"
|
|
|
|
namespace pst
|
|
{
|
|
Curve2DPlotor::Curve2DPlotor(QWidget* parent)
|
|
|
|
{
|
|
std::srand(QDateTime::currentDateTime().toMSecsSinceEpoch() / 1000.0);
|
|
|
|
this->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectAxes |
|
|
QCP::iSelectLegend | QCP::iSelectPlottables);
|
|
this->xAxis->setRange(-8, 8);
|
|
this->yAxis->setRange(-5, 5);
|
|
this->axisRect()->setupFullAxesBox();
|
|
|
|
this->plotLayout()->insertRow(0);
|
|
QCPTextElement* title = new QCPTextElement(this, "二维图", QFont("sans", 10, QFont::Bold));
|
|
this->plotLayout()->addElement(0, 0, title);
|
|
|
|
this->xAxis->setLabel("x 轴");
|
|
this->yAxis->setLabel("y 轴");
|
|
this->legend->setVisible(false);
|
|
QFont legendFont = font();
|
|
legendFont.setPointSize(8);
|
|
this->legend->setFont(legendFont);
|
|
this->legend->setSelectedFont(legendFont);
|
|
this->legend->setSelectableParts(QCPLegend::spItems); // legend box shall not be selectable, only legend items
|
|
|
|
this->rescaleAxes();
|
|
|
|
// connect slot that ties some axis selections together (especially opposite axes):
|
|
connect(this, SIGNAL(selectionChangedByUser()), this, SLOT(selectionChanged()));
|
|
// connect slots that takes care that when an axis is selected, only that direction can be dragged and zoomed:
|
|
connect(this, SIGNAL(mousePress(QMouseEvent*)), this, SLOT(mousePress()));
|
|
connect(this, SIGNAL(mouseWheel(QWheelEvent*)), this, SLOT(mouseWheel()));
|
|
|
|
// make bottom and left axes transfer their ranges to top and right axes:
|
|
connect(this->xAxis, SIGNAL(rangeChanged(QCPRange)), this->xAxis2, SLOT(setRange(QCPRange)));
|
|
connect(this->yAxis, SIGNAL(rangeChanged(QCPRange)), this->yAxis2, SLOT(setRange(QCPRange)));
|
|
|
|
// connect some interaction slots:
|
|
connect(this, SIGNAL(axisDoubleClick(QCPAxis*, QCPAxis::SelectablePart, QMouseEvent*)), this, SLOT(axisLabelDoubleClick(QCPAxis*, QCPAxis::SelectablePart)));
|
|
connect(this, SIGNAL(legendDoubleClick(QCPLegend*, QCPAbstractLegendItem*, QMouseEvent*)), this, SLOT(legendDoubleClick(QCPLegend*, QCPAbstractLegendItem*)));
|
|
connect(title, SIGNAL(doubleClicked(QMouseEvent*)), this, SLOT(titleDoubleClick(QMouseEvent*)));
|
|
|
|
// connect slot that shows a message in the status bar when a graph is clicked:
|
|
connect(this, SIGNAL(plottableClick(QCPAbstractPlottable*, int, QMouseEvent*)), this, SLOT(graphClicked(QCPAbstractPlottable*, int)));
|
|
|
|
// setup policy and connect slot for context menu popup:
|
|
this->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextMenuRequest(QPoint)));
|
|
|
|
}
|
|
|
|
void Curve2DPlotor::setXAxisTitleName(const QString& titleName)
|
|
{
|
|
this->xAxis->setLabel(titleName);
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::setXAxisFontFamily(const QString& family)
|
|
{
|
|
auto font = QFont(family, this->xAxis->labelFont().pointSize());
|
|
this->xAxis->setLabelFont(font);
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::setXAxisFontSize(int size)
|
|
{
|
|
this->xAxis->setLabelFont(QFont(this->xAxis->labelFont().family(), size));
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::setXAxisRangeMin(double min)
|
|
{
|
|
this->xAxis->setRangeLower(min);
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::setXAxisRangeMax(double max)
|
|
{
|
|
this->xAxis->setRangeUpper(max);
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::setXAxisRangeStep(double step)
|
|
{
|
|
QSharedPointer<QCPAxisTickerFixed> fixed(QSharedPointer<QCPAxisTickerFixed>(new QCPAxisTickerFixed()));
|
|
fixed->setTickStep(step);
|
|
fixed->setScaleStrategy(QCPAxisTickerFixed::ScaleStrategy::ssNone);
|
|
this->xAxis->setTicker(fixed);
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::setYAxisTitleName(const QString& titleName)
|
|
{
|
|
this->yAxis->setLabel(titleName);
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::setYAxisFontFamily(const QString& family)
|
|
{
|
|
auto font = QFont(family, this->yAxis->labelFont().pointSize());
|
|
this->yAxis->setLabelFont(font);
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::setYAxisFontSize(int size)
|
|
{
|
|
this->yAxis->setLabelFont(QFont(this->yAxis->labelFont().family(), size));
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::setYAxisRangeMin(double min)
|
|
{
|
|
this->yAxis->setRangeLower(min);
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::setYAxisRangeMax(double max)
|
|
{
|
|
this->yAxis->setRangeUpper(max);
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::setYAxisRangeStep(double step)
|
|
{
|
|
QSharedPointer<QCPAxisTickerFixed> fixed(QSharedPointer<QCPAxisTickerFixed>(new QCPAxisTickerFixed()));
|
|
fixed->setTickStep(step);
|
|
fixed->setScaleStrategy(QCPAxisTickerFixed::ScaleStrategy::ssNone);
|
|
this->yAxis->setTicker(fixed);
|
|
//this->yAxis->ticker()->setTickStepStrategy(QCPAxisTicker::TickStepStrategy::tssMeetTickCount);
|
|
//this->yAxis->setTickStep(step);
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::titleDoubleClick(QMouseEvent* event)
|
|
{
|
|
Q_UNUSED(event)
|
|
if (QCPTextElement* title = qobject_cast<QCPTextElement*>(sender()))
|
|
{
|
|
// Set the plot title by double clicking on it
|
|
bool ok;
|
|
QString newTitle = QInputDialog::getText(this, "QCustomPlot example", "New plot title:", QLineEdit::Normal, title->text(), &ok);
|
|
if (ok)
|
|
{
|
|
title->setText(newTitle);
|
|
this->replot();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Curve2DPlotor::axisLabelDoubleClick(QCPAxis* axis, QCPAxis::SelectablePart part)
|
|
{
|
|
// Set an axis label by double clicking on it
|
|
if (part == QCPAxis::spAxisLabel) // only react when the actual axis label is clicked, not tick label or axis backbone
|
|
{
|
|
bool ok;
|
|
QString newLabel = QInputDialog::getText(this, "QCustomPlot example", "New axis label:", QLineEdit::Normal, axis->label(), &ok);
|
|
if (ok)
|
|
{
|
|
axis->setLabel(newLabel);
|
|
this->replot();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Curve2DPlotor::legendDoubleClick(QCPLegend* legend, QCPAbstractLegendItem* item)
|
|
{
|
|
// Rename a graph by double clicking on its legend item
|
|
Q_UNUSED(legend)
|
|
if (item) // only react if item was clicked (user could have clicked on border padding of legend where there is no item, then item is 0)
|
|
{
|
|
QCPPlottableLegendItem* plItem = qobject_cast<QCPPlottableLegendItem*>(item);
|
|
bool ok;
|
|
QString newName = QInputDialog::getText(this, "QCustomPlot example", "New graph name:", QLineEdit::Normal, plItem->plottable()->name(), &ok);
|
|
if (ok)
|
|
{
|
|
plItem->plottable()->setName(newName);
|
|
this->replot();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Curve2DPlotor::selectionChanged()
|
|
{
|
|
/*
|
|
normally, axis base line, axis tick labels and axis labels are selectable separately, but we want
|
|
the user only to be able to select the axis as a whole, so we tie the selected states of the tick labels
|
|
and the axis base line together. However, the axis label shall be selectable individually.
|
|
|
|
The selection state of the left and right axes shall be synchronized as well as the state of the
|
|
bottom and top axes.
|
|
|
|
Further, we want to synchronize the selection of the graphs with the selection state of the respective
|
|
legend item belonging to that graph. So the user can select a graph by either clicking on the graph itself
|
|
or on its legend item.
|
|
*/
|
|
|
|
// make top and bottom axes be selected synchronously, and handle axis and tick labels as one selectable object:
|
|
if (this->xAxis->selectedParts().testFlag(QCPAxis::spAxis) || this->xAxis->selectedParts().testFlag(QCPAxis::spTickLabels) ||
|
|
this->xAxis2->selectedParts().testFlag(QCPAxis::spAxis) || this->xAxis2->selectedParts().testFlag(QCPAxis::spTickLabels))
|
|
{
|
|
this->xAxis2->setSelectedParts(QCPAxis::spAxis | QCPAxis::spTickLabels);
|
|
this->xAxis->setSelectedParts(QCPAxis::spAxis | QCPAxis::spTickLabels);
|
|
}
|
|
// make left and right axes be selected synchronously, and handle axis and tick labels as one selectable object:
|
|
if (this->yAxis->selectedParts().testFlag(QCPAxis::spAxis) || this->yAxis->selectedParts().testFlag(QCPAxis::spTickLabels) ||
|
|
this->yAxis2->selectedParts().testFlag(QCPAxis::spAxis) || this->yAxis2->selectedParts().testFlag(QCPAxis::spTickLabels))
|
|
{
|
|
this->yAxis2->setSelectedParts(QCPAxis::spAxis | QCPAxis::spTickLabels);
|
|
this->yAxis->setSelectedParts(QCPAxis::spAxis | QCPAxis::spTickLabels);
|
|
}
|
|
|
|
// synchronize selection of graphs with selection of corresponding legend items:
|
|
for (int i = 0; i < this->graphCount(); ++i)
|
|
{
|
|
QCPGraph* graph = this->graph(i);
|
|
QCPPlottableLegendItem* item = this->legend->itemWithPlottable(graph);
|
|
if (item->selected() || graph->selected())
|
|
{
|
|
item->setSelected(true);
|
|
graph->setSelection(QCPDataSelection(graph->data()->dataRange()));
|
|
}
|
|
}
|
|
}
|
|
|
|
void Curve2DPlotor::mousePress()
|
|
{
|
|
// if an axis is selected, only allow the direction of that axis to be dragged
|
|
// if no axis is selected, both directions may be dragged
|
|
|
|
if (this->xAxis->selectedParts().testFlag(QCPAxis::spAxis))
|
|
this->axisRect()->setRangeDrag(this->xAxis->orientation());
|
|
else if (this->yAxis->selectedParts().testFlag(QCPAxis::spAxis))
|
|
this->axisRect()->setRangeDrag(this->yAxis->orientation());
|
|
else
|
|
this->axisRect()->setRangeDrag(Qt::Horizontal | Qt::Vertical);
|
|
}
|
|
|
|
void Curve2DPlotor::mouseWheel()
|
|
{
|
|
// if an axis is selected, only allow the direction of that axis to be zoomed
|
|
// if no axis is selected, both directions may be zoomed
|
|
|
|
if (this->xAxis->selectedParts().testFlag(QCPAxis::spAxis))
|
|
this->axisRect()->setRangeZoom(this->xAxis->orientation());
|
|
else if (this->yAxis->selectedParts().testFlag(QCPAxis::spAxis))
|
|
this->axisRect()->setRangeZoom(this->yAxis->orientation());
|
|
else
|
|
this->axisRect()->setRangeZoom(Qt::Horizontal | Qt::Vertical);
|
|
}
|
|
|
|
void Curve2DPlotor::addRandomGraph()
|
|
{
|
|
int n = 50; // number of points in graph
|
|
double xScale = (std::rand() / (double)RAND_MAX + 0.5) * 2;
|
|
double yScale = (std::rand() / (double)RAND_MAX + 0.5) * 2;
|
|
double xOffset = (std::rand() / (double)RAND_MAX - 0.5) * 4;
|
|
double yOffset = (std::rand() / (double)RAND_MAX - 0.5) * 10;
|
|
double r1 = (std::rand() / (double)RAND_MAX - 0.5) * 2;
|
|
double r2 = (std::rand() / (double)RAND_MAX - 0.5) * 2;
|
|
double r3 = (std::rand() / (double)RAND_MAX - 0.5) * 2;
|
|
double r4 = (std::rand() / (double)RAND_MAX - 0.5) * 2;
|
|
QVector<double> x(n), y(n);
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
x[i] = (i / (double)n - 0.5) * 10.0 * xScale + xOffset;
|
|
y[i] = (qSin(x[i] * r1 * 5) * qSin(qCos(x[i] * r2) * r4 * 3) + r3 * qCos(qSin(x[i]) * r4 * 2)) * yScale + yOffset;
|
|
}
|
|
|
|
this->addGraph();
|
|
this->graph()->setName(QString("曲线 %1").arg(this->graphCount() - 1));
|
|
this->graph()->setData(x, y);
|
|
this->graph()->setLineStyle((QCPGraph::LineStyle)(std::rand() % 5 + 1));
|
|
if (std::rand() % 100 > 50)
|
|
this->graph()->setScatterStyle(QCPScatterStyle((QCPScatterStyle::ScatterShape)(std::rand() % 14 + 1)));
|
|
QPen graphPen;
|
|
graphPen.setColor(QColor(std::rand() % 245 + 10, std::rand() % 245 + 10, std::rand() % 245 + 10));
|
|
graphPen.setWidthF(std::rand() / (double)RAND_MAX * 2 + 1);
|
|
this->graph()->setPen(graphPen);
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::removeSelectedGraph()
|
|
{
|
|
if (this->selectedGraphs().size() > 0)
|
|
{
|
|
this->removeGraph(this->selectedGraphs().first());
|
|
this->replot();
|
|
}
|
|
}
|
|
|
|
void Curve2DPlotor::removeAllGraphs()
|
|
{
|
|
this->clearGraphs();
|
|
this->replot();
|
|
}
|
|
|
|
void Curve2DPlotor::contextMenuRequest(QPoint pos)
|
|
{
|
|
QMenu* menu = new QMenu(this);
|
|
menu->setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
if (this->legend->selectTest(pos, false) >= 0) // context menu on legend requested
|
|
{
|
|
menu->addAction("Move to top left", this, SLOT(moveLegend()))->setData((int)(Qt::AlignTop | Qt::AlignLeft));
|
|
menu->addAction("Move to top center", this, SLOT(moveLegend()))->setData((int)(Qt::AlignTop | Qt::AlignHCenter));
|
|
menu->addAction("Move to top right", this, SLOT(moveLegend()))->setData((int)(Qt::AlignTop | Qt::AlignRight));
|
|
menu->addAction("Move to bottom right", this, SLOT(moveLegend()))->setData((int)(Qt::AlignBottom | Qt::AlignRight));
|
|
menu->addAction("Move to bottom left", this, SLOT(moveLegend()))->setData((int)(Qt::AlignBottom | Qt::AlignLeft));
|
|
}
|
|
else // general context menu on graphs requested
|
|
{
|
|
menu->addAction("Add random graph", this, SLOT(addRandomGraph()));
|
|
if (this->selectedGraphs().size() > 0)
|
|
menu->addAction("Remove selected graph", this, SLOT(removeSelectedGraph()));
|
|
if (this->graphCount() > 0)
|
|
menu->addAction("Remove all graphs", this, SLOT(removeAllGraphs()));
|
|
}
|
|
|
|
menu->popup(this->mapToGlobal(pos));
|
|
}
|
|
|
|
void Curve2DPlotor::moveLegend()
|
|
{
|
|
if (QAction* contextAction = qobject_cast<QAction*>(sender())) // make sure this slot is really called by a context menu action, so it carries the data we need
|
|
{
|
|
bool ok;
|
|
int dataInt = contextAction->data().toInt(&ok);
|
|
if (ok)
|
|
{
|
|
this->axisRect()->insetLayout()->setInsetAlignment(0, (Qt::Alignment)dataInt);
|
|
this->replot();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Curve2DPlotor::graphClicked(QCPAbstractPlottable* plottable, int dataIndex)
|
|
{
|
|
// since we know we only have QCPGraphs in the plot, we can immediately access interface1D()
|
|
// usually it's better to first check whether interface1D() returns non-zero, and only then use it.
|
|
double dataValue = plottable->interface1D()->dataMainValue(dataIndex);
|
|
QString message = QString("点击了图形 '%1' 在数据点 #%2 值为 %3.").arg(plottable->name()).arg(dataIndex).arg(dataValue);
|
|
//ui->statusBar->showMessage(message, 2500);
|
|
ElaMessageBar::success(ElaMessageBarType::PositionPolicy::BottomLeft, "提示", message, 1000, this);
|
|
}
|
|
} |