How can I detect when one window occludes another in PyQt5? - python

I'm using PyQt5 to create an app with multiple main windows. I want to be able to allow the user to save and load window sizes and window positions. That's easy with, e.g., QMainWindow.saveGeometry() and QMainWindow.loadGeometry() or the corresponding .saveState() and .loadState() variants. These work great for position and size, but if the user moves or resizes one window so that it occludes another, I want to also restore this positioning. I don't mind writing my own code to save the info for each window, but I can't see any way to detect the relative Z order of windows. Am I missing it in the docs, or is this not possible?
To see what I mean, try this:
from PyQt5.QtWidgets import QApplication, QMainWindow, QPlainTextEdit
from PyQt5.QtCore import QSettings
from PyQt5.QtGui import QCloseEvent
'''
context: Linux Mint 19.3 Tricia x86_64
Python 3.9
PyQt5 5.15.1
'''
class RememberWin(QMainWindow):
def __init__(self, win_name: str):
super(RememberWin, self).__init__()
self.win_name = win_name
self.setWindowTitle(win_name)
self.can_close = False
def restore_window(self) -> bool:
try:
settings = QSettings("PyQtExamples", "RememberWinTest")
self.restoreGeometry(settings.value(f'{self.win_name} Geometry'))
self.restoreState(settings.value(f'{self.win_name} State'))
return True
except:
return False
def closeEvent(self, event: QCloseEvent):
if not self.can_close:
event.ignore()
else:
settings = QSettings("PyQtExamples", "RememberWinTest")
settings.setValue(f'{self.win_name} Geometry', self.saveGeometry())
settings.setValue(f'{self.win_name} State', self.saveState())
QMainWindow.closeEvent(self, event)
class ControlWindow(RememberWin):
def __init__(self, win_name: str = "ControlWindow"):
super(ControlWindow, self).__init__(win_name=win_name)
self.can_close = True
self.window1 = RememberWin(win_name='WindowOne')
self.window2 = RememberWin(win_name='WindowTwo')
self.text = QPlainTextEdit(self)
s = "Try making Window1 wide enough to cover Window2.\n" \
"Then close this window (auto closes others).\n" \
"Re-run the app and you'll notice that Window2\n" \
"is not on top of Window1 which means that this\n" \
"info isn't getting saved."
self.text.setPlainText(s)
self.setCentralWidget(self.text)
if not self.restore_window():
self.setGeometry(100, 390, 512, 100)
if not self.window1.restore_window():
self.window1.setGeometry(100, 100, 512, 384)
if not self.window2.restore_window():
self.window2.setGeometry(622, 100, 512, 384)
self.window1.show()
self.window2.show()
def closeEvent(self, event: QCloseEvent):
for win in (self.window1, self.window2):
win.can_close = True
win.close()
super(ControlWindow, self).closeEvent(event)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = ControlWindow(win_name='ControlWindow (You can only close this one)')
window.show()
sys.exit(app.exec_())

The simplest way to do what you want to achieve is to keep track of the current focused widget, or, to be precise, the top level window of the last focused widget.
You can store the focused windows in the settings as a list, using a unique objectName for each window (you are already doing this, so you just need to use setObjectName()), then restore the window by showing them in the correct order as long as the object name matches.
class RememberWin(QMainWindow):
def __init__(self, win_name: str):
super(RememberWin, self).__init__()
self.win_name = win_name
self.setObjectName(win_name)
self.setWindowTitle(win_name)
self.can_close = False
# ...
class ControlWindow(RememberWin):
def __init__(self, win_name: str = "ControlWindow"):
# ...
self.settings = QSettings("PyQtExamples", "RememberWinTest")
self.zOrder = []
QApplication.instance().focusObjectChanged.connect(self.focusChanged)
windowOrder = self.settings.value('windowOrder', type='QStringList')
topLevelWindows = QApplication.topLevelWidgets()
if windowOrder:
for objName in windowOrder:
for win in topLevelWindows:
if win.objectName() == objName:
win.show()
else:
self.window1.show()
self.window2.show()
def focusChanged(self, obj):
if not obj or obj.window() == self.window():
return
if obj.window() in self.zOrder[:-1]:
self.zOrder.remove(obj.window())
self.zOrder.append(obj.window())
def closeEvent(self, event: QCloseEvent):
for win in (self.window1, self.window2):
win.can_close = True
win.close()
self.settings.setValue('windowOrder',
[w.window().objectName() for w in self.zOrder])
super(ControlWindow, self).closeEvent(event)

Related

Why while loop freeze api gui [duplicate]

This question already has answers here:
Equivalent to time.sleep for a PyQt application
(5 answers)
Closed 1 year ago.
I trying create GUI Api. First i build python script with only print information in console.
So I wanted to rebuild applications into applications with an interface. I decided to use PyQt5
Like this:
To(first look):
I ran into a problem with the loop While. Aplication just freeze when while is runing
I prepared a short script simulating the problem. The main program looks different
import sys
from PyQt5.QtWidgets import *
from PyQt5 import QtWidgets
from termcolor import colored
import time
class App(QMainWindow):
def __init__(self):
super().__init__()
self.title = 'API NORD'
self.left = 0
self.top = 0
self.width = 300
self.height = 200
self.setWindowTitle(self.title)
self.resize(800, 600)
self.center()
self.table_widget = MyTableWidget(self)
self.setCentralWidget(self.table_widget)
self.show()
def center(self):
# geometry of the main window
qr = self.frameGeometry()
# center point of screen
cp = QDesktopWidget().availableGeometry().center()
# move rectangle's center point to screen's center point
qr.moveCenter(cp)
# top left of rectangle becomes top left of window centering it
self.move(qr.topLeft())
class MyTableWidget(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
self.pushButton1 = QPushButton("Run")
self.layout.addWidget(self.pushButton1)
self.pushButton1.clicked.connect(self.button2_clicked)
self.textedit = QtWidgets.QTextEdit(readOnly=True)
self.layout.addWidget(self.textedit)
self.textedit.setText("STATUS")
def onClicked(self):
radioButton = self.sender()
if radioButton.isChecked():
x=0
# print("Shop is %s" % (radioButton.shop))
self.Sklep=radioButton.shop
self.l1.setText(self.Sklep)
return
def checkBulkStatus(self):
Status = "Start"
x=0
self.textedit.setText("Start")
while x < 5:
print("Aktualny Status:", colored(Status,"yellow"))
Status="Running"
self.textedit.append(Status)
if Status=="FAILED":
print("Error")
break
time.sleep(2.5)
x+=1
print("Aktualny Status: ", colored("COMPLETED", "green"))
self.textedit.setText("COMPLETED")
def button2_clicked(self):
self.checkBulkStatus()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
In main program I ussing while to check status of BULK request in GraphQL:
def checkBulkStatus(self):
self.url = self.auth(self.Sklep)["url_auth"]
print(self.url)
Status = "Start"
self.textedit.setText("Start")
while Status != "COMPLETED":
print("Aktualny Status:", colored(Status,"yellow"))
checking = self.Core.callShopifyGraphQL(self.Core.CheckQuery,self.url)
result = checking.json()
Status=result["data"]["currentBulkOperation"]["status"]
self.textedit.append(Status)
if Status=="FAILED":
print(result["data"]["currentBulkOperation"])
break
time.sleep(2.5)
print("Aktualny Status: ", colored("COMPLETED", "green"))
URL_bulk=result["data"]["currentBulkOperation"]["url"]
The problem is that the gui runs in the same thread as the script, so when you run the script it freezes the interface. To prevent this from happening, you need to run the script in a thread, as this way you can share variables with the main thread.
I hope it helps you, greetings.

Force QTabBar tabs to stay as small as possible and ignore sizeHint

I'm trying to have a + button added to a QTabBar. There's a great solution from years ago, with a slight issue that it doesn't work with PySide2. The problem is caused by the tabs auto resizing to fill the sizeHint, which in this case isn't wanted as the extra space is needed. Is there a way I can disable this behaviour?
I've tried QTabBar.setExpanding(False), but according to this answer, the property is mostly ignored:
The bad news is that QTabWidget effectively ignores that property, because it always forces its tabs to be the minimum size (even if you set your own tab-bar).
The difference being in PySide2, it forces the tabs to be the preferred size, where I'd like the old behaviour of minimum size.
Edit: Example with minimal code. The sizeHint width stretches the tab across the full width, whereas in older Qt versions it doesn't do that. I can't really override tabSizeHint since I don't know what the original tab size should be.
import sys
from PySide2 import QtCore, QtWidgets
class TabBar(QtWidgets.QTabBar):
def sizeHint(self):
return QtCore.QSize(100000, super().sizeHint().height())
class Test(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
tabWidget = QtWidgets.QTabWidget()
tabWidget.setTabBar(TabBar())
layout.addWidget(tabWidget)
tabWidget.addTab(QtWidgets.QWidget(), 'this shouldnt be stretched')
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
I think there may be an easy solution to your problem (see below). Where the linked partial solution calculated absolute positioning for the '+' button, the real intent with Qt is always to let the layout engine do it's thing rather than trying to tell it specific sizes and positions. QTabWidget is basically a pre-built amalgamation of layouts and widgets, and sometimes you just have to skip the pre-built and build your own.
example of building a custom TabWidget with extra things across the TabBar:
import sys
from PySide2 import QtWidgets
from random import randint
class TabWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
#layout for entire widget
vbox = QtWidgets.QVBoxLayout(self)
#top bar:
hbox = QtWidgets.QHBoxLayout()
vbox.addLayout(hbox)
self.tab_bar = QtWidgets.QTabBar()
self.tab_bar.setMovable(True)
hbox.addWidget(self.tab_bar)
spacer = QtWidgets.QSpacerItem(0,0,QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
hbox.addSpacerItem(spacer)
add_tab = QtWidgets.QPushButton('+')
hbox.addWidget(add_tab)
#tab content area:
self.widget_stack = QtWidgets.QStackedLayout()
vbox.addLayout(self.widget_stack)
self.widgets = {}
#connect events
add_tab.clicked.connect(self.add_tab)
self.tab_bar.currentChanged.connect(self.currentChanged)
def add_tab(self):
tab_text = 'tab' + str(randint(0,100))
tab_index = self.tab_bar.addTab(tab_text)
widget = QtWidgets.QLabel(tab_text)
self.tab_bar.setTabData(tab_index, widget)
self.widget_stack.addWidget(widget)
self.tab_bar.setCurrentIndex(tab_index)
def currentChanged(self, i):
if i >= 0:
self.widget_stack.setCurrentWidget(self.tab_bar.tabData(i))
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
test = TabWidget()
test.show()
sys.exit(app.exec_())
All that said, I think the pre-built QTabWidget.setCornerWidget may be exactly what you're looking for (set a QPushButton to the upper-right widget). The example I wrote should much easier to customize, but also much more effort to re-implement all the same functionality. You will have to re-implement some of the signal logic to create / delete / select / rearrange tabs on your own. I only demonstrated simple implementation, which probably isn't bulletproof to all situations.
Using the code from Aaron as a base to start on, I managed to implement all the functionality required to work with my existing script:
from PySide2 import QtCore, QtWidgets
class TabBar(QtWidgets.QTabBar):
def minimumSizeHint(self):
"""Allow the tab bar to shrink as much as needed."""
minimumSizeHint = super(TabBar, self).minimumSizeHint()
return QtCore.QSize(0, minimumSizeHint.height())
class TabWidgetPlus(QtWidgets.QWidget):
tabOpenRequested = QtCore.Signal()
tabCountChanged = QtCore.Signal(int)
def __init__(self, parent=None):
self._addingTab = False
super(TabWidgetPlus, self).__init__(parent=parent)
# Main layout
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# Bar layout
self._tabBarLayout = QtWidgets.QHBoxLayout()
self._tabBarLayout.setContentsMargins(0, 0, 0, 0)
self._tabBarLayout.setSpacing(0)
layout.addLayout(self._tabBarLayout)
self._tabBar = TabBar()
self._tabBarLayout.addWidget(self._tabBar)
for method in (
'isMovable', 'setMovable',
'tabsClosable', 'setTabsClosable',
'tabIcon', 'setTabIcon',
'tabText', 'setTabText',
'currentIndex', 'setCurrentIndex',
'currentChanged', 'tabCloseRequested',
):
setattr(self, method, getattr(self._tabBar, method))
self._plusButton = QtWidgets.QPushButton('+')
self._tabBarLayout.addWidget(self._plusButton) # TODO: Find location to insert
self._plusButton.setFixedWidth(20)
self._tabBarLayout.addStretch()
# Content area
self._contentArea = QtWidgets.QStackedLayout()
layout.addLayout(self._contentArea)
# Signals
self.currentChanged.connect(self._currentChanged)
self._plusButton.clicked.connect(self.tabOpenRequested.emit)
# Final setup
self.installEventFilter(self)
#QtCore.Slot(int)
def _currentChanged(self, i):
"""Update the widget."""
if i >= 0 and not self._addingTab:
self._contentArea.setCurrentWidget(self.tabBar().tabData(i))
def eventFilter(self, obj, event):
"""Intercept events until the correct height is set."""
if event.type() == QtCore.QEvent.Show:
self.plusButton().setFixedHeight(self._tabBar.geometry().height())
self.removeEventFilter(self)
return False
def tabBarLayout(self):
return self._tabBarLayout
def tabBar(self):
return self._tabBar
def plusButton(self):
return self._plusButton
def tabAt(self, point):
"""Get the tab at a given point.
This takes any layout margins into account.
"""
offset = self.layout().contentsMargins().top() + self.tabBarLayout().contentsMargins().top()
return self.tabBar().tabAt(point - QtCore.QPoint(0, offset))
def addTab(self, widget, name=''):
"""Add a new tab.
Returns:
Tab index as an int.
"""
self._addingTab = True
tabBar = self.tabBar()
try:
index = tabBar.addTab(name)
tabBar.setTabData(index, widget)
self._contentArea.addWidget(widget)
finally:
self._addingTab = False
return index
def insertTab(self, index, widget, name=''):
"""Inserts a new tab.
If index is out of range, a new tab is appended.
Returns:
Tab index as an int.
"""
self._addingTab = True
tabBar = self.tabBar()
try:
index = tabBar.insertTab(index, name)
tabBar.setTabData(index, widget)
self._contentArea.insertWidget(index, widget)
finally:
self._addingTab = False
return index
def removeTab(self, index):
"""Remove a tab."""
tabBar = self.tabBar()
self._contentArea.removeWidget(tabBar.tabData(index))
tabBar.removeTab(index)
if __name__ == '__main__':
import sys
import random
app = QtWidgets.QApplication(sys.argv)
test = TabWidgetPlus()
test.addTab(QtWidgets.QPushButton(), 'yeah')
test.insertTab(0, QtWidgets.QCheckBox(), 'what')
test.insertTab(1, QtWidgets.QRadioButton(), 'no')
test.removeTab(1)
test.setMovable(True)
test.setTabsClosable(True)
def tabTest():
name = 'Tab ' + str(random.randint(0, 100))
index = test.addTab(QtWidgets.QLabel(name), name)
test.setCurrentIndex(index)
test.tabOpenRequested.connect(tabTest)
test.tabCloseRequested.connect(lambda index: test.removeTab(index))
test.show()
sys.exit(app.exec_())
The one difference is if you're using tabWidget.tabBar().tabAt(point), this is no longer guaranteed to be correct as it doesn't take any margins into account. I set the margins to 0 so this shouldn't be an issue, but I also included those corrections in TabWidgetPlus.tabAt.
I only copied a few methods from QTabBar to QTabWidget as some may need extra testing.

PyQt5 dynamic creation/destruction of widgets

I have an application where upon start up the user is presented with a dialog to chose number of 'objects' required. This then generates necessary objects in the main window using a for loop (i.e. object1, object2, etc.). I want to move this selection into the main window so that this can be changed without the need to restart the application. I have no idea how to approach this as I'm not sure how to dynamically create/destroy once the application is running. Here's an example code that generates tabs in a tab widget with some elements in each tab.
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class SelectionWindow(QDialog):
def __init__(self):
QDialog.__init__(self)
self.settings = QSettings('Example', 'Example')
self.numberOfTabs = QSpinBox(value = self.settings.value('numberOfTabs', type=int, defaultValue = 3), minimum = 1)
self.layout = QFormLayout(self)
self.button = QPushButton(text = 'OK', clicked = self.buttonClicked)
self.layout.addRow('Select number of tabs', self.numberOfTabs)
self.layout.addRow(self.button)
def buttonClicked(self):
self.settings.setValue('numberOfTabs', self.numberOfTabs.value())
self.accept()
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.settings = QSettings('Example', 'Example')
self.tabs = self.settings.value('numberOfTabs', type = int)
self.tabWidget = QTabWidget()
for i in range(1, self.tabs + 1):
exec(('self.tab{0} = QWidget()').format(i))
exec(("self.tabWidget.addTab(self.tab{0}, 'Tab{0}')").format(i))
exec(('self.lineEdit{0} = QLineEdit()').format(i))
exec(('self.spinBox{0} = QSpinBox()').format(i))
exec(('self.checkBox{0} = QCheckBox()').format(i))
exec(('self.layout{0} = QFormLayout(self.tab{0})').format(i))
exec(("self.layout{0}.addRow('Name', self.lineEdit{0})").format(i))
exec(("self.layout{0}.addRow('Value', self.spinBox{0})").format(i))
exec(("self.layout{0}.addRow('On/Off', self.checkBox{0})").format(i))
self.setCentralWidget(self.tabWidget)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
dialog = SelectionWindow()
dialog.show()
if dialog.exec_() == SelectionWindow.Accepted:
mainwindow = MainWindow()
mainwindow.show()
sys.exit(app.exec_())
First of all, you should never use exec for things like these. Besides the security issues of using exec, it also makes your code less readable (and then much harder to debug) and hard to interact with.
A better (and more "elegant") solution is to use a common function to create tabs and, most importantly, setattr.
Also, you shouldn't use QSettings in this way, as it is mostly intended for cross-session persistent data, not to initialize an interface. For that case, you should just override the exec() method of the dialog and initialize the main window with that value as an argument.
And, even if it was the case (but I suggest you to avoid the above approach anyway), remember that to make settings persistent, at least organizationName and applicationName must be set.
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.settings = QSettings('Example', 'Example')
# this value does not need to be a persistent instance attribute
tabCount = self.settings.value('numberOfTabs', type = int)
# create a main widget for the whole interface
central = QWidget()
mainLayout = QVBoxLayout(central)
tabCountSpin = QSpinBox(minimum=1)
mainLayout.addWidget(tabCountSpin)
tabCountSpin.setValue(tabCount)
tabCountSpin.valueChanged.connect(self.tabCountChanged)
self.tabWidget = QTabWidget()
mainLayout.addWidget(self.tabWidget)
for t in range(tabCount):
self.createTab(t)
self.setCentralWidget(central)
def createTab(self, t):
t += 1
tab = QWidget()
self.tabWidget.addTab(tab, 'Tab{}'.format(t))
layout = QFormLayout(tab)
# create all the widgets
lineEdit = QLineEdit()
spinBox = QSpinBox()
checkBox = QCheckBox()
# add them to the layout
layout.addRow('Name', lineEdit)
layout.addRow('Value', spinBox)
layout.addRow('On/Off', checkBox)
# keeping a "text" reference to the widget is useful, but not for
# everything, as tab can be accessed like this:
# tab = self.tabWidget.widget(index)
# and so its layout:
# tab.layout()
setattr(tab, 'lineEdit{}'.format(t), lineEdit)
setattr(tab, 'spinBox{}'.format(t), spinBox)
setattr(tab, 'checkBox{}'.format(t), checkBox)
def tabCountChanged(self, count):
if count == self.tabWidget.count():
return
elif count < self.tabWidget.count():
while self.tabWidget.count() > count:
# note that I'm not deleting the python reference to each object;
# you should use "del" for both the tab and its children
self.tabWidget.removeTab(count)
else:
for t in range(self.tabWidget.count(), count):
self.createTab(t)

Why doesn't QTreeView.scrollTo() work initially

The code below just displays a tree view of computer drives. Each time a new file/folder is selected, the view scrolls to make this new selection visible.
Question 1: While this works, the initial selection after the application is launched doesn't trigger the scroll. Why?
Question 2: If the instructions:
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)
are inverted:
self.my_view.resizeColumnToContents(0)
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
the first column size is not adjusted either on the initial display, only after. Why?
import sys
from PyQt5.QtCore import Qt, QModelIndex, QDir
from PyQt5.QtWidgets import QApplication, QTreeView, QMainWindow, QFileSystemModel, QAbstractItemView
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
# Instance variables
self.my_view = QTreeView()
self.my_model = QFileSystemModel()
# Init FS model to show all computer drives
model_root_path = str(self.my_model.myComputer())
self.my_model.setRootPath(model_root_path)
# Init tree view
self.my_view.setModel(self.my_model)
self.my_view.setRootIndex(self.my_model.index(model_root_path))
self.my_view.setSelectionMode(QAbstractItemView.SingleSelection)
self.my_view.setSelectionBehavior(QAbstractItemView.SelectRows)
# Connect selection change events to custom slot
select_model = self.my_view.selectionModel()
select_model.currentRowChanged.connect(self.current_row_changed)
# Main window
self.setCentralWidget(self.my_view)
self.setGeometry(200, 200, 800, 600)
# Select initial row on view
focus_path = QDir.currentPath()
focus_index = self.my_model.index(focus_path)
self.my_view.setCurrentIndex(focus_index)
def current_row_changed(self):
"""Current row of the model has changed"""
# Scroll view to new row
index = self.my_view.selectionModel().currentIndex()
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)
# Show path of current row in window title
absolute_path = self.my_model.filePath(index)
self.setWindowTitle(absolute_path)
def main():
a = QApplication(sys.argv)
mw = MyWindow()
mw.show()
sys.exit(a.exec_())
if __name__ == '__main__':
main()
`
Edit: After using the good solution provided by #ekhumoro, my sample code above worked. However this other piece of code still didn't:
import os
import sys
from PyQt5.QtCore import pyqtSignal, QTimer, QDir, Qt
from PyQt5.QtWidgets import QMainWindow, QGridLayout, QWidget, QTreeView, QAbstractItemView, QFileSystemModel, \
QApplication
class AppWindow(QMainWindow):
default_folder_path = "."
def __init__(self):
super().__init__()
self.folder_view = FolderTreeView()
self.folder_view.folder_has_changed.connect(self.folder_changed)
self.build_ui()
self.show()
# Select initial folder
self.select_initial_folder()
def build_ui(self):
main_widget = QWidget()
layout = QGridLayout(main_widget)
layout.addWidget(self.folder_view)
self.setCentralWidget(main_widget)
self.setGeometry(200, 100, 800, 600)
def select_initial_folder(self):
folder_index = self.folder_view.get_index(AppWindow.default_folder_path)
if folder_index.isValid():
self.folder_view.select_folder(folder_index)
def folder_changed(self, folder_path):
if not os.path.isdir(folder_path):
print("Non existing folder:", folder_path)
return
class FolderTreeView(QTreeView):
folder_has_changed = pyqtSignal(str)
def __init__(self):
super().__init__()
self.folder_tree_model = FolderTreeModel()
self.setModel(self.folder_tree_model)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
def select_folder(self, folder_index):
self.setCurrentIndex(folder_index)
def currentChanged(self, current, previous):
super(FolderTreeView, self).currentChanged(current, previous)
# Scroll the view to current item and resize folder name column
QTimer.singleShot(50, lambda: self.delayed_scroll(current))
# Emit signal for other uses
self.folder_has_changed.emit(self.folder_tree_model.filePath(current))
def delayed_scroll(self, index):
self.scrollTo(index, QAbstractItemView.EnsureVisible)
self.resizeColumnToContents(0)
def get_index(self, folder_path):
return self.folder_tree_model.index(folder_path)
class FolderTreeModel(QFileSystemModel):
def __init__(self):
super().__init__()
self.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot)
self.setRootPath("")
def main():
app = QApplication(sys.argv)
window = AppWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
The first problem may be caused if, by default, the model initialises its current index to the current directory. This would mean that if you set it again to the same index, the row-change signal will not be emitted (because nothing changed). This can be fixed by calling the row-change handler directly:
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
...
focus_path = QDir.currentPath()
focus_index = self.my_model.index(focus_path)
self.my_view.setCurrentIndex(focus_index)
self.current_row_changed()
def current_row_changed(self):
index = self.my_view.currentIndex()
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)
...
As to the second problem: when you call scrollTo, it may have to expand several directories in order to select the required index. This could obviously change the width of the first column, so you should always call resizeColumnToContents afterwards in order to get the correct width.
UPDATE:
I think there is also another problem caused by timing issues. The QFileSystemModel must work asynchronously to some extent, because it has to request resources from the operating system and then wait for the response. Also, before it gets the response, it cannot know in advance exactly how much data it is going to receive, because the file-system may have been updated while it was waiting. Potentially, the response could include data from a huge directory containing thousands of files. So in order to keep the GUI responsive, the data is processed in batches which are of a sufficient size to fill the current view. If the current index is set before the window has been shown and all its widgets fully laid out, there is no guarantee that the view will be able to resize its columns correctly.
This can be fixed by explicitly re-calling the row-change handler via a one-shot timer with a small delay. This should allow the view to recalculate its column widths correctly:
...
focus_path = QDir.currentPath()
focus_index = self.my_model.index(focus_path)
self.my_view.setCurrentIndex(focus_index)
QTimer.singleShot(50, self.current_row_changed)
def current_row_changed(self):
index = self.my_view.currentIndex()
self.my_view.scrollTo(index, QAbstractItemView.EnsureVisible)
self.my_view.resizeColumnToContents(0)

Keep menu open after clicking on the button it is launched with

I have a QToolButton with a menu. When the QToolButton is clicked, the menu appears. The default behavior is that when an action is clicked from the menu, the menu disappears. How can I make it so that the menu stays open until the user clicks elsewhere?
Here is minimal code that shows the behavior:
from PyQt4 import QtGui, QtCore
import sys, os
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
toolButton = QtGui.QToolButton()
toolButton.setText('Select')
toolMenu = QtGui.QMenu()
for i in range(3):
action = toolMenu.addAction(str(i))
action.setCheckable(True)
toolButton.setMenu(toolMenu)
toolButton.setPopupMode(QtGui.QToolButton.InstantPopup)
toolButton.show()
sys.exit(app.exec_())
Shamelessly porting this code from this c++ answer:
from PyQt4 import QtGui, QtCore
import sys, os
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
toolButton = QtGui.QToolButton()
toolButton.setText('Select')
toolMenu = QtGui.QMenu()
for i in range(3):
checkBox = QtGui.QCheckBox(str(i), toolMenu)
checkableAction = QtGui.QWidgetAction(toolMenu)
checkableAction.setDefaultWidget(checkBox)
toolMenu.addAction(checkableAction)
toolButton.setMenu(toolMenu)
toolButton.setPopupMode(QtGui.QToolButton.InstantPopup)
toolButton.show()
sys.exit(app.exec_())
I made a PyQt5 version based on #three_pineapples's answer and solved what #Space Hornet tried to solve--get the states of checkboxes.
According to the doc of QWidgetAction:
Note that it is up to the widget to activate the action, for example
by reimplementing mouse event handlers and calling QAction::trigger().
So I think one needs to connect the checkbox's stateChanged signal to the action's trigger method.
I also added the a text to the action therefore action.text() gives the same text label as the checkbox. May not be necessary though.
Complete code below:
import sys
from PyQt5 import QtWidgets
from PyQt5.QtCore import pyqtSlot
#pyqtSlot(QtWidgets.QAction)
def menuTriggered(action):
print('state change=',action.text())
return
#pyqtSlot(QtWidgets.QMenu)
def buttonTriggered(menu):
actions=menu.findChildren(QtWidgets.QWidgetAction)
for actii in actions:
wii=actii.defaultWidget()
stateii=wii.isChecked()
print('action', actii.text(), 'is checked:',stateii)
return
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
toolButton = QtWidgets.QToolButton()
toolButton.setText('Select')
toolMenu = QtWidgets.QMenu()
for i in range(3):
checkBox = QtWidgets.QCheckBox(str(i), toolMenu)
checkableAction = QtWidgets.QWidgetAction(toolMenu)
checkableAction.setDefaultWidget(checkBox)
# Add a text to action, for easier handling in slot
checkableAction.setText(str(i))
# Connect the checkbox's stateChanged to QAction.trigger
checkBox.stateChanged.connect(checkableAction.trigger)
toolMenu.addAction(checkableAction)
toolMenu.triggered.connect(menuTriggered)
toolButton.setMenu(toolMenu)
toolButton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
# NOTE that toolButton.clicked work, toolButton.triggered not
toolButton.clicked.connect(lambda: buttonTriggered(toolMenu))
toolButton.show()
sys.exit(app.exec_())
The easiest solution I've managed to find is to make an addition to actionEvent:
class myMenu(QtGui.QMenu):
def actionEvent(self, event):
super().actionEvent(event)
self.show()
I was looking for the exact same thing and used the code from three_pineapples, but I had trouble connecting it the way I wanted. I thought I'd share my solution in case anyone else finds it useful.
The button function is very similar but my code includes my solution for connecting the checkboxes to a function. Also, since they are stored in a list one can connect them individually or in a loop if that's easier.
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys, os
##### main window class #####
class main_window(QMainWindow):
def __init__(self):
super(main_window, self).__init__()
self.resize(300, 200)
wdgMain = QWidget()
self.setCentralWidget(wdgMain)
layMain = QGridLayout(wdgMain)
wdgMain.setLayout(layMain)
## checkable tool button ##
tlbToolButton1 = QToolButtonChx("Check Me Out!")
layMain.addWidget(tlbToolButton1, 0, 0)
tlbToolButton1.addItems(["Item" + str(n) for n in range(8)])
## connect tool button checkboxes ##
for i in range(tlbToolButton1.length()):
tlbToolButton1.index(i).stateChanged.connect(self.checkbox_tester)
def checkbox_tester(self, choice):
objSender = self.sender()
strObjectName = objSender.objectName()
print "Action Checker::", strObjectName, ":", choice
##### end of main window class #####
##### checkable tool button class #####
class QToolButtonChx(QToolButton):
def __init__(self, strText=""):
super(QToolButtonChx, self).__init__()
self.setText(strText)
tlbMenu = QMenu(self)
self.setMenu(tlbMenu)
self.setPopupMode(QToolButton.MenuButtonPopup)
self.lstchxItems = []
def addItem(self, strItem):
self.lstchxItems.append(QCheckBox(strItem, self.menu()))
actCheckItem = QWidgetAction(self.menu())
actCheckItem.setDefaultWidget(self.lstchxItems[-1])
self.lstchxItems[-1].setObjectName('chx' + strItem)
self.menu().addAction(actCheckItem)
def addItems(self, lstItems):
for strItem in lstItems:
self.lstchxItems.append(QCheckBox(strItem, self.menu()))
actCheckItem = QWidgetAction(self.menu())
actCheckItem.setDefaultWidget(self.lstchxItems[-1])
self.lstchxItems[-1].setObjectName('chx' + strItem)
self.menu().addAction(actCheckItem)
def index(self, intIndex):
return self.lstchxItems[intIndex]
def length(self):
return len(self.lstchxItems)
##### end of checkable tool button class #####
if __name__ == '__main__':
app = QApplication(sys.argv)
winMain = QMainWindow()
gui = main_window()
gui.show()
sys.exit(app.exec_())

Categories

Resources