Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing High-DPI Aware Bar Charts with QCustomPlot

Tech May 18 2

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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.