How can I populate a QTreeView from a "flat" QAbstractItemModel?
-
The Jambi tree examples that I've found use the file system as the model and relies on the filesystem to provide the parent-child relationship, which doesn't help me.
I have a flat list of objects in a QAbstractItemModel. I use this model with other views so I can't just extend QTreeModel instead. I've created a QAbstractProxyModel that the tree view will use; my QAbstractItemModel will be the source model.
For my tree view, I just want to use a property of each object to group the objects in a simple 2 level tree. Based on the parameters passed to the abstract methods in QAbstractItemModel, I can't figure out how I can determine what the view is requesting.
Trying to learn how the tree view uses the model, I extended QDirModel and put print statements in the data method, I found, for example, the (row,column) - aka QModelIndex - for the items in the first level of the tree is (0,0), (1,0), (2,0), etc. If I choose one of those levels, then the children also have the same index rows and columns: (0,0), (1,0), (2,0). So, in the method data(QModelIndex index, int role), how can I know what index is referring to? Same goes for the parent(QModelIndex child) and rowCount(QModelIndex parent) methods. The parameters seem too vague to figure out exactly which tree item the view is requesting: one of the groups or one of the objects in the group.
Thanks,
Josh
-
@
(0, 0)
* (x0, y0) -> parent = (0,0)
(1, 0)
* (x1, y1) -> parent = (1,0)
(2, 0)
* (x2, y2) -> parent = (2,0)
@
so to know the index that an index is refering too you call@
QModelIndex::parent()
@that returns the parent index, or QModelIndex if the index has no parent.
-
Ok, so I understand that given an index, I should traverse up the tree using the child-parent relationship to figure out where the index is.
However, how do I implement the method parent(QModelIndex child) ? If child has the row,col (0,0) how do I know if it's the first node at the highest (group) level or if it's the first node in a group? It seems like a catch-22: I need to call child.parent() to figure out where the child index is in the tree, but that just recursively calls the model's aforementioned parent method that I'm supposed to be implementing.
-
You can store a piece of data in every model index created. See QAbstractItemModel::createIndex(). The last argument of that function is a void* or a quint32 depending on which overload you use. This piece of data can be used by the model to identify every node.
Please be aware that tree models are just plain hard to build and get right. There is no easy way.
Is your software GPL? If so, I can point you to a template implementation that may be of help to you.
-
I'm using Jambi, so I only have the createIndex with a third parameter of 'long' (internalId). In my index() method, I set this to -1 when creating parent indexes and to the row of the parent when creating child indexes. Since my tree is only 2 levels, this works. I find these design issues quite disturbing: it's as if the development team and other developers haven't really used this functionality much in the real world.
I'm somewhat concerned with my methodology though, since the proxy model docs say to not use internalId since the proxy model uses it -- I haven't encountered any issues though.
Now, I have to figure out how I'm going to implement mapFromSource and mapToSource....ugh, more trouble than it's worth.
-
Hello I'm one of the QtJambi devs that is interested in getting to the bottom of this concern.
I believe Josh is going to provide (may already have provided) a Java example of the matter (to better convery / explain it to us).
Then from this we are best making it into a C++ example (that works using any valid best-practise C++ methodology, maybe assistance on that can be provided here, I am kind of aware that more than one implementation approach maybe possible in C++ and that fewer of those approaches translate to QtJambi / Java, but initially we just need one C++ that works for every use-case Josh expects).
Then I can map that into QtJambi and come back to this forum with the restrictions that may exist (by using QtJambi/Java). Then I hope a C++ Model-View guru here can assist and advise how it might be possible to re-implement it in C++ obeying these restrictions. etc..
From going through this process Josh should get an answer to his question but also I can identify if/where QtJambi could be improved by better understanding more diverse use-cases.
Infact has anyone for C++ implemented a bunch of example Model-View use-cases to explain each and every possible feature that Qt provides. Such a resource would be useful to label each pattern and refer to it as a starting point when this more advanced area comes up for discussion. Also with this suite of examples it would be possible to re-implement each one in Java using QtJambi and see where the major inefficiencies end up being to improve QtJambi.
-
Here's my example program. I tested on multiple Qt Jambi versions and platforms: Win32: 4.5.2, 4.6.3; Linux x64: 4.5.2, 4.6.3, 4.7.0. Along with other issues, I also found that Linux apparently defines QModelIndex internalId differently than Windows. If I create an index with internalId set to -1, then it works in Windows, but Linux will interpret it as the unsigned equivalent value: 4294967295
I hit the 6000 character limit, so my code will be in subsequent posts. I outlined at least 6 issues in the comments at the top of my code:
This is my example program for trying to display a list model in a tree view by using a proxy model to morph the list structure into a tree-like hierarchy. The approach I used in the proxy model is ugly and limited to a single level in the tree (which is all I need) but it's the only somewhat tolerable way I could figure out. However, (per #2 below) I can't select anything in the tree view for some unknown reason.
Test Platforms: Win32: Qt Jambi 4.5.2 and 4.6.3; Linux x64: 4.5.2, 4.6.3, and 4.7.0
Issues:
-
In the proxy model method parent(QModelIndex child), I'd like to call child.parent(), but that causes recursive method calls leading to a StackOverflowError exception. This seems like a bug, but absent any real-world example of what I'm trying to do, I don't know if calling child.parent() is valid and allowed in that method.
-
For some reason, I can't select anything in the tree. If I set the tree view's model to the word model (the proxy model's source model) then I can select tree items, but the tree becomes a list without using the proxy model. I'm not sure what it is about my proxy model that disables the selection of tree items.
-
On Linux, I found that QModelIndex internalId is defined differently than Windows. If I set internalId to -1, it is -1 on Windows, but 4294967295 on Linux.
-
I have no idea, and can't find any documentation or examples, of how to implement the proxy model method mapToSource(QModelIndex proxyIndex) when proxyIndex is one of the group indexes that I derive from the list data in the source model. By definition, the group index doesn't map to any single or specific item in the source model.
-
Since I'm using internalId in QModelIndex to what an index refers to, the parent-child relationship, my tree is limited to a single level. Using additional steps of re-direction, internalId could probably be used for more levels in the tree but it would be horribly ugly; There must be a better way (I hope).
-
My entire proxy model implementation feels bad and is bad. I'm not sure if it's my complete misunderstanding of how it's supposed to work, if it's from Qt Jambi having a slightly less flexible QModelIndex than native C++ Qt, or if it's a design flaw in Qt's model-view framework.
-
-
Due to 6000 character limit, I had to break my code into several posts
@
/*- This is my example program for trying to display a list model in a tree view by using a proxy model to
- morph the list structure into a tree-like hierarchy. The approach I used in the proxy model is
- ugly and limited to a single level in the tree (which is all I need) but it's the only somewhat
- tolerable way I could figure out. However, (per #2 below) I can't select anything in the tree view
- for some unknown reason.
- Test Platforms: Win32: Qt Jambi 4.5.2 and 4.6.3; Linux x64: 4.5.2, 4.6.3, and 4.7.0
- Issues:
-
- In the proxy model method parent(QModelIndex child), I'd like to call child.parent(), but
-
that causes recursive method calls leading to a StackOverflowError exception. This seems like
-
a bug, but absent any real-world example of what I'm trying to do, I don't know if calling
-
child.parent() is valid and allowed in that method.
-
- For some reason, I can't select anything in the tree. If I set the tree view's model to the
-
word model (the proxy model's source model) then I _can_ select tree items, but the tree becomes
-
a list without using the proxy model. I'm not sure what it is about my proxy model that disables
-
the selection of tree items.
-
- On Linux, I found that QModelIndex internalId is defined differently than Windows. If I set
-
internalId to -1, it is -1 on Windows, but 4294967295 on Linux.
-
- I have no idea, and can't find any documentation or examples, of how to implement the proxy model
-
method mapToSource(QModelIndex proxyIndex) when proxyIndex is one of the group indexes that I
-
derive from the list data in the source model. By definition, the group index doesn't map to any single
-
or specific item in the source model.
-
- Since I'm using internalId in QModelIndex to what an index refers to, the parent-child relationship,
-
my tree is limited to a single level. Using additional steps of re-direction, internalId could
-
probably be used for more levels in the tree but it would be horribly ugly; There must be a
-
better way (I hope).
-
- My entire proxy model implementation feels bad and is bad. I'm not sure if it's my complete
-
misunderstanding of how it's supposed to work, if it's from Qt Jambi having a slightly less
-
flexible QModelIndex than native C++ Qt, or if it's a design flaw in Qt's model-view framework.
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;import com.trolltech.qt.core.QAbstractItemModel;
import com.trolltech.qt.core.QModelIndex;
import com.trolltech.qt.core.Qt;
import com.trolltech.qt.gui.QAbstractProxyModel;
import com.trolltech.qt.gui.QApplication;
import com.trolltech.qt.gui.QListView;
import com.trolltech.qt.gui.QMainWindow;
import com.trolltech.qt.gui.QSplitter;
import com.trolltech.qt.gui.QTreeView;public class TestListToTreeProxyModel extends QMainWindow
{
public static final String APP_NAME = "TestListToTreeProxyModel";public static void main(String[] args)
{
QApplication.initialize(args);
new TestListToTreeProxyModel().show();
QApplication.exec();
}private TestListToTreeProxyModel()
{
super();
setWindowTitle(APP_NAME);WordsModel wordsModel = new WordsModel();
QListView list = new QListView();
list.setModel(wordsModel);ListToTreeProxyModel proxyModel = new ListToTreeProxyModel();
proxyModel.setSourceModel(wordsModel);QTreeView tree = new QTreeView();
tree.setModel(proxyModel);QSplitter splitter = new QSplitter();
splitter.addWidget(tree);
splitter.addWidget(list);setCentralWidget(splitter);
}
// next post will be proxy model
@
-
@
// (continued from previous post)private static class ListToTreeProxyModel extends QAbstractProxyModel
{
// Indicates that a QModelIndex is a parent node.
// Note that this only allows a very limited tree with a single level of parent-child
private static final int PARENT_INTERNAL_ID = 99999999; // used -1 on Windows, but it doesn't work in Linux// groupsList is duplicating information in treeData, but I think this is needed to keep an ordered list of groups (esp. for rowCount) private ArrayList<String> groupsList = new ArrayList<String>(); private HashMap<String, List<String>> treeData = new HashMap<String, List<String>>(); public ListToTreeProxyModel() {} // group words by uppercase first letter private String getGroup(String word) { if (word != null && word.length() > 0) { return word.substring(0, 1).toUpperCase(); } return null; } public void setSourceModel(WordsModel model) { super.setSourceModel(model); // build the tree structure from the list data: String group; for (String word : model.getWords()) { group = getGroup(word); if (!treeData.containsKey(group)) { groupsList.add(group); treeData.put(group, new ArrayList<String>()); } treeData.get(group).add(word); } } @Override public QModelIndex mapFromSource(QModelIndex sourceIndex) { // not reliable in general, but works in this example String word = (String)sourceModel().data(sourceIndex); if (word != null) { String group = getGroup(word); int parentRow = groupsList.indexOf(group); int row = treeData.get(group).indexOf(word); QModelIndex mi = createIndex(row, 0, parentRow); //System.out.println("mapFromSource(): sourceIndex: "+sourceIndex+" maps to proxyIndex: "+mi); return mi; } return null; } @Override public QModelIndex mapToSource(QModelIndex proxyIndex) { // since the group nodes in the tree (parents) have no // index in the source model, I have no idea how to map them.... //System.out.println("mapToSource(): "+proxyIndex); if (proxyIndex.internalId() == PARENT_INTERNAL_ID) { // not sure what to do here since parents (groups) don't have corresponding indexes in the source //System.out.println("mapToSource(): trying to map group index to source, which is undefined. Returning null"); return null; } String group = groupsList.get((int)proxyIndex.internalId()); String word = treeData.get(group).get(proxyIndex.row()); int row = ((WordsModel)sourceModel()).getWords().indexOf(word); QModelIndex mi = null; if (row >= 0) { mi = createIndex(row, proxyIndex.column()); } //System.out.println("mapToSource(): proxyIndex: "+proxyIndex+" maps to sourceIndex: "+mi); return mi; } @Override public int columnCount(QModelIndex arg0) { return 1; } @Override public Object data(QModelIndex index, int role) { if (index.parent() == null) { if (role == Qt.ItemDataRole.DisplayRole) { return groupsList.get(index.row()); } else { return null; } } else { return sourceModel().data(mapToSource(index), role); } } @Override public QModelIndex index(int row, int column, QModelIndex parent) { // if parent is not null, then this is a child, so set the internalId to the row of its parent return createIndex(row, column, parent == null ? PARENT_INTERNAL_ID : parent.row()); } @Override public QModelIndex parent(QModelIndex child) { try { if (child.internalId() != PARENT_INTERNAL_ID) { // This is what I have to do since child.parent() causes an exception // if internalId is a valid row index, then child is actually a child node // and its internalId is the row index of its parent return createIndex((int)child.internalId(), 0, PARENT_INTERNAL_ID); // This will cause a StackOverflowError Exception from too many recursive calls //System.out.println("ListToTreeProxyModel.parent(): calling QModelIndex.parent() for child "+child); //return child.parent(); } else { return null; } } catch (StackOverflowError soe) { // this is to catch the StackOverflowError exception that occurs if child.parent() is called soe.printStackTrace(); // exit so we can see the stack trace before the GUI calls this method again System.exit(0); } return null; } @Override public int rowCount(QModelIndex parent) { if (parent == null) { return groupsList.size(); } else if (parent.internalId() == PARENT_INTERNAL_ID) { System.out.println("rowCount(): "+parent); String group = groupsList.get(parent.row()); return treeData.get(group).size(); } else { return 0; } }
}
@ -
@
// (continued from previous post)
private static class WordsModel extends QAbstractItemModel
{
private ArrayList<String> words = new ArrayList<String>(Arrays.asList(new String[]{"cantelope", "apricot", "banana", "coconut", "cherry","apple"}));public WordsModel() { super(); } public List<String> getWords() { return words; } @Override public int rowCount(QModelIndex arg0) { return words.size(); } @Override public int columnCount(QModelIndex arg0) { return 1; } @Override public Object headerData(int section, Qt.Orientation orientation, int role) { if (role == Qt.ItemDataRole.DisplayRole) { if (orientation == Qt.Orientation.Vertical) { return null; } else if (orientation == Qt.Orientation.Horizontal) { return (section == 0? "Word" : null); } } return null; } @Override public Object data(QModelIndex index, int role) { if (role == Qt.ItemDataRole.DisplayRole) { if (index.column() == 0) { return words.get(index.row()); } } return null; } @Override public QModelIndex index(int row, int column, QModelIndex parent) { if (parent == null) { return createIndex(row, column); } return null; } @Override public QModelIndex parent(QModelIndex arg0) { return null; }
}
}
@ -
This really isn't medium to be posting the program, please create and account and attach to the bug tracker http://redmine.smar.fi/projects/qtjambi/issues (even through we are not sure if it really is a bug or what at this stage).
You are comparing Win32 against Linux 64bit, so the -1 issue maybe related to that.
Yes it is known that the C++ ModelIndex is more flexiable for C++ because it follows the language features. There are some obvious patterns on how to do things in Java that QtJambi does not support because those patterns are not how things get done in C++.
Code like this is bad "new TestListToTreeProxyModel().show();". If you intended the life-time of the object to exist across the call the QApplication.exec() then you should assign it to a local varable. A memory restricted JVM (such as those used in embedded situations) is perfectly entitled to destroy your object before calling exec() which defeats the purpose of your application. This only works due to a quirk of the implementation of how Desktop Java works but the program as-is is not correct.