PDA

View Full Version : QFileSystemModel /QTreeView, check and expend all items under a user checked item



Guett_31
14th March 2013, 07:58
I’m having an issue for a while with a QFileSystemModel /QTreeView implementation. What I’m trying to achieve form the application user prospective. I have a QTreeView displaying the file system it only shows the directory name and file names. Each item in the view is checkable. That part is easy to design but what comes next is much harder. I’d like the application automatically expend and check all the items in all sub-directories if the user checks a directory. And I’d like it to automatically uncheck all the items in all sub-directories if the user unchecks a directory too.

I have been trying to implement the second part for a while. I searched the forum and the internet for hints and I found that other people had the same problem and could not find solid answers.Some people tried to iterate recursively through the indexes in the model starting from the parent index associated with the checked directory in the view. (If the children in level#2 are parents then we iterate through them, and so on in all the sub level, until there is no more parents to iterate through.) And most people have issues with:
Int rowCount(const QModelIndex & parent = QModelIndex()) const
In my case, it returns inconsistent results (often 1 or 0) even if there are many more child elements under the parent element.

Here is one of the unsuccessful attempts I made to check all the elements under the user selected directory in my QFileSystemModel subclass:

bool FileSysSelectModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (role == Qt::CheckStateRole)
{

//if the current item is in the checklist then remove it.
//if the current item is not in the checklist then add it.
if(value == Qt::Checked) m_checkTable.insert(index); else m_checkTable.remove(index);
emit dataChanged(index, index);

QModelIndex *currentIndex;
QModelIndex *currentChildIndex;
std::vector<QModelIndex*> parentTable(1,&const_cast<QModelIndex&>(index));

//Check all the sub-items.
while (!parentTable.empty())
{
currentIndex = parentTable[parentTable.size()-1];
parentTable.pop_back();
if(value == Qt::Checked) m_checkTable.insert(*currentIndex); else m_checkTable.remove(*currentIndex);
emit dataChanged(*currentIndex, *currentIndex);
if (hasChildren(*currentIndex))
{

for (int i(0); i < rowCount(*currentIndex); i++)
{
currentChildIndex = &FileSysSelectModel::index(i,0,*currentIndex);
parentTable.push_back(currentChildIndex);
}
}
}
return true;
}
// regular QFileSystemModel case.
return QFileSystemModel::setData(index, value, role)
Question1: Does the data in the tree model depend on the exposed items in the tree view?
For example, does the data corresponding to a collapsed branch exist in the model? I believe it does not and I think that the model retrieves the data corresponding to a directory only when the user asks for expending that same directory in the tree view. I’d like to confirm that because the QFileSystemModel doc says very little about it and it not clear to me at all.

Question2: If my assumption in question 1 is correct, how do we access the data of the collapsed branches in order to iterate through them?

A few people mentioned the use of the two following methodes to resolve the issue in one or two threads I’ve seen:

Virtual Bool canFetchMore(const QModelIndex & parent) const
Virtual void fetchMore(const QModelIndex & parent)

Here again, the documentation does not say much about it.

Question 3: From my understanding fetchMore populates the data under the parent QModelIndex and adds it to the model. Is that correct?
Here is another attempt. I tried to rework the same piece of code but it does not do better:

//Check all the sub-items.
while (!parentTable.empty())
{
currentIndex = parentTable[parentTable.size()-1];
parentTable.pop_back();
if(value == Qt::Checked) m_checkTable.insert(*currentIndex); else m_checkTable.remove(*currentIndex);
emit dataChanged(*currentIndex, *currentIndex);
if (hasChildren(*currentIndex))
{
while (canFetchMore(*currentIndex))
{
fetchMore(*currentIndex);
}

for (int i(0); i < rowCount(*currentIndex); i++)
{
currentChildIndex = &FileSysSelectModel::index(i,0,*currentIndex);
parentTable.push_back(currentChildIndex);
}
}
}
Some fetchMore users say that the reason why that kind of implementation does not work is because the FileSystemModel updated its data with an independent thread, so when the main thread executes rowCount, fetchMore has not finished updating the data.
I tried to use the directoryLoaded(const QString & path) signal by implementing the SLOT below in my FileSystemModel sub class to make sure fetchMore more had finish executing when calling rowCount.

void FileSysSelectModel::dataReady(const QString &currentPath)
{
m_currentPath = currentPath;
}

//Check all the sub-items.
while (!parentTable.empty())
{
currentIndex = parentTable[parentTable.size()-1];
parentTable.pop_back();
if(value == Qt::Checked) m_checkTable.insert(*currentIndex); else m_checkTable.remove(*currentIndex);
emit dataChanged(*currentIndex, *currentIndex);
if (hasChildren(*currentIndex))
{
QString test1 = filePath(*currentIndex); //debug
while ((canFetchMore(*currentIndex)) || (m_currentPath != filePath(*currentIndex)))
{
fetchMore(*currentIndex);
}

for (int i(0); i < rowCount(*currentIndex); i++)
{
currentChildIndex = &FileSysSelectModel::index(i,0,*currentIndex);
parentTable.push_back(currentChildIndex);
}
}
}
return true;
That does not work either.
Any idea?

Thanks

ChrisW67
14th March 2013, 09:43
QModelIndex is a value class. No need for all the pointer stuff which, AFAICT, is just asking for crashes given you are not allocating them on the heap..

QFileSystemModel is documented as using a thread to load data to allow for responsiveness and memory minimisation in the face of 1000000+ file directory trees (like mine). This loading is lazy, i.e. not occurring until a view/program requests some part of the tree that has not been loaded. A call to fetchMore() triggers the load of the immediate children only and emits directoryLoaded() when it is done. You need to account for the asynchronous nature of this process.

Guett_31
15th March 2013, 01:05
Thank you for you answer. It confirms that the use of fetchMore() and directoryLoaded() is the way to go in that case.

Please, could you tell me more about your first statement? I don't really understand what that means:
"QModelIndex is a value class. No need for all the pointer stuff which, AFAICT, is just asking for crashes given you are not allocating them on the heap.."

I don't know what a "value class" is. How does that affect my code?

Thanks,

ChrisW67
15th March 2013, 03:18
By value class I mean a class that is intended to convey a value around, much like an intrinsic int or float type. The class is designed to be copied and treated much like the intrinsic types. QString is another example. Generally two distinct value objects are considered equal if they convey the same meaning as a value.

Objects like QWidget are not value classes, they cannot be copied, and two objects are distinct even if e.g. they represent a label with precisely the same text on it. Generally these objects travel around in Qt programs as pointers.


Firstly, you are holding pointers to QModelIndex in your vector and having to constantly dereference them, rather than simply holding the indexes themselves. Not an error... just makes the code uglier than it need be and doesn't buy you anything. It does leave you open to the second point though.

Secondly, and more dangerously, you are holding pointers to temporary QModelIndex instances and using them later. Take lines 27 and 28 of your first listing:


currentChildIndex = &FileSysSelectModel::index(i,0,*currentIndex);
parentTable.push_back(currentChildIndex);

The call to index() returns a temporary QModelIndex, you take the address of that return value and store it in currentChildIndex, the temporary return value is then destroyed at the end of the statement call. You are left with a classic dangling pointer that points to memory that may no longer hold anything resembling correct data. Keeping a pointer to an object does not ensure the object persists. The normal usage pattern of:


{
QModelIndex currentChildIndex = index(i,0,currentIndex); // copies the return value
parentTable.push_back(currentChildIndex); // puts another copy into the list
}
// currentChildIndex is now out-of-scope and destroyed, the copy in the list still exists

takes a copy of the returned object that persists past the end of statement for as long as you keep it in scope.

Guett_31
15th March 2013, 19:17
Thank you very much for this very clear and detailed info about “value class”.
It seems to be some very basic knowledge but I could not find any valuable info about it anywhere else. One more question about that. Is there something in the class prototype or somewhere else that tells it is a “value class”? I would like to able to avoid the same kind of mistake in the future.

Secondly, as you suggested, I took out all the pointer stuff to be able to keep copies of the indexes generated in the “For loop” even when the original indexes are destroyed. But I’m still having a major issue with the fetchMore() function.
Has you can see in the code below, I have implemented a dataReady(const QString &) Slot in my FileSystemModel sub class that is just designed to save the path of the last loaded directory in the m_currentPath member variable. In FileSysSelectModel::setData(), I call fetchMore(currentIndex) and I “wait” until m_currentPath is equals to the currentIndex’s path (meaning that the data under currentIndex has finished loading and that I’m ready to use the rowCount() function).

bool FileSysSelectModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (role == Qt::CheckStateRole)
{

//if the current item is in the checklist then remove it.
//if the current item is not in the checklist then add it.
if(value == Qt::Checked) m_checkTable.insert(index); else m_checkTable.remove(index);
emit dataChanged(index, index);

QModelIndex currentIndex;
QModelIndex currentChildIndex;
std::vector<QModelIndex> parentTable(1,index);

//Check all the sub-items.
while (!parentTable.empty())
{
currentIndex = parentTable[parentTable.size()-1];
parentTable.pop_back();
if(value == Qt::Checked) m_checkTable.insert(currentIndex); else m_checkTable.remove(currentIndex);
emit dataChanged(currentIndex, currentIndex);
if (hasChildren(currentIndex))
{

// Load the currentIndex children and wait until it has finished loading.
while (m_currentPath != filePath(currentIndex))
{
if (canFetchMore(currentIndex)) fetchMore(currentIndex);
}

// Explore the currentIndex children and hold the ones that are parents themselves.
for (int i(0); i < rowCount(currentIndex); i++)
{
currentChildIndex = FileSysSelectModel::index(i,0,currentIndex);
parentTable.push_back(currentChildIndex);
}
}
}
return true;
}
// regular QFileSystemModel case.
return QFileSystemModel::setData(index, value, role);
}


void FileSysSelectModel::dataReady(const QString &currentPath)
{
m_currentPath = currentPath;
}
The issue is that I’m running in an infinite loop. I never receive a directoryLoaded() signal when I call fetchMore(). I've put a breakpoint in the FileSysSelectModel::dataReady() Slot and it never stops there after calling fetchMore() However, the execution does stop on the break point whenever I expend a branch in the QTreeView and I do get the path of the expended branch too, as expected. That means that the Signal, the Slot and the Connection work properly. It must come from fetchMore() itself or the way I use it. But I have no idea what is wrong…

Thanks,

Guett_31
27th March 2013, 01:29
Hi,
I'm still stuck with that issue. I'm expecting a directoryLoaded() signal after I called fetchMore() and it never comes.

A directoryLoaded() is emited when I expand an item in the QtreeView though, so I decided to look into QtreeView.cpp to see how the fetchMore() calls are implemented.
I looked at the expand all implementation, because it is very close to what I want to acheive after all. (Expand all the items under a user checked item)

Actually QTreeView::expandAll() calls a generic member of its private Class, QTreeViewPrivate::layout(int i, bool recursiveExpanding, bool afterIsUninitialized), which runs recursively and which calls fetchmore()
as follows:

int count = 0;
if (model->hasChildren(parent)) {
if (model->canFetchMore(parent))
model->fetchMore(parent);
count = model->rowCount(parent);
}
I don't get it. I've seen in several other threads pepole complaining about unexpected rowcount() returned values. And the given explanation for that behavior was the asynchorneous nature of the QFileSystemModel data update process. (The data is still being updated when rowCount() is called.)
That was the all point of what I'm trying to do, making sure that the data is updateed when I call fetchmore(). (See code in my previous message)
By looking a the QtreeVieww implementation, rowCount() is called right after fetchMore(). It doesn't seem to be a problem, execpt that when I try the same implentation in my code I end up with the same unexpected count value. And when I try to detect directoryLoaded() after calling fetchmore(), I never get it... I'm stuck!

Could someone help me with that? Thanks