How to create delegate for QTreeWidget? - c++

Here is what I'm trying to do (all parents and children must have a close button on the right, in the future, only the hovered item will be able to show the **close ** button):
My delegate code:
class CloseButton : public QItemDelegate
{
Q_OBJECT
public:
CloseButton( QObject* parent = 0 )
: QItemDelegate( parent )
{};
QWidget* createEditor( QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index ) const
{
if ( index.column() == 1 )
{
QToolButton* button = new QToolButton( parent );
button->setIcon( QIcon( CLOSE_ICON ) );
//button->setFixedSize( 16, 16 );
//button->setAutoRaise( true );
//button->setVisible( true );
CONNECT( button, SIGNAL( clicked() ), this, SLOT( emitCommitData() ) );
return button;
}
return ( new QWidget );
}
private slots:
void emitCommitData()
{
emit commitData( qobject_cast< QWidget* >( sender() ) );
}
private:
//Q_DISABLE_COPY( CloseButton );
};
With QTreeWidget connection code:
recipientsView()->setItemDelegateForColumn( 1, new CloseButton( this ) );
where recipientsView() is a simple QTreeWidget.
Problem is that QToolButtons are not shown at all (it must be in the second column, i.e. column index in the tree is 1). What I'm doing wrong?
I have checked already all Qt demo examples about delegates and the first Google result about QItemDelegate's and similar stuff.

You can use the QStyledDelegate::paint function to draw the close icon, without using any widget, and the editorEvent to receive mouse events for the item, even if you don't use the editor or make the item editable.
class CloseButton : public QStyledItemDelegate {
Q_OBJECT
public:
explicit CloseButton(QObject *parent = 0,
const QPixmap &closeIcon = QPixmap())
: QStyledItemDelegate(parent)
, m_closeIcon(closeIcon)
{
if(m_closeIcon.isNull())
{
m_closeIcon = qApp->style()
->standardPixmap(QStyle::SP_DialogCloseButton);
}
}
QPoint closeIconPos(const QStyleOptionViewItem &option) const {
return QPoint(option.rect.right() - m_closeIcon.width() - margin,
option.rect.center().y() - m_closeIcon.height()/2);
}
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const {
QStyledItemDelegate::paint(painter, option, index);
// Only display the close icon for top level items...
if(!index.parent().isValid()
// ...and when the mouse is hovering the item
// (mouseTracking must be enabled on the view)
&& (option.state & QStyle::State_MouseOver))
{
painter->drawPixmap(closeIconPos(option), m_closeIcon);
}
}
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QSize size = QStyledItemDelegate::sizeHint(option, index);
// Make some room for the close icon
if(!index.parent().isValid()) {
size.rwidth() += m_closeIcon.width() + margin * 2;
size.setHeight(qMax(size.height(),
m_closeIcon.height() + margin * 2));
}
return size;
}
bool editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index)
{
// Emit a signal when the icon is clicked
if(!index.parent().isValid() &&
event->type() == QEvent::MouseButtonRelease) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
QRect closeButtonRect = m_closeIcon.rect()
.translated(closeIconPos(option));
if(closeButtonRect.contains(mouseEvent->pos()))
{
emit closeIndexClicked(index);
}
}
return false;
}
signals:
void closeIndexClicked(const QModelIndex &);
private:
QPixmap m_closeIcon;
static const int margin = 2; // pixels to keep arount the icon
Q_DISABLE_COPY(CloseButton)
};

First, I should ask if you are really using a QTreeWidget, or rather a QTreeView? You cannot use custom delegates with a QTreeWidget according to the documentation for QTreeView, and will have to use a QTree*View* and some form of QAbstractItemModel for you to be able to use a custom delegate.
Ah, scratch that. I see that you're calling setItemDelegateForColumn, which is a QTreeView function, but you should be aware of the difference, so I'm keeping the above paragraph. :)
I would check that your model's flags() function is returning Qt::ItemIsEditable as part of its item flags. The createEditor() method is called whenever an edit event is reported by the view. (The view events that will trigger an edit depend on the model's EditTriggers) Usually a double-click on the delegate will trigger an edit by default, among other things.
I doubt that you want the close button to appear only on double-click, though. To get the button to appear all the time, you'll have to reimplement the delegate's paint() function to draw a button, Among other things. I found Qt's StarDelegate example to be quite helpful in this regard, and I suspect you'll find it useful too.

You can use a QItemDelegate with a QTreeWidget this way (example in PyQt, sorry):
myTreeWidget = QtGui.QTreeWidget()
myTreeWidget.setItemDelegate(myDelegate())
class myDelegate(QtGui.QItemDelegate):
def paint(self, painter, option, index):
#Custom Draw Column 1
if index.column() == 1:
icon = QtGui.QIcon(index.data(QtCore.Qt.DecorationRole))
if icon:
icon.paint(painter, option.rect)
#You'll probably want to pass a different QRect
#Use the standard routine for other columns
else:
super(myDelegate, self).paint(painter, option, index)

Related

How to get currentIndex of QListView in QStyledItemDelegate::paint()

I defined pure virtual method QStyledItemDelegate::paint as:
void FooViewDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const
{
bool selected = option.state & QStyle::State_Selected;
// ...
// drawing code
}
But I cant't figure how to know is the drawing item current or no (The same item as from QListView::currentIndex()).
Qt MVC is not designed for such usecases, because, theoretically, delegate should not know, what view you are using (it may be QListView or QTableView).
So, a "good way" is to keep this information inside your delegate (because model may be used by sevaral views). Fox example (pseudo-code):
class FooViewDelegate : ...
{
private:
QModelIndex _currentIndex;
void connectToView( QAbstractItemView *view )
{
connect( view, &QAbstractItemView::currentChanged, this, &FooViewDelegate ::onCurrentChanged );
}
void onCurrentChanged( const QModelIndex& current, const QModelIndex& prev )
{
_currentIndex = current;
}
public:
void paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const
{
bool selected = index == _currentIndex;
// ...
// drawing code
}
}
The parent of the delegate is the view, you can directly obtain the current index from the view.
void FooViewDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const
{
bool selected = index == parent()->currentIndex();
}
You're along the right track:
auto current = option.state & QStyle::State_HasFocus;
The item with focus is the current item.

QStyledItemDelegate reimplementation: color item background when mouse over

My original aim was to feed items (QStandardItem) of a specific column with rich text, therefore I implemented a subclass delegate as suggested.
Everything looked fine except one thing: when I moved the mouse pointer over these items, they were not highlighted at all. (The other items in the row - where the original paint method is used - were highlighted.) Item selection worked fine although. Then I added line
if ( optionV4.state & QStyle::State_MouseOver )
in which I was able to handle the item text highlight, but I have no idea how to highlight the background too. It is still white. Any ideas?
Here is the relevant code:
class MStyledItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
protected:
void paint ( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const;
QSize sizeHint ( const QStyleOptionViewItem& option, const QModelIndex& index ) const;
};
void MStyledItemDelegate::paint( QPainter* aPainter, const QStyleOptionViewItem& aOption, const QModelIndex& aIndex ) const
{
// ...
QStyleOptionViewItemV4 optionV4 = aOption;
initStyleOption( &optionV4, aIndex );
QStyle* style = optionV4.widget ? optionV4.widget->style() : QApplication::style();
QTextDocument doc;
doc.setHtml( optionV4.text );
optionV4.text = QString();
style->drawControl( QStyle::CE_ItemViewItem, &optionV4, aPainter );
// highlight text
QAbstractTextDocumentLayout::PaintContext ctx;
if ( optionV4.state & QStyle::State_MouseOver )
{
ctx.palette.setColor( QPalette::Text, Qt::blue );
}
// draw
aPainter->save();
QRect textRect = style->subElementRect( QStyle::SE_ItemViewItemText, &optionV4 );
aPainter->translate( textRect.topLeft() );
aPainter->setClipRect( textRect.translated( - textRect.topLeft() ) );
doc.documentLayout()->draw( aPainter, ctx );
aPainter->restore();
}
I suppose you use it with a QTableView, call the method setMouseTracking(true).

Qt: start editing of cell after one click

By default the cell in QTableView starts being edited after double click. How to change this behavior. I need it to start editing after one click.
I have set combo-box delegate to the cell. When clicking the cell it only selects it. When double clicking on the cell the QComboBox editor is activated but not expanded. I want it to expand after just one click as if I added QComboBox by setCellWidget function of QTableWidget. I need the same effect by using model-view-delegate.
You can just set edit trigger use this function setEditTriggers
C++
yourView->setEditTriggers(QAbstractItemView::AllEditTriggers)
Python:
yourView.setEditTriggers(QAbstractItemView.AllEditTriggers)
enum QAbstractItemView::EditTrigger
flags QAbstractItemView::EditTriggers
This enum describes actions which will initiate item editing.
Constant Value Description
QAbstractItemView::NoEditTriggers 0 No editing possible.
QAbstractItemView::CurrentChanged 1 Editing start whenever current item changes.
QAbstractItemView::DoubleClicked 2 Editing starts when an item is double clicked.
QAbstractItemView::SelectedClicked 4 Editing starts when clicking on an already selected item.
QAbstractItemView::EditKeyPressed 8 Editing starts when the platform edit key has been pressed over an item.
QAbstractItemView::AnyKeyPressed 16 Editing starts when any key is pressed over an item.
QAbstractItemView::AllEditTriggers 31 Editing starts for all above actions.
The EditTriggers type is a typedef for QFlags. It stores an OR combination of EditTrigger values.
Edit after one click
You can reimplement mousePressEvent in view you are using
void YourView::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
QModelIndex index = indexAt(event->pos());
if (index.column() == 0) { // column you want to use for one click
edit(index);
}
}
QTreeView::mousePressEvent(event);
}
Expanded QCombobox when edit
You should imlement setEditorData in your subclass of QItemDelegate and at the end call showPopup.
But it has some unexpected behaviour. QComboBox disappears when mouse leave its area. But for me it is advantage.
I can select different item with single click and release.
void IconDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
Q_UNUSED(index);
QComboBox *comboBox = qobject_cast<QComboBox*>(editor);
// Add data
comboBox->addItem(QIcon(":/icons/information16.png"), "info");
comboBox->addItem(QIcon(":/icons/warning16.png"), "warning");
comboBox->addItem(QIcon(":/icons/send16.png"), "send");
comboBox->addItem(QIcon(":/icons/select16.png"), "select");
comboBox->showPopup(); // <<<< Show popup here
}
Together it works fast way. Click and hold to choose item and commit data on release ( Just one click and release )
If you want click to show expanded qcombobox and next click to choose/hide, I do not know solution for now.
Based on the idea provided by Jason, I came up with this solution.
To launch the editor on single click, I connected QAbstractItemView::clicked(const QModelIndex &index) signal of my view, to QAbstractItemView::edit(const QModelIndex &index) slot of that same view.
If you use Qt4, you need to create a slot in your delegate. Pass your combobox as an argument to this slot. In this slot you call QComboBox::showPopup. So it will look like this:
void MyDelegate::popUpComboBox(QComboBox *cb)
{
cb->showPopup();
}
But first we need to register the QComboBox* type. You can call this in the constructor of your delegate:
qRegisterMetaType<QComboBox*>("QComboBox*");
The reason we need this slot, is because we can't show the pop up straight away in MyDelegate::createEditor, because the position and the rect of the list view are unknown. So what we do is in MyDelegate::createEditor, we call this slot with a queued connection:
QComboBox *cb = new QComboBox(parent);
// populate your combobox...
QMetaObject::invokeMethod(const_cast<MyDelegate*>(this), "popUpComboBox", Qt::QueuedConnection, Q_ARG(QComboBox*, cb));
This will show the list view of the combobox correctly when the editor is activated.
Now if you are using Qt5, the slot is not needed. All you do is call QComboBox::showPopup with a queued connection from MyDelegate::createEditor. The easiest way to do this is with a QTimer:
QTimer::singleShot(0, cb, &QComboBox::showPopup);
For some extra information, this is how you can paint the combobox so it is shown all the time, not only when the editor is shown:
void MyDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if(index.column() == 1) // show combobox only in the second column
{
QStyleOptionComboBox box;
box.state = option.state;
box.rect = option.rect;
box.currentText = index.data(Qt::EditRole).toString();
QApplication::style()->drawComplexControl(QStyle::CC_ComboBox, &box, painter, 0);
QApplication::style()->drawControl(QStyle::CE_ComboBoxLabel, &box, painter, 0);
return;
}
QStyledItemDelegate::paint(painter, option, index);
}
This solution works perfeclty for me. Single click on a cell, and the combo pops up.
class GFQtComboEnumItemDelegate : public QStyledItemDelegate
{
void setEditorData(QWidget *editor, const QModelIndex &index) const
{
QComboBox* pE = qobject_cast<QComboBox*>(editor);
... // init the combo here
if(m_must_open_box)
{
m_must_open_box = false;
pE->showPopup();
}
}
bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)
{
if (event->type() == QEvent::MouseButtonRelease)
{
QMouseEvent* pME = static_cast<QMouseEvent*>(event);
if(pME->button() == Qt::LeftButton)
{
QAbstractItemView* pView = qobject_cast<QAbstractItemView*>( const_cast<QWidget*>(option.widget) );
if(pView != nullptr)
{
emit pView->setCurrentIndex(index);
m_must_open_box = true;
emit pView->edit(index);
}
return true;
}
}
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
mutable bool m_must_open_box;
};
If you override QStyledItemDelegate::createEditor() then you can expand the combo box after it is created.

Animation of list items/redrawing Qt QListView

What I'd like to achieve:
A visual indication to draw attention to newly added items in a QListView. I had in mind having the background color 'throb' once (fading from a color to the background).
The setup
I have a model/view using QListView displaying QStandardItems. Qt version 4.7
What I've tried:
I've created a new class derived from QStyledItemDelegate. I have my own paint method to render the item. That part works. I created a QTimeLine object and set it up to create events to redraw the items.
I can't figure out how to trigger redraws of the QListView item.
In the item delegate constructor:
timeLine = new QTimeLine( 3000, this );
timeLine->setFrameRange( 100, 0 );
connect( timeLine, SIGNAL( frameChanged( int ) ), this, SLOT( update() ) );
timeLine->start();
I tried connecting to the sizehintChanged event but this does not work
void myDelegate::update()
{
const QModelIndex index;
emit QStyledItemDelegate::sizeHintChanged( index );
}
Any suggestions? Can this be done with style sheets?
The standard practice to include animations into code is to use state machines.
Animations in Qt cannot be achieved using QtStylesheets. Either use QML or use QStyledItemDelegate and a state machine.
/*CustomItemDelegate*/
int state;
enum states{
animating,
normal
}
void setstate(int state){
this->state = state;
/*Start animation depending on state ,by starting a QTimer and calling
repaint when the timer expires,also change animation variables like opacity ,
angle etc etc*/
}
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const{
switch(state){
case animating:
break;
case normal;
break;
}
}
....
/*CustomListView*/
slots:
void dataChanged ( const QModelIndex & topLeft, const QModelIndex & bottomRight ){
( (CustomItemDelegate)itemDelegate(topleft) )->setState(animating);
}
....
/*Mainwindow*/
connect(model,SIGNAL(datachanged(QModelIndex,QModelindex)),view,SLOTS(QModelindex,QModelindex));

How to open an URL in a QTableView

What is the best way to present a clickable URL in a QTableView (or QTreeView, QListView, etc...)
Given a QStandardItemModel where some of the columns contain text with URLs I'd like them to become clickable and then handle the click by using QDesktopServices::openURL()
I was hoping there would be some easy way to leverage QLabel's textInteraction flags and to cram them into the table. I can't believe there's not an easier way to handle this. I really hope I'm missing something.
You'll need to create a delegate to do the painting. The code should look something like this:
void RenderLinkDelegate::paint(
QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index
) const
{
QString text = index.data(Qt::DisplayRole).toString();
if (text.isEmpty())
return;
painter->save();
// I only wanted it for mouse over, but you'll probably want to remove
// this condition
if (option.state & QStyle::State_MouseOver)
{
QFont font = option.font;
font.setUnderline(true);
painter->setFont(font);
painter->setPen(option.palette.link().color());
}
painter->drawText(option.rect, Qt::AlignLeft | Qt::AlignVCenter, text);
painter->restore();
}
Well, you can use delegates to render rich text in a qtableview with custom delegates reimplementing the paint method such as:
void CHtmlDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QStyleOptionViewItemV4 opt(option);
QLabel *label = new QLabel;
label->setText(index.data().toString());
label->setTextFormat(Qt::RichText);
label->setGeometry(option.rect);
label->setStyleSheet("QLabel { background-color : transparent; }");
painter->translate(option.rect.topLeft());
label->render(painter);
painter->translate(-option.rect.topLeft());
}
However, it will not make hyperlinks clickable.
To do so, you can use the following hack. Reimplement the setModel method of your table/list view and use setIndexWidget.
void MyView::setModel(QAbstractItemModel *m)
{
if (!m)
return;
QTableView::setModel(m);
const int rows = model()->rowCount();
for (int i = 0; i < rows; ++i)
{
QModelIndex idx = model()->index(i, 1);
QLabel *label = new QLabel;
label->setTextFormat(Qt::RichText);
label->setText(model()->data(idx, CTableModel::HtmlRole).toString());
label->setOpenExternalLinks(true);
setIndexWidget(idx, label);
}
}
In the example above, I replace column 1 with qlabels. Note that you need to void the display role in the model to avoid overlapping data.
Anyway, I would be interested in a better solution based on delegates.
Sadly, its not that easy to render a QLabel with setOpenExternalLinks() when using a QTableView (as opposed to using a QTableWidget). There are no magic two lines of code you can call and have the job done.
use a delegate
set the delegate to the column of your table
use QTextDocument combined with setHTML() to render a html link
this means your model needs to provide an HTML fragment containing a href
calculate the geometry of the link and provide event handlers for to intercept the mouse
change the cursor when it is over the link
execute the action when the link is clicked
what a mess :( i want painter->setWidgetToCell()
--
void LabelColumnItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if( option.state & QStyle::State_Selected )
painter->fillRect( option.rect, option.palette.highlight() );
painter->save();
QTextDocument document; // <---- RichText
document.setTextWidth(option.rect.width());
QVariant value = index.data(Qt::DisplayRole);
if (value.isValid() && !value.isNull())
{
document.setHtml(value.toString()); // <---- make sure model contains html
painter->translate(option.rect.topLeft());
document.drawContents(painter);
}
painter->restore();
}