Enable/disable text entry in IntCtrl on RadioButton click in wxPython - radio-button

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()

Related

Multi-QComboBox Hide/Show Popup on LineEdit Press (PyQT5)

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.

PyQt5: Why does QPushButton.setDefault() ignore spacebar but work for enter/return?

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;

QLayout.replace not replacing

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.

Changing Multiple Widget Styles from pushButton in a loop

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.

Moving a QGraphicsProxyWidget with ItemIgnoresTransformations after changing QGraphicsView scale

I have a QGraphicsScene that contains multiple custom QGraphicsItems. Each item contains a QGraphicsProxyWidget which itself contains whatever widgets are needed by the business logic. The proxy has a Qt::Window flag applied to it, so that it has a title bar to move it around. This is all working well, except when moving a proxy widget when the view has been scaled.
The user can move around the scene à la google maps, ie by zooming out then zooming in back a little farther away. This is done with calls to QGraphicsView::scale. Items should always be visible no matter the zoom value, so they have the QGraphicsItem::ItemIgnoresTransformations flag set.
What happens when moving a proxyWidget while the view has been scaled is that on the first move event the widget will jump to some location before properly being dragged.
I had this issue with Qt5.7.1, and could reproduce it with PyQt5 as it is simpler to reproduce and hack around, please see the snippet below.
Steps to reproduce:
move the widget around, notice nothing unusual
use the mouse wheel to zoom in or out. The higher the absolute scale, the higher the effect on the issue.
click on the widget, and notice how it jumps on the first moving of the mouse.
Snippet:
import sys
import PyQt5
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsProxyWidget, QGraphicsWidget, QGraphicsObject
global view
global scaleLabel
def scaleScene(event):
delta = 1.0015**event.angleDelta().y()
view.scale(delta, delta)
scaleLabel.setPlainText("scale: %.2f"%view.transform().m11())
view.update()
if __name__ == '__main__':
app = QApplication(sys.argv)
# create main widget
w = QWidget()
w.resize(800, 600)
layout = QVBoxLayout()
w.setLayout(layout)
w.setWindowTitle('Example')
w.show()
# rescale view on mouse wheel, notice how when view.transform().m11() is not 1,
# dragging the subwindow is not smooth on the first mouse move event
w.wheelEvent = scaleScene
# create scene and view
scene = QGraphicsScene()
scaleLabel = scene.addText("scale: 1")
view = QGraphicsView(scene)
layout.addWidget(view)
view.show();
# create item in which the proxy lives
item = QGraphicsWidget()
scene.addItem(item)
item.setFlag(PyQt5.QtWidgets.QGraphicsItem.ItemIgnoresTransformations)
item.setAcceptHoverEvents(True)
# create proxy with window and dummy content
proxy = QGraphicsProxyWidget(item, Qt.Window)
button = QPushButton('dummy')
proxy.setWidget(button)
# start app
sys.exit(app.exec_())
The jump distance is:
proportional to the scaling of the view , and to the distance of the mouse from the scene origin
goes from scene position (0,0) towards the mouse position (I think)
might be caused by the proxy widget not reporting the mouse press/move properly. I'm hinted at this diagnostic after looking at QGraphicsProxyWidgetPrivate::mapToReceiver in qgraphicsproxywidget.cpp (sample source), which does not seem to take scene scaling into account.
I am looking for either
confirmation that this is an issue with Qt and I did not misconfigured the proxy.
an explanation on how fix the mouse location given by the proxy to its children widgets (after installing a eventFilter)
any other workaround
Thanks
Almost 2 years later I got back to this issue again, and finally found a solution. Or rather a workaround, but a simple one at least. It turns out I can easily avoid getting into the issue with local/scene/ignored transforms in the first place.
Instead of parenting the QGraphicsProxyWidget to a QGraphicsWidget, and explicitly setting the QWidget as proxy target, I get the proxy directly from the QGraphicsScene, letting it set the window flag on the wrapper, and set the ItemIgnoresTransformations flag on the proxy. Then (and here's the workaround) I install an event filter on the proxy, intercept the GraphicsSceneMouseMove event where I force the proxy position to currentPos+mouseDelta (both in scene coordinates).
Here's the code sample from above, patched with that solution:
import sys
import PyQt5
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import *
global view
global scaleLabel
def scaleScene(event):
delta = 1.0015**event.angleDelta().y()
view.scale(delta, delta)
scaleLabel.setPlainText("scale: %.2f"%view.transform().m11())
view.update()
class ItemFilter(PyQt5.QtWidgets.QGraphicsItem):
def __init__(self, target):
super(ItemFilter, self).__init__()
self.target = target
def boundingRect(self):
return self.target.boundingRect()
def paint(self, *args, **kwargs):
pass
def sceneEventFilter(self, watched, event):
if watched != self.target:
return False
if event.type() == PyQt5.QtCore.QEvent.GraphicsSceneMouseMove:
self.target.setPos(self.target.pos()+event.scenePos()-event.lastScenePos())
event.setAccepted(True)
return True
return super(ItemFilter, self).sceneEventFilter(watched, event)
if __name__ == '__main__':
app = QApplication(sys.argv)
# create main widget
w = QWidget()
w.resize(800, 600)
layout = QVBoxLayout()
w.setLayout(layout)
w.setWindowTitle('Example')
w.show()
# rescale view on mouse wheel, notice how when view.transform().m11() is not 1,
# dragging the subwindow is not smooth on the first mouse move event
w.wheelEvent = scaleScene
# create scene and view
scene = QGraphicsScene()
scaleLabel = scene.addText("scale: 1")
view = QGraphicsView(scene)
layout.addWidget(view)
view.show();
button = QPushButton('dummy')
proxy = scene.addWidget(button, Qt.Window)
proxy.setFlag(PyQt5.QtWidgets.QGraphicsItem.ItemIgnoresTransformations)
itemFilter = ItemFilter(proxy)
scene.addItem(itemFilter)
proxy.installSceneEventFilter(itemFilter)
# start app
sys.exit(app.exec_())
Hoping this may help someone who's ended up in the same dead end I was :)