I need show files from QFileSystemModel in QTreeView and customize that tree to show one more column with QCheckBox, so user can pick 0..N files from that QTreeView.
I read doc from Qt to understand model/view architecture and i am now in my code at point, where i have custom delegate CustomItemDelegatefor specific column, but actually i don't know how to create QCheckBox in paint method of my custom delegate (to be more specific i know how, but this is 99% bad way).
customitemdelegate.h
#ifndef CUSTOMITEMDELEGATE_H
#define CUSTOMITEMDELEGATE_H
#include <QStyledItemDelegate>
class CustomItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit CustomItemDelegate(QObject *parent = 0);
void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const;
signals:
public slots:
};
#endif // CUSTOMITEMDELEGATE_H
customitemdelegate.cpp
#include "customitemdelegate.h"
#include <QCheckBox>
#include <iostream>
#include <QTreeView>
using namespace std;
CustomItemDelegate::CustomItemDelegate(QObject *parent) :
QStyledItemDelegate(parent)
{
}
void CustomItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const {
((QTreeView *)parent())->setIndexWidget(index, new QCheckBox());
}
You don't create a QCheckbox, you paint one using the current style. Look at the docs regarding QStyle, specifically for drawControl(..). There's also a customised example I wrote for a question on SO which you can get an idea from.
Mouse handling has to be handled by the view (because the control doesn't actually exist), and for most styles that will include mouse-over updating.
It is a bit of a pain (things may have gotten easier in v5.0+, I last did this in v4.8), but it's well worth it. Creating 'real' QCheckBoxs is inefficient (in your example it will cause a massive memory leak), and becomes noticeably slow for large datasets. Whereas painting a 'fake' one only when required (i.e. visible) is very fast.
Related
Context
TLTR: For a command-line console widget, I need to be able to select the texts on a QListView row.
In a similar way that your browser command-line (e.g. pressing F12 on Firefox and going to "Console", similar for Chrome, others). I am creating a command-line console for interacting with my application.
Each command and it result is pushed into a list above the input text-box, allowing each item to be drawn nicely and user-friendly:
The text goes through a QSyntaxHighlighter
Long lines, or multiple lines are elided
Results which are objects can be expanded or collapsed
etc..
Most of those goals are not yet implemented, but it's clear that I need a custom-widget to represent each row.
Now, I need the text on that QListView item to be selectable, and copiable.
The problem
TLTR: Selecting the texts requiers to enter edit-mode, I don't like having to double-click first for selecting text.
Using a QStyledItemDelegate,
Overriding paint: I managed to have a draft of the apparence I need. But that apparence is mostly static, no interaction exists.
Overriding createEditor and setEditorData, the content can be edited. I can set the QTextEdit as readOnly and then, it is just selectable as required.
However, the list items needs to be double-clicked, or selected+clicked in order to get into edit-mode, and being able to select the text.
But, the user expects to select the text as it is, just by pressing+moving+relasing the mouse.
The text should be selectable as it is, without double-clicking the row for getting into edit-mode
Some code
#include <QApplication>
#include <QListView>
#include <QStyledItemDelegate>
#include <QLabel>
#include <QPainter>
#include <QPaintEvent>
#include <QStandardItemModel>
#include <QTextEdit>
class CommandLineItemDelegate: public QStyledItemDelegate
{
Q_OBJECT
mutable QTextEdit m_editor;
public:
void paint(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
QRect rect(QPoint(0, 0), option.rect.size());
m_editor.setPlainText( index.data().toString());
m_editor.resize(option.rect.size());
p->save();
p->translate(option.rect.topLeft());
m_editor.render(p, QPoint(), QRegion());
p->restore();
}
QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override
{
return QSize(200,50);
}
QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
auto* edit = new QTextEdit(parent);
edit->setGeometry(option.rect);
edit->setReadOnly(true);
return edit;
}
void setEditorData(QWidget *editor, const QModelIndex &index) const override
{
auto* textEditor = dynamic_cast<QTextEdit*>(editor);
if (textEditor != nullptr)
{
textEditor->setPlainText(index.data().toString());
}
}
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override
{
auto* textEditor = dynamic_cast<QTextEdit*>(editor);
if (textEditor != nullptr)
{
model->setData(index, textEditor->toPlainText());
}
}
};
class CommandLineListView: public QListView
{
Q_OBJECT
CommandLineItemDelegate m_delegate;
QStandardItemModel m_model;
public:
explicit CommandLineListView( QWidget* parent=nullptr)
: QListView(parent)
, m_delegate()
{
setModel(&m_model);
m_model.insertColumn(0);
m_model.insertRows(0,3);
m_model.setData(m_model.index(0,0),"var adri = function(a,b){return a+b; }; // function to sum");
m_model.setData(m_model.index(1,0),"Math.PI");
m_model.setData(m_model.index(2,0),"2+2");
setSelectionMode(QAbstractItemView::SelectionMode::NoSelection); //Text selection, but no row selection.
setItemDelegate(&m_delegate);
}
};
#include "main.moc"
int main(int argn, char* argv[])
{
QApplication app(argn, argv);
CommandLineListView list;
list.show();
app.exec();
}
As explained before, this "mostly" works, except that the user needs to double-click the row to enter edit-mode and selecting the text, which is not acceptable.
Quick solution: easy, but not very performant for large list views
A quick solution is to keep all the editors open, by calling openPersistentEditor for each added row.
Note that this is not the most performant solution (for very large list view), but may be good enough for your use case.
Alternative 1: Implement your own QStyledItemDelegate
This allows full customisation of the formatting, but also requires that you implement the text selection feature yourself.
Alternative 2: Use HTML
Displaying it as HTML (which is probably what Chrome and Firefox do) allows you full customisation and using the built-in selection feature.
Alternative 3: Use QML
QML is often easier to (rapidly) create a custom user interface.
My application starts with an empty table, and I want to imlement different methods to add items. One should be by double-clicking the table's unused area (or "background") that is not occupied by any cells. When a cell is double-clicked, I want the default behavior.
I've found way to do this by re-implementing QAbstractScrollArea::mouseDoubleClickEvent() method in my TestTable class:
#include <QMouseEvent>
#include <QTableView>
class TestTable : public QTableView
{
Q_OBJECT
signals:
void backgroundDoubleClickEvent(void);
protected:
void mouseDoubleClickEvent (QMouseEvent* e)
{
if (indexAt(e->pos()).isValid())
{
QTableView::mouseDoubleClickEvent(e);
}
else
{
e->accept();
emit backgroundDoubleClickEvent();
}
}
};
This works, but is there a more elegant way of doing this without subclassing QTableView?
I'm not aware of any limitations of my current implementation. Are there obvious caveats?
If you don't want to subclass QTableView, try installEventFilter
I use drag and drop in my QTableView (works). However, I do not see any drop indicator. I should see a line where the drop is supposed to be inserted, shouldn't I? At least here they say so.
My init is pretty much standard.
// see model for implementing logic of drag
this->viewport()->setAcceptDrops(allowDrop);
this->setDragEnabled(allowDrag);
this->setDropIndicatorShown(true);
this->m_model->allowDrop(allowDrop);
I have no idea why I do not see the indicator. A style sheet is used with the views, could that be the reason. However, I have disabled the stylesheet and still do not see it.
The view uses entire rows for selection, not sure if this causes an issue. So any hint is appreciated.
-- Edit --
As of the comment below, tried all selection modes: single, multi or extended, no visual effect. Also tried cell instead of row selection, again no improvement.
-- Edit 2 --
Currently evaluating another style proxy example, similar to the one below, originally referenced here
-- Related --
QTreeView draw drop indicator
How to highlight the entire row on mouse hover in QTableWidget: Qt5
https://forum.qt.io/topic/12794/mousehover-entire-row-selection-in-qtableview/7
https://stackoverflow.com/a/23111484/356726
I faced the same problem, I tried two options which both worked for me. IIRC, the help came from an answer on SO.
if you are subclassing QTreeView, you can override its paintEvent() method. It is calling by default the drawTree() method and the paintDropIndicator() one (the latter being part of QAbstractItemView private class).
You can call drawTree() from your paintEvent(), and it should override the default drag and drop indicator as well :
class MyTreeView : public QTreeView
{
public:
explicit MyTreeView(QWidget* parent = 0) : QTreeView(parent) {}
void paintEvent(QPaintEvent * event)
{
QPainter painter(viewport());
drawTree(&painter, event->region());
}
};
the other method is to subclass QProxyStyle and overriding the drawPrimitive() method. When you get the element QStyle::PE_IndicatorItemViewItemDrop as a parameter, you can paint it your own way.
The code will look like this:
class MyOwnStyle : public QProxyStyle
{
public:
MyOwnStyle(QStyle* style = 0) : QProxyStyle(style) {}
void drawPrimitive(PrimitiveElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const
{
if (element == QStyle::PE_IndicatorItemViewItemDrop)
{
//custom paint here, you can do nothing as well
QColor c(Qt::white);
QPen pen(c);
pen.setWidth(1);
painter->setPen(pen);
if (!option->rect.isNull())
painter->drawLine(option->rect.topLeft(), option->rect.topRight());
}
else
{
// the default style is applied
QProxyStyle::drawPrimitive(element, option, painter, widget);
}
}
};
I have following problem:
when I call update() on QListView, its paintEvent() is not triggered unless some other event occurs over the widget (mouse move, got focus....)
I am using Qt 4.8.3, and unless this is definitely bug in the version, I would prefer not upgrading (as from my experience upgrades bears more trouble than benefits).
The question:
How to make QListView (and Q...View) to update next time main loop gets control?
Some background as what I am solving if it helps:
Meant as single threaded application.
At the bottom is some independent (no Qt) model, which is hierarchical, and consumers request subitems. Items at the bottom of hierarchy may be modified.
On modification, consumer requests W(ritable)Item. At that point, Model parts influenced by the change reports "modified" via observer approach. So observers are notified at the start of the change (model returns writable object, has no control or idea when change ends).
Consumer is expected to finish modification before returning from function/method which started modification.
Modifying methods/functions are expected to be called from main thread, so next time main thread tinkers with GUI, model is in consistent state, and consumers can refresh.
QModels are done to provide data from model below in Qt accessible format.
Next are QWidgets (lists/textboxes/labels) visualising data to user,
these are modified to support Desync() method, which mark visualised data to be out of sync, and overridden paintEvent, which checks for inSync state. For simple QWidgets like labels, on synchronization, callback is called, which just fills in the data. For Q...View, I assumed to force models to emit modelReset, so list reloads number of rows and content of visible ones.
On the top is class collecting it all together under its region, which is hooked to observers, and on reported changes Desync relevant widgets.
All methods changing anything are connected via signal/slot Qt thingie to buttons/comboboxes/other GUI elements, so I assume it all runs under main thread.
Idea of change:
GUI induced event, main thread starts processing consumer's change method
consumer obtains necessary items for change
consumer obtains writable items
real model reports modified to observers
observers mark (Desync) relevant QWidgets as out of sync
QWidgets are marked as out of sync, and scheduled to update, yet do not attempt to access anything, as we run under main thread
consumer performs change, during which real model might be even inconsistent
consumer returns control to whatever called it
main loop performs updates, which are overridden to sync widgets
* What I observed: *
update() results in paintEvent for most widgets that do not have model at all (label/ textbox...)
update() does not result in paintEvent for QListView
repaint() does not help (was just wild attempt)
moving mouse over widget results in paintEvent, and QWidget synchronizes
trying to visible(false); update(); visible(true); repaints immediately
that is wrong, as QWidget synchronizes before consumer performs the change
switching windows (i.e. visual studio) or back results in paintEvent being called
Simplified sources where to get the behavior:
myList.h
#ifndef __myList_h__
#define __myList_h__
#include <qlistview.h>
class myList : public QListView
{
bool inSync;
void sync();
protected:
virtual void paintEvent(QPaintEvent * event) override;
public:
myList(QWidget * parent);
void Desync();
virtual ~myList();
};
#endif
myList.cpp
#include "myList.h"
#include "myModel.h"
void myList::sync()
{
if (inSync)
return;
inSync = true; //< set early, to prevent loops
((myModel*)model())->ResetModel();
}
void myList::paintEvent(QPaintEvent * event)
{
sync();
QListView::paintEvent(event);
}
myList::myList(QWidget * parent) : QListView(parent), inSync(false)
{}
void myList::Desync()
{
inSync = false;
update();
}
myList::~myList()
{}
myModel.h
#ifndef __myModel_h__
#define __myModel_h__
#include <QAbstractListModel>
class myModel : public QAbstractListModel
{
Q_OBJECT;
int & externalComplexData;
public:
myModel(int & externalComplexData);
virtual int rowCount(QModelIndex const & parent = QModelIndex()) const override;
virtual QVariant data(QModelIndex const & index, int role) const override;
void ResetModel();
virtual ~myModel();
};
#endif
myModel.cpp
#include "myModel.h"
myModel::myModel(int & externalComplexData) : externalComplexData(externalComplexData)
{}
int myModel::rowCount(QModelIndex const & parent) const
{
return 1;
}
QVariant myModel::data(QModelIndex const & index, int role) const
{
if (role != Qt::DisplayRole)
return QVariant();
return QString::number(externalComplexData);
}
void myModel::ResetModel()
{
reset();
}
myModel::~myModel()
{}
tmp.h
#ifndef __Tmp_H__
#define __Tmp_H__
#include <QtGui/QMainWindow>
#include "ui_tmp.h"
class tmp : public QMainWindow
{
Q_OBJECT
public:
tmp(QWidget *parent = 0, Qt::WFlags flags = 0);
~tmp();
private:
Ui::tmpClass ui;
private slots:
void clicked();
};
#endif
tmp.cpp
#include "tmp.h"
#include "myModel.h"
int localComplexData = 0;
tmp::tmp(QWidget *parent, Qt::WFlags flags)
: QMainWindow(parent, flags)
{
ui.setupUi(this);
ui.lst->setModel(new myModel(localComplexData));
connect(ui.btn, SIGNAL(clicked()), this, SLOT(clicked()));
}
void tmp::clicked()
{
ui.lst->Desync();
++localComplexData;
}
tmp::~tmp()
{}
Behavior:
Clicking button updates external model, yet the list is not synchronized.
When moving mouse over the list, it synchronizes.
Expected Behavior:
Registering programmer's wish to update(), and result in paintEvent next time main loop gets the control (or even few loops later).
You did it wrong.
Do not touch QListView you don't have to. Just fix data model (Qt) and rest will work out of box.
Your model myModel should simply invoke proper methods when data are changing. This model should observe source of real data.
When something will happen to the data:
data are changed - emit signal dataChanged
data are added or removed call beginInsertRows and endInsertRows or other respective versions
If you do this properly nothing else is needed.
My application starts with an empty table, and I want to imlement different methods to add items. One should be by double-clicking the table's unused area (or "background") that is not occupied by any cells. When a cell is double-clicked, I want the default behavior.
I've found way to do this by re-implementing QAbstractScrollArea::mouseDoubleClickEvent() method in my TestTable class:
#include <QMouseEvent>
#include <QTableView>
class TestTable : public QTableView
{
Q_OBJECT
signals:
void backgroundDoubleClickEvent(void);
protected:
void mouseDoubleClickEvent (QMouseEvent* e)
{
if (indexAt(e->pos()).isValid())
{
QTableView::mouseDoubleClickEvent(e);
}
else
{
e->accept();
emit backgroundDoubleClickEvent();
}
}
};
This works, but is there a more elegant way of doing this without subclassing QTableView?
I'm not aware of any limitations of my current implementation. Are there obvious caveats?
If you don't want to subclass QTableView, try installEventFilter