How to implement QToolButton with a drop-down menu that displays the last action in Python?
-
Hi,
I'm a beginner in Python and trying to create a desktop GUI app with PySide2. I created whole GUI in Qt Designer 5, but there is no option for this kind of button. In my Python code I'm calling ui file with QtUiTools.QUiLoader() as described here, so I'm not converting it to Python code.
I would like to implement "QToolButton with a drop-down menu that displays the last action" which is good described here with example for C++.
I would like to recreate that in Python, and it would be great if it followed the new signal and slot syntax.
Since there is no option for this in Qt Designer, how can I create a 'placeholder' for it in ui file? Should I add regular QToolButton and somehow later replace it with new, upgraded QToolButton object? Is this an option, or should I convert .ui to .py with pyside-uic?This is kind of start, don't know if it's any good:
class QToolButtonLastAction(QtWidgets.QToolButton): def __init__(self, parent=None): self.setPopupMode('MenuButtonPopup') self.QObject.connect(self.Signal(self.triggered(self.QAction), self.Slot(self.setDefaultAction(self.QAction))))
-
@phidrho
I use the existing PyQt to do my Python work, not the new PySide2 you are using. So I cannot be sure if my comments apply to you, but judging from PyQt....self.setPopupMode('MenuButtonPopup')
http://doc.qt.io/qt-5/qtoolbutton.html#popupMode-prop
setPopupMode(QToolButton::ToolButtonPopupMode mode)
It accepts an enumeration (http://doc.qt.io/qt-5/qtoolbutton.html#ToolButtonPopupMode-enum), but you pass a string. Unless PySide2 maps this for you that's not right. From PyQt all the enumerations are made available, so I would go something like
self.setPopupMode(QtWidgets.MenuButtonPopup)
If you can't do that, you might have more success passing literal
1
for the parameter.The
Signal
/Slot
syntax you use taken from https://www.walletfox.com/course/customqtoolbutton.php is old-style. I also don't think it would convert like you have to Python. New-style is simplified, and works neatly from PyQt, where I would just do something like:class MyToolButton(QtWidgets.QToolButton) def __init()__: self.triggered.connect(self.defaultAction) def defaultAction(self): ....
I'm not saying my "skeleton" is just right for whatever you are doing, but I think you will at least need to read how PySide2 tells you to do signal/slot connection.
-
I figured it out finally, some things around OOP are still very confusing for me, I guess I need time and more experience to settle all down. Below is a functional example of simple window with two QToolButtons - a regular one and modified one.
@JonB: I think that there is no difference between using PyQt and PySide, except for import statements, and loading ui, I think that PyQt cannot load ui file directly, it must be converted to py, maybe I'm wrong.
Now there are four things that bother me:
- how to use regular QtToolButton as a classic button with drop-down menu (see my example), it seems that triggered signal is always called. Is it designed to work that way? If I don't add signal in code than button does nothing?
- are there any major flaws in my example that should be corrected/fixed?
- how to "inject" my custom made object into ui at wanted location? In my example I just inserted it inside grid_layout. Can I make a placeholder somehow? Let's say I have a more complicated window with many widgets, for example:
and I want to replace that Add Something button with custom one. What would be the best procedure to do this?
4) is calling GUI directly from a ui XML file smart thing to do or it would be better to convert it to py first?My example, program.py, and form.ui:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Docs """ import os, sys from PySide2 import QtWidgets, QtUiTools class CustomToolButton(QtWidgets.QToolButton): ''' CustomToolButton description ''' def __init__(self, parent=None): ''' Constructor ''' super(CustomToolButton, self).__init__(parent) self.setPopupMode(self.MenuButtonPopup) self.triggered.connect(self.setDefaultAction) return None class Program(QtWidgets.QWidget): ''' Program description ''' def __init__(self, parent=None): ''' Constructor ''' super(Program, self).__init__(parent) self.program_path = os.path.dirname(os.path.realpath(__file__)) self.loader = QtUiTools.QUiLoader() ui_file_path = os.path.join(self.program_path, 'form.ui') self.window = self.loader.load(ui_file_path, parent) self.setup_ui(self.window) return None def setup_ui(self, window): self.grid_layout = window.gridLayout # tool button created in Qt Designer self.tool_btn = window.toolButton ### THIS RUNS EVERY TIME BUTTON IS CLICKED NO MATTER ### WHAT ACTION YOU CHOOSE FROM DROP-DOWN MENU ### IT SEEMS THAT IN THIS CASE YOU CANNOT USE BUTTON LIKE ### CLASSIC BUTTON, ### THEREFORE IT SEEMS THAT "setDefaultAction" MAKES NO SENSE self.tool_btn.triggered.connect(self.clicked_tool_btn) # custom tool button self.custom_tool_btn = CustomToolButton() self.custom_tool_btn.setText('Custom button') self.custom_tool_btn.setMinimumSize(150,50) self.custom_tool_btn.setMaximumSize(150,50) self.grid_layout.addWidget(self.custom_tool_btn) self.define_ui_elements() def define_ui_elements(self): # tool button created in Qt Designer tool_btn_menu = QtWidgets.QMenu(self) tool_btn_menu.addAction('Action 1', self.action1_activated) tool_btn_menu.addAction('Action 2', self.action2_activated) tool_btn_menu.addAction('Action 3', self.action3_activated) tool_btn_default_action = QtWidgets.QAction('Action 1',self) self.tool_btn.setMenu(tool_btn_menu) self.tool_btn.setDefaultAction(tool_btn_default_action) # custom tool button ''' 1) creating actions ''' action1 = QtWidgets.QAction('Action 1', self) action2 = QtWidgets.QAction('Action 2', self) action3 = QtWidgets.QAction('Action 3', self) ''' 2) creating connections ''' action1.triggered.connect(self.action1_activated) action2.triggered.connect(self.action2_activated) action3.triggered.connect(self.action3_activated) ''' 3) creating btn menu ''' custom_tool_btn_menu = QtWidgets.QMenu(self) custom_tool_btn_menu.addAction(action1) custom_tool_btn_menu.addAction(action2) custom_tool_btn_menu.addAction(action3) ''' 4) setting up menu and default action ''' self.custom_tool_btn.setMenu(custom_tool_btn_menu) self.custom_tool_btn.setDefaultAction(action1) ### THIS RUNS EVERY TIME BUTTON IS CLICKED NO MATTER WHAT ACTION YOU CHOOSE FROM DROP-DOWN MENU def clicked_tool_btn(self): # calling action1_activated() because it is default option self.action1_activated() print('Qt Designer button is clicked.') def action1_activated(self): print('Action 1 activated.') def action2_activated(self): print('Action 2 activated.') def action3_activated(self): print('Action 3 activated.') if __name__ == '__main__': program = QtWidgets.QApplication(sys.argv) main_window = Program() main_window.window.show() sys.exit(program.exec_())
<?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>Form</class> <widget class="QWidget" name="Form"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>400</width> <height>300</height> </rect> </property> <property name="sizePolicy"> <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="minimumSize"> <size> <width>400</width> <height>300</height> </size> </property> <property name="maximumSize"> <size> <width>400</width> <height>300</height> </size> </property> <property name="windowTitle"> <string>Form</string> </property> <layout class="QGridLayout" name="gridLayout"> <item row="1" column="0"> <widget class="QToolButton" name="toolButton"> <property name="minimumSize"> <size> <width>150</width> <height>50</height> </size> </property> <property name="maximumSize"> <size> <width>150</width> <height>50</height> </size> </property> <property name="text"> <string>QtDsgnrBtn</string> </property> <property name="popupMode"> <enum>QToolButton::MenuButtonPopup</enum> </property> </widget> </item> </layout> </widget> <resources/> <connections/> </ui>