Friday, December 10, 2010

Creating custom QItemDelegate with QPushButton in Qt

Recently I was working on Audiobook Reader application for maemo. For this application I have created one custom item delegate for QListView which contains two clickable item and generate signal accordingly.

For application I was using some different mechanism to create button inside custom item delegate, but now I found better way to create custom item delegate with QPushButton. So thought to share code.

Following is code for custom item delegate derived from QItemDelegate.

Update: I have uploaded following code to gitorous, Please visit this link if you want a working sample code.

#include <QItemDelegate>
class CustomItemDelegate : public QItemDelegate
{
    Q_OBJECT
public:
    CustomItemDelegate(QObject *parent = 0);
    virtual void paint(QPainter *painter,
                       const QStyleOptionViewItem &option,
                       const QModelIndex &index) const ;

    virtual QSize sizeHint(const QStyleOptionViewItem &option,
                           const QModelIndex &index) const ;

    bool editorEvent(QEvent *event, QAbstractItemModel *model, 
                           const QStyleOptionViewItem &option, 
                           const QModelIndex &index);

signals:
    void buttonClicked(const QModelIndex &index);
private:
    QStyle::State  _state;
};

#include "customitemdelegate.h"
...

CustomItemDelegate::CustomItemDelegate(QObject *parent) :
    QItemDelegate(parent)
{
    _state =  QStyle::State_Enabled;
}

void CustomItemDelegate::paint(QPainter *painter,
                   const QStyleOptionViewItem &option,
                   const QModelIndex &index) const
{
   const QStandardItemModel* model = 
   static_cast<const QStandardItemModel*>(index.model());
   QStandardItem* item = model->item(index.row());

   QString text = item->text();
   QRect rect = option.rect;

    QRect textRect( rect);
    textRect.setHeight( 30);
    painter->drawText(textRect,text);

    QRect buttonRect( rect);
    buttonRect.setY(textRect.y()+ 35);
    buttonRect.setHeight( 30);
    QStyleOptionButton button;
    button.rect = buttonRect;
    button.text = text;
    button.state = _state | QStyle::State_Enabled;

    QApplication::style()->drawControl
        (QStyle::CE_PushButton, &button, painter);
}

QSize CustomItemDelegate::sizeHint(const QStyleOptionViewItem &/*option*/,
                       const QModelIndex &/*index*/) const
{
    //hard coding size for test purpose, 
    //actual size hint can be calculated from option param
    return QSize(800,70);
}

bool CustomItemDelegate::editorEvent(QEvent *event, 
    QAbstractItemModel *model, 
    const QStyleOptionViewItem &option, 
    const QModelIndex &index)
{
    if( event->type() == QEvent::MouseButtonPress ||
        event->type() == QEvent::MouseButtonRelease ) {
    } else {
         //ignoring other mouse event and reseting button's state
         _state = QStyle::State_Raised;
        return true;
    }

    QRect buttonRect( option.rect);
    buttonRect.setY(option.rect.y()+ 35);
    buttonRect.setHeight( 30);

    QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
    if( !buttonRect.contains( mouseEvent->pos()) ) {
        _state = QStyle::State_Raised;
        return true;
    }

    if( event->type() == QEvent::MouseButtonPress) {            
        _state = QStyle::State_Sunken;
    } else if( event->type() == QEvent::MouseButtonRelease) {
        _state = QStyle::State_Raised;
        emit buttonClicked( index);
    }    
    return true;
}
Basically in above code, I am calculating rect where I want to draw my button and drawing QPushButton on list item using QStyleOptionButton class.

And in editor event on mouse press and release event, I am checking if mouse position on click falls into my button's rect or not. If it falls inside my button's rect then I am emitting signal.

I am using item's signal as shown in below code.
CustomList::CustomList(QWidget *parent) :
    QWidget(parent),_view(0)
{
    _view = new QListView();
    _view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    _view->setEditTriggers(QAbstractItemView::NoEditTriggers);

    //creating custom item delegate and setting  it to view
    CustomItemDelegate* itemDelegate = new CustomItemDelegate(_view);
    _view->setItemDelegate( itemDelegate );

    _view->setModel(&_model); 
   
    //connecting delegate's signal to this class's slot
    connect(itemDelegate,SIGNAL(buttonClicked(QModelIndex)),
    this,SLOT(listButtonClicked(QModelIndex)));

    QHBoxLayout* mainLayout = new QHBoxLayout(this);
    mainLayout->addWidget( _view);

    //creating and adding data to model
    QStandardItem* item = new QStandardItem;
    item->setText("testing");

    QStandardItem* item1 = new QStandardItem;
    item1->setText("testing1");

    _model.appendRow(item);
    _model.appendRow(item1);
}

// following slot will be invoked when delegate's button is clicked
void CustomList::listButtonClicked(const QModelIndex &index)
{
    qDebug() << "######### listbutton clicked ######### " << index.row();
}
Following is snap of how custom item delegate looks.

13 comments:

  1. Hi Kunal, ur blog is really helpful one for beginning to use Qt.

    I have sent you a mail, in case you have missed it. Thanx. :)

    ReplyDelete
  2. Hi,Thanks for writing. I will check and reply.

    ReplyDelete
  3. Very nice code!! i have tried it, but i have the problem that just the first click to a button it make me all the buttons as clicked... but if click another place of table it doesn't happens.. do you know why can it be?

    ReplyDelete
  4. Currently not sure of exact problem, It seems to me that there is some problem regarding calculation to check mouse point inside button rect or not.

    I will look in to my sample and see what happens there.

    Thanks for writing.

    ReplyDelete
  5. Hi Kunal, the code is pretty good. One quick question, are you able to check the problem that just the first click to a button it make me all the buttons as clicked, I have the exact same problem. Thanks!

    ReplyDelete
  6. Ahh.. looks like i forget to work on this, I will soon try to answer you. Thanks for checking my blog.

    ReplyDelete
  7. Hi, I tried to reproduce the problem, but was not able to reproduce it. I have uploaded my code to gitorous, which is working fine. Please use this code and see if has same issue or not.

    https://gitorious.org/kunaltest/kunaltest/trees/master/testdelegate

    ReplyDelete
  8. Anyone solved this problem ? Got also that same one...

    ReplyDelete
  9. I solved this problem.

    All you need is to add parameter, for example:
    int _currentRow
    It will show current row in paint function.
    In editorEvent set this parameter in case of QMousePressEvent:
    _currentRow = index.row();
    And in case of QMouseReleaseEvent:
    _currentRow = -1;
    And of course in constructor set:
    _currentRow = -1;
    Than in delegate paint function do something like that:
    if (_currentRow == index.row)
    {
    /// Draw button with QStyle::State_Sunken style
    }
    else
    {
    /// Draw button with QStyle::State_Sunken style
    }

    That's all. It works!
    If somebody needs code example, my mail BootGenius@yandex.ru

    ReplyDelete
    Replies
    1. I have made a mistake.
      After last else it needs to write:
      /// Draw button with QStyle::State_Raised style

      Delete
  10. What if you want to paint focus on the element clicked? if say, it was a qcheckbox?

    ReplyDelete