Implementing High-DPI Aware Bar Charts with QCustomPlot
Core Architecture and Scaling Strategy
QCustomPlot (QCP) is a lightweight, standalone Qt plotting library renowned for its modularity and layer-based rendering pipeline. When building desktop applications that require crisp vector graphics and sharp typography, standard UI toolkits often struggle with non-standard display scaling. To resolve this, a dedicated DPI adaptation layer can be integrated directly into the graphical context. This approach guarantees pixel-perfect rendering at integer scaling factors and handles fractional scaling gracefully, avoiding common artifacts like bitmap interpolation blur or misaligned glyphs. The following implementation demonstrates how to extend QCP to create a high-DPI compliant bar chart that maintains consistent physical dimensions across mixed-resolution workstations.
Custom Tooltip Layer
Tightly coupling tooltips to parent chart elements complicates maintenance and degrades rendering performance. By subclassing QCP's QCPLayerable base class, we can isolate tooltip rendering in to a dedicated draw pass. This decoupled architecture allows independant visibility toggling and prevents unnecessary recomputation of other chart layers.
#ifndef CHARTTOOLTIP_H
#define CHARTTOOLTIP_H
#include <QtGui/QPainter>
#include "qcustomplot.h"
class BaseChartTooltip : public QCPLayerable {
Q_OBJECT
public:
explicit BaseChartTooltip(QCustomPlot *parentPlot);
~BaseChartTooltip();
QString getLayerName() const;
void setVisibility(bool enabled);
void setTextItems(const QStringList &texts);
void setPosition(const QPointF &anchorPoint);
protected:
void applyDefaultAntialiasingHint(QCPPainter *painter) const override final {}
void draw(QCPPainter *painter) override;
virtual void renderTooltip(QCPPainter *painter) = 0;
protected:
QPointF mAnchorPoint;
QStringList mTextLines;
};
class LeftAlignedTooltip : public BaseChartTooltip {
Q_OBJECT
public:
explicit LeftAlignedTooltip(QCustomPlot *parentPlot);
protected:
void renderTooltip(QCPPainter *painter) override;
};
class TopAlignedTooltip : public BaseChartTooltip {
Q_OBJECT
public:
explicit TopAlignedTooltip(QCustomPlot *parentPlot);
private:
float cornerRadius = 3.0f;
float boxHeight = 46.0f;
float boxWidth = 125.0f;
};
#endif // CHARTTOOLTIP_H
The rendering routine utilizes a dynamic scaling multiplier injected into the painter context. Instead of hardcoding pixel dimensions, all geometric primitives and font sizes are multiplied by the current device pixel ratio, ensuring accurate physical sizing regardless of host resolution.
#define APPLY_SCALE(factor, painter) (factor) * painter->viewport().rect().width() / painter->device()->logicalDotsPerInchX()
void LeftAlignedTooltip::renderTooltip(QCPPainter *painter)
{
float scale = painter->viewport().rect().width() / painter->device()->logicalDotsPerInchX();
// Draw anchor indicator
painter->setPen(Qt::NoPen);
painter->setBrush(QColor(255, 181, 26));
painter->drawEllipse(mAnchorPoint.toPointF(), qRound(APPLY_SCALE(3, painter)), qRound(APPLY_SCALE(3, painter)));
// Calculate bounding rectangle
QRectF boundRect(APPLY_SCALE(0, painter), APPLY_SCALE(0, painter),
APPLY_SCALE(100, painter), APPLY_SCALE(30, painter));
// Adjust position relative to anchor
boundRect.moveBottomRight(mAnchorPoint.x() - APPLY_SCALE(8, painter), mAnchorPoint.y());
// Prevent clipping outside plot area
QRectF plotArea = painter->clipRegion().boundingRect();
if (boundRect.left() < plotArea.left()) {
boundRect.moveBottomLeft(mAnchorPoint.x() + APPLY_SCALE(8, painter), mAnchorPoint.y());
}
// Render background
painter->setPen(QColor(255, 204, 51));
painter->setBrush(QColor(0, 0, 0, static_cast<int>(255 * 0.7)));
painter->drawRoundedRect(boundRect, APPLY_SCALE(2, painter), APPLY_SCALE(2, painter));
// Render text
QFont labelFont(QStringLiteral("Microsoft YaHei"));
labelFont.setPointSizeF(APPLY_SCALE(10, painter));
painter->setFont(labelFont);
QPointF textOffset(boundRect.x() + APPLY_SCALE(8, painter), boundRect.y() + APPLY_SCALE(13, painter));
painter->drawText(textOffset, QStringLiteral("Margin Balance: %1").arg(mTextLines[0]));
textOffset.setY(boundRect.y() + APPLY_SCALE(25, painter));
painter->drawText(textOffset, QStringLiteral("Index Value: %1").arg(mTextLines[1]));
}
Note on DPI Handling: Native QCPPainter lacks a direct device-pixel-ratio property. The scaling factor is computed dynamically from viewport dimensions and device metrics during initialization, then refreshed whenever window geometry changes. This guarantees coordinate accuracy across mixed-DPI environments without manual recalculation.
Interactive Bar Control
To enable hover interactions, we subclass QCPBars and implement precise boundary detection. The custom control intercepts mouse movement, calculates which bar segment intersects the cursor, and emits a dedicated signal.
#ifndef HOVERAWAREBARS_H
#define HOVERAWAREBARS_H
#include "qcustomplot.h"
class QMouseEvent;
class HoverAwareBars : public QCPBars {
Q_OBJECT
public:
explicit HoverAwareBars(double scaleFactor, QCPAxis *keyAxis, QCPAxis *valueAxis);
~HoverAwareBars();
void setDataLabelVisible(bool show);
public slots:
void checkForHover(QMouseEvent *event);
signals:
void hoveredDataIndex(int index);
protected:
void draw(QCPPainter *painter) override;
private:
bool mShowLabels = false;
int mLabelOffsetY = 0;
};
#endif // HOVERAWAREBARS_H
The hover detection algorithm iterates through visible data ranges, converts logical coordinates to screen space, and performs point-in-rectangle collision tests.
void HoverAwareBars::checkForHover(QMouseEvent *event)
{
QCPBarsDataContainer::const_iterator visStart, visEnd;
getVisibleDataBounds(visStart, visEnd);
QList<QCPDataRange> selectedSegs, unselectedSegs, fullRange;
getDataSegments(selectedSegs, unselectedSegs);
fullRange.append(unselectedSegs).append(selectedSegs);
int matchedIndex = -1;
for (int i = 0; i < fullRange.size(); ++i) {
auto beg = visStart, end = visEnd;
mDataContainer->limitIteratorsToDataRange(beg, end, fullRange.at(i));
if (beg == end) continue;
for (auto it = beg; it != end; ++it) {
QRectF barRect = getBarRect(it.key(), it.value());
barRect.adjusted(0, -mLabelOffsetY, 0, 0);
if (barRect.contains(event->pos())) {
matchedIndex = i;
break;
}
}
if (matchedIndex != -1) break;
}
// Ensure cursor remains within valid plotting area
if (!mParentPlot->axisRect()->rect().contains(event->pos())) {
matchedIndex = -1;
}
emit hoveredDataIndex(matchedIndex);
}
Upon successful intersection, the hoveredDataIndex signal fires with the corresponding data index. A return value of -1 indicates the cursor exited the interactive zone, triggering tooltip dismissal.
Integration and Usage
Constructing the chart involves instantiating the wrapper class, binding datasets, and configuring axis ranges. The underlying structure isolates complex state management, keeping the public API lean.
QWidget* createInteractiveChart(double dpr)
{
AutoScalingBarChart* chartWidget = new AutoScalingBarChart(dpr);
QVector<ChartDataPair> dataSource;
dataSource << ChartDataPair(1585816691, 200.0)
<< ChartDataPair(1588408691, 150.0)
<< ChartDataPair(1591087091, 220.0)
<< ChartDataPair(1593679092, 100.0);
chartWidget->loadDataset(dataSource);
chartWidget->configureYAxisRange(QCPRange(0, 250), 5);
return chartWidget;
}
Internal references typically include tick generators, auxiliary graph layers, margin groupings, and the primary QCustomPlot canvas. All rendering ultimately routes through the canvas's paintEvent, which leverages optimized backbuffer strategies (QCPAbstractPaintBuffer) to minimize repaint overhead and maintain fluid animations during user interaction.