I'm trying to support rearranging a list of large objects in a QListView. It seems that the model/view framework insists on serializing the items which, in this case, is highly inefficient. I've tried to shorten things by using shared_ptr's, so only one bit of data needs passing, but even so, it feels very messy and is not at all reliable.
Worse still, during the move I notice the serialization functions are called repeatedly (dozens/hundreds of times). Surely they should only be called once each.
I'm sure I'm missing a trick, what's the correct way to achieve simple, internal move, list ordering without heavy serialize overhead?
class Big
{
public:
Big();
~Big();
...
};
using BigPtr = std::shared_ptr<Big>;
Q_DECLARE_METATYPE(BigPtr) // and qRegisterMetaTypeStreamOperators
// Required serialization operators
// This is very ugly, and what I want rid of...
QDataStream &operator<<(QDataStream &out, const BigPtr &big)
{
qDebug() << "Out <<";
out << (quint64)big.get();
return out;
}
// ...This too...
QDataStream &operator>>(QDataStream &in, BigPtr &big)
{
qDebug() << "In >>";
quint64 raw = 0;
in >> raw;
li.reset((LayerPtr::element_type *)raw);
return in;
}
class BigModel : public QAbstractListModel
{
Q_OBJECT
public:
BigModel (std::vector<BigPtr> &bigs, QObject *parent = Q_NULLPTR);
int rowCount(const QModelIndex &parent = QModelIndex()) const;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
Qt::ItemFlags flags(const QModelIndex &index) const; // Qt::ItemIsDragEnabled or Qt::ItemIsDropEnabled depending on valid index
Qt::DropActions supportedDropActions() const; // return Qt::MoveAction
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex());
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());
private:
std::vector<BigPtr> &_bigs;
};
...
// dialog setup...
ui->listView->setSelectionMode(QAbstractItemView::SingleSelection);
ui->listView->setSelectionBehavior(QAbstractItemView::SelectRows);
ui->listView->setEditTriggers(QAbstractItemView::DoubleClicked);
ui->listView->setDragEnabled(true);
ui->listView->setDragDropMode(QAbstractItemView::InternalMove);
ui->listView->setDropIndicatorShown(true);
ui->listView->viewport()->setAcceptDrops(true);
EDIT:
After more head scratching, I decided to overload the mimeData and dropMimeData functions and sub-class QMimeData to carry the BigPtr directly.
class BigMimeData : public QMimeData
{
Q_OBJECT
public:
BigMimeData(const QVariant &_big)
: big(_big)
{ }
bool hasFormat(const QString &/*mimeType*/) const { return true; }
QVariant big;
};
...
QMimeData *BigModel::mimeData(const QModelIndexList &indexes) const
{
if(indexes.count() > 0) {
return new BigMimeData(indexes.at(0).data());
}
return Q_NULLPTR;
}
bool BigModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int /*column*/, const QModelIndex &parent)
{
if(!data || !(action == Qt::CopyAction || action == Qt::MoveAction)) {
return false;
}
// start of list
if(row == -1) {
row = rowCount(parent);
}
auto dragged = static_cast<const BigMimeData *>(data);
// setData with stored value
insertRow(row, parent);
setData(index(row, 0, parent), dragged->big);
return true;
}
Which works, but seriously!? All that just to enable simple list reordering in the UI? It can't be this complex surely?
BTW, a lot of these answer seem to be focusing on the underlying data structure, that's not the question. The problem is that simple internal move drag/drop rearrangement in a list control seem to be ridiculously complex in QListView because it requires costly serialization. It's a UI/Widgets/MVC question.
Related
The items in the tree view hold a instance of class container.
I want to implement drag and drop functionality in the view.
According to the QT tutorial for the data to copy i need specify the mime type and than write the Mimedata and dropMimeData functions.
The QT Example is dealing with a simple string so i am totally clueless of how to implement these function in case of custom objects.
1) What should be the mime type in my case ?
2) How to implement the current mimedata function for Container object data?
3) How to implement the current dropmimedata function for Container object data?
/////////////////////////////////////////
class Container
{
private:
std::string stdstrContainerName;
std::string stdstrPluginType;
int iSegments;
float fRadius;
public:
Container();
Container(std::string , std::string , int , float);
Container(const Container& obj);
~Container();
std::string GetName();
std::string GetType();
void SetName(std::string stdstrName);
};
Q_DECLARE_METATYPE( Container )
////////////////////////////////////////////////////////////
QMimeData *DragDropListModel::mimeData(const QModelIndexList &indexes)
const
{
QMimeData *mimeData = new QMimeData();
QByteArray encodedData;
QDataStream stream(&encodedData, QIODevice::WriteOnly);
foreach (const QModelIndex &index, indexes) {
if (index.isValid()) {
QString text = data(index, Qt::DisplayRole).toString();
// I have a GetContainer function which returns the Container
//object and i can use the GetContainer instead of data function.
stream << text;
}
}
mimeData->setData("application/vnd.text.list", encodedData);
return mimeData;
}
//////////////////////////////////////////////////////////////////
bool DragDropListModel::dropMimeData(const QMimeData *data,
Qt::DropAction action, int row, int column, const QModelIndex
&parent)
{
if (action == Qt::IgnoreAction)
return true;
if (!data->hasFormat("application/vnd.text.list"))
return false;
if (column > 0)
return false;
int beginRow;
if (row != -1)
beginRow = row;
else if (parent.isValid())
beginRow = parent.row();
else
beginRow = rowCount(QModelIndex());
QByteArray encodedData = data->data("application/vnd.text.list");
QDataStream stream(&encodedData, QIODevice::ReadOnly);
QStringList newItems;
int rows = 0;
while (!stream.atEnd()) {
QString text;
stream >> text;
newItems << text;
++rows;
}
insertRows(beginRow, rows, QModelIndex());
foreach (const QString &text, newItems) {
QModelIndex idx = index(beginRow, 0, QModelIndex());
setData(idx, text);
beginRow++;
}
return true;
}
The header file for TreeItem.
class TreeItem
{
public:
explicit TreeItem( const Container &data , TreeItem *parent = 0 );
~TreeItem();
TreeItem *parent();
void appendChild(TreeItem *child);
TreeItem *child(int iNumber);
int childCount() const;
int childNumber() const;
Container data() const;
bool setData(const Container &data , QVariant value);
void setContainer(const Container &data);
bool insertChildren(int position, int count );
bool removeChildren( int position , int count );
private:
QList<TreeItem*> childItems;
Container itemData;
TreeItem* parentItem;
}
You can add your custom mime types to specify the type of container you want to drag/drop. See this post for details.
The QDrag object constructed by the source contains a list of MIME types that it uses to represent the data (ordered from most appropriate to least appropriate), and the drop target uses one of these to access the data.
First of all, try to find a compatible standard mime type. Those are the most common one assigned by the IANA.
If the one you are looking for is not in the list, then you can label your custom one and serialize your data into a QByteArray to share it.
QByteArray output;
// do whatever
mimeData->setData("my-awesome-mime-type", output);
Now, in your custom widget, don't forget to accept the drops of this mime type:
void Window::dragEnterEvent(QDragEnterEvent *event) {
if (event->mimeData()->hasFormat("my-awesome-mime-type"))
event->acceptProposedAction();
}
You can find a complete example in this project.
I just want to display elements from list using QML, but not using item roles.
For ex. I want to call a method getName() that returns the name of the item to be displayed.
Is it possible? I didn't find nothing clear reffering to this.
You can use one special role to return the whole item as you can see below:
template<typename T>
class List : public QAbstractListModel
{
public:
explicit List(const QString &itemRoleName, QObject *parent = 0)
: QAbstractListModel(parent)
{
QHash<int, QByteArray> roles;
roles[Qt::UserRole] = QByteArray(itemRoleName.toAscii());
setRoleNames(roles);
}
void insert(int where, T *item) {
Q_ASSERT(item);
if (!item) return;
// This is very important to prevent items to be garbage collected in JS!!!
QDeclarativeEngine::setObjectOwnership(item, QDeclarativeEngine::CppOwnership);
item->setParent(this);
beginInsertRows(QModelIndex(), where, where);
items_.insert(where, item);
endInsertRows();
}
public: // QAbstractItemModel
int rowCount(const QModelIndex &parent = QModelIndex()) const {
Q_UNUSED(parent);
return items_.count();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const {
if (index.row() < 0 || index.row() >= items_.count()) {
return QVariant();
}
if (Qt::UserRole == role) {
QObject *item = items_[index.row()];
return QVariant::fromValue(item);
}
return QVariant();
}
protected:
QList<T*> items_;
};
Do not forget to use QDeclarativeEngine::setObjectOwnership in all your insert methods. Otherwise all your objects returned from data method will be garbage collected on QML side.
void MyWindow::initializeModelBySQL(QSqlQueryModel *model,QTableView *table,QString sql){
model = new QSqlQueryModel(this);
model->setQuery(sql);
}
With this method i can set a QSQlQueryModels to my QTableviews.
But How i can set color to a row based on a cell value?
The view draws the background based on the Qt::BackgroundRole role of the cell which is the QBrush value returned by QAbstractItemModel::data(index, role) for that role.
You can subclass the QSqlQueryModel to redefine data() to return your calculated color, or if you have Qt > 4.8, you can use a QIdentityProxyModel:
class MyModel : public QIdentityProxyModel
{
QColor calculateColorForRow(int row) const {
...
}
QVariant data(const QModelIndex &index, int role)
{
if (role == Qt::BackgroundRole) {
int row = index.row();
QColor color = calculateColorForRow(row);
return QBrush(color);
}
return QIdentityProxyModel::data(index, role);
}
};
And use that model in the view, with the sql model set as source with QIdentityProxyModel::setSourceModel.
OR
You can keep the model unchanged and modify the background with a delegate set on the view with QAbstractItemView::setItemDelegate:
class BackgroundColorDelegate : public QStyledItemDelegate {
public:
BackgroundColorDelegate(QObject *parent = 0)
: QStyledItemDelegate(parent)
{
}
QColor calculateColorForRow(int row) const;
void initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const
{
QStyledItemDelegate::initStyleOption(option, index);
QStyleOptionViewItemV4 *optionV4 =
qstyleoption_cast<QStyleOptionViewItemV4*>(option);
optionV4->backgroundBrush = QBrush(calculateColorForRow(index.row()));
}
};
As the last method is not always obvious to translate from C++ code, here is the equivalent in python:
def initStyleOption(self, option, index):
super(BackgroundColorDelegate,self).initStyleOption(option, index)
option.backgroundBrush = calculateColorForRow(index.row())
Your best bet is to define a custom model (QAbstractTableModel subclass). You probably want to have a QSqlQueryModel as a member in this custom class.
If it's a read-only model, you need to implement at least these methods:
int rowCount(const QModelIndex &parent) const;
int columnCount(const QModelIndex &parent) const;
QVariant data(const QModelIndex &index, int role) const;
and for well behaved models also
QVariant headerData(int section, Qt::Orientation orientation, int role) const;
If you need the model to be able to edit/submit data, things get a bit more involved and you will also need to implement these methods:
Qt::ItemFlags flags(const QModelIndex &index) const;
bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole);
bool insertRows(int position, int rows, const QModelIndex &index=QModelIndex());
bool removeRows(int position, int rows, const QModelIndex &index=QModelIndex());
What will actually change a row appearance lies in the return value of this method:
QVariant data(const QModelIndex &index, int role) const;
A dumb example:
QVariant MyCustomModel::data(const QModelIndex &index, int role) const
{
if ( !index.isValid() )
return QVariant();
int row = index.row();
int col = index.column();
switch ( role )
{
case Qt::BackgroundRole:
{
if(somecondition){
// background for this row,col is blue
return QVariant(QBrush (QColor(Qt::blue)));
}
// otherwise background is white
return QVariant(QBrush (QColor(Qt::white)));
}
case Qt::DisplayRole:
{
// return actual content for row,col here, ie. text, numbers
}
case Qt::TextAlignmentRole:
{
if (1==col)
return QVariant ( Qt::AlignVCenter | Qt::AlignLeft );
if (2==col)
return QVariant ( Qt::AlignVCenter | Qt::AlignTrailing );
return QVariant ( Qt::AlignVCenter | Qt::AlignHCenter );
}
}
}
I've cooked up a QAbstractListModel whose model indexes contain a pointer I absolutely needed in order to process data. I add the data like so:
void PointListModel::addPoint(int frameNumber, QPoint const& pos)
{
PointItem *pointItem = new PointItem( frameNumber, pos );
QModelIndex newRow = this->createIndex( m_points.count(), 0, pointItem );
qDebug() << newRow.internalPointer();
beginInsertRows( newRow, m_points.count(), m_points.count() );
m_points.insert( m_points.count( ), pointItem );
endInsertRows();
emit pointAdded( pointItem, pos );
}
It was only later that I realized that the argument to beginInsertRows is asking for the parent model index of the new row, not the new row's actual model index.
So, at this point in time, Qt has given me no way of supplying a QModelIndex to associate with this particular row. How do I create my own model index for this new row?
Okay, I'm rewriting my answer as after some research I've found out that I got it wrong.
You shouldn't do anything special to create a new index when you add new data. You code should look like this:
PointItem *pointItem = new PointItem( frameNumber, pos );
// assume you insert a top level row, having no parent
beginInsertRows( QModelIndex(), m_points.count(), m_points.count() );
m_points.insert( m_points.count( ), pointItem );
endInsertRows();
Then you should implement the index() method which will create indexes on demand and the parent() method which will determine the parent of some index, but since you have a list model, it should probably always just return QModelIndex(). Here is a good article about creating custom models.
Here is a complete example of a working QAbstractListModel:
class MyModel: public QAbstractListModel {
Q_OBJECT
public:
virtual QModelIndex index(int row, int column = 0,
const QModelIndex &parent = QModelIndex()) const;
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
void add(int i);
private:
QList<int> list;
};
void MyModel::add(int i)
{
beginInsertRows(QModelIndex(), list.size(), list.size());
list.append(i);
endInsertRows();
}
QModelIndex MyModel::index(int row, int column,
const QModelIndex &parent) const
{
return hasIndex(row, column, parent) ? createIndex(row, column, (void*)&list[row])
: QModelIndex();
}
int MyModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return list.size();
}
QVariant MyModel::data(const QModelIndex &index,
int role) const
{
if (!index.isValid())
return QVariant();
if (role != Qt::DisplayRole)
return QVariant();
return QVariant(QString::number(*static_cast<int*>(index.internalPointer())));
}
I've cooked up a QAbstractListModel whose model indexes contain a pointer I absolutely needed in order to process data.
If you start with wrong requirements, you end up with wrong solutions :)
A list model is simple enough so that you don't need more than the QModelIndex's row() to uniquely define the data the index addresses.
So, given a QModelIndex mi, when you before did
PointItem * item = static_cast<PointItem*>(mi.internalPointer());
you can instead do
PointItem * item = plm->pointItemFromIndex(mi);
where plm is your PointListModel. If you don't have a pointer to it lying around when you need to access the PointItem, you can reconstruct it like this:
PointItemModel * plm = qobject_cast<PointItemModel*>(mi.model());
// check for !plm here (!mi.isValid() || qobject_cast fails)
In turn, PointListMode::pointItemFromIndex() would do the actual work:
PointItem * PointListMode::pointItemFromindex(const QModelIndex &mi) const {
return mi.isValid() ? m_points[mi.row()] : 0 ;
}
This is the most important thing to realize when working with QAbstractListModel in Qt: Mentally replace QModelIndex with int row, ignore everything else it has (an invalid QModelIndex has row() == -1).
Same for QAbstractTableModel: mentally reduce the QModelIndex to int row, int column. Forget everything else.
The only time you need the full QModelIndex (including its internalPointer() or internalId() is when you implement a tree model (QAbstractItemModel).
i got a godgiven list of xyz (the code says int, just an example) glued into a QList (to big to move anywhere). How can I create a Model View for that? I allready read the Qt doc which tells me, I have to reimplement data, index, parent, rowCount, columnCount functions. But the preprocessor/compiler cries for more reimplemented functions? I allready read a chapter in my Qt Book but it did not help either. Here my hacked away code:
class XModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit XModel(QList<int> *valuelist, QObject *parent = 0);
virtual ~XModel();
int rowCount(const QModelIndex &) const;
int columnCount(const QModelIndex &) const;
QModelIndex index( int row, int column, const QModelIndex & parent = QModelIndex()) const;
QModelIndex parent(const QModelIndex &index) const;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
private:
QList<int>* blah;
signals:
public slots:
};
XModel::XModel(QList<int> *valuelist, QObject *parent) :
QAbstractListModel(parent),
blah(valuelist)
{
}
XModel::~XModel()
{
}
int XModel::rowCount(const QModelIndex &) const
{
return blah->size();
}
int XModel::columnCount(const QModelIndex &) const
{
return 1;
}
QModelIndex XModel::index(int row, int column, const QModelIndex &parent) const
{
return createIndex(row, column, (void)&(blah->at(row)));
}
QModelIndex XModel::parent(const QModelIndex &index) const
{
return createIndex(index->row(), index->column(), NULL);
}
QVariant XModel::data(const QModelIndex &index, int role = Qt::DisplayRole) const
{
return QVariant(blah->at(index.row()));
}
Do I even have to use QAbstractItemModel or does QAbstractListModel work the exact same way? How do I give the Model the source of the actual data? Is this only within data function? Please tell me what I am doing wrong, I do not see it and give advice on howto do it properly, (good) howtos welcome.
This is fixed, but...
EDIT:
Widget::Widget(QWidget *parent)
: QWidget(parent),
valuelist(),
xm(&valuelist) //xm = XModel
{
valuelist.append(1);
valuelist.append(2);
valuelist.append(3);
valuelist.append(4);
valuelist.append(5);
valuelist.append(6);
valuelist.append(7);
valuelist.append(8);
valuelist.append(9);
view = new QListView(this);
view->setModel(&xm);
//how to force the XModel to reread the QList`?
view->show();
}
Add to and remove data from XModel and have XModel modify the underlying list (reference?) for you:
Widget::Widget(QWidget *parent)
: QWidget(parent),
valuelist(),
xm(&valuelist) //xm = XModel
{
xm.append(1);
xm.append(2);
xm.append(3);
xm.append(4);
xm.append(5);
xm.append(6);
xm.append(7);
xm.append(8);
xm.append(9);
view = new QListView(this);
view->setModel(&xm);
xm.append(10); // should call beginInsertRows, append to valuelist, and call endInsertRows.
Q_ASSERT(valuelist.contains(10));
view->show();
}
Otherwise, you could perhaps create a mix of QObject and QList that can emit signals to notify XModel of changes, but I think the first approach is better.
Edit:
Here is a silly example. It will create a list-backed model that will append more elements to the list every second:
#include <QtGui/QApplication>
#include <QtGui/QListView>
#include <QtCore/QAbstractListModel>
#include <QtCore/QTimer>
class ListBackedModel : public QAbstractListModel
{
Q_OBJECT
QList<int>* m_list;
public:
ListBackedModel(QList<int>* list, QObject* parent = 0)
: QAbstractListModel(parent)
, m_list(list)
{}
~ListBackedModel()
{}
int rowCount(const QModelIndex &parent = QModelIndex()) const
{
Q_UNUSED(parent);
return m_list->size();
}
QVariant data(const QModelIndex &index, int role) const
{
if (index.row() >= rowCount()) { return QVariant(); }
if (index.row() < 0) { return QVariant(); }
int element = m_list->at(index.row());
if (Qt::DisplayRole == role) {
return QString::number(element);
}
if (Qt::ToolTipRole == role) {
return tr("%1 is element #%2").arg(element).arg(index.row() + 1);
}
return QVariant();
}
void append(int element)
{
/*
First is the new index of the first element that will be inserted.
Last is the new index of the last element that will be inserted.
Since we're appending only one element at the end of the list, the
index of the first and last elements is the same, and is equal to
the current size of the list.
*/
int first = m_list->size();
int last = first;
beginInsertRows(QModelIndex(), first, last);
m_list->append(element);
endInsertRows();
}
void startAddingMoreElements()
{
QTimer::singleShot(1000, this, SLOT(addMoreElements()));
}
private slots:
void addMoreElements()
{
append(qrand() % 100);
startAddingMoreElements();
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QList<int> list;
list << 1 << 10 << 34 << 111;
ListBackedModel model(&list);
QListView listView;
listView.setModel(&model);
listView.show();
model.startAddingMoreElements();
return a.exec();
}
#include "main.moc"