QML TableView access model properties from delegate - qt

I have a TableView for which I've defined my own itemDelegate. Now, from within this delegate I can access the value for the column using styleData.value, but I'd also need to access the other properties in this same item but I can't find how to.
I need this, because the text styling needs to change depending on some other property of the item model.
Any ideas? thanks!

There is some documentation missing. Within the item delegate you can access the following (taken from the source code of TreeView.qml):
styleData (see documentation)
model (currently not documented)
modelData (currently not documented, not sure about this but I guess it's similar to ListView)
(By the way, what's also missing in the documentation but which is useful is styleData.role. Also, the documentation of the other delegates lacks some available properties too; the best is to peek into the source code of the QML file and have a look for the Loader element which instantiates your delegate. As a plus you learn how that creepy stuff works. ;))
With model and the row/column information you can then navigate to the item data. This code depends on the type of model.
If you're using QML's ListModel, then you can use model.get: model.get(styleData.row)[styleData.role] should then work (untested since I use it rarely, please give feedback).
If you're using a C++ QAbstractItemModel or friends, the best is to add a slot to the model class which takes just the row and role name, since that's the information the TableView works with (nor with role numbers nor with columns...).
However in both cases you shouldn't use the expression in a property binding! The notification system will not work since you don't use the property system for accessing the data. According to your question, I guess you wanted to use it in a expression with binding. I don't know how to properly listen to changes in the model manually.
An alternative approach is to access the other items of the row and provide a property there. Some hints:
From within one item, you can access other items of the same row by walking the object tree up twice (first to the Loader which instantiates your component, then to the actual row) and then down twice (first to the particular child object which is a Loader, then its instantiated item). You need to know the column number you want to access (not the role name), I assume you want to access the first column (index 0):
parent.parent.children[0].item
You can provide the model data using a property in each item. Assuming a simple Text element this might be:
Text {
property variant value: styleData.value // <-- Here you make it available
// your other stuff
}
Putting them together could look like the following. In this example I assume the first row contains an integer, and if it is zero, the second column should be red.
// (within TableView)
itemDelegate: Text {
property variant value: styleData.value
text: styleData.value
color: (styleData.column == 1 && parent.parent.children[0].item.value === 0)
"red" : "black"
}

I think it's pretty easy if you read the source code of TableViewItemDelegateLoader.qml (it is a private code in qtquickcontrol)
To access any role you use use : model[your_role_name] .
For exp: model["comment"]

Faced with same problem today, this is result of my investigations (Qt 5.2.x)
If you have hard limit to TableView, there is only one correct solution - use model.get(styleData.row)["roleForStyling"] as #leemes wrote. But it will very slow if you have big amount of data in model and using, for example, proxy model for sorting/filtering.
Direct solution from #leemes answer is great, but in general case not be working, because in TableView any Item wrapped in Loader and therefore independent from parent and other items:
When some item is created (where you want to change text style)
another element (from which to receive identity) cannot yet be
created
You may not have "parent" on item creation (i.e. binding will
be broken)
In my case, the best solution for deep customise was creation of the simple wrapper for ListView. In this case you have access for complete row data in delegate without the overhead. Highlights for making component ("My own ListView as table"):
Create standalone header (Rectangle or Item) - do not use header form ListView.This make it fixed for any amount of data.
Wrap ListView to ScrollView (if you need scrollbars)
Use Clip: true property in list for make correct
Set style for highlight and set highlightFollowsCurrentItem:true in ListView
As bonus in future this may be used for make "TreeTable" :)

Related

How is the concept of a "parent" meaningful in a QAbstractListModel?

I have a subclass of QAbstractListModel that is the model for a QML ListView (using PySide6). Each row of the list has a checkbox in it, and when the user checks/unchecks a box, it updates a boolean in that row of the listmodel using my override of setData(), which works as expected. I also have Buttons that should select/clear all of the checkboxes in the list.
My model subclass provides the following method to select all, which can be called when the user hits the Button or when other things happen in the application:
def select_all(self):
for i in range(self.rowCount()):
row = self._rows[i]
row['selected'] = True
# Emitting this for each row works...
self.dataChanged.emit(self.index(i), self.index(i), [])
# ... whereas emitting just one signal for ALL rows does NOT work
# self.dataChanged.emit(self.index(0), self.index(self.rowCount()), [])
As you can see from the comments, I need to emit dataChanged for each row in order for the checkboxes in the ListView to be updated. Emitting the signal once and using the topLeft and bottomRight parameters does not update the state of the checkboxes in the ListView (but the model data is correctly updated).
According to the documentation, the dataChanged signal provided by QAbstractItemModel (and inherited by QAbstractListModel) has this caveat:
If the items are of the same parent, the affected ones are those between topLeft and bottomRight inclusive. If the items do not have the same parent, the behavior is undefined.
It seems likely that I'm running into the scenario where the rows in my model do NOT have the same parent, and therefore, "the behavior is undefined." I suppose that makes sense, because I never do anything to establish a parent/child relationship for any of my rows. I also saw this answer which implies that Qt behaves differently when the topLeft and bottomRight indices are the same. So, I would like to understand this better, and I have a few questions:
Am I correct that this parent concept is the reason this does work when emitted for each row and does not work for all rows?
Is the concept of a parent/child relationship only meaningful for tree-like models that would extend QAbstractItemModel instead of QAbstractListModel? Does it make any kind of sense for a list?
If it does make sense for a list, then what would be the "parent" and what would be the "child?" How would I configure a subclass of QAbstractListModel such that dataChanged can be emitted once to update multiple rows?
Thanks!
While the base implementation of Qt item views just updates indiscriminately the view whenever the topLeft and bottomRight indexes doesn't match (so you can just provide two "random", but still sibling indexes), the indexes must not only share a common parent (which for one and two dimensional models is always an invalid index), but also both valid.
With this line, the second index is not valid:
self.dataChanged.emit(self.index(0), self.index(self.rowCount()), [])
This is because the indexes are always 0-based, and the index of the last row is actually rowCount - 1. While they theoretically are siblings, since they share the same parent (the parent of a root index is invalid, as it is the parent of an invalid index), they are not both valid.
So, the correct syntax is:
self.dataChanged.emit(self.index(0), self.index(self.rowCount() - 1), [])
^^^^
Note that the roles argument is optional and already defaults to an empty list (QVector in C++), so unless you actually want to specify the changed roles, you don't need to provide that.

How can I detect whether a tooltip is visible at a given moment?

I'm looking for a way to detect whether a Qt widget's ToolTip is visible at the moment when a particular key combination is pressed. If it is, I want to copy the ToolTip's text to the clipboard.
Specifically, I have a QListView containing abbreviated strings, which is set up (via the Qt::ToolTipRole of the associated model) to show the full string of the appropriate list item when the mouse is hovered over it. The behaviour I'm looking for is that if the user presses CTRL-C (as detected by a QShortcut) while the tooltip is visible, then the tooltip text is copied to the clipboard.
My original idea was to use the children() method of the QListView widget to see if there was a tooltip preset among them:
// Inisde the slot connected to QShortcut::activated...
auto children = _ui -> myListView -> children();
QString selectionText;
for (const auto & child : children)
{
if (qobject_cast<QToolTip *>(child))
{
selectionText = qobject_cast<QToolTip *>(child) -> text();
break;
}
}
...but this failed because it turns out that QToolTip does not inherit from QObject.
I've also thought of screening for QEvent::QToolTip events in the ListView's main event handler, and while I could probably get this to work it seems excessively low-level; I'd need to use screen co-ordinates to determine which item in the list was being hovered over and look for the widget's timeout to check that the tooltip hadn't disappeared again by the time that the QShortcut was fired. I'd be disappointed if there weren't a simpler way.
Is there an obvious way forward that I've failed to see?
There are probably several possible solutions, but I am afraid none of them is simple. What I would do is to use the implementation detail that the tooltip actual widget is called QTipLabel. See https://code.woboq.org/qt5/qtbase/src/widgets/kernel/qtooltip.cpp.html#QTipLabel and it inherits from QLabel so you can easily get the text from it.
I am afraid the following solution is just a savage hack. I have not tested it, but it should work.
I would override the data model for your view, specifically override method data() which would call the data() method of the original model class but cache the last value which was returned when this method is called with role == Qt::ToolTipRole.
Then you need to catch the shortcut you are interested in. After it is caught, you get all qApp->topLevelWidgets() https://doc.qt.io/qt-5/qapplication.html#topLevelWidgets` and go through them and check if any of them has class name equal to QTipLabel (use QMetaObject::className()) and is visible, i.e. isVisible() == true.
If you get this visible QTipLabel widget (you hold it via QWidget*), qobject_cast it to QLabel* (you cannot cast it to QTipLabel beause you do not have access to the definition of QTipLabel class because it is in private Qt source file) and get the text with QLabel::text(). If the text is the same as the text which you stored in step 1, then yes, this is the text you are looking for and you can copy it to clipboard or do whatever yo want with it.
Nasty, isn't it? But it is the simplest what I can think of.
PS: I believe that step 1 can be implemented also by catching QEvent::QToolTip for your view and then do some magic to get the text, but I think that overriding data() for model can be a bit easier.
PPS: One obvious drawback is that Qt can rename QTipLabel class in the future. But I would not be worry about it. That won't happen becaus ethey do not change QtWidgets module any more. And if it happens, then you just rename the class in your code. No problem.
PPPS: Another potential corner-case is that some other widget (whose tooltip you do NOT want to capture with that shortcut) actually has the same tooltip text as any of the items in your list view (which you DO want to capture). Then if you display tooltip for your list item, then you move your mouse over to that other widget and hover so that its tooltip gets shown (but you do NOT want to capture it) and then you press that shortcut... But I guess that in reality this will not be your case. I doubt there will be this unlikely clash of tooltips.
With thanks to #V.K., this is what worked:
auto candidates = qApp->topLevelWidgets();
QString selectionText;
for (const auto & candidate : candidates)
{
if (strcmp(candidate->metaObject()->className(), "QTipLabel") == 0)
{
QLabel * label = qobject_cast<QLabel *>(candidate);
if (label->isVisible())
{
selectionText = label -> text();
break;
}
}
}
if (!selectionText.isEmpty())
QGuiApplication::clipboard() -> setText(selectionText);

QTreeView values of parents / children changed in QModel

I have a hierarchical datastructure which I wrapped in a QModel (inherited from QAbstractItemModel) and which I show and edit in a QTreeView.
Let's assume that QTreeView shows the following data:
Item1
|----Item2
|----Item3
|-----Item4
|----Item5
Now the following shall happen:
1) I edit Item3 and change it's value to Item3_a.
2) The QModel recognizes the change and changes the items' values of parents and children in the wrapped model to:
Item1_a
|----Item2_a
|----Item3_a
|-----Item4_a
|----Item5_a
3) The QTreeView gets informed by the model about the additional changes (Item1,2,4 and 5). Only the displayed values are changed. The hierarchical structure remains the same.
My questions aims on step 3:
How do I notify the QTreeView about the changed data properly?
This is what I tried:
I know that there is modelReset, but then the QTreeView gets collapsed. However it should keep its collpase/expanded state.
According to the docs using models setData method with different parent indices gives undefined behaviour. I tried calling setData recursivly from setData for each parent/child, but this leads to program crash.
I'm using qt5.
Pretty sure that what you're looking for is "rowsInserted" and the methods that relate to it. The "dataChanged" signal indicates that a given cell (or range) has changed values; it's not about changing table structure.
What you're doing here is removing and inserting rows as you move entries from one parent to another. You need to implement all of the methods related to that. There's also a "rowsMoved" that may better suit your needs.

Deleting a row which has children in QTreeView

I want to delete a row which has children from a QTreeView. I use QAbstractItemModel's removeRow ( int row, const QModelIndex & parent = QModelIndex() ) method,pass the row and parent index of the deleted row. But this method returns false.
How can I delete a row which has children? Do I need write a method to recursively delete rows?
You should look at this: removeRow
This is a convenience function that calls removeRows(). The QAbstractItemModel implementation of removeRows() does nothing.
And here in QAbstractItemModel::removeRows():
The base class implementation does nothing and returns false.
If you implement your own model, you can reimplement this function if you want to support removing. Alternatively, you can provide your own API for altering the data.
I know I'm a bit late to the party, but I wanted to document this because I can't seem to find any good answer to the original question.
You need to implement QAbstractItemModel::removeRows in your model. There is a Qt example, Editable Tree Model, that shows how this should be done. The basic procedure is to call beginRemoveRows, delete the item(s), then call endRemoveRows.
Your tree item class should have a method for deleting a range of its child items. You call this method on the parent item from within your reimplementation of removeRows. The tree item class should be set up to delete its children when it is destroyed. The Qt example does this through the destructor, though with C++11 and later this is best done by storing children in a container of smart pointers so they get automatically deleted when the container goes out of scope.
You do not need to account for child items being collapsed or expanded in the tree view - QTreeView knows whether or not child items are visible or not and will update the view accordingly. In other words, if you delete a tree item with a few child items that were visible in the tree view, they will be automatically removed from the view. This is something that I can't find documented anywhere, but from personal experience I can say that it works (as long as you call beginRemoveRows and endRemoveRows correctly).

Flex: select tree node right after the dataProvider is been assigned / updated / replace

i have a Flex tree control and im trying to select a tree node 3 levels down right after the dataProvider is assigned with a collection object like the following.
basically treeItem1, treeItem2, treeItem3 are the nodes in the tree and treeitem3 is a child of treeItem2 which is a child of treeItem1. Assume these treeItem(1,2,3) are referenced correctly from the collection items.
my problem is that if i wait for the whole component to load completely then select the nodes, it open/select/scrolltoIndex correctly. However, if i were to select the node right after the dataProvider is assigned, then it doesn't even open or select (basically the this.treeService.selectedItem is always null).
can anyone point out what i did wrong? is there anything needs to happen after the dataProvider is assigned?
thanks
this.treeService.dataProvider = oPricingHelper.getCurrentPricingSercicesTreeSource();
this.treeService.expandItem(treeItem1, true);
this.treeService.expandItem(treeItem2, true);
this.treeService.selectedItem = treeItem3;
this.treeService.scrollToIndex(this.treeService.selectedIndex);
I have used the updateComplete event to know when a component (such as a DataGroup or List) has completed rendering after performing a simple task (such as updating the dataProvider reference). Of course, you have to be careful and remove listening to updateComplete because it can run a lot, unless you have a need for it to run.
Something like:
//...some function...
this.treeService.addEventListener(FlexEvent.UPDATE_COMPLETE, onTreeUpdateComplete);
this.treeService.dataProvider = oPricingHelper.getCurrentPricingSercicesTreeSource();
//...rest of some function...
private function onTreeUpdateComplete(event:FlexEvent):void {
this.treeService.removeEventListener(FlexEvent.UPDATE_COMPLETE, onTreeUpdateComplete);
this.treeService.expandItem(treeItem1, true);
this.treeService.expandItem(treeItem2, true);
this.treeService.selectedItem = treeItem3;
this.treeService.scrollToIndex(this.treeService.selectedIndex);
}
I'm not positive your experiencing the same issue but I seem to have the same type of problem with using the advanced data grid, it appears in these cases where the dataprovider is acceptable as multiple types, the components do some extra work in the background to wrap things up into something Hierarchical (HierarchicalData or HierarchicalCollectionView) and in doing so the dataprovider setter call is not synchronous (so it will return before actually having assigned the internal property storing the dataprovider). I've used callLater in this case with moderate success, callLater is generally a bad practice but basically adds a function to a list of functions to call once background processing is done, so this is assuming that something in the dataprovider setter called UIComponent.suspendBackgroundProcessing() and that it will subsequently call UIComponent.resumeBackgroundProcessing() and then it will execute the list of functions added by using callLater. Alternatively you could use setTimeout(someFunction,1000).
These are both "hacks" the real solution is to dig into the framework code and see what it's really doing when you tell it to set the dataprovider. Wherever you see that it actually has set the dataprovider you could extend that class and dispatch an event that you could listen for to run the function to do the selections after this point.
If anyone has a better solution please by all means correct me (I would love to have a better answer than this)

Resources