PDA

View Full Version : QFileDialog and QSortFilterProxyModel



drmacro
11th November 2017, 18:50
I've been beating my head against this for a while now.

Ultimately, I'd like to filter on a substring in the file name.
If I understand correctly, the vanilla QFileDialog will only do file extensions (i.e. the characters after the period, like .exe or .xml)

So I wrote the following in an attempt to "try it".:



import sys

from PyQt5 import Qt, QtCore, QtGui, QtWidgets
from PyQt5.QtGui import QColor, QBrush
from PyQt5.QtGui import QPainter, QColor, QPen, QFont
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

class FileFilterProxyModel(QtCore.QSortFilterProxyModel) :
def __init__(self, parent=None):
super(FileFilterProxyModel, self).__init__(parent)

def filterAcceptsRow(self, source_row, srcidx):
model = self.sourceModel()
index0 = model.index(source_row, 0, QtCore.QModelIndex())
index1 = model.index(source_row, 1, QtCore.QModelIndex())
index2 = model.index(source_row, 2, QtCore.QModelIndex())
str0 = model.data(index0, Qt.DisplayRole)
str1 = model.data(index1, Qt.DisplayRole)
str2 = model.data(index2, Qt.DisplayRole)
print('{}, data: {}{}{}'.format(source_row, str0, str1, str2))
return True

app = QtWidgets.QApplication(sys.argv)
dlg = QFileDialog()
proxymodel = FileFilterProxyModel(dlg)
dlg.setProxyModel(proxymodel)
#dlg.setOption(QFileDialog.DontUseNativeDialog)
dlg.show()
app.exec_()


But, I can't seem to figure out how to get the file name to decide whether it has the substring.

"model" is a QFileSystemModel.
The code above returns '/' for str0, blank for str1, and the word 'Drive'.
Tried model.data(index0, QFileSystemModel.FileNameRole) returns nothing
In other versions of this code I tried model.fileName(index) which always returns nothing.

But, it does display the file dialog with all files listed (with return = True in filterAcceptsRow) and no files (return False)

Once again I'm confused...happens to me a lot. :confused: :(

high_flyer
12th November 2017, 00:53
amm... your filterAcceptsRow() returns hard coded 'true'.
So no wonder it shows everything or nothing if change that to 'false'.
The return value should depend on if the string you want to filter out is found in the current examined string.
For that you need to set the string you what to use as a filter, which I don't see in your code.(setFilterRegExp())
It seems your code is based on the example from the documentation - so why did you left the actual string filtering out?
And while you are at it, you might want to read all the QSortFilterProxyModel documentation.

drmacro
12th November 2017, 01:59
amm... your filterAcceptsRow() returns hard coded 'true'.
So no wonder it shows everything or nothing if change that to 'false'.



Yes, as I mentioned, I tried it with true and get everything, false, get nothing.
Yes, I realize it SHOULD, return true or false based on what the file name contains.
(I got that from reading the documentation you assume I didn't read.)



The return value should depend on if the string you want to filter out is found in the current examined string.
For that you need to set the string you what to use as a filter, which I don't see in your code.(setFilterRegExp())

Is it required that I use setFilterRegExp ()?
Why can't I just check the contents in fileAcceptsRow?
And my question is how to get the string to be searched, in this case the file name.



It seems your code is based on the example from the documentation - so why did you left the actual string filtering out?



Gleaned from several examples actually.
I left it out because no two examples I've found do it the same and investment trying to figure out how this works.
Not to mention trying to translate from c to python.



And while you are at it, you might want to read all the QSortFilterProxyModel documentation.


The qt docs are great if you need to reference how a function name is spelled or what the arguments are...not so much help on the how to or why...IMHO

In addition, and yes I've read the docs, the question which I obviously wasn't clear on. How do I access the file name string from the model...so I can actually filter it? As noted in my original question, I tried different things and could not get the file name.

d_stranz
12th November 2017, 17:18
Is it required that I use setFilterRegExp ()?

No.


Why can't I just check the contents in fileAcceptsRow?

It's "filterAcceptsRow()" actually.

You can, but by doing that, you will be, in effect, doing exactly what setting the filter expression does for you. Why reinvent a wheel and likely introduce your bugs into it when someone else has already provided a debugged wheel for you?

The filterAcceptsRow() method is more general, allowing you to accept or reject rows based on any set of criteria using any Qt:: ItemRole (or not). setFilterRegExp() is used in conjunction with the base class' implementation of filterAcceptsRow() to implement regular expression-based matching and filtering based on Qt:: DisplayRole.

admkrk
12th November 2017, 20:18
How do I access the file name string from the model...so I can actually filter it? As noted in my original question, I tried different things and could not get the file name.
Try QFileInfo.

drmacro
12th November 2017, 21:01
After much trials and error the following code works, that is, it shows only folders and xml files that contain the string "_project":


import sys

from PyQt5 import Qt, QtCore, QtGui, QtWidgets
from PyQt5.QtGui import QColor, QBrush
from PyQt5.QtGui import QPainter, QColor, QPen, QFont
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

class FileFilterProxyModel(QtCore.QSortFilterProxyModel) :
def __init__(self, parent=None):
super(FileFilterProxyModel, self).__init__(parent)

def filterAcceptsRow(self, source_row, srcidx):
model = self.sourceModel()
index0 = model.index(source_row, 0, srcidx)
index1 = model.index(source_row, 1, srcidx)
index2 = model.index(source_row, 2, srcidx)
str0_filenamerole = model.data(index0, QFileSystemModel.FileNameRole)
str0_displayrole = model.data(index0, Qt.DisplayRole)
fname = model.fileName(index0)
fname1 = model.fileName(index1)
str1_filenamerole = model.data(index1, QFileSystemModel.FileNameRole)
str1_displayrole = model.data(index1, Qt.DisplayRole)
str2_filenamerole = model.data(index2, QFileSystemModel.FileNameRole)
str2_displayrole = model.data(index2, Qt.DisplayRole)
print('source_rowe: {}'.format(source_row))
print('str0_filenamerole: {}'.format(str0_filenamerole))
print('str0_displayrole: {}'.format(str0_displayrole))
print('str1_filenamerole: {}'.format(str1_filenamerole))
print('str1_displayrole: {}'.format(str1_displayrole))
print('str2_filenamerole: {}'.format(str2_filenamerole))
print('str2_displayrole: ~{}~'.format(str2_displayrole))
if str2_displayrole in ('Folder', 'Drive'): return True
if '_project' in str1_filenamerole and str2_displayrole == 'xml File':
print('Returning True')
return True
else:
return False

app = QtWidgets.QApplication(sys.argv)
dlg = QFileDialog()
proxymodel = FileFilterProxyModel(dlg)
dlg.setDirectory('/home/mac/Shows/Fiddler/')
dlg.setProxyModel(proxymodel)
dlg.show()
app.exec_()

My initial confusion was that model.fileName() did not return anything. Of course when I gave it the correct argument, model.fileName(index0), the file name is returned.
With that is a simple matter to find the existence desired substring and return True/False accordingly.

To respond to d_stranz:
I have no problem using ready made wheels when I understand what and how they do what they do.
Unfortunately, I also tend to grock things a bit at a time. This seems to be a real drawback in figuring out how things work in qt. But, thats my burden.

In addition, I use regular expressions so infrequently, that I always end cross eyed when I use them. :rolleyes:

In any case, I have now attempted to to use the reqex way...to no avail. The following only get content in the dialog when the regex is "".
I tried "_project" and a bunch of other combinations to no avail. Interestingly, "_?" returns all files and folders.
I'm tired, blurry eyed, etc. and have to re-learn (for the umteenth time over 40 years) regex tomorrow.



import sys

from PyQt5 import Qt, QtCore, QtGui, QtWidgets
from PyQt5.QtGui import QColor, QBrush
from PyQt5.QtGui import QPainter, QColor, QPen, QFont
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

class FileFilterProxyModel(QtCore.QSortFilterProxyModel) :
def __init__(self, parent=None):
super(FileFilterProxyModel, self).__init__(parent)


app = QtWidgets.QApplication(sys.argv)
dlg = QFileDialog()
proxymodel = QtCore.QSortFilterProxyModel(dlg)
proxymodel.setFilterRole(Qt.DisplayRole)
proxymodel.setFilterRegExp("")
dlg.setDirectory('/home/mac/Shows/Fiddler/')
dlg.setProxyModel(proxymodel)
dlg.show()
app.exec_()

high_flyer
12th November 2017, 21:28
EDIT: ah you beat me to it... never the less, I'll leave the answer.


Is it required that I use setFilterRegExp ()?
As d_stranz said, you don't have to use that specific method.
What ever way you choose to set your search string, you have to do it.
Your code does not include any such way.
Again, in your filterAcceptsRow() you are not doing any filtering.
What ever way you choose to bring the filter string in, you have to use it.
Your filterAcceptsRow() as you posted it simply gets a string for the 3 first columns in your row, and then returns without doing anything with these strings.

Anyways, As I said in my first reply,in the documentation for QSortFilterProxyModel the following code snippet is doing EXACTLY what you are trying to do with the only difference being the roles of the data you are interested in:


bool MySortFilterProxyModel::filterAcceptsRow(int sourceRow,
const QModelIndex &sourceParent) const
{
QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent);
QModelIndex index1 = sourceModel()->index(sourceRow, 1, sourceParent);
QModelIndex index2 = sourceModel()->index(sourceRow, 2, sourceParent);

return (sourceModel()->data(index0).toString().contains(filterRegExp())
|| sourceModel()->data(index1).toString().contains(filterRegExp()))
&& dateInRange(sourceModel()->data(index2).toDate());
}


as you can see the return here depends on if the examined tiring contains the filter string - which would have solved your problem with getting all or nothing.

To your second question:

How do I access the file name string from the model...so I can actually filter it?

Again this is found in the docs, in this case for QFileSystemModel:


enumQFileSystemModel::FileIconRole Qt::DecorationRole

QFileSystemModel::FilePathRole Qt::UserRole + 1
QFileSystemModel::FileNameRole Qt::UserRole + 2
QFileSystemModel::FilePermissions Qt::UserRole + 3 QFileSystemModel::Roles


Which means, in your filterAcceptsRow() you call data() with the role you are interested in.


The qt docs are great if you need to reference how a function name is spelled or what the arguments are...not so much help on the how to or why...IMHO
As you can see, the docs offer more then just the signatures of methods.

d_stranz
13th November 2017, 17:45
I have no problem using ready made wheels when I understand what and how they do what they do.
Unfortunately, I also tend to grock things a bit at a time. This seems to be a real drawback in figuring out how things work in qt. But, that's my burden.

The comments weren't meant as a criticism. All of us went (and are still going) through the same learning curve - reading the docs, not understanding them, looking at examples that don't quite apply, and then eventually trying enough different things that you finally find something that works. Hopefully you will also understand -why- it works at the end of it all. There are plenty of parts of Qt I haven't even looked at, much less used.

Qt's Model-View system is very powerful and flexible and it is worth taking the time to learn how to use it effectively.

drmacro
14th November 2017, 16:00
The comments weren't meant as a criticism. All of us went (and are still going) through the same learning curve - reading the docs, not understanding them, looking at examples that don't quite apply, and then eventually trying enough different things that you finally find something that works. Hopefully you will also understand -why- it works at the end of it all. There are plenty of parts of Qt I haven't even looked at, much less used.

Qt's Model-View system is very powerful and flexible and it is worth taking the time to learn how to use it effectively.

As with most powerful systems, the concept is rather simple, the implementation not always.

Just to be complete I worked at it until I had a functional version of the code that used the filterRegExp.

I did go off and do a refresher of regex, but, it really wasn't needed for the results I wanted to attain.

The following code shows any xml file that has the string "_project" in the file name. Also retaining folders/drives so the user can search elsewhere.

There may be better ways to code this example, comments are welcome.

As I mentioned earlier, I found that index 0 and 2 of the model had the filename and the type respectively by trial and error.
I was unable to to glean this info from the docs on FileSystemModel. It may be there, I just didn't find it or interpret it from what I read.
(I'd be happy to be instructed on how to discern it.)



import sys

from PyQt5 import Qt, QtCore, QtWidgets
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

class FileFilterProxyModel(QtCore.QSortFilterProxyModel) :
def __init__(self, parent=None):
super(FileFilterProxyModel, self).__init__(parent)

def filterAcceptsRow(self, source_row, srcidx):
model = self.sourceModel()
index0 = model.index(source_row, 0, srcidx)
index2 = model.index(source_row, 2, srcidx)
str0_filenamerole = model.data(index0, QFileSystemModel.FileNameRole)
str2_displayrole = model.data(index2, Qt.DisplayRole)
indexofstring = self.filterRegExp().indexIn(str0_filenamerole)
if (indexofstring >= 0 and str2_displayrole == 'xml File')\
or (str2_displayrole in ('Folder', 'Drive')):
return True
else:
return False

app = QtWidgets.QApplication(sys.argv)
dlg = QFileDialog()
proxymodel = FileFilterProxyModel(dlg)
proxymodel.setFilterRegExp(QRegExp("_project", Qt.CaseInsensitive, QRegExp.FixedString))
dlg.setDirectory('/home/mac/Shows/Fiddler/')
dlg.setProxyModel(proxymodel)
dlg.show()
app.exec_()