QCustomPlotのレイアウト管理とイベント処理

バージョン間のイベント処理の比較

QCPLayoutElementの解説に入る前に、バージョン1.3.2と2.0.0のイベント処理の違いを比較します。バージョン1.3.2では、マウスクリックイベントはまずQCPLayoutElementに渡され、以下のような処理が行われていました:

void QCustomPlot::mousePressEvent(QMouseEvent *event)
{
    emit mousePress(event);
    mMousePressPos = event->pos();
    
    mMouseEventElement = layoutElementAt(event->pos());
    if (mMouseEventElement)
        mMouseEventElement->mousePressEvent(event);
    
    QWidget::mousePressEvent(event);
}

一方、バージョン2.0.0では、イベント処理が大幅に変更されました。マウスイベントはQCPLayoutElementだけでなく、QCPLayerableを継承するすべての要素で処理可能になりました:

void QCustomPlot::mousePressEvent(QMouseEvent *event)
{
    emit mousePress(event);
    mMouseHasMoved = false;
    mMousePressPos = event->pos();
    
    if (mSelectionRect && mSelectionRectMode != QCP::srmNone)
    {
        if (mSelectionRectMode != QCP::srmZoom || 
            qobject_cast(axisRectAt(mMousePressPos)))
            mSelectionRect->startSelection(event);
    }
    else
    {
        QList<QVariant> details;
        QList candidates = layerableListAt(mMousePressPos, false, &details);
        
        for (int i = 0; i < candidates.size(); ++i)
        {
            event->accept();
            candidates.at(i)->mousePressEvent(event, details.at(i));
            if (event->isAccepted())
            {
                mMouseEventLayerable = candidates.at(i);
                mMouseEventLayerableDetails = details.at(i);
                break;
            }
        }
    }
    
    event->accept();
}

レイアウト要素の階層構造

QCPLayoutElementから派生する主要なクラスには以下があります:

  • QCPAbstractLegendItem:凡例項目
  • QCPAxisRect:座標軸領域
  • QCPColorScale:カラースケール
  • QCPLayout:レイアウトの抽象基底クラス
  • QCPTextElement:テキスト要素

これらの要素はすべてQCPLayerableを継承しており、マウスイベントを処理することができます。

QCPLayoutとその派生クラス

QCPLayoutには2つの直接派生クラスがあります:QCPLayoutGridとQCPLayoutInsetです。

QCPLayoutGridの更新プロセス

QCPLayoutGridの更新は3つのフェーズで行われます:

mPlotLayout->update(QCPLayoutElement::upPreparation);
mPlotLayout->update(QCPLayoutElement::upMargins);
mPlotLayout->update(QCPLayoutElement::upLayout);

各フェーズの処理内容:

void QCPLayout::update(UpdatePhase phase)
{
    QCPLayoutElement::update(phase);
    
    if (phase == upLayout)
        updateLayout();
    
    const int elementCount = getItemCount();
    for (int index = 0; index < elementCount; ++index)
    {
        if (QCPLayoutElement *child = getElement(index))
            child->update(phase);
    }
}

マージンの計算はQCPLayoutElement::updateで行われます:

void QCPLayoutElement::update(UpdatePhase phase)
{
    if (phase == upMargins)
    {
        if (mAutoMargins != QCP::msNone)
        {
            QMargins newMargins = mMargins;
            QList sides = {QCP::msLeft, QCP::msRight, QCP::msTop, QCP::msBottom};
            
            for (auto side : sides)
            {
                if (mAutoMargins.testFlag(side))
                {
                    if (mMarginGroups.contains(side))
                        QCP::setMarginValue(newMargins, side, 
                            mMarginGroups[side]->commonMargin(side));
                    else
                        QCP::setMarginValue(newMargins, side, 
                            calculateAutoMargin(side));
                    
                    if (QCP::getMarginValue(newMargins, side) < 
                        QCP::getMarginValue(mMinimumMargins, side))
                        QCP::setMarginValue(newMargins, side, 
                            QCP::getMarginValue(mMinimumMargins, side));
                }
            }
            setMargins(newMargins);
        }
    }
}

複数座標軸の実装例

以下は、新しい座標軸領域を動的に追加する例です:

QCPBars* CustomPlotWidget::addVolumeAxis(bool showVolume)
{
    PrivateData* data = m_private;
    
    if (showVolume)
    {
        if (!data->volumeEnabled)
        {
            data->volumeEnabled = true;
            
            QCPAxisRect* volumeAxis = new QCPAxisRect(m_customPlot);
            
            // X軸の同期
            connect(m_customPlot->xAxis, 
                static_cast(&QCPAxis::rangeChanged),
                volumeAxis->axis(QCPAxis::atBottom),
                static_cast(&QCPAxis::setRange));
            
            connect(volumeAxis->axis(QCPAxis::atBottom),
                static_cast(&QCPAxis::rangeChanged),
                m_customPlot->xAxis,
                static_cast(&QCPAxis::setRange));
            
            volumeAxis->setMaximumSize(QSize(QWIDGETSIZE_MAX, 100));
            volumeAxis->axis(QCPAxis::atBottom)->setLayer("axes");
            volumeAxis->axis(QCPAxis::atBottom)->grid()->setLayer("grid");
            volumeAxis->setAutoMargins(QCP::msLeft | QCP::msRight | QCP::msBottom);
            volumeAxis->setMargins(QMargins(0, 0, 0, 0));
            
            m_customPlot->plotLayout()->addElement(1, 0, volumeAxis);
            volumeAxis->setMarginGroup(QCP::msLeft | QCP::msRight, data->marginGroup);
            
            QCPBars* volumeBars = createBars(volumeAxis->axis(QCPAxis::atBottom),
                                            volumeAxis->axis(QCPAxis::atLeft));
            
            return volumeBars;
        }
    }
    else
    {
        data->volumeEnabled = false;
        m_customPlot->plotLayout()->remove(m_customPlot->plotLayout()->element(1, 0));
    }
    
    m_customPlot->plotLayout()->simplify();
    return nullptr;
}

QCPLayoutInsetの役割

QCPLayoutInsetは1次元の配列として機能し、主にQCPAxisRect内のouterRectの管理に使用されます。以下のように2つの矩形情報を保持します:

QRect innerRect() const { return mRect; }
QRect outerRect() const { return mOuterRect; }

このクラスにより、座標軸領域の外側に追加の要素を配置することができます。

5月29日 09:13 投稿