PyQt dynamically switch between QSpinBox to QTimeEdit widgets depending on QComboBox data - python

I have a combo box with 2 options 'time' or 'interval' when 'time' is selected I would like to show a QTimeEdit and When 'interval is selected I would like to show a QSpinBox.
I can hide the interval widget and show the time widget but I do not know how to re-position it so that it is displayed where the interval widget was.
Here is what I have so far:
import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtGui as qtg
from PyQt5 import QtCore as qtc
class MainWindow(qtw.QMainWindow):
def __init__(self):
super().__init__()
form = qtw.QWidget()
self.setCentralWidget(form)
layout = qtw.QFormLayout()
form.setLayout(layout)
self.when_list = qtw.QComboBox(currentIndexChanged=self.on_change)
self.when_list.addItem('Every X Minutes', 'interval')
self.when_list.addItem('At a specific time', 'time')
self.interval_box = qtw.QSpinBox()
self.time_edit = qtw.QTimeEdit()
self.event_list = qtw.QComboBox()
self.event_list.addItem('Event 1')
self.event_list.addItem('Event 2')
self.event_msg = qtw.QLineEdit()
self.add_button = qtw.QPushButton('Add Event', clicked=self.add_event)
layout.addRow(self.when_list, self.interval_box)
layout.addRow(self.event_list)
layout.addRow(self.event_msg)
layout.addRow(self.add_button)
self.show()
def on_change(self):
if self.when_list.currentData() == 'time':
# Hide interval
self.interval_box.hide()
# Show time - how do I put this where interval_box was?
self.time_edit.show()
elif self.when_list.currentData() == 'interval':
# Hide time - ERROR object has no attribute time_edit
self.time_edit.hide()
# show interval - ERROR object has no attribute interval_box
self.interval_box.show()
def add_event(self):
pass
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
mw = MainWindow()
sys.exit(app.exec())
How can I fix the errors and dynamically switch between the widgets?

Instead of hiding and showing widgets, you can use a QStackedWidget (which is similar to a tab widget, but without tabs) and use the combo box signal to select which one show.
Note that you should not connect to a *changed signal in the constructor if you're going to set properties that could call that signal and the slot uses objects that don't exist yet: in your case you connected the currentIndexChanged signal in the constructor, but that signal is always called when an item is added to a previously empty combobox, and since at that point the time_edit object has not been created, you'll get an AttributeError as soon as you add the first item.
While using signal connections in the constructor can be useful, it must always be used with care.
class MainWindow(qtw.QMainWindow):
def __init__(self):
# ...
self.when_list = qtw.QComboBox()
self.when_list.addItem('Every X Minutes', 'interval')
self.when_list.addItem('At a specific time', 'time')
self.when_list.currentIndexChanged.connect(self.on_change)
# ...
self.time_stack = qtw.QStackedWidget()
self.time_stack.addWidget(self.interval_box)
self.time_stack.addWidget(self.time_edit)
layout.addRow(self.when_list, self.time_stack)
# ...
def on_change(self):
if self.when_list.currentData() == 'time':
self.time_stack.setCurrentWidget(self.time_edit)
elif self.when_list.currentData() == 'interval':
self.time_stack.setCurrentWidget(self.interval_box)
Another solution could be to remove the widget that is to be hidden and insert a row in the same place using QFormLayout functions, but while that layout is useful in many situations, it's mostly intended for pretty "static" interfaces. The alternative would be to use a QGridLayout, which allows setting more widgets on the same "cell": in that case, you can easily toggle visibility of items, but it could create some issues as the layout would also try to adjust its contents everytime (which can be solved by using setRetainSizeWhenHidden() for the widget's size policy.

Related

Adding the same object to a QTabWidget

I have a few toggleable QPushButtons that I need to add to all tabs of a QTabWidget. Each tab should track the current state of each toggleable QPushButton which is why I am adding the same object instead of creating new buttons. The position of the widgets doesn't really matter, as long as I have the same tbutton object on all tabs. (i.e. when tbutton is enabled on Tab 1, it should be enabled on Tabs 2 and 3).
Below is the code I'm using. Note that in this example I'm only showing one toggleable QPushButton.
import sys
from PyQt5.QtWidgets import QTabWidget, QPushButton, QApplication, QWidget, QHBoxLayout
if __name__ == '__main__':
app = QApplication(sys.argv)
tabW = QTabWidget()
tbutton = QPushButton("foo")
tbutton.setCheckable(True)
w1 = QWidget()
layout1 = QHBoxLayout(w1)
layout1.addWidget(QPushButton("bar"))
layout1.addWidget(tbutton)
w2 = QWidget()
layout2 = QHBoxLayout(w2)
layout2.addWidget(QPushButton("baz"))
layout2.addWidget(tbutton)
tabW.addTab(w1, "Tab 1")
tabW.addTab(w2, "Tab 2")
tabW.addTab(tbutton, "Tab 3")
tabW.show()
sys.exit(app.exec_())
When the app is run, only Tab 3 contains tbutton. While it is possible to always show a single button using QTabWidget's setCornerWidget method, it is more difficult to modify the layout when the corner widget added is more complicated (i.e. more toggleable buttons).
Widgets cannot be "shared" between parents and/or shown multiple times. Every time you add a widget to a new layout, then it will be removed from the previous.
While you could reparent the widget by adding it to the layout everytime the tab is changed, it's really not a practical solution, especially if complex layout systems are involved, and reparenting is normally used only in very specific cases where it's really important to use the same instance elsewhere due to its complex state (for instance, a toolbar). Since you're just using a button, there's little point in reusing the same instance.
A more logical and safe solution is to create a button for every tab and link their state using signals, then if you also need to connect the toggled signal to another function, do it to just to one of them (not all):
if __name__ == '__main__':
app = QApplication(sys.argv)
tabW = QTabWidget()
w1 = QWidget()
layout1 = QHBoxLayout(w1)
layout1.addWidget(QPushButton("bar"))
tbutton1 = QPushButton("foo", checkable=True)
layout1.addWidget(tbutton1)
w2 = QWidget()
layout2 = QHBoxLayout(w2)
layout2.addWidget(QPushButton("baz"))
tbutton2 = QPushButton("foo", checkable=True)
layout2.addWidget(tbutton2)
tbutton3 = QPushButton("foo", checkable=True)
tabW.addTab(w1, "Tab 1")
tabW.addTab(w2, "Tab 2")
tabW.addTab(tbutton3, "Tab 3")
buttons = tbutton1, tbutton2, tbutton3
def toggleButtons(state):
for button in buttons:
button.setChecked(state)
for button in buttons:
button.toggled.connect(toggleButtons)
tabW.show()
sys.exit(app.exec_())
The above indication is because the code as it's written triggers some level of recursion, as the signal will be emitted for all setChecked calls on buttons that have a different state. Since Qt is well written, all state changes emit signals only when the new state is actually different, but if you want to do things more correctly, just block the signals temporarily:
def toggleButtons(state):
for button in buttons:
blocked = button.blockSignals(True)
button.setChecked(state)
button.blockSignals(blocked)
But, the best solution is to use QSignalBlocker which does almost the same thing, but in a safer fashion:
def toggleButtons(state):
for button in buttons:
with QtCore.QSignalBlocker(button):
button.setChecked(state)
This has a catch: since the signal will then be only emitted once, for the button that has actually triggered, if you need to do something else when the state changes, you either do it in toggleButtons or you connect the function to all buttons.
If you want to separate the logic of "group toggling" from the actual function that reacts to the state change, a better solution is to use a custom signal, but you must use a class.
class TabWidget(QTabWidget):
stateChanged = QtCore.pyqtSignal(bool)
if __name__ == '__main__':
app = QApplication(sys.argv)
tabW = TabWidget()
# ...
buttons = tbutton1, tbutton2, tbutton3
def toggleButtons(state):
for button in buttons:
with QtCore.QSignalBlocker(button):
button.setChecked(state)
tabW.stateChanged.emit(state)
for button in buttons:
button.toggled.connect(toggleButtons)
def doSomething(state):
print('state changed', state)
tabW.stateChanged.connect(doSomething)
tabW.show()
sys.exit(app.exec_())
Please consider that this "procedural" flow is fine for educational purposes, but using a class and implement both UI and logic in it is usually a better practice, and the above functions should then become methods in that class so that they can act in the context of the instace.

Adding items to QComboBox

I'm trying to add items to two combo boxes.
The code below runs with no errors, I see the list I'm trying to add and "fin" is printed to the terminal, but the combo boxes are showing up empty.
from PyQt5.QtWidgets import QMainWindow
from PyQt5 import QtWidgets
# import GUI from designer file
from main_menu import Ui_main_menu
# import other functions
from add_functions import ChangeLists
class Main(QMainWindow, Ui_main_menu):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
self.setupUi(self)
self.init_lists()
def init_lists(self):
# Team List
team_list_file = open(r'C:\NHLdb_pyqt\files\NHLteams.txt', 'r')
team_list = team_list_file.read().splitlines()
team_list_file.close()
print("team list: ", team_list)
# Initial Player List
player_list_init = "Please Select a Team"
# Populate combo box lists
self.team_select_combobox.addItems(team_list)
self.player_select_combobox.addItem(player_list_init)
# connect combo box to function that will change player list based on team list selection
# self.team_select_combobox.currentTextChanged.connect(ChangeLists.team_changed)
print("fin")
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
main_menu = QtWidgets.QDialog()
ui = Main()
ui.setupUi(main_menu)
# main_menu = Main()
main_menu.show()
sys.exit(app.exec_())
You're using two methods of loading the ui at the same time, the multiple inheritance approach and the "direct approach", and you're actually showing the main_menu instance of QDialog (which doesn't have any init_lists function).
The result is that even if the init_lists is called, it's "shown" (actually, not) in the wrong window, since you're showing the main_menu instance.
Clearly, you should not use both of them, as the first is enough (and usually the most used/suggested), and then show the correct instance object:
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
main_menu = Main()
main_menu.show()
sys.exit(app.exec_())
Do note that there's something else that is wrong with your implementation: you're inheriting from QMainWindow in the Main class, but later you're trying to set up the ui using a QDialog.
Only the base class created in Designer should be used (it doesn't matter what approach you use to load the ui). I can assume that the ui was created using a QDialog (otherwise an exception would have occurred, as a QMainWindow ui would try to use setCentralWidget() which is a function that doesn't exist for QDialog).
So, you either create a new main window in Designer and copy your existing layout in that (if you need the features of a QMainWindow, such as a menu bar, a status bar, dock widgets or toolbars), or you correctly use the QDialog class in the constructor:
class Main(QDialog, Ui_main_menu):
# ...

pyqt Multiple objects share context menu

So I'm relatively new to qt and fairly seasoned with python.
However, I have an instance where I want multiple widgets (in this case labels) to share the same custom context menu but I need to get access to the widget's information.
When I use setContextMenuPolicy and customContextMenuRequested.connect on each label I only get information for the first label despite accessing the context menu with the second label.
Below is a stripped down version of what I'm working with:
from PyQt5 import QtGui
from PyQt5 import QtCore
from PyQt5.QtWidgets import QApplication, QMainWindow, QMenu, QLabel
import sys
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.title = "PyQt5 Context Menu"
self.top = 200
self.left = 500
self.width = 200
self.height = 100
self.InitWindow()
def InitWindow(self):
self.setWindowIcon(QtGui.QIcon("icon.png"))
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.firstLabel = QLabel(self)
self.firstLabel.setText("Meep!")
self.firstLabel.setObjectName("firstLabel")
self.firstLabel.setStyleSheet("background-color: rgb(252, 233, 79);")
self.firstLabel.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.firstLabel.customContextMenuRequested.connect(self.customMenuEvent)
self.firstLabel.setGeometry(QtCore.QRect(0,0,50,30))
self.secondLabel = QLabel(self)
self.secondLabel.setText("Peem!")
self.secondLabel.setObjectName("secondLabel")
self.secondLabel.setStyleSheet("background-color: rgb(79,233, 252);")
self.secondLabel.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.secondLabel.customContextMenuRequested.connect(self.customMenuEvent)
self.secondLabel.setGeometry(QtCore.QRect(80,40,50,30))
print("FIRST:", self.firstLabel)
print("SECOND:", self.secondLabel)
self.show()
def customMenuEvent(self, eventPosition):
child = self.childAt(eventPosition)
print(child)
contextMenu = QMenu(self)
getText = contextMenu.addAction("Text")
getName = contextMenu.addAction("Name")
quitAct = contextMenu.addAction("Quit")
action = contextMenu.exec_(self.mapToGlobal(eventPosition))
if action == getText:
print(child.text())
if action == getName:
print(child.objectName())
if action == quitAct:
self.close()
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
From the documentation about the customContextMenuRequested(pos) signal:
The position pos is the position of the context menu event that the widget receives
This means that you will always receive the mouse position relative to the widget that fires the signal.
You're using QWidget.childAt(), which is relative to the parent geometry, but since the provided position is relative to the child widget, you'll always end up with coordinates that are relative to the top left corner of the parent.
This becomes clear if you try to set the geometry of the first widget in a position that's not the top left corner: even if you right-click on the first widget, you'll see that the menu will not appear where you clicked. If you look closely, you'll also see that the menu appears exactly at the position you clicked, based on the coordinates of the parent top left corner.
For the sake of simplicity, an easy solution would be to map the coordinates from the "sender" (which is the object that fired the last signal the receiver has received) to its parent:
def customMenuEvent(self, eventPosition):
child = self.childAt(self.sender().mapTo(self, eventPosition))
contextMenu = QMenu(self)
getText = contextMenu.addAction("Text")
getName = contextMenu.addAction("Name")
quitAct = contextMenu.addAction("Quit")
# note that the mapToGlobal is referred to the child!
action = contextMenu.exec_(child.mapToGlobal(eventPosition))
# ...
But be aware that this could lead to some inconsistency, especially when dealing with multithreading (as, theoretically, another thread could have fired another signal between the right click event and the moment the receiver actually receives it).
There are various different approaches to avoid that, but IT mostly depends on how you are going to structure your program.
For example, you could use a lambda to add an argument that helps to identify the source from which the event has been sent:
self.firstLabel.customContextMenuRequested.connect(
lambda pos, child=self.firstLabel: self.customMenuEvent(pos, child))
# ...
def customMenuEvent(self, eventPosition, child):
# ...

Python PyQt5 how to show the full QMenuBar with a QWidget

I'm getting this weird result when using QMenuBar I've used this exact code before for the QMenuBar and it worked perfectly. But it doesn't show more than 1 QMenu
This is my code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
from functools import partial
class MainMenu(QWidget):
def __init__(self, parent = None):
super(MainMenu, self).__init__(parent)
# background = QWidget(self)
lay = QVBoxLayout(self)
lay.setContentsMargins(5, 35, 5, 5)
self.menu()
self.setWindowTitle('Control Panel')
self.setWindowIcon(self.style().standardIcon(getattr(QStyle, 'SP_DialogNoButton')))
self.grid = QGridLayout()
lay.addLayout(self.grid)
self.setLayout(lay)
self.setMinimumSize(400, 320)
def menu(self):
menubar = QMenuBar(self)
viewMenu = menubar.addMenu('View')
viewStatAct = QAction('Dark mode', self, checkable=True)
viewStatAct.setStatusTip('enable/disable Dark mode')
viewMenu.addAction(viewStatAct)
settingsMenu = menubar.addMenu('Configuration')
email = QAction('Set Email', self)
settingsMenu.addAction(email)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainMenu()
main.show()
sys.exit(app.exec_())
Result:
I am aware that I am using QWidget when I should be using QMainWindow But is there a workaround???
(I apologize in advance for the terrible quality of the image, there is no good way to take a picture of a QMenuBar)
The problem is that with a QWidget you are not using the "private" layout that a QMainWindow has, which automatically resizes specific children widgets (including the menubar, the statusbar, the dock widgets, the toolbars and, obviously, the "centralWidget").
Remember that a QMainWindow has its own layout (which can't and shouldn't be changed), because it needs that specific custom layout to lay out the aforementioned widgets. If you want to set a layout for the main window, you'll need to apply it to its centralWidget.
Read carefully how the Main Window Framework behaves; as the documentation reports:
Note: Creating a main window without a central widget is not supported. You must have a central widget even if it is just a placeholder.
In order to work around that when using a basic QWidget, you'll have to manually resize the children widgets accordingly. In your case, you only need to resize the menubar, as long as you have a reference to it:
def menu(self):
self.menubar = QMenuBar(self)
# any other function has to be run against the *self.menubar* object
viewMenu = self.menubar.addMenu('View')
# etcetera...
def resizeEvent(self, event):
# calling the base class resizeEvent function is not usually
# required, but it is for certain widgets (especially item views
# or scroll areas), so just call it anyway, just to be sure, as
# it's a good habit to do that for most widget classes
super(MainMenu, self).resizeEvent(event)
# now that we have a direct reference to the menubar widget, we are
# also able to resize it, allowing all actions to be shown (as long
# as they are within the provided size
self.menubar.resize(self.width(), self.menubar.height())
Note: you can also "find" the menubar by means of self.findChild(QtWidgets.QMenuBar) or using the objectName, but using an instance attribute is usually an easier and better solution.
Set minimum width
self.setMinimumSize(320,240)

Opening a new window to collect data and process it in main window

I have a main window, I use another window to get some values and i want to display those values in the main window, kinda like how an alarm clock would work. At first it will be empty and then second window lets you choose the time and then after that is done, I want these data to be displayed on the first window.
I want this to work similarly to how the Qmessagebox works.
def Add_new_schedule(self):
if(self.TABLE_LENGTH == 5):
self.TOAST_WARNING.setText("LIMIT REACHED!")
else:
from Clock import Ui_Clock
self.CLOCK_WINDOW = Ui_Clock()
self.CLOCK_WINDOW.show()
Here, the clock window is called, and after setting the values there, on clicking a button 'Ok' that signal connects to a function in the main window.
def Get_clock_values(self, TIME_DETAILS):
DATA = {}
DATA['index'] = len(self.DATA_FROM_DB)+1
DATA['start_time'] = TIME_DETAILS[0]
DATA['end_time'] = TIME_DETAILS[1]
DATA['mode'] = TIME_DETAILS[2]
DATA['node_1'] = True
DATA['node_2'] = True
DATA['node_3'] = True
DATA['node_4'] = True
self.DATA_FROM_DB.append(DATA)
self.Clear_table()
self.Set_table()
The DATA_FROM_DB is a list of dictionaries of the available schedules and I'm able to successfully append the new values inside this list.
The Clear_Table() clears the existing widgets to blank and the Set_table() is supposed to Create the new table with the newly modified list(DATA_FROM_DB).
I am able to modify the table with this logic as long as a second window is not called, i.e appending some hard coded values in the list and then modifying the table. However, with my current code, the table exists in its original state.
Since your question is too broad I'll be giving you an example that:
Have the main window with a button and a disabled line.
The button makes it shows a second window.
Put information inside the line of the 2nd window.
This line of the 2nd window updates the line in the main window in real time.
This is happening by signals emission.
So you have something like that:
import sys
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QLineEdit
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
class MainWidget(QWidget):
def __init__(self):
super(MainWidget, self).__init__()
self.setFixedSize(500,500)
self.window2 = Window2(self)
self.btn_show_window2 = QPushButton("Open Window 2")
self.btn_show_window2.clicked.connect(self.show_window2)
self.layout = QVBoxLayout()
self.setLayout(self.layout)
self.text_from_window2 = QLineEdit()
self.text_from_window2.setStyleSheet("color: red;")
self.text_from_window2.setDisabled(True)
self.layout.addWidget(self.text_from_window2)
self.layout.addWidget(self.btn_show_window2)
def show_window2(self):
self.window2.show()
def close(self):
self.window2.close()
super(MainWidget, self).close()
#pyqtSlot(str)
def update_label(self, txt):
self.text_from_window2.setText(txt)
class Window2(QWidget):
def __init__(self, parent):
super(Window2, self).__init__()
self.setFixedSize(300,200)
self.layout = QVBoxLayout()
self.setLayout(self.layout)
self.line_edit = QLineEdit()
self.line_edit.textChanged.connect(parent.update_label)
self.layout.addWidget(self.line_edit)
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWidget()
mw.show()
sys.exit(app.exec_())
Note: You have a 'million' ways to do that, this is just one of many approaches you can follow. For example instead of using the textChanged you could also have another button in the second window and only send the text back when clicking on it, in the same way the button from the first window makes the second appear.

Categories

Resources