Controlling the axis range with a scrollbar

While the most intuitive way of controlling the axis ranges is the range dragging and zooming mechanism, it might be desirable to also provide scrollbars for this purpose. This can be achieved by connecting an axis with a scrollbar via signals and slots. An intermediate slot is needed to translate between the QCPRange of an axis and the integer value of the scrollbar.

The example project accompanying this tutorial is called scrollbar-axis-range-control and is part of the full package download.

Preliminaries

The signals that will be relevant to propagate changes back and forth between scrollbar and axis are QScrollBar::valueChanged(int) and QCPAxis::rangeChanged(QCPRange). Since we want to keep the normal range dragging and zooming, the scrollbar slider position and size must be updated when the axis' rangeChanged signal is emitted.

QScrollBar is integer based. For this reason, we need a factor that transforms the integer scrollbar values to axis coordinates. For example, if we want to be able to smoothly scroll the axis over the coordinate range -5 to 5, we could set the factor to something like 0.01 (i.e. divide scrollbar values by 100) and thus the range of the scrollbar to -500..500.

  ui->horizontalScrollBar->setRange(-500, 500);
  ui->verticalScrollBar->setRange(-500, 500);

If the accessible coordinate range shall change at any point, just change the maximum/minimum values of the scrollbar.

The intermediate slots that will do the coordinate transformations are called horzScrollBarChanged, vertScrollBarChanged, xAxisChanged, and yAxisChanged. They are connected to the appropriate signals of the scrollbars and x-/y-Axes:

  connect(ui->horizontalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(horzScrollBarChanged(int)));
  connect(ui->verticalScrollBar, SIGNAL(valueChanged(int)), this, SLOT(vertScrollBarChanged(int)));
  connect(ui->plot->xAxis, SIGNAL(rangeChanged(QCPRange)), this, SLOT(xAxisChanged(QCPRange)));
  connect(ui->plot->yAxis, SIGNAL(rangeChanged(QCPRange)), this, SLOT(yAxisChanged(QCPRange)));

The coordinate transformation slots

Both types of slots (axis range to scrollbar and scrollbar to axis range) are fairly simple. They take the changed value of the scrollbar or axis, apply the transformation and set the result to the axis or scrollbar, respectively. These are the slots for updating the axis ranges upon moving the scrollbar slider:

void MainWindow::horzScrollBarChanged(int value)
{
  if (qAbs(ui->plot->xAxis->range().center()-value/100.0) > 0.01) // if user is dragging plot, we don't want to replot twice
  {
    ui->plot->xAxis->setRange(value/100.0, ui->plot->xAxis->range().size(), Qt::AlignCenter);
    ui->plot->replot();
  }
}

void MainWindow::vertScrollBarChanged(int value)
{
  if (qAbs(ui->plot->yAxis->range().center()+value/100.0) > 0.01) // if user is dragging plot, we don't want to replot twice
  {
    ui->plot->yAxis->setRange(-value/100.0, ui->plot->yAxis->range().size(), Qt::AlignCenter);
    ui->plot->replot();
  }
}

There are two things worth mentioning:

First of all, we see here the transformation of the scrollbar value to axis coordinates by dividing by 100.0. Also note that the vertical scrollbar has a low value when the slider is at the top and a high value when it is at the bottom. For plot axes this is the other way around, which is why a minus sign is added to expressions containing value of the vertical scrollbar, e.g. when setting the yAxis range.

The condition qAbs(ui->plot->xAxis->range().center()-value/100.0) > 0.01 is necessary such that range dragging doesn't cause double replots, caused by a back-and-forth between change-signals and slots. This could happen because upon range dragging, the QCustomPlot automatically replots itself and emits the rangeChanged signals of the dragged axes. In this application the rangeChanged signal will call the slot xAxisChanged or yAxisChanged which, as we will see, updates the scrollbar slider position by calling the scrollbar's setValue method. This method in turn emits the scrollbar's valueChanged signal which is connected to the slots above. Here, the second replot would happen, if the check wasn't in place. The check makes sure the replot is only performed if the current axis range is actually different from the new (transformed) scrollbar value. This is not the case if the user dragged the axis range, so the redundant replot and axis range update is skipped.

The slots for updating the scrollbars upon axis range changes are simple:

void MainWindow::xAxisChanged(QCPRange range)
{
  ui->horizontalScrollBar->setValue(qRound(range.center()*100.0)); // adjust position of scroll bar slider
  ui->horizontalScrollBar->setPageStep(qRound(range.size()*100.0)); // adjust size of scroll bar slider
}

void MainWindow::yAxisChanged(QCPRange range)
{
  ui->verticalScrollBar->setValue(qRound(-range.center()*100.0)); // adjust position of scroll bar slider
  ui->verticalScrollBar->setPageStep(qRound(range.size()*100.0)); // adjust size of scroll bar slider
}

They simply transform the range center to a scrollbar value and the the range size to the scrollbar's page step (the scrollbar slider size).