Search code examples
qtqt4qpainterqtembeddedqstyle

Use Clipping in Qt


Is it possible to use clipping in an widgets painEvent, if the widget is using stylesheets?

The background and reason for my question is that I want to make the widget animating when it appears and disappears. (Something like a resizing circle or square, that gets bigger starting as a small area from the center).

My first (and only) thought on how to solve this, was to use the clipping of a QPainter, so that only the required area is drawn.

If I make the Background of the widget transparent and use the primitive drawing functions from QPainter it works fine. But how can I solve this, if the widget has a stylesheet applied? Is it even possible?

The used Qt version is Qt 4.8.6

My questions are:

  • Is it possible to achieve what I want with the mentioned strategy?
  • Is it possible in any way to clip all the children, too?
  • Is my strategy appropriate or is it a bad Idea to solve it that way?
  • Are there any other ideas, best practices, Qt Classes, ... that can give me what I want?

Additional Information

I haven't much code to show, because I stuck with this clipping things. But here is something to get an idea of what I have tried:

This works.

/* Shows a small red circle inside the widget as expected */
void MyAnimatingWidget::paintEvent(QPaintEvent *ev) {
    QPainter painter(this);
    QRect rect = this->geometry()
    QStyleOption opt;

    painter.setClipRegion(QRegion(rect.width()/2, 
                                  rect.height()/2,
                                  150, 150, 
                                  QRegion::Ellipse));
    painter.setPen(QColor(255, 0, 0));
    painter.setBrush(QColor(255, 0, 0));
    painter.setOpacity(1);

    painter.drawRect(rect);
}

But the following doesn't change anything:

/* This shows the widget as usual */
void MyAnimatingWidget::paintEvent(QPaintEvent *ev) {
    QPainter painter(this);
    QRect rect = this->geometry();
    QStyleOption opt;

    painter.setClipRegion(QRegion(rect.width()/2, 
                                  rect.height()/2,
                                  150, 150, 
                                  QRegion::Ellipse));
    painter.setRenderHint(QPainter::Antialiasing);
    painter.setOpacity(1);

    opt.init(this);
    style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this);
}

Moreover I have noticed, that the stylesheet is also drawn, even if I remove the style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this); line at all.


Solution

  • The stylesheet you apply to your widget overrides the OS-specific style(s) widgets are equipped with by default. This can even cause problems, if you want to have a, say, Windows look, but still want to use a stylesheet. Anyway, you can check what each style does in the Qt source directory: src/gui/styles. For style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this);, the code reads:

    case PE_Widget:
        if (w && !rule.hasDrawable()) {
            QWidget *container = containerWidget(w);
            if (styleSheetCaches->autoFillDisabledWidgets.contains(container)
                && (container == w || !renderRule(container, opt).hasBackground())) {
                //we do not have a background, but we disabled the autofillbackground anyway. so fill the background now.
                // (this may happen if we have rules like :focus)
                p->fillRect(opt->rect, opt->palette.brush(w->backgroundRole()));
            }
            break;
        }
    

    As you can see clipping is not meddled with in any way, so your idea of setting a clip region should work. Now for the painting mystery. The painting of the background happens in void QWidgetPrivate::paintBackground(QPainter *painter, const QRegion &rgn, int flags) const, which is called from void QWidgetPrivate::drawWidget(QPaintDevice *pdev, const QRegion &rgn, const QPoint &offset, int flags, QPainter *sharedPainter, QWidgetBackingStore *backingStore). You can find the code in: /src/gui/kernel/qwidget.cpp. The relevant code reads:

    if (q->testAttribute(Qt::WA_StyledBackground)) {
        painter->setClipRegion(rgn);
        QStyleOption opt;
        opt.initFrom(q);
        q->style()->drawPrimitive(QStyle::PE_Widget, &opt, painter, q);
    }
    

    Maybe turning the attribute off would help? The basic lesson you should draw from my answer is to get accustomed to source diving. The idea behind Qt is nice (instantiating controls, without bothering about implementation details), but it rarely works in practice, i.e. you often need to source dive.

    To clip widget's children to arbitrary clip regions, you can capture them into a pixmap, example:

    QPixmap pixmap(widget->size());
    widget->render(&pixmap);
    

    And then draw the pixmap manually. You might also be able to prevent them repainting automatically (via setUpdatesEnabled() or by hiding them) and then calling their render in you paintEvent handler manually.