c++qtqt5

How to force QListWidget to keep items widgets style?


Preliminary information: I don't want to use stylesheets.


I use QPalette to change the text (foreground) color of a QLabel as follows:

QLabel * label = new QLabel("foobar", parent);

QPalette pal = label->palette();
pal.setColor(label->foregroundRole(), Qt::blue);
label->setPalette(pal);

And it works as expected.

Now I insert this label into a QListWidget:

QListWidget * list_widget = new QListWidget(parent);

QListWidgetItem * item = new QListWidgetItem(list_widget);
list_widget->addItem(item);
list_widget->setItemWidget(item, label);
item->setSizeHint(label->sizeHint());

Issue: Then the text color is ignored and the default (black) is used.

Question: Is there a way to force the QListWidget to display the given widgets with their dedicated QPalette ? If so, how ?

I tried to replace the call to the label's backgroundRole() with QPalette::WindowText directly, but it didn't change anything.


Of course, in my real use-case, I have a custom widget which contains QLabels. It would be a non-sense to use QListWidget::setItemWidget() with a single QLabel since QListWidgetItem already supports colored text (normally).


Solution

  • tl;dr

    You need to explicitly set the foreground role:

    label->setForegroundRole(label->foregroundRole());
    

    Explanation

    Only a few widgets have explicitly set background and foreground roles (for instance QPushButton sets QPalette::Button and QPalette::ButtonText respectively), otherwise their internal values are QPalette::NoRole.

    When the related backgroundRole() and foregroundRole() functions are called, they will return the internal role set (eventually explicitly set through the setter functions) unless it's NoRole; if it is, then they will return a suitable role (but not NoRole).

    From the backgroundRole() docs:

    If no explicit background role is set, the widget inherts its parent widget's background role.

    The above means that if the widget has a NoRole background role, it will walk the parent tree until any parent returns a role that is not NoRole, or until it reaches the top level window and eventually return QPalette::Window. Here is the source of that function:

    QPalette::ColorRole QWidget::backgroundRole() const
    {
        const QWidget *w = this;
        do {
            QPalette::ColorRole role = w->d_func()->bg_role;
            if (role != QPalette::NoRole)
                return role;
            if (w->isWindow() || w->windowType() == Qt::SubWindow)
                break;
            w = w->parentWidget();
        } while (w);
        return QPalette::Window;
    }
    

    From the foregroundRole() docs:

    If no explicit foreground role is set, the function returns a role that contrasts with the background role.

    This means that if a NoRole foreground is set it will call backgroundRole() (doing everything explained above) and return a suitable role that contrasts with it. Here is how it works:

    QPalette::ColorRole QWidget::foregroundRole() const
    {
        Q_D(const QWidget);
        QPalette::ColorRole rl = QPalette::ColorRole(d->fg_role);
        if (rl != QPalette::NoRole)
            return rl;
        QPalette::ColorRole role = QPalette::WindowText;
        switch (backgroundRole()) {
        case QPalette::Button:
            role = QPalette::ButtonText;
            break;
        case QPalette::Base:
            role = QPalette::Text;
            break;
        case QPalette::Dark:
        case QPalette::Shadow:
            role = QPalette::Light;
            break;
        case QPalette::Highlight:
            role = QPalette::HighlightedText;
            break;
        case QPalette::ToolTipBase:
            role = QPalette::ToolTipText;
            break;
        default:
            ;
        }
        return role;
    }
    

    Since QLabel is among those widgets that have no default roles set, then the following happens when used standalone:

    That assumed role is not only the same you get when doing pal->setColor(label->foregroundRole(), Qt::blue);, but also and most importantly the one used by QPainter when it draws the label.

    When you add the label to the view, things change:

    In order to override that, the solution looks unintuitive other than pointless, but it makes sense considering what explained above: you need to explicitly set the palette role for the foreground, based on the "assumed" one:

    label->setForegroundRole(label->foregroundRole());
    

    You can do the above before or after setting the label to the view, but it's mandatory that you then do pal.setColor() and label->setPalette(pal) after doing setForegroundRole(), because backgroundRole() (and, therefore, foregroundRole()) will have a different result depending on the parent.