Paint widget directly on QListView with QStyledItemDelegate::paint()



  • I'm able to paint a widget on QListView. However, the painting is done through a QPixmap. The widget appears, and I can see a progress bar (I'm willing to paint a whole widget, not only a progress bar). However, it's a little "pixelated" (due to using QPixmap). Is it possible to paint/render directly as a normal widget? That's my question.

    The following is what I do:

    void FileQueueItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
    {
        QPaintDevice* original_pdev_ptr = painter->device();
    
        FileQueueListItem* itemWidget = reinterpret_cast<FileQueueListItem*>(index.data(Qt::UserRole).value<void*>());
    
        itemWidget->setGeometry(option.rect);
        painter->end();
    
        QPixmap pixmap(itemWidget->size());
        if (option.state & QStyle::State_Selected)
            pixmap.fill(option.palette.highlight().color());
        else
            pixmap.fill(option.palette.background().color());
        itemWidget->render(&pixmap,QPoint(),QRegion(),QWidget::RenderFlag::DrawChildren);
    
        painter->begin(original_pdev_ptr);
        painter->drawPixmap(option.rect, pixmap);
    }
    

    I learned how to do what I did with the hints from here. There, the rendering is done directly on QListView, which is what I'm looking to achieve. What am I doing wrong for the following attempt not to work:

    void FileQueueItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
    {
        std::cout<<"Painting..."<<std::endl;
        QPaintDevice* original_pdev_ptr = painter->device();
    
        FileQueueListItem* itemWidget = reinterpret_cast<FileQueueListItem*>(index.data(Qt::UserRole).value<void*>());
    
        itemWidget->setGeometry(option.rect);
        painter->end();
    
        if (option.state & QStyle::State_Selected)
            painter->fillRect(option.rect, option.palette.highlight());
        else
            painter->fillRect(option.rect, option.palette.background());
    
        itemWidget->render(painter->device(),
                           QPoint(option.rect.x(), option.rect.y()),
                           QRegion(0, 0, option.rect.width(), option.rect.height()),
                           QWidget::RenderFlag::DrawChildren);
        painter->begin(original_pdev_ptr);
    }
    

    The list just remains empty, and nothing happens. Although the selection on the empty items can be seen, the widget doesn't show up.


  • Lifetime Qt Champion

    Hi,

    Out of curiosity, why are you painting a rendered widget inside a QStyledItemDelegate ? Why not paint directly the content you want ?



  • @SGaist most likely it's ignorance from my side, because I don't know how to do this properly. You say it's already rendered, could you please show me how to directly paint the rendered version into the table widget?


  • Lifetime Qt Champion

    No, what I'm saying is that rather than painting what you want to present, you are first rendering a widget then painting that result on top of the rest.

    So it seems that you are trying to somehow workaround the use of widgets in a QListView.

    If you have a QListView only made of widgets, why use QListView at all ? A QScrollArea containing a QWidget with a QVBoxLayout would achieve the same effect.

    If you need that QListView, then paint what you want through the delegate.



  • @SGaist Thank you for your response. I see what you mean. The reason I'm using QListView is that I have a model that can have thousands of elements. I'm rendering only the widgets that the user wants to see (using the model, which contains the widgets). The first call of the data() method in the model constructs the widget if, it's not constructed yet.

    Quoting from you:

    If you need that QListView, then paint what you want through the delegate.

    That's exactly what I need and that's exactly what's not working (please take a look at the first question's code) and that's what I'm looking for help with. I was able to do that by rendering into a QPixmap, and then painting on the QListView. I would like to stop using QPixmap because it's making the widget look ugly. Is this possible?


  • Lifetime Qt Champion

    One thing that looks out of place in your code is the call to painter->end(); in the middle.



  • @SGaist I have to be honest and say that I don't know what I'm doing there. Notice that end() comes before the paint command and begin() comes after it's finished. I'm following the recipe in the link I provided in the question. Besides, I tried all kinds of combinations. I couldn't understand the painting mechanism from the manual, and I'd appreciate pointing me in the right direction, or assisting me in anyway to get this to work.


  • Lifetime Qt Champion

    I noticed that and it doesn't make sense, the painter is already active and your just stopping it. Did you confuse these method with the save and restore methods ?

    In any case, did you already saw the Star Delegate Example ? It shows a fully customised paint method.



  • @SGaist I succeeded in painting polygons a long time ago. That's easy. In fact, like I said in the question, the selection painting works fine (in addition to polygons). The guy here got +9 for this way. He explains that the reason for stopping is that "2 painters cannot work at the same time". Check it here:

    https://stackoverflow.com/questions/6452838/render-qwidget-in-paint-method-of-qwidgetdelegate-for-a-qlistview/18983353#18983353



  • I would like to point out that the problem is still there, and I'd appreciate the help of an expert.



  • you can use this as a base class, it's far from perfect (it does not support stylesheet and it's quite slow) but does its job, it just requires you to reimplement the setEditorData method, the paint is handled already:

    #include <QStyledItemDelegate>
    #include <QPainter>
    template <class T>
    class WidgetDelegate : public QStyledItemDelegate{
    #ifdef Q_COMPILER_STATIC_ASSERT
        static_assert(std::is_base_of<QWidget,T>::value,"Template argument must be a QWidget");
    #endif
        Q_DISABLE_COPY(WidgetDelegate)
    public:
        explicit WidgetDelegate(QObject* parent = Q_NULLPTR)
            :QStyledItemDelegate(parent)
            , m_baseWid(new T)
        {}
        virtual ~WidgetDelegate(){
            delete m_baseWid;
        }
        virtual void paint(QPainter *painter, const QStyleOptionViewItem &option,const QModelIndex &index) const Q_DECL_OVERRIDE{
            setEditorData(m_baseWid,index);
            m_baseWid->resize(option.rect.size());
            QPixmap pixmap(option.rect.size());
            m_baseWid->render(&pixmap);
            painter->drawPixmap(option.rect,pixmap);
        }
        virtual QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE{
            Q_UNUSED(option);
            setEditorData(m_baseWid,index);
            return m_baseWid->sizeHint();
        }
        virtual void setEditorData(QWidget *editor, const QModelIndex &index) const Q_DECL_OVERRIDE =0;
    private:
        T* m_baseWid;
    };
    

    An example usage would be:

    class TestDelegate : public WidgetDelegate<QLabel>{
        Q_OBJECT
        Q_DISABLE_COPY(TestDelegate)
    public:
        explicit TestDelegate(QObject* parent=Q_NULLPTR)
            :WidgetDelegate<QLabel>(parent)
        {}
        virtual void setEditorData(QWidget *editor, const QModelIndex &index) const Q_DECL_OVERRIDE{
            QLabel* const lab = qobject_cast<QLabel*>(editor);
            Q_ASSERT(lab);
            lab->setText("Content: <b>"+index.data().toString()+"</b>");
        }
    };
    
    int main(int argc, char *argv[])
    {
    
        QApplication app(argc,argv);
        QListWidget w;
        w.model()->insertColumns(0,2);
        w.model()->insertRows(0,2);
        w.model()->setData(w.model()->index(0,0),"0,0");
        w.model()->setData(w.model()->index(1,0),"1,0");
        TestDelegate* tempDelegate = new TestDelegate(&w);
        w.setItemDelegate(tempDelegate);
        w.show();
        return app.exec();
    }
    


  • @VRonin First, sorry for the late response. I moved to another city and things were messy.

    Thanks for working out a viable solution, but I don't understand how this is an improvement to what I provided already. I provided in my question a solution that uses a pixmap without needing an editor, and I was complaining that a pixmap was needed. Your solution needs a pixmap + editor.

    A pixmap makes the widget look ugly, and dynamic widgets (like progress bar that glows on Windows from left to right) is always rendered at the starting time, making the glow show only at one place repeatedly. If using a pixmap is the only way to go, could you please explain how to make the rendering time-dependent? (so that a glowing progress bar will glow over time correctly).



  • You are right in pointing out this is not the ultimate solution but just a "make-it-work" one.

    If, for example, you want a glowing progressbar you'd need something like:

    #include <QStyledItemDelegate>
    #include <QPainter>
    #include <QVariantAnimation>
    #include <QLinearGradient>
    class TestDelegate : public QStyledItemDelegate {
        Q_OBJECT
        Q_DISABLE_COPY(TestDelegate)
    public:
        TestDelegate(QObject* parent=Q_NULLPTR)
            : QStyledItemDelegate(parent)
            , m_glowAnimation(new QVariantAnimation(this))
        {
            m_glowAnimation->setEasingCurve(QEasingCurve(QEasingCurve::Linear));
            m_glowAnimation->setDuration(1000);
            m_glowAnimation->setLoopCount(-1);
            m_glowAnimation->setStartValue(0.0);
            m_glowAnimation->setKeyValueAt(0.5,1.0);
            m_glowAnimation->setEndValue(0.0);
            connect(m_glowAnimation,&QVariantAnimation::valueChanged,this,&TestDelegate::requestRepaint,Qt::QueuedConnection);
            m_glowAnimation->start();
        }
        Q_SIGNAL void requestRepaint();
    
        virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE{
            painter->save();
            QRect progressRect(
                        option.rect.topLeft()
                        ,QSize(static_cast<double>(option.rect.width()) *index.data(Qt::UserRole).toDouble() /100.0,option.rect.height())
                        );
            QLinearGradient glowGradient;
            const double currGlow =m_glowAnimation->currentValue().toDouble();
            glowGradient.setColorAt(0.0,Qt::green);
            glowGradient.setColorAt(1.0,Qt::green);
            glowGradient.setColorAt(currGlow,Qt::white);
            glowGradient.setStart(progressRect.topLeft().x(),progressRect.topLeft().y()/2.0);
            glowGradient.setFinalStop(progressRect.bottomRight().x(),progressRect.topLeft().y()/2.0);
            painter->fillRect(progressRect,glowGradient);
            painter->restore();
            QStyledItemDelegate::paint(painter,option,index);
        }
    
    private:
        QVariantAnimation* m_glowAnimation;
        double m_glowPosition;
    };
    


  • @VRonin Thank you for the another example. I have tried for a few hours to get this to work, but it wouldn't work. There are many problems in getting this to work. The first problem is that, even if this works, it'll be generic and all progress bars will be glowing at the same vertical position, since the delegate uses the same glowing position for all list widgets. Now assuming we're OK with that last issue, the example you provided draws directly on the rectangle of the widget, not the progress bar, which takes us back to square-one, where I completely fail at painting without a pixmap. So the next attempt for me was to draw this on the pixmap, which means that I have to do the math to calculate the position of the progress bar with respect to the widget, and given that I'm not an expert in QPainter, there's no feedback mechanism to know whether what I'm doing makes sense, except to just compile and try again and again, until I gave up.

    So, could you please provide the same code with something compatible with the working version of my code that uses a QPixmap from my original question? I provide the code below again for convenience. As this is incomplete, just assume that itemWidget has a member progressBar of type QProgressBar* that we have to make glow.

    void FileQueueItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
    {
        QPaintDevice* original_pdev_ptr = painter->device();
    
        FileQueueListItem* itemWidget = reinterpret_cast<FileQueueListItem*>(index.data(Qt::UserRole).value<void*>());
    
        itemWidget->setGeometry(option.rect);
        painter->end();
    
        QPixmap pixmap(itemWidget->size());
        if (option.state & QStyle::State_Selected)
            pixmap.fill(option.palette.highlight().color());
        else
            pixmap.fill(option.palette.background().color());
        itemWidget->render(&pixmap,QPoint(),QRegion(),QWidget::RenderFlag::DrawChildren);
    
        painter->begin(original_pdev_ptr);
        painter->drawPixmap(option.rect, pixmap);
    }
    


  • @SamerAfach said in Paint widget directly on QListView with QStyledItemDelegate::paint():

    The first problem is that, even if this works, it'll be generic and all progress bars will be glowing at the same vertical position

    This is easily solved by just adding the glow position as another role in your model.

    @SamerAfach said in Paint widget directly on QListView with QStyledItemDelegate::paint():

    So the next attempt for me was to draw this on the pixmap

    The problem is that you are trying to take an existing widget and paint it. What you should do is, in the case of a progressbar, is fill a QStyleOptionProgressBar struct with what you want to draw, then call qApp->style()->drawControl(QStyle::CE_ProgressBar, optionProgressBar, painter)



  • @VRonin Thanks for the response, and I'd like to say that I really appreciate your patience and helpfulness.

    Actually, I know this call/solution (QStyleOptionProgressBar) from the torrent example in Qt. However, this has the problem that it paints a progress bar only. Is it possible to use this and draw a whole widget that includes a progress bar as a part of it? I would appreciate an example on that.



  • @SamerAfach said in Paint widget directly on QListView with QStyledItemDelegate::paint():

    Is it possible to use this and draw a whole widget that includes a progress bar as a part of it?

    It is, if you lay out things vertically or horizontally it's very easy. if you go to grid or anything more complex it gets hard really fast.

    The trick btw is just to set the rect member of the relevant QStyleOption to the rectangle where you want the widget to be printed



  • @VRonin
    Well, if I lay everything horizontally, then I could just use a QTableView :-)

    I assume you're saying that things get hard really fast because one has to calculate the xy coordinates of the controls before painting them. If this is correct, it tells me that it's possible to use drawControl() multiple times in the same paint() to draw in different positions. Is there an example that shows how to do this and draw different controls in different positions (rects)? If such an example exists, I can imagine a viable solution of getting the rect of every control in the widget, and drawing it myself in the delegate's paint() using drawControl(). What do you think?



  • All QStyleOption have a rect member that determines where the control is printed



  • @VRonin Thanks. I'll try to paint everything with drawControl() and provide feedback.



  • @VRonin I managed to paint everything manually... I had to calculate the coordinates of every widget and draw it, and it worked. I have to say it's quite depressing that Qt doesn't have a solution to paint a widget as is.

    Thanks for the help, and have a nice day :-)


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.