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.
Related
My app allows double-clicking on a QTableWidget cell to edit the content. However, when I double-click on a cell, the existing content is selected.
How can I allow a cell's content to be edited without having the existing text automatically selected?
Qt item views automatically call selectAll() when an editor is created and it's a QLineEdit or a Q[Double]SpinBox.
Since the call to selectAll() is done after the editor is created, a simple solution would be to connect to the selectionChanged signal and deselect the text automatically. This can be done in an item delegate that would just override createEditor().
Note that this must be done only once, otherwise any user selection would become impossible. This also means that the signal must be disconnected immediately, and before deselecting (otherwise the function would be called again).
class NoSelectDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = super().createEditor(parent, option, index)
if isinstance(editor, (QLineEdit, QSpinBox, QDoubleSpinBox)):
def deselect():
# Important! First disconnect, otherwise editor.deselect()
# will call again this function
editor.selectionChanged.disconnect(deselect)
editor.deselect()
editor.selectionChanged.connect(deselect)
return editor
# ...
table.setItemDelegate(NoSelectDelegate(table))
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.
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()
We are making a GUI using PyQt and Qt Designer. Now we need that an image(pixmap) placed in a QLabel rescales nicely keeping ratio when the window is resized.
I've been reading other questions/answers but all of them use extended classes. As we are making constant changes in our UI, and it's created with Qt Creator, the .ui and (corresponding).py files are automatically generated so, if I'm not wrong, using a class-solution is not a good option for us because we should manually change the name of the class each time we update the ui.
Is there any option to autoresize the pixmap in a QLAbel keeping the ratio and avoiding using extended clases?
Thanks.
There are a couple of ways to do this.
Firstly, you can promote your QLabel in Qt Designer to a custom subclass that is written in python. Right-click the QLabel and select "Promote to...", then give the class a name (e.g. "ScaledLabel") and set the header file to the python module that the custom subclass class will be imported from (e.g. 'mylib.classes').
The custom subclass would then re-implement the resizeEvent like this:
class ScaledLabel(QtGui.QLabel):
def __init__(self, *args, **kwargs):
QtGui.QLabel.__init__(self)
self._pixmap = QtGui.QPixmap(self.pixmap())
def resizeEvent(self, event):
self.setPixmap(self._pixmap.scaled(
self.width(), self.height(),
QtCore.Qt.KeepAspectRatio))
For this to work properly, the QLabel should have its size policy set to expanding or minimumExpanding, and the minimum size should be set to a small, non-zero value (so the image can be scaled down).
The second method avoids using a subclass and uses an event-filter to handle the resize events:
class MainWindow(QtGui.QMainWindow):
def __init__(self):
...
self._pixmap = QtGui.QPixmap(self.label.pixmap())
self.label.installEventFilter(self)
def eventFilter(self, widget, event):
if (event.type() == QtCore.QEvent.Resize and
widget is self.label):
self.label.setPixmap(self._pixmap.scaled(
self.label.width(), self.label.height(),
QtCore.Qt.KeepAspectRatio))
return True
return QtGui.QMainWindow.eventFilter(self, widget, event)
Set background-image:, background-repeat: and background-position QSS properties for your label. You may do it via Forms editor or in code QWidget::setStyleSheet.
A good starting point for QSS (with examples) - http://doc.qt.io/qt-5/stylesheet-reference.html
One way is to create a QWidget/QLabel subclass and reimplement the resizeEvent.
void QWidget::resizeEvent(QResizeEvent * event) [virtual protected]
This event handler can be reimplemented in a subclass to receive widget resize events which are passed in the event parameter. When resizeEvent() is called, the widget already has its new geometry. The old size is accessible through QResizeEvent::oldSize().
The widget will be erased and receive a paint event immediately after processing the resize event. No drawing need be (or should be) done inside this handler.
This would need to be done in C++ though, not PyQt.
Having that done, you could add your custom widget to the QtDesigner as follows:
Using Custom Widgets with Qt Designer
Incredibly, after seven years #ekhumoro's excellent answer is still pretty much the only working Python implementation that can be found around; everyone else tells what to do, but nobody gives the actual code.
In spite of this, it did not work at first for me, because I happened to have the pixmap generation somewhere else in the code - specifically, my pixmap was generated inside a function which was only activated when clicking on a button, so not during the window intialization.
After figuring out how #ekhumoro's second method worked, I edited it in order to accomodate this difference. In pratice I generalised the original code, also because I did not like (for efficiency reasons) how it added a new _pixmap attribute to the label, which seemed to be nothing more than a copy of the original pixmap.
The following his is my version; mind that I have not fully tested it, but since it is a shorter version of my original working code, it too should work just fine (corrections are welcome, though):
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Initialize stuff here; mind that no pixmap is added to the label at this point
def eventFilter(self, widget, event):
if event.type() == QEvent.Resize and widget is self.label:
self.label.setPixmap(self.label.pixmap.scaled(self.label.width(), self.label.height(), aspectRatioMode=Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation))
return True
return QMainWindow.eventFilter(self, widget, event)
def apply_pixelmap(self, image): # This is where the pixmap is added. For simplicity, suppose that you pass a QImage as an argument to this function; however, you can obtain this in any way you like
pixmap = QPixmap.fromImage(image).scaled(new_w, new_h, aspectRatioMode=Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
self.label.setPixmap(pixmap)
self.label.pixmap = QPixmap(pixmap) # I am aware that this line looks like a redundancy, but without it the program does not work; I could not figure out why, so I will gladly listen to anyone who knows it
self.label.installEventFilter(self)
return
This works by setting the ScaledContents property to False and the SizePolicy to either Expanding or Ignored. Note that it might not work if the label containing the image is not set as the central widget (self.setCentralWidget(self.label), where self refers to MainWindow).
I am facing the problem to need tabs in a pygtk app. Pretty much just like gedit has, but without any per-child widget content.
I’ve come across gtk.Notebook, but that requires me to put a widget for each tab, which I don't want.
The reason is, that I have one widget, but would only like to updates its content based on which tab is selected.
Any hints on how to do that?
My idea so far would be to just add some invisible widget for each tab and then connect to the select-page signal. Which widget could I use as invisible widget, or is there a better/alternative way of achieving my goal?
The invisble widget idea works. But not with gtk.Invisible (this just crashes), but with gtk.HBox() or any other thing that seems empty.
self.notebook.append_page(gtk.HBox(), gtk.Label("title"))
Now if I want to display stuff inside the tab actually, I can use reparent to move the widget to the current tab like this.
class Tab(gtk.HBox):
def __init__(self, child):
self.child = child
self.notebook.append_page(Tab(myWidget), gtk.Label("title"))
def pageSelected(self, notebook, page, pagenum):
box = notebook.get_nth_page(pagenum)
box.child.reparent(box)
You can have global widgets, one per tab as you want, in order to access them easily when the tab is selected.
self.notebook.append_page(self.rightBox, gtk.Label("Orders"))
Then connect to the "switch page" signal
self.notebook.connect("switch-page", self.pageSelected)
and :
def pageSelected(self, notebook, page, pagenum):
name = notebook.get_tab_label(notebook.get_nth_page(pagenum))
Now you have "name" with the label of the currently selected page. Just test it (if name == "Orders" ...) to interact.
Hope this was of some help !