I am using PyQt5 to make a GUI, this GUI has several buttons on the top of the page which changes the index of the lower StackedWidget based on the selection. When the button is clicked, the selected button changes the style sheeet of that selected button to selected_style, but changes all others to unselected_style. I read the simplest and most universal way of doing this was to add single actions per line. But that has become quite redundant and culminates to excessive amounts of lines.This is currently being implemented by single lines, like:
self.pshBtn_1.clicked.connect(lambda: self.stckWdgt.setCurrentIndex(0))
self.pshBtn_1.clicked.connect(lambda: self.pshBtn_1.setStyleSheet(selected)
self.pshBtn_1.clicked.connect(lambda: self.pshBtn_2.setStyleSheet(unselected)
self.pshBtn_1.clicked.connect(lambda: self.pshBtn_3.setStyleSheet(unselected)
self.pshBtn_2.clicked.connect(lambda: self.stckWdgt.setCurrentIndex(1))
self.pshBtn_2.clicked.connect(lambda: self.pshBtn_2.setStyleSheet(selected)
self.pshBtn_2.clicked.connect(lambda: self.pshBtn_1.setStyleSheet(unselected)
self.pshBtn_2.clicked.connect(lambda: self.pshBtn_3.setStyleSheet(unselected)
# And so on, and so on, and so on
I attempted to loop through a list of the names of the buttons in a function under self, but I keep ending up with NameErrors. Something like:
self.pshBtn_1.clicked.connect(self.set_btn_style, pshBtn_1)
self.pshBtn_2.clicked.connect(self.set_btn_style, pshBtn_2)
self.pshBtn_3.clicked.connect(self.set_btn_style, pshBtn_3)
btn_dict = {pshBtn_1 : 0, pshBtn_2: 1, pshBtn_3: 2} #Button Name, Stacked Widget Index
def set_btn_style(self, var_name):
for i, j in btn_dict:
btn_name = i
idx = j
if btn_name == var_name:
self.keyname.setStyleSheet(button_selected)
self.stckWdgt.setCurrentIndex(idx))
else:
self.btn.setStyleSheet(button_unselected)
I guess the big question here is how can I have one function be used by several buttons or GUI actions, but pass in a specifc tableWidget, stackWidget, etc. as a parameter so I don't have to repeat some function 10 times for each button I want to use that function. Thanks.
Instead of setting a stylesheet each time the buttons are pressed, the simplest way is to enable the checkable property of the QPushButtons and thus use it as a filter in the Qt Style Sheet. To change the page and only one is selected, a QButtonGroup is used.
from PyQt5 import QtCore, QtWidgets
QSS = """
Button {
background-color: #00ff00;
}
Button:checked {
background-color: #ff0000;
}
"""
class Button(QtWidgets.QPushButton):
pass
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
page1 = QtWidgets.QLabel("page1", alignment=QtCore.Qt.AlignCenter)
page2 = QtWidgets.QLabel("page2", alignment=QtCore.Qt.AlignCenter)
page3 = QtWidgets.QLabel("page3", alignment=QtCore.Qt.AlignCenter)
options = ["Page1", "Page2", "Page3"]
stackedwidget = QtWidgets.QStackedWidget()
hlay = QtWidgets.QHBoxLayout()
group = QtWidgets.QButtonGroup(self)
group.buttonClicked[int].connect(stackedwidget.setCurrentIndex)
for i, (option, widget) in enumerate(zip(options, (page1, page2, page3))):
button = Button(text=option, checkable=True)
ix = stackedwidget.addWidget(widget)
group.addButton(button, ix)
hlay.addWidget(button)
if i == 0:
button.setChecked(True)
vbox = QtWidgets.QVBoxLayout(self)
vbox.addLayout(hlay)
vbox.addWidget(stackedwidget)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
app.setStyle("fusion")
app.setStyleSheet(QSS)
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
I run into this often where I want to pass a widget name to another function. I was able to solve this by making a nested dictionary to link one item to the widget name I want to pass in. The function is called in a for loop within the main class. In the function I use self.sender() and sending_button.objectName() to get the name of the item I used to start the function.
class Ui_MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(Ui_MainWindow, self).__init__(parent)
self.setupUi(self)
# ^^ Up here would be a bunch of buttons I want to do something with
# Make a nested Dictionary to hold the string name of button,
# the actual widget name, and the stacked widget index it corresponds to.
self.btn_change_sel = {'pushButton_1': {self.pushButton_1: 0},
'pushButton_2': {self.pushButton_2: 1} }
# if any button is pressed, launch function.
for item in self.btn_change_sel.keys():
for button in self.btn_change_sel[item].keys():
button.clicked.connect(self.btn_style_change)
def btn_style_change(self):
sending_button = self.sender()
check_sender = str(sending_button.objectName()) # Get the string name of sender button
for sender_name in self.btn_change_sel.keys(): # Get string name to match
# If the clicked button matches the string value, do some stuff
if check_sender == sender_name:
for btn_item in self.btn_change_sel[sender_name].keys():
btn_item.setStyleSheet(self.button_selected)
for stack_idx in self.btn_change_sel[sender_name].values():
self.stackedWidget_all.setCurrentIndex(stack_idx)
# Do something with all other buttons not selected, change to a different style
if check_sender != sender_name:
for btn_item2 in self.btn_change_sel[sender_name].keys():
btn_item2.setStyleSheet(self.button_unselected)
So on this example, if the clicked button matches, it changes the stylesheet and selects the stackedWidget that corresponds to the button click. All others receive the Unselected style. This makes the displayed widget button different so you easily can tell which widget page your on.
Related
I have created a checkable QComboBox where all the options are checkboxes. Everything works fine, except I can no longer click on the QLineEdit to open/close the combobox pop-up, the way a regular QComboBox would work.
I have tried to apply an event filter to the QLineEdit, as shown below, that should ideally close the combobox pop-up if it is currently open, and open it if it is currently closed. But instead, clicking on QLineEdit only opens the pop-up everytime.
I believe this is because the mouse button press (QEvent.MouseButtonPress) closes the pop-up (hence setting the self.isPopup boolean to False), so the mouse button release (QEvent.MouseButtonRelease) will always open the pop-up. I've tried to get the QCombobox to ignore the MouseButtonPress event, but to no avail. I'm not sure where I've gone wrong here - if anyone has any suggestions, it would be much appreciated.
(Here's the relevant parts of the code)
class CustomComboBox(QtWidgets.QComboBox):
def __init__(self):
super(CustomComboBox, self).__init__()
self.setModel(QtGui.QStandardItemModel(self)) # setting up widget to make it checkable
self.setEditable(True)
self.lineEdit().setReadOnly(True)
self.lineEdit().setPlaceholderText("--Select Option--")
self.isPopup = False # bool to close or open pop up
self.lineEdit().installEventFilter(self) # event filter for lineedit presses
def hidePopup(self):
super().hidePopup()
self.isPopup = False
def showPopup(self):
super().shwoPopup()
self.isPopup = True
def eventFilter(self, widget, event):
if widget == self.lineEdit():
if event.type() == QtCore.QEvent.MouseButtonRelease:
if self.isPopup:
self.hidePopup()
else:
self.showPopup()
return True
elif event.type() == QtCore.QEvent.MouseButtonPress:
event.ignore()
return True
return super(CustomComboBox, self).eventFilter(widget, event)
The problem is caused by the fact that making the combo editable, you actually have two widgets that can handle mouse events, and since QComboBox handles mouse buttons in a specific way (to allow proper popup management) that makes things difficult, because the popup normally closes after the button press.
Since your requirement for the editable line edit is just to write custom text, then just override the paintEvent by slightly changing the default behavior:
class CustomComboBox(QtWidgets.QComboBox):
customText = ''
def setCustomText(self, text):
if self.customText != text:
self.customText = text
self.update()
def paintEvent(self, event):
if not self.customText:
super().paintEvent(event)
return
painter = QStylePainter(self)
painter.setPen(self.palette().color(QPalette.Text))
opt = QStyleOptionComboBox()
self.initStyleOption(opt)
painter.drawComplexControl(QStyle.CC_ComboBox, opt)
opt.text = self.customText
painter.drawControl(QStyle.CE_ComboBoxaLabel, opt)
With the code above, you don't need to make the combo editable, and therefore there is no event filtering.
My app allows double-clicking on a QTableWidget cell to edit the content, but the cursor is always placed at the end of the existing content.
I've arranged for the content not to be selected on edit. How can I go about positioning the edit cursor where the mouse was clicked?
Here is a way to place the insertion point at the location where you clicked in the grid cell. There are a few things you need to put in the right place, as noted below. This uses a QTableView control instead of a QTableWidget. I'm not sure how much will translate across.
import PySide2.QtWidgets as qtw
import PySide2.QtGui as qgui
from PySide2.QtCore import Qt
# place the cursor where the mouse was clicked in a cell
# based on https://stackoverflow.com/a/72792962 and a comment
# from #musicamanta at https://stackoverflow.com/q/73346426
class ClickPositionDelegate(QStyledItemDelegate):
# override the createEditor behavior so we can capture the
# first `selectAll` that occurs automatically after the
# QLineEdit control is created.
def createEditor(self, parent, option, index):
editor = super().createEditor(parent, option, index)
# set margins so text in the control aligns with the grid (optional)
editor.setTextMargins(4, 2, 2, 4)
if isinstance(editor, qtw.QLineEdit):
def position_cursor():
# Catch the initial selectAll event via the selectionChanged
# signal; this ensures the position is calculated after the
# control is placed on screen, so cursorPositionAt will work
# correctly.
# Disconnect so setCursorPosition won't call this func again
editor.selectionChanged.disconnect(deselect)
# Get cursor position within the editor's coordinate system
gpos = qgui.QCursor.pos()
lpos = editor.mapFromGlobal(gpos)
editor.setCursorPosition(editor.cursorPositionAt(lpos))
# queue up the positioning function if and only if we got here
# via a simple mouse click (left mouse button is currently down
# with no modifiers)
if (
qgui.QGuiApplication.mouseButtons() == Qt.LeftButton
and qgui.QGuiApplication.keyboardModifiers() == Qt.NoModifier
):
editor.selectionChanged.connect(position_cursor)
return editor
class MainWindow(QMainWindow):
# Constructor
def __init__(self):
# Call the parent class's constructor
super().__init__()
...
# Create the data table
self.table = QTableView(self)
table_view = self.table
# start editing as soon as a cell is selected (no need for Enter
# or double-click)
self.table.setEditTriggers(QAbstractItemView.AllEditTriggers)
# or self.table.setEditTriggers(QTableView.CurrentChanged)
# call our special delegate to position the cursor correctly when
# clicking on a cell
self.table.setItemDelegate(ClickPositionDelegate(self.table))
# Set the central widget of the main window
self.setCentralWidget(self.table)
...
# be sure to run self.table.setModel(some_model) at some point
...
app = QApplication()
window = MainWindow()
window.show()
app.exec_()
I have a modal with two buttons, one Accept and one Cancel.
I set the cancel button to be the default with .setDefault() and .setAutoDefault()
Pressing return activates the cancel-button, but when I press spacebar the accept-button is activated.
Why is the application/accept-button ignoring the defaultness-configuration and activates on spacebar presses rather than the cancel button? It seems like the accept-button has focus or something despite there being a different default.
Why would the default not have focus?
If I call cancel_button.setFocus() just before showing the modal (but not earlier than that), even the spacebar will activate the Cancel-button instead of the Acccept-button, so that solves the underlying problem.
The question is why spacebar and enter do not both activate the default button.
Minimal example:
The modal shows up when the program is run, as well as when the user presses X.
Press ctrl+Q to close the application.
import sys
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QApplication, QMainWindow, QGroupBox, QHBoxLayout, QVBoxLayout, \
QWidget, QShortcut, QDialog, QPushButton
class Modal(QDialog):
def __init__(self, parent):
super().__init__(parent)
self.resize(QSize(600, 300))
self.setParent(parent)
self.setWindowModality(True)
layout = QVBoxLayout()
self.setLayout(layout)
buttons = self.create_buttons()
layout.addWidget(buttons)
# This sets focus (when pressing spacebar), and makes the modal work as expected.
# The question is why is this needed to make spacebar default to activating Cancel?
# Why is spacebar activating Accept by default without this line?:
#self.cancel_button.setFocus()
def create_buttons(self):
button_groupbox = QGroupBox()
button_box_layout = QHBoxLayout()
button_groupbox.setLayout(button_box_layout)
# Despite setting the defaultness, pressing spacebar still activates the accept-button.
# Pressing return activates the cancel-button, however, and is expected behaviour.
# Why is the Accept-button being activated when space is pressed?
accept_button = QPushButton("Accept")
accept_button.clicked.connect(self.accept)
accept_button.setDefault(False)
accept_button.setAutoDefault(False)
self.accept_button = accept_button
cancel_button = QPushButton("Cancel")
cancel_button.clicked.connect(self.reject)
cancel_button.setDefault(True)
cancel_button.setAutoDefault(True)
self.cancel_button = cancel_button
# This does not set focus (when pressing spacebar), maybe because it has not been added yet?
#cancel_button.setFocus()
button_box_layout.addWidget(accept_button)
button_box_layout.addWidget(cancel_button)
return button_groupbox
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
shortcut = QShortcut(QKeySequence("Ctrl+Q"), self)
shortcut.activated.connect(app.quit)
shortcut = QShortcut(QKeySequence("X"), self)
shortcut.activated.connect(self.run_modal)
self.resize(QSize(800, 600))
self.show()
def showEvent(self, event):
self.run_modal()
def run_modal(self):
self.modal = Modal(self)
self.modal.finished.connect(self.modal_finished)
self.modal.show()
def modal_finished(self, result):
if result == 0:
print("CANCEL")
elif result == 1:
print("ACCEPT")
else:
raise Exception("BAD RESULT")
if __name__ == '__main__':
app = QApplication(sys.argv)
mainwindow = MainWindow()
sys.exit(app.exec_())
By default, widgets receive focus based on the order in which they are added to a parent. When the top level window is shown, the first widget that accepts focus, following the order above, will receive input focus, meaning that any keyboard event will be sent to that widget first.
Note that when widgets are added to a layout, but were not created with the parent used for that layout, then the order follows that of the layout insertion.
The default property of QPushButtons, instead will "press" the button whenever the top level widget receives the Return or Enter keys are pressed, no matter of the current focused widget, and as long as the focused widget does not handle those keys.
In your case, the currently focused widget is the "Accept" button (since it's the first that has been added to the window), which results in the counter-intuitive behavior you're seeing.
If you want the cancel button to react to both Return/Enter keys (no matter what is the focused widget) and the space bar upon showing, then you have to explicitly call setFocus(). But there's a catch: since setFocus() sets the focus on a widget in the active window, it can only work as long as that widget already belongs to that window.
In your case, the cancel_button.setFocus() call done within create_buttons won't work because, at that point, the button doesn't belong to the top level window yet.
It does work when you do that after layout.addWidget(buttons), because then the button is part of the window.
So, considering the above:
if you want to set the focus on a widget, that widget must already belong to the top level widget before calling setFocus();
the default button will always be triggered upon Return/Enter keypress even if another button has focus;
With your current code, you either do what you already found out (using setFocus() on the instance attribute after adding the widget), or use a basic QTimer in the create_buttons function:
QTimer.singleShot(0, cancel_button.setFocus)
Note that:
while creating separate functions can help you to better organize your code, having a separate function that is just called once is often unnecessary (other than misleading and forcing the creation of instance attributes where they're not actually necessary); just separate code blocks with empty lines, unless those functions can be overridden by further subclasses;
setting a "Cancel" button that can be activated by Return/Enter is not a very good idea, as those keys are generally used for "Accept/Apply/Commit/Write/etc." purposes;
if you want to show a dialog as soon as its parent is shown, you shall only use a QTimer: QTimer.singleShot(0, self.run_modal); the paint event is certainly not a viable option (paint events occur very, very often, and in some systems even when the widget loses focus, which can cause recursion), nor is the showEvent() since that could happen when switching virtual desktops or unminimizing the window;
I have the following code to replace a widget (self.lbl) each time I click on a button (self.btn):
import sys
from PySide2.QtCore import Slot
from PySide2.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget, \
QPushButton
class Workshop(QWidget):
def __init__(self):
super().__init__()
self.n = 0
self.btn = QPushButton('Push me')
self.lbl = QLabel(str(self.n))
self.main_layout = QVBoxLayout()
self.sub_layout = QVBoxLayout()
self.sub_layout.addWidget(self.lbl)
self.sub_layout.addWidget(self.btn)
self.main_layout.addLayout(self.sub_layout)
self.btn.clicked.connect(self.change_label)
self.setLayout(self.main_layout)
self.show()
#Slot()
def change_label(self):
new_label = QLabel(str(self.n + 1))
self.main_layout.replaceWidget(self.lbl, new_label)
self.n += 1
self.lbl = new_label
if __name__ == '__main__':
app = QApplication()
w = Workshop()
sys.exit(app.exec_())
Right after its initialization, the object w looks like this:
When I click on the "Push me" button (self.btn), the number is incremented as wanted, but the initial "0" remains in the background:
But the other numbers do not however remain in the background ; only "0" does. Fore example, here is "22" (result after I clicked 22 times on "Push me"):
Note: I know that I could achieve the resultant I want with the setText method, but this code is just a snippet that I will adapt for a class in which I will not have a method like setText.
Thank you!
When you replace the widget in the layout, the previous one still remains there.
From replaceWidget():
The parent of widget from is left unchanged.
The problem is that when a widget is removed from a layout, it still keeps its parent (in your case, the Workshop instance), so you can still view it. This is more clear if you set the alignment to AlignCenter for each new QLabel you create: you'll see that if you add a new label and resize the window, the previous one will keep its previous position:
class Workshop(QWidget):
def __init__(self):
# ...
self.lbl = QLabel(str(self.n), alignment=QtCore.Qt.AlignCenter)
# ...
def change_label(self):
new_label = QLabel(str(self.n + 1), alignment=QtCore.Qt.AlignCenter)
# ...
You have two possibilities, which are actually very similar:
set the parent of the "removed" widget to None: the garbage collector will remove the widget as soon as you overwrite self.lbl:
self.lbl.setParent(None)
remove the widget by calling deleteLater() which is what happens when reparenting a widget to None and, if it has no other persisting references, gets garbage collected:
self.lbl.deleteLater()
For your pourposes, I'd suggest you to go with deleteLater(), as calling setParent() (which is a reimplementation of QObject's setParent) actually does lots of other things (most importantly, checks the focus chain and resets the widget's window flags), and since the widget is going to be deleted anyway, all those things are actually unnecessary, and QObject's implementation of setParent(None) would be called anyway.
The graphic "glitch" you are facing might depend on the underlying low-level painting function, which has some (known) unexpected behaviors on MacOS in certain cases.
I have a simple gui that has two radio buttons and an IntCtrl for text entry. Initially, I have the top radio button selected and the IntCtrl disabled (unfortunately, I can't figure out how to set it to blank or "grayed out"):
Relevant code snippet:
def loadSettingsPanel(self):
panel = wx.Panel(self)
self.exposureAutomatic = wx.RadioButton(panel, label="Automatic (1ms)", style=wx.RB_GROUP)
self.exposureManual = wx.RadioButton(panel, label="Manual")
self.exposureValue = wx.lib.intctrl.IntCtrl(panel, style=wx.TE_READONLY)
self.exposureManual.Bind(wx.EVT_RADIOBUTTON, self.onClick)
# Add sizers, etc.
I want to "enable" the IntCtrl area in the onClick method, but I can't figure out how to do this. SetStyle() doesn't seem to have the option clear out the wx.TE_READONLY styling and I'd prefer not to recreate the IntCtrl entirely because then it gets annoying to reshuffle everything in the sizer. If there's some way to do this using a TextCtrl, I'd be happy to switch to that and just do the character filtering manually, but I haven't been able to figure out how to enable/disable those either.
Use the Enable function rather than a style.
import wx
import wx.lib.intctrl
class MyFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "Intctrl Demo")
panel = wx.Panel(self)
self.exposureAutomatic = wx.RadioButton(panel, label="Automatic (1ms)", style=wx.RB_GROUP, pos=(50,50))
self.exposureManual = wx.RadioButton(panel, label="Manual", pos=(50,80))
self.ic = wx.lib.intctrl.IntCtrl(panel, -1, pos=(150, 80))
self.ic.Enable(False)
self.Bind(wx.EVT_RADIOBUTTON, self.onClick)
def onClick(self, event):
self.ic.Enable(self.exposureManual.GetValue())
app = wx.App()
frame = MyFrame(None)
app.SetTopWindow(frame)
frame.Show()
app.MainLoop()