Show different levels of C++ DOM model in different QML views - c++

I need to create a QML application with pages of items that are defined by a xml file. The xml must be parsed in C++. Each page of items will be a StackView page containing a ListView of the items. Each item on a page has several values defining text, colour, size, etc.
As a start, my DOM model creation is based on the Qt Simple DOM Model Example. The model is wrapped with a QAbstractItemModel.
I've exposed the C++ model to QML using the rootContext->setContextProperty.
I'm struggling with splitting the data between the StackView pages. I assume that I need to assign the different levels of the hierarchical model(page parent and item children) to different UserRoles in order to filter them to the QML views but I'm struggling to find any suitable examples of how to go about this.
So my question is:
Can you show me an example of assigning UserRoles to a C++ DOM Model and the associated data method for returning the item data by UserRole and hierarchical level?
or
Am I going in the wrong direction and there's a better way to achieve this?

OK, I've now got this going. After some more playing I decided to select the data by xml Node name - I used "Page" & "Item". If anyone else is looking at hierarchical models in C++ and views in QML the other key requirement I found to implementing a solution is the QML DelegateModel (formally VisualDataModel). Here are the snippets that directly answer the questions...
From DomModel.h
public:
enum menuRoles
{
PageNumberRole = Qt::UserRole + 1,
PageNameRole,
ItemNumberRole,
ItemNameRole
};
...
QVariant data(const QModelIndex &index, int role) const;
...
protected:
QHash<int, QByteArray> roleNames() const;
From DomModel.cpp
QVariant DomModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
else
{
DomItem *item = static_cast<DomItem*>(index.internalPointer());
QDomNode node = item->node();
if(node.nodeName() == "Page")
{
switch (role)
{
case PageNumberRole:
return node.attributes().namedItem("number").nodeValue();
break;
case PageNameRole:
return node.attributes().namedItem("name").nodeValue();
break;
default:
return QVariant();
break;
}
}
else if(node.nodeName() == "Item")
{
switch (role)
{
case ItemNumberRole:
return node.attributes().namedItem("number").nodeValue();
break;
case ItemNameRole:
return node.attributes().namedItem("name").nodeValue();
break;
default:
return QVariant();
break;
}
}
else
return QVariant();
}
}
...
QHash<int, QByteArray> DomModel::roleNames() const
{
// This tells the subscribing views what data roles are available
// Any changes must be reflected in the DomModel::data function
QHash<int, QByteArray> roles;
roles[PageNumberRole] = "pageNumber";
roles[PageNameRole] = "pageName";
roles[ItemNumberRole] = "itemNumber";
roles[ItemNameRole] = "itemName";
return roles;
}

Related

Why can't you use setData() to set background color of a Cell in QTreeView?

I'm using the following code to try to change the background color of a cell at a given QModelIndex.
ui->TreeView->model()->setData(index, QVariant(QBrush (QColor(Qt::green))) , Qt::BackgroundRole);
where index is given by the dataChanged() signal.
This isn't working. Any ideas why?
Here's my reimplemented setData function.
bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
TreeItem *item = getItem(index); //gets item a given index
bool result = item->setData(index.column(), value);
if (result)
emit dataChanged(index, index);
return result;
}
And here is the setData method for the underlying item:
bool TreeItem::setData(int column, const QVariant &value)
{
if (column < 0 || column >= itemData.size())
return false;
itemData[column] = value;
return true;
}
Apologies for the vague question. I've managed to solve it by myself so I will post here in case anyone is ever stuck on a similar issue.
The problem for me was that I hadn't reimplemented QAbstractItemView's data() method to account for the new role.
QVariant TreeModel::data(const QModelIndex &index, int role) const
{
TreeItem *item = getItem(index);
if (role == Qt::BackgroundRole)
return item->data(index.column());
//and so on...
AFAIK the data() method gives the treeview the data out of the model that it needs to present. Within this method I hadn't accounted for the case when role == Qt::BackgroundRole so the view was never given the appropriate information out of the model.

QTableView: change rowCount on the fly

I use a QTableView to show read-only data. The model is based upon a QList of a custom type that contains another QList. Something like this:
typedef struct
{
int range;
QString description;
} Field;
typedef struct
{
QString name;
QList<Field> fields;
} Item;
QList<Item> items;
In my QAbstractTableModel implementation I have a slot that selects the current item:
void setCurrentItem(int idx)
{
// checks for errors (omissis)
currentItemIdx = idx;
// ask to redraw the table
emit dataChanged(this->index(0, 0), this->index(rowCount(), columnCount()));
}
All the model's logic rely on that item, for example:
int MyModel::rowCount(const QModelIndex&) const {
return items.at(m_currentItemIdx).fields.count();
}
QVariant MyModel::data(const QModelIndex &index, int role) const {
if (!index.isValid()) return QVariant();
if (index.row() >= items.at(m_currentItemIdx).fields.count()) return QVariant();
Field f = items.at(m_currentItemIdx).fields.at(index.row);
switch (role) {
case Qt::DisplayRole:
switch (index.column()){
case 0:
return f.range;
break;
case 1:
return f.description;
break;
default:
return QVariant();
}
break;
}
return QVariant();
}
The problem is when I change the current item using setCurrentItem() the code uses the new data, but the QTableView doesn't change its rows and then doesn't request the new content.
I thought emit dataChanged() was enought but it seems it doesn't.
What should I do to notify the view that I've changed the size of my model?
I'm trying to avoid the insert/remove row mechanism because I don't want to change the actual data in the QList, I just want to pick up a different set of information.
Call the QAbstractItemModel::​beginResetModel() before and the QAbstractItemModel::​endResetModel() after changing your model data.
Like this:
void setCurrentItem(int idx)
{
beginResetModel();
currentItemIdx = idx;
endResetModel();
}

QT Multiple item selection from a list of files and folders

I'm developing a Qt C++ application in Unix and I've been trying to do something similar to what this image shows:
As you can see, there is a list of files and folders and a user can select multiple of them (if a folder is selected, all childs also get selected). I don't really care if the folder/file icons are shown.
I was able to create a list of QDir which stores all the files and folders paths given a root path. The problem is that I don't really know which widgets to use to design the selection panel.
By the way, the lis of QDir is a vector, but it can be easily modified to anything else.
Thanks!
You can try to make proxy model for QFileSystemModel, override flags() with Qt::ItemIsUserCheckable, override setData() and apply the model to QTreeView. Full example can be found at https://github.com/em2er/filesysmodel. This code is just a concept, i have not tested it thoroughly, but you can take some ideas from it. It will look smth like on the screenshot:
.
Also you can combine it with Merged Proxy Model to display multiple starting paths at one view.
You might want to consider the QTreeWidget, or it's a tad more advanced version - QTreeView and an appropriate data model.
As some users suggested, I ended up using QFileSystemModel. I'm gonna give a full description of how I implemented it, in case someone else comes up with this problem and needs a clear response.
First of all, a QFileSystemModel is a file tree without checkboxes, to add them, a new class which extends QFileSystemModel and at least 3 methods must be overriden.
class FileSelector : public QFileSystemModel
{
public:
FileSelector(const char *rootPath, QObject *parent = nullptr);
~FileSelector();
bool setData(const QModelIndex& index, const QVariant& value, int role);
Qt::ItemFlags flags(const QModelIndex& index) const;
QVariant data(const QModelIndex& index, int role) const;
private:
QObject *parent_;
/* checklist_ stores all the elements which have been marked as checked */
QSet<QPersistentModelIndex> checklist_;
};
When creating the model a flag, to indicate that it should have a checkable box, must be set. This is why we will use the flags function:
Qt::ItemFlags FileSelector::flags(const QModelIndex& index) const
{
return QFileSystemModel::flags(index) | Qt::ItemIsUserCheckable;
}
When a click is made in the checkbox, the method setData will be called, with the index of the element that was clicked (not the checkbox itself, but the :
bool FileSelector::setData(const QModelIndex& index, const QVariant& value, int role)
{
if (role == Qt::CheckStateRole && index.column() == 0) {
QModelIndexList list;
getAllChildren(index, list); // this function gets all children
// given the index and saves them into list (also saves index in the head)
if(value == Qt::Checked)
{
for(int i = 0; i < list.size(); i++)
{
checklist_.insert(list[i]);
// signals that a change has been made
emit dataChanged(list[i], list[i]);
}
}
else if(value == Qt::Unchecked)
{
for(int i = 0; i < list.size(); i++)
{
checklist_.remove(list[i]);
emit dataChanged(list[i], list[i]);
}
}
return true;
}
return QFileSystemModel::setData(index, value, role);
}
When dataChanged is signaled or you open a new path of the tree, the data function will be called. Here you have to make sure to only display the checkbox at the first column (next to the filename), and to retrieve the state of the checkbox, to mark it as checked/unchecked.
QVariant FileSelector::data(const QModelIndex& index, int role) const
{
if (role == Qt::CheckStateRole && index.column() == 0) {
if(checklist_.contains(index)) return Qt::Checked;
else return Qt::Unchecked;
}
return QFileSystemModel::data(index, role);
}
The only thing I was not able to accomplish was getting all childs, since the folders must be open to retrieve the childs. So a closed folder won't have any child until you open it.
Hope this can help someone who has the same problem as I did!

Custom delegate Paint persist when editing

I am trying to write custom delegate and custom model as a part of learning Qt.
I made a simple custom model based on QAbstractTableModel. I did not do anything complicated. It only generates the data in its constructor as well as minimally implement the pure virtual function.
I made a custom delegate which display numerical data in terms of bars. I also implemented a spin box as an editor to edit data.
The program works well. I can view, edit and modify data through a QTableView with the delegate set.
But there is a small problem. When I call the editor, the data bar persists, which means I see the data bar at the background and the spin box on top.
Initially, I think it is because the Qt::EditRole in the QAbstractTableModel::data() has not been set properly. But, surprisingly, I find that the Qt::EditRole has never been called.
So, there are two question:
How to remove the data bar when I am having the spin box editor?
Why is the EditRole never been called in my custom model?
Here is part of my code:
My Custom Model:
MyModel::MyModel(QObject* parent):QAbstractTableModel(parent)
{
for (int i = 0; i < 10; ++i)
localData.push_back(i*i);
}
QVariant MyModel::data(const QModelIndex &index, int role) const
{
switch(role)
{
case Qt::EditRole:
qDebug() << "EditRole"; //Never Print Out
return 0;
case Qt::DisplayRole :
if (index.column() == 0)
return (index.row());
if (index.column() == 1)
return (localData.at(index.row()));
default:
return QVariant();
}
}
My Custom Delegate:
void MyDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
{
painter->save();
painter->setPen(Qt::red);
painter->setBrush(Qt::red);
double factor = 0;
if (index.data().toDouble() > 100)
factor = 1;
else
factor = index.data().toDouble() / (double) (100.0);
painter->drawRect(option.rect.x()+5, option.rect.y()+3, (option.rect.width()-10)*factor, option.rect.height()-6);
painter->restore();
}
QWidget* MyDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QSpinBox* box = new QSpinBox(parent);
box->setMinimum(0);
box->setMaximum(100);
return box;
}
Try to set setAutoFillBackground(true) for your view
Editrole is not called because your custom editor does not query the model for that data. You don not set any value for your spin box. Try to set it as:
box->setValue(model.data(Qt::EditRole));
in the MyDelegate::createEditor() function.

QTableView and unique IDs

I'm new to Qt and coming from C# .Net. I am trying to replicate a fairly simple program I wrote in C# in Qt as a learning tool. I have a data model that inherits QAbstractTableModel and implements:
rowCount,
columnCount,
data,
setData,
headerData
flags
My data structure is a map
std::map<int, CBDataRow>
So the idea was that each row would have a unique int ID and a struct containing the rest of the row information.
What I am stuck on now is how to update my data model when the user makes an edit in the QTableView object. The setData function does get called. Here it is:
bool CBDatabaseModel::setData(const QModelIndex &index, const QVariant &value, int role) {
bool success = false;
if(role == Qt::EditRole) {
success = m_data.UpdateRow(index, value);
}
if(success) {
emit dataChanged(index, index);
return true;
} else {
return false;
}
}
Now you see that the UpdateRow() function gets called here on an edit. That function should find the unique id in the map and update the appropriate members of its CBDataRow struct. My problem is that I have no idea how to get the unique ID out of the QModelIndex object that gets passed into the edit function.
For example:
User edits the "CB Name" cell of row 3. The data in row three has a unique ID of 100. That value of 100 is in the QTableView in a hidden column, column index 0. So what I need to do is simply:
(Psuedo code)
it = m_data.find(unique_id);
it->second.cb_name = value.toString();
Since the user was editing column 1, how do i find the unique ID that is contained in column 0?
I would recommend to reimplement index() method of your model and there create indexes by using the call createIndex(row,col, unique_id);
Then in any place where you got QModelIndex, you can always extract unique_id = model_index.internalId();
In my opinion you can store your data in an array and index your element simply accessing through index.row():
QVector<CBDataRow> m_data;
....
bool CBDatabaseModel::setData(const QModelIndex &index, const QVariant &value, int role) {
bool success = false;
if(role == Qt::EditRole && index.row() < m_data.size()) {
success = m_data.at(index.row()).UpdateRow(index.column(), value);
}
if(success) {
emit dataChanged(index, index);
return true;
} else {
return false;
}
}
if you are worrying about element sorting you can derive your model from a QSortFilterProxyModel (instead of a QAbstractTableModel) and then reimplement
bool CBDatabaseModel::lessThan(const QModelIndex &left,
const QModelIndex &right) const
without define a internal id by yourself.
I hope this can help you.