I want to create a menu with a checkable list. To prevent the menu from closing when the action is clicked, I'm setting the DefaultWidget to be a QCheckBox. The problem is when I'm trying to get isClicked from the action - it doesn't seem to be synced to the checkbox. How do I get the value of the action to change when the checkbox is clicked?
tool_button = QtWidgets.QToolButton()
menu = QtWidgets.QMenu()
check_box = QtWidgets.QCheckBox(menu)
check_box.setText("abc")
check_box.setChecked(True)
action_button = QtWidgets.QWidgetAction(menu)
action_button.setDefaultWidget(check_box)
menu.addAction(action_button)
tool_button.setMenu(menu)
print(check_box.text()) # returns abc
print(check_box.isChecked()) # returns True
print(action_button.isChecked()) # returns False - it's not picking up the values from check_box
Since QWidgetAction acts as some sort of container for any kind of widget, it has no way to know if its defaultWidget could even have any support for a bool state like "isChecked", so you have to provide it.
The simplest way is to subclass a specific QWidgetAction class for that action, and override its isChecked() method so that it returns the checked value based on its default widget.
from PyQt5 import QtWidgets
class CheckableWidgetAction(QtWidgets.QWidgetAction):
def setDefaultWidget(self, widget):
super().setDefaultWidget(widget)
try:
# if the widget has the toggled signal, connect that signal to the
# triggered signal
widget.toggled.connect(self.triggered)
except:
pass
def isChecked(self):
try:
return self.defaultWidget().isChecked()
except:
# failsafe, in case no default widget has been set or the provided
# widget doesn't have a "checked" property
return super().isChecked()
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
tool_button = QtWidgets.QToolButton()
layout.addWidget(tool_button)
menu = QtWidgets.QMenu()
check_box = QtWidgets.QCheckBox(menu)
check_box.setText("abc")
check_box.setChecked(True)
self.action_button = CheckableWidgetAction(menu)
self.action_button.setDefaultWidget(check_box)
self.action_button.triggered.connect(self.action_triggered)
menu.addAction(self.action_button)
tool_button.setMenu(menu)
controlButton = QtWidgets.QPushButton('is checked?')
layout.addWidget(controlButton)
controlButton.clicked.connect(self.is_checked)
def is_checked(self):
print('checked is {}'.format(self.action_button.isChecked()))
def action_triggered(self, state):
print('triggered {}'.format(state))
if __name__ == '__main__':
import sys
a = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(a.exec_())
Related
I've created a QMenu and added a number of checkable QAction items to the menu. All of these items connect to the same slot function.
If a checkable QAction is clicked, the triggered signal is emitted and the status of the check-mark is passed as an argument to the connected slot function.
In the slot function I want to distinguish between the clicked items. I tried to do that by passing an index to the slot function using a lambda , in addition to passing the checked state.
The only thing I can't achieve is to have both the checked state and my index passed to the slot function.
See example code below. The different variants of the slot variable in the example shows what I've tried so far.
import sys
from PySide2 import QtCore
from PySide2.QtWidgets import (QMainWindow, QApplication, QAction)
class Window(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
# create menu
self.dynamic_menu = self.menuBar().addMenu('tools')
# add menu items
labels = ['tool0', 'tool1', 'tool2']
for index, label in enumerate(labels):
# passes 'checked' correctly, but 'index' is always 2 (last loop value)
# slot = lambda checked: self.run_menu_item(checked, index)
#
# Throws exception:
# TypeError: <lambda>() missing 1 required positional argument: 'checked'
# slot = lambda checked, i=index: self.run_menu_item(checked, i)
#
# passes 'checked' always as False, but index is correct
slot = lambda checked=False, i=index: self.run_menu_item(checked, i)
action = QAction(label, self)
action.triggered.connect(slot)
action.setCheckable(True)
self.dynamic_menu.addAction(action)
#QtCore.Slot(bool, int)
def run_menu_item(self, checked, index):
print('checked:{} index:{}'.format(checked, index))
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.setMinimumSize(400, 100)
print('starting app...')
window.show()
sys.exit(app.exec_())
How can I define slot so I get both the checked state of the menu item and also the correct index as parameters to my slot function run_menu_item?
Since triggered is an overload signal, then the signature must be pointed out: action.triggered[bool].connect(slot).
class Window(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
# create menu
self.dynamic_menu = self.menuBar().addMenu("tools")
# add menu items
labels = ["tool0", "tool1", "tool2"]
for index, label in enumerate(labels):
slot = lambda checked, i=index: self.run_menu_item(checked, i)
action = QAction(label, self)
action.triggered[bool].connect(slot)
action.setCheckable(True)
self.dynamic_menu.addAction(action)
#QtCore.Slot(bool, int)
def run_menu_item(self, checked, index):
print("checked:{} index:{}".format(checked, index))
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)
Importing libraries.
from PySide2 import QtWidgets,QtCore,QtGui
from UI import main
This is my class which is inheriting and have a constructor
class MyQtApp(main.Ui_MainWindow,QtWidgets.QMainWindow):
def __init__(self):
super(MyQtApp,self).__init__()
self.setupUi(self)
window size
self.showNormal()
self.submit_pb.clicked.connect(self.fill_box)
self.Menu()
This fill_box is for the combo box, and I have three combo boxes
model_cb, size_cb and color_cb
And the zeroth index is just an empty string for all of them
def fill_box(self):
model = self.model_cb.currentText()
color = self.model_cb.currentText()
size = self.size_cb.currentText()
none = ""
check = True
if model is none:
QtWidgets.QMessageBox.about(self,"Name Required","Please enter the Name!")
check = False
return
if color is none:
QtWidgets.QMessageBox.about(self,"Color Required","Please enter the Color!")
check = False
return
if size is none:
QtWidgets.QMessageBox.about(self,"Size Required","Please Enter the Size!")
check = False
return
if check:
QtWidgets.QMessageBox.about(self, "Done","Submitted")
This Menu function is for the menu bar options what happens if someone click close. I have only two options here 1)close which works fine and 2) Refresh which does not work.
def Menu(self):
self.actionClose.triggered.connect(self.close)
self.actionRefresh.triggered.connect(self.fill_box1)
def fill_box1(self):
#If I do this It will reset all the boxes to its original empty
#strings but then I would not have all the methods in the class
#to add the functionality.
self.setupUi(self)
if __name__ == '__main__':
app = QtWidgets.QApplication()
qt_app = MyQtApp()
qt_app.show()
app.exec_()
I have a QTabWidget in PyQt which provides the possibility to tear off tabs and the re-attach them when they are closed. It works well except that newly attached tabs aren't immediately showing. A blank widget is showing and the widget is shown only after the current tab has been changed and then changed back.
I've searched StackOverflow and the answers to similar problems have pointed out that the added widget's show()-method needs to be called after the new tab is added. I tried that but the newly added tab is still not showing.
Slimmed down example code:
from PyQt4 import QtGui, QtCore
class DetachableTabWidget(QtGui.QTabWidget):
""" Subclass of QTabWidget which provides the ability to detach
tabs and making floating windows from them which can be reattached.
"""
def __init__(self, *args, **kwargs):
super(DetachableTabWidget, self).__init__(*args, **kwargs)
self.setTabBar(_DetachableTabBar())
def detach_tab(self, i):
""" Make floating window of tab.
:param i: index of tab to detach
"""
teared_widget = self.widget(i)
widget_name = self.tabText(i)
# Shift index to the left and remove tab.
self.setCurrentIndex(self.currentIndex() - 1 if self.currentIndex() > 0 else 0)
self.removeTab(i)
# Store widgets window-flags and close event.
teared_widget._flags = teared_widget.windowFlags()
teared_widget._close = teared_widget.closeEvent
# Make stand-alone window.
teared_widget.setWindowFlags(QtCore.Qt.Window)
teared_widget.show()
# Redirect windows close-event into reattachment.
teared_widget.closeEvent = lambda event: self.attach_tab(teared_widget, widget_name)
def attach_tab(self, widget, name):
""" Attach widget when receiving signal from child-window.
:param widget: :class:`QtGui.QWidget`
:param name: name of attached widget
"""
widget.setWindowFlags(widget._flags)
widget.closeEvent = widget._close
self.addTab(widget, name)
self.setCurrentWidget(widget)
self.currentWidget().show()
class _DetachableTabBar(QtGui.QTabBar):
def __init__(self, *args, **kwargs):
super(_DetachableTabBar, self).__init__(*args, **kwargs)
self._start_drag_pos = None
self._has_dragged = False
self.setMovable(True)
def mousePressEvent(self, event):
# Keep track of where drag-starts.
self._start_drag_pos = event.globalPos()
super(_DetachableTabBar, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
# If tab is already detached, do nothing.
if self._has_dragged:
return
# Detach-tab if drag in y-direction is large enough.
if abs((self._start_drag_pos - event.globalPos()).y()) >= QtGui.QApplication.startDragDistance()*8:
self._has_dragged = True
self.parent().detach_tab(self.currentIndex())
def mouseReleaseEvent(self, event):
self._has_dragged = False
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = QtGui.QMainWindow()
widget = DetachableTabWidget()
widget.addTab(QtGui.QLabel('Tab 1'), 'tab 1')
widget.addTab(QtGui.QLabel('Tab 2'), 'tab 2')
window.setCentralWidget(widget)
window.show()
sys.exit(app.exec_())
It seems than when you accept the QCloseEvent (default behavior) the widget is hidden at the end of your closeEvent handler. But your call to show() occurs before the end of the handler. The solution is to ignore the QCloseEvent.
...
teared_widget.closeEvent = lambda event: self.attach_tab(teared_widget, widget_name, event)
def attach_tab(self, widget, name, event):
""" Attach widget when receiving signal from child-window.
:param widget: :class:`QtGui.QWidget`
:param name: name of attached widget
:param event: close Event
"""
widget.setWindowFlags(widget._flags)
widget.closeEvent = widget._close
self.addTab(widget, name)
self.setCurrentWidget(widget)
event.ignore()
What is the proper way to interact with a button without actually clicking on it?
I have a button "button", that can, upon click :
Call the method "the_method" that will print what argument (here "filename") has been passed to it
toggle its own attributes, here its icon.
And I have a treeview, whose rows must, upon double click :
Call the method "the_method" that will print what argument (here "filename") has been passed to it
toggle "button"'s attributes, here its icon.
And only the 1st part works. The "foo" function is called (via a callback for the button, directly for the treeview item) and the argument ("filename") is retrieved OK, but how to execute part 2 of the job (changing "button"'s attributes, here its icon)?
import gtk
class Lister(object):
def __init__(self):
self.hbox = gtk.HBox()
liststore = gtk.ListStore(str)
liststore.append(["foo"])
liststore.append(["bar"])
treeview = gtk.TreeView(liststore)
self.hbox.pack_start(treeview, False)
cell = gtk.CellRendererText()
col = gtk.TreeViewColumn("Column 1")
col.pack_start(cell, True)
col.set_attributes(cell,text=0)
treeview.connect('row-activated', self.open_file)
treeview.append_column(col)
def open_file(self, button, *args):
Buttons().the_method(self, "foo")
class Buttons(object):
OPEN_IMAGE = gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)
CLOSED_IMAGE = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_BUTTON)
def __init__(self):
self.button = gtk.Button() # THIS is the button to modify
self.hbox = gtk.HBox()
self.hbox.pack_start(self.button, False)
self.button.set_image(self.OPEN_IMAGE)
self.button.connect('clicked', self.the_method, "plop")
self.toggled = True
def the_method(self, button, filename):
print filename
print vars(self)
if self.toggled:
self.button.set_image(self.CLOSED_IMAGE)
self.toggled = False
else:
self.button.set_image(self.OPEN_IMAGE)
self.toggled = True
class GUI(object):
def delete_event(self, widget, event, data=None):
gtk.main_quit()
return False
def __init__(self):
self.window = gtk.Window()
self.window.set_size_request(100, 150)
self.window.connect("delete_event", self.delete_event)
vbox = gtk.VBox()
vbox.pack_start(Buttons().hbox, False, False, 1)
vbox.pack_start(Lister().hbox)
self.window.add(vbox)
self.window.show_all()
return
def main():
gtk.main()
if __name__ == "__main__":
GUI()
main()
I strongly disagree with user1146332 answer. This is not a GTK+ issue, nor a strong design issue, just an object oriented programming issue. The cause of your bug is that you call the_method like this:
Buttons().the_method(self, "foo")
This can't work, because you're mixing up two different fundamental things: a class, and an instance of a class. When you call Buttons(), you're creating a new instance of the Buttons class. Thus, as this class is not a singleton, you're in fact creating a new instance, with a new GtkButton, and end up not interacting with the button you previously created.
The solution here is to make the Lister object aware of what it needs to modify, which means storing around the Buttons instance you previously created, for example in self.button, and calling the_method on it.
self.button.the_method("foo")
Here's a slightly modified version of your code. The important thing is that the Lister instance is now aware of the Buttons instance it needs to modify.
import gtk
class Lister(object):
def __init__(self, button):
self.hbox = gtk.HBox()
self.button = button
liststore = gtk.ListStore(str)
liststore.append(["foo"])
liststore.append(["bar"])
treeview = gtk.TreeView(liststore)
self.hbox.pack_start(treeview, False)
cell = gtk.CellRendererText()
col = gtk.TreeViewColumn("Column 1")
col.pack_start(cell, True)
col.set_attributes(cell,text=0)
treeview.connect('row-activated', self.open_file)
treeview.append_column(col)
def open_file(self, button, *args):
self.button.the_method("foo")
class Buttons(object):
OPEN_IMAGE = gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)
CLOSED_IMAGE = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_BUTTON)
def __init__(self):
self.button = gtk.Button() # THIS is the button to modify
self.hbox = gtk.HBox()
self.hbox.pack_start(self.button, False)
self.button.set_image(self.OPEN_IMAGE)
self.button.connect('clicked', self.the_method, "plop")
self.toggled = True
def the_method(self, filename):
print filename
print vars(self)
if self.toggled:
self.button.set_image(self.CLOSED_IMAGE)
self.toggled = False
else:
self.button.set_image(self.OPEN_IMAGE)
self.toggled = True
class GUI(object):
def delete_event(self, widget, event, data=None):
gtk.main_quit()
return False
def __init__(self):
self.window = gtk.Window()
self.window.set_size_request(100, 150)
self.window.connect("delete_event", self.delete_event)
vbox = gtk.VBox()
buttons = Buttons()
vbox.pack_start(buttons.hbox, False, False, 1)
vbox.pack_start(Lister(buttons).hbox)
self.window.add(vbox)
self.window.show_all()
return
def main():
gtk.main()
if __name__ == "__main__":
GUI()
main()
However, there's still lots of room for improvement. I suggest you don't use the __init__ function to create your widgets, but a create method that will return the toplevel widget of your widget tree. This is because you can't return anything in __init__, so it's easier to use a different method instead of raising exceptions there.
b = Buttons()
vbox.pack_start(b.create(), False, False, 1)
l = Lister(b)
vbox.pack_start(l.create(), False, False, 1)
Other improvement might be (sorry, i'm using the C naming here for GTK classes/functions, which I know better than the python one):
using a GtkToggleButton instead of tracking the button state yourself
using gtk_button_set_use_stock to tell the button to interpret the label you will set in the button as the stock id for the button (this may print the associated text too, not sure about this)
switching to GTK 3 (uses pyGObject), as this is GTK 2 code (uses pyGTK), unless you want Windows compatibility
See you on linuxfr :-)
First of all i don't know anything about python but i have some experiences with gtk+ and i'm more or less familiar with its concepts.
The first thing i noticed is that you define a class called GUI and two separate classes called Buttons and Lister. For me such approach makes only sense if you design the two last mentioned classes in a way that they are a kind of (composite) widget theirselves. So that you can instantiate them at a higher level for example in the GUI class. This would be a generic approach and makes perfectly sense if you want to reuse these new widgets.
The way you did it doesn't make sense to me. From what i have gathered so far the actual aim of Buttons and Lister is to populate your main application window with widgets, to connect callbacks to signals of those widgets and to define these callbacks as methods.
I think you limit the flexibility of gtk if you make it this way. For example you connect signals to callbacks at a point where in principle you aren't able to access all the widgets of your interface. In contrast, I prefer a common place in the code at which i connect signals to callbacks and at which i can principally pass all widgets of interest to a specific callback.
In fact one often have to act upon several widgets from inside a callback. So you have to consider to implement the callbacks as methods of your GUI class where they can principally access all elements of your user interface.
Also you should consider to design your interface with glade. That way your code would be much more legible.
Supplement (after some code pushing):
import gtk
class GUI(object):
OPEN_IMAGE = gtk.image_new_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON)
CLOSED_IMAGE = gtk.image_new_from_stock(gtk.STOCK_REFRESH, gtk.ICON_SIZE_BUTTON)
toggled = True
def __init__(self):
self.window = gtk.Window()
self.window.set_size_request(100, 150)
self.window.connect("delete_event", gtk.main_quit)
vbox = gtk.VBox()
self.button = gtk.Button() # THIS is the button to modify
self.button.set_image(self.OPEN_IMAGE)
liststore = gtk.ListStore(str)
liststore.append(["foo"])
liststore.append(["bar"])
self.treeview = gtk.TreeView(liststore)
cell = gtk.CellRendererText()
col = gtk.TreeViewColumn("Column 1")
col.pack_start(cell, True)
col.set_attributes(cell,text=0)
self.treeview.append_column(col)
vbox.pack_start(self.button, False, False, 1)
vbox.pack_start(self.treeview, False, False, 1)
self.treeview.connect('row-activated', self.the_method_wrapper, "plop")
self.button.connect('clicked', self.the_method, "plop")
self.window.add(vbox)
self.window.show_all()
return
def the_method_wrapper(self, button, *args):
self.the_method(self, "foo")
def the_method(self, button, filename):
print filename
print vars(self)
if self.toggled:
self.button.set_image(self.CLOSED_IMAGE)
self.toggled = False
else:
self.button.set_image(self.OPEN_IMAGE)
self.toggled = True
def main():
gtk.main()
if __name__ == "__main__":
GUI()
main()