My scrip ist currently using QtWidgets.QFileDialog.getOpenFileNames() to let the user select files within Windows explorer. Now I´m wondering if there is a way to let them select also folders, not just files. There are some similar posts, but none of them provides a working solution. I really dont want to use the QFileDialog file explorer to get around this.
QFileDialog doesn't allow that natively. The only solution is to create your own instance, do some small "patching".
Note that in order to achieve this, you cannot use the native dialogs of your OS, as Qt has almost no control over them; that's the reason of the dialog.DontUseNativeDialog flag, which is mandatory.
The following code works as much as static methods do, and returns the selected items (or none, if the dialog is cancelled).
def getOpenFilesAndDirs(parent=None, caption='', directory='',
filter='', initialFilter='', options=None):
def updateText():
# update the contents of the line edit widget with the selected files
selected = []
for index in view.selectionModel().selectedRows():
selected.append('"{}"'.format(index.data()))
lineEdit.setText(' '.join(selected))
dialog = QtWidgets.QFileDialog(parent, windowTitle=caption)
dialog.setFileMode(dialog.ExistingFiles)
if options:
dialog.setOptions(options)
dialog.setOption(dialog.DontUseNativeDialog, True)
if directory:
dialog.setDirectory(directory)
if filter:
dialog.setNameFilter(filter)
if initialFilter:
dialog.selectNameFilter(initialFilter)
# by default, if a directory is opened in file listing mode,
# QFileDialog.accept() shows the contents of that directory, but we
# need to be able to "open" directories as we can do with files, so we
# just override accept() with the default QDialog implementation which
# will just return exec_()
dialog.accept = lambda: QtWidgets.QDialog.accept(dialog)
# there are many item views in a non-native dialog, but the ones displaying
# the actual contents are created inside a QStackedWidget; they are a
# QTreeView and a QListView, and the tree is only used when the
# viewMode is set to QFileDialog.Details, which is not this case
stackedWidget = dialog.findChild(QtWidgets.QStackedWidget)
view = stackedWidget.findChild(QtWidgets.QListView)
view.selectionModel().selectionChanged.connect(updateText)
lineEdit = dialog.findChild(QtWidgets.QLineEdit)
# clear the line edit contents whenever the current directory changes
dialog.directoryEntered.connect(lambda: lineEdit.setText(''))
dialog.exec_()
return dialog.selectedFiles()
Related
I have a Treeview (inherits from QTreeView) with model QFileSystemModel. This Treeview object is added to another widget with a QPushButton. The button will open a directory through a QFileDialog, and call the Treeview's set_directory method. The method will set the root path of the model to the selected directory and will modify the root index of the tree view. The top-most item in the treeview is then selected.
However, when selecting a directory for the first time, the top-most item is not selected. Commenting out line 11: self.model.directoryLoaded.connect(self.set_directory), solves the issue. But this means the set_directory method is called twice:
# default directory opened
<class 'NoneType'>
# open new directory, set_directory called twice
<class 'NoneType'>
<class 'PyQt5.QtWidgets.QFileSystemModel'>
As seen on the command line output, the QModelIndex.model() method returns a NoneType when selecting a directory for the first time. How do I set the treeview's current index to the top most item without calling the set_directory method twice? And why is the QModelIndex model a NoneType when the directory has not been visited?
Main script below:
from PyQt5.QtWidgets import QPushButton, QTreeView, QFileSystemModel, QWidget, QFileDialog, QApplication, QHBoxLayout
import sys
class Treeview(QTreeView):
def __init__(self, parent=None):
super(QTreeView, self).__init__(parent)
self.model = QFileSystemModel()
self.setModel(self.model)
self.set_directory(".")
# self.model.directoryLoaded.connect(self.set_directory)
def set_directory(self, path):
self.setRootIndex(self.model.setRootPath(path))
self.setCurrentIndex(self.model.index(0, 0, self.rootIndex()))
print(type(self.model.index(0, 0, self.rootIndex()).model()))
class MainWidget(QWidget):
def __init__(self, parent=None):
super(QWidget, self).__init__(parent)
self.tview = Treeview(self)
vlayout = QHBoxLayout(self)
vlayout.addWidget(self.tview)
open_button = QPushButton(self)
open_button.clicked.connect(self.open_dir)
vlayout.addWidget(open_button)
def open_dir(self):
filepath = QFileDialog.getExistingDirectory(self)
if filepath:
self.tview.set_directory(filepath)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = MainWidget()
widget.show()
sys.exit(app.exec_())
There are three problems:
directoryLoaded is called whenever the contents of a directory are loaded the first time, which happens asynchronously on a separate thread; if the contents of the "root" directory has never been loaded yet, it will have no children yet, so any attempt to access the children of the root index will return invalid indexes (that's why you get None);
the signal is also sent whenever a directory is expanded in the tree for the first time, which creates a problem if you connect it to set_directory: if you open the folder "home" and then expand the directory "documents", the model will load all its contents and emit the directoryLoaded for the "documents" folder, which will then call set_directory in turn once again;
the file system model also sorts items (alphabetically by default, with directories first), and this means that it will need some more time to get what you believe is the first item: for performance reasons, the OS returns the contents of a directory depending on the file system, which depends on its implementation: sometimes it's sorted by creation/modification time, others by the physical position/index of the blocks, etc;
Considering the above, you cannot rely on directoryLoaded (and surely you should not connect it to set_directory), but you can use the layoutChanged signal, since it's always emitted whenever sorting has completed (and QFileSystemModel always sorts the model when the root changes); the only catch is that you must do it only when needed.
The solution is to create a function that tries to set the top item, if it's not valid then it will connect itself to the layoutChanged signal; at that point, the signal will be emitted when the model has completed its job, and the top index has become available. Using a flag helps us to know if the signal has been connected, and then disconnect it, which is important in case you need to support sorting.
class Treeview(QTreeView):
layoutCheck = False
def __init__(self, parent=None):
super(QTreeView, self).__init__(parent)
self.model = QFileSystemModel()
self.setModel(self.model)
self.set_directory(QDir.currentPath())
self.setSortingEnabled(True)
def setTopIndex(self):
topIndex = self.model.index(0, 0, self.rootIndex())
print(topIndex.isValid(), topIndex.model())
if topIndex.isValid():
self.setCurrentIndex(topIndex)
if self.layoutCheck:
self.model.layoutChanged.disconnect(self.setTopIndex)
self.layoutCheck = False
else:
if not self.layoutCheck:
self.model.layoutChanged.connect(self.setTopIndex)
self.layoutCheck = True
def set_directory(self, path):
self.setRootIndex(self.model.setRootPath(path))
self.setTopIndex()
Please consider that the layoutCheck flag is very important, for many reasons:
as explained before, layoutChanged is always emitted when the model is sorted; this not only happens when trying to sort using the headers, but also when files or directories are being added;
signal connections are not exclusive, and you can even connect the same signal to the same slot/function more than once, with the result that the function will be called as many time as it's been connected; if you're not very careful, you could risk recursion;
the flag works fine also for empty directories, avoiding the above risk of recursion; if a new root directory is opened and it's empty, it will obviously return an invalid index for the top item (since there's none), but the signal will be disconnected in any case: if another directory (with contents) is opened but not yet loaded, the signal won't be connected again, and if, instead, the contents have been already loaded, it will disconnect it no matter what;
A possibility is to use the Qt.UniqueConnection connection type and a try block for the disconnection, but, while this approach works and is consistent, it's a bit cumbersome: as long as the connection is always paired with the setting, using a basic boolean flag is much simpler and easier to read and understand.
I've been trying to implement a file browsing widget in the GUI I'm designing. I am using the QFileDialog module, which works great - I can browse and save a file with the following line of code:
filenames = QFileDialog.getOpenFileName()
My widget is set up with a QLineEdit, which I would like to display the name of the file selected, and a QPushButton, which I would like to initiate the above line of code. However, I'd like to know if there's a way I can set a "default" option. If the browse push button is not clicked, I would like the file to be the following:
filenames = str(glob.glob('*.npy')[0])
Which would be saved as the filename in question and show up in my LineEdit. My problem is coming from trying to display a different file name in the LineEdit, depending on whether or not the browse push button has been clicked. If it has been clicked, I would like the LineEdit to show the user-selected file instead of the default option. Here are the applicable lines of code in my retranslate function:
def retranslateUi(self, ROIGUI):
self.lineEdit.setText(_translate("ROIGUI", self.fileSelect(False), None))
self.Browse.setText(_translate("ROIGUI", "Browse...", None))
self.Browse.clicked.connect(self.fileSelect(True))
Which link to the following function. As you can see, this is currently not working correctly because in the LineEdit, tripped is always False. Very silly.
def fileSelect(self,tripped):
filenames = str(glob.glob('*.npy')[0])
if tripped==True:
filenames = QFileDialog.getOpenFileName()
self.lineEdit.setText(_translate("ROIGUI", filenames, None))
return filenames
I've been trying different ways of getting this to work, but everything I tried either (a) never updates my LineEdit after file browsing, or (b) runs file browsing immediately without ever using the default option. Thoughts? I'm sure there's a way of doing this that I'm just not seeing.
Thank you in advance.
Edited To Add
I think I have fixed most of my problem - my Browse button is now connected via buttonGroup to an integer, so my fileSelect looks like this:
def fileSelect(self):
signal = self.buttonGroup2.checkedId()
if signal==-1:
filenames = str(glob.glob('*.npy')[0])
elif signal==1:
filenames = QFileDialog.getOpenFileName()
if (filenames.isNull()):
filenames = str(glob.glob('*.npy')[0])
return filenames
And my "retranslate" browse button and lineEdit look like this:
self.lineEdit.setText(_translate("ROIGUI", str(self.fileSelect()), None))
self.Browse.clicked.connect(self.fileSelect)
My only problem is getting the text of my lineEdit to update; although the file in use itself updates after it's selected with Browse, the text itself doesn't update. Help?
If the cancel button is selected from the QFileDialog filenames variable will be a null QString so, you could:
filenames = QFileDialog.getOpenFileName()
if (filenames.isNull()):
self.lineEdit.setText(_translate("ROIGUI", filenames, None))
else:
# The alternative code. Set the default value here to the QLineEdit.
Reference: QFileDialog.getOpenFileName()
I've been trying to implement a file browsing widget in the GUI I'm designing. I am using the QFileDialog module, which works great - I can browse and save a file with the following line of code:
filenames = QFileDialog.getOpenFileName()
My widget is set up with a QLineEdit, which I would like to display the name of the file selected, and a QPushButton, which I would like to initiate the above line of code. However, I'd like to know if there's a way I can set a "default" option. If the browse push button is not clicked, I would like the file to be the following:
filenames = str(glob.glob('*.npy')[0])
Which would be saved as the filename in question and show up in my LineEdit. My problem is coming from trying to display a different file name in the LineEdit, depending on whether or not the browse push button has been clicked. If it has been clicked, I would like the LineEdit to show the user-selected file instead of the default option. Here are the applicable lines of code in my retranslate function:
def retranslateUi(self, ROIGUI):
self.lineEdit.setText(_translate("ROIGUI", self.fileSelect(False), None))
self.Browse.setText(_translate("ROIGUI", "Browse...", None))
self.Browse.clicked.connect(self.fileSelect(True))
Which link to the following function. As you can see, this is currently not working correctly because in the LineEdit, tripped is always False. Very silly.
def fileSelect(self,tripped):
filenames = str(glob.glob('*.npy')[0])
if tripped==True:
filenames = QFileDialog.getOpenFileName()
self.lineEdit.setText(_translate("ROIGUI", filenames, None))
return filenames
I've been trying different ways of getting this to work, but everything I tried either (a) never updates my LineEdit after file browsing, or (b) runs file browsing immediately without ever using the default option. Thoughts? I'm sure there's a way of doing this that I'm just not seeing.
Thank you in advance.
Edited To Add
I think I have fixed most of my problem - my Browse button is now connected via buttonGroup to an integer, so my fileSelect looks like this:
def fileSelect(self):
signal = self.buttonGroup2.checkedId()
if signal==-1:
filenames = str(glob.glob('*.npy')[0])
elif signal==1:
filenames = QFileDialog.getOpenFileName()
if (filenames.isNull()):
filenames = str(glob.glob('*.npy')[0])
return filenames
And my "retranslate" browse button and lineEdit look like this:
self.lineEdit.setText(_translate("ROIGUI", str(self.fileSelect()), None))
self.Browse.clicked.connect(self.fileSelect)
My only problem is getting the text of my lineEdit to update; although the file in use itself updates after it's selected with Browse, the text itself doesn't update. Help?
If the cancel button is selected from the QFileDialog filenames variable will be a null QString so, you could:
filenames = QFileDialog.getOpenFileName()
if (filenames.isNull()):
self.lineEdit.setText(_translate("ROIGUI", filenames, None))
else:
# The alternative code. Set the default value here to the QLineEdit.
Reference: QFileDialog.getOpenFileName()
I have a QTableView with associated QAbstractTableModel that contains directory names in some columns. I would like to use a QFileDialog as the editor to change those columns. This a little unusual, as the editor is not going to be inside the table cell (not enough space).
I got the basics working using a QStyledItemDelegate:
class DirectorySelectionDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = QFileDialog(parent)
editor.setFileMode(QFileDialog.Directory)
editor.resize(400, 400)
return editor
def setEditorData(self, editor, index):
val = index.model().data(index, Qt.DisplayRole)
fs = val.rsplit(os.path.sep, 1)
if len(fs) == 2:
bdir, vdir = fs
else:
bdir = "."
vdir = fs[0]
editor.setDirectory(bdir)
editor.selectFile(vdir)
def setModelData(self, editor, model, index):
model.setData(index, editor.selectedFiles())
When double-clicking the cell it starts a QFileDialog, I can select the directory I want and on Choose it is set in the model.
However, if for whatever reason the QFileDialog loses focus it is closed, and the data is set to the original value. I would prefer the dialog to be open until the user clicks Cancel or Choose, but I cannot find a way to do that.
Bonus question: for some reason the dialog ignores the resize() call and starts up very small (which makes losing the focus all the more likely). How can I change the size of the dialog?
This is expected behaviour.
The standard views are not containers of widgets, each cell is drawn when necessary. The widget used for editing is only created and superimposed on top of the view whenever an editing trigger is generated. The delegate is then called to create the appropriate editing widget and the location and size of the cell are passed in as an argument.
The view retains ownership of the editor widget because, whenever focus is lost, you can obviously not be able to edit anymore so the view deletes the editor.
If you do not pass anything back in the setModelData function, the model will not be updated. It is not correct to say that the model is set back to the original data because it never gets changed in the first place.
What I would suggest you try is setting the QFileDialog to open modally (editor.setModal(true)) so that the dialog has to be closed before focus can be transferred to another widget.
I am making a program to manage a database and i need a nice way of choosing the save locations of files within the program. I was wondering if it is possible to open windows explore from my program, select a folder to save a file in, enter the file name and return the file path as a string to the main program.
Look up the Tkinter options in tkFileDialog
I don't think you necessarily need to put together a fully working GUI, you can simply call the method and select a folder location and then use that selected location in your code.
A simplistic example would be:
import Tkinter, tkFileDialog
root = Tkinter.Tk()
x = tkFileDialog.askopenfilename() # Can pass optional arguments for this...
root.destroy()