I am trying to display the value of a Boolean variable using a QCheckBox widget, and render the user unable to change the displayed value. I don't want to disable it, as the resulting graying doesn't look good. I have tried to approximate the effect by changing the new value back to its previous value when the user clicks the QCheckBox. However, the problem is compounded by the fact that the state of the widget is described by the "checked" properties of the QAbstractButton parent class, and the "state" properties of the QCheckBox class itself. This gives rise to a combinatorial exercise of signals and slots, of which I have been unable to obtain any good result.
var_ctrl = QtGui.QCheckBox( 'some name' )
def rdslot1(state):
if state == QtCore.Qt.Checked:
var_ctrl.setCheckState( QtCore.Qt.Unchecked )
else:
var_ctrl.setCheckState( QtCore.Qt.Checked )
def rdslot2(state):
if var_ctrl.isChecked():
var_ctrl.setChecked(False)
else:
var_ctrl.setChecked(True)
# Signal/Slot combinations (only one should be active)
var_ctrl.stateChanged.connect( rdslot1 )
var_ctrl.toggled.connect( rdslot2 )
var_ctrl.stateChanged.connect( rdslot2 )
var_ctrl.toggled.connect( rdslot1 )
I'm late to the party - it seems like you got a solution that works. For future reference tho, another way you can do it would be to consume the mouse events - which keeps all of your signals working the way they should:
from PyQt4 import QtGui, QtCore
class MyCheckBox(QtGui.QCheckBox):
def __init__( self, *args ):
super(MyCheckBox, self).__init__(*args) # will fail if passing **kwargs
self._readOnly = False
def isReadOnly( self ):
return self._readOnly
def mousePressEvent( self, event ):
if ( self.isReadOnly() ):
event.accept()
else:
super(MyCheckBox, self).mousePressEvent(event)
def mouseMoveEvent( self, event ):
if ( self.isReadOnly() ):
event.accept()
else:
super(MyCheckBox, self).mouseMoveEvent(event)
def mouseReleaseEvent( self, event ):
if ( self.isReadOnly() ):
event.accept()
else:
super(MyCheckBox, self).mouseReleaseEvent(event)
# Handle event in which the widget has focus and the spacebar is pressed.
def keyPressEvent( self, event ):
if ( self.isReadOnly() ):
event.accept()
else:
super(MyCheckBox, self).keyPressEvent(event)
#QtCore.pyqtSlot(bool)
def setReadOnly( self, state ):
self._readOnly = state
readOnly = QtCore.pyqtProperty(bool, isReadOnly, setReadOnly)
Setting the code up this way gets you a few things (which you may or may not care about) but can be useful when developing custom Qt widgets:
Consuming the event blocks the signal emission, so you can still connect other slots to things like clicked & toggled. If you're looking for those signals and then just switching the value on/off - then other widgets listening for those signals will be triggered incorrectly
Using isReadOnly/setReadOnly keeps the class following the Qt coding style
Creating pyqtSignals & pyqtSlots will help if you expose the plugin to Qt's Designer
Well later on I came up with a shortcut, which simply catches the clicks of the user and handles them according to a specifiable 'Modifiable' property. I have made this class:
class MyQCheckBox(QtGui.QCheckBox):
def __init__(self, *args, **kwargs):
QtGui.QCheckBox.__init__(self, *args, **kwargs)
self.is_modifiable = True
self.clicked.connect( self.value_change_slot )
def value_change_slot(self):
if self.isChecked():
self.setChecked(self.is_modifiable)
else:
self.setChecked(not self.is_modifiable)
def setModifiable(self, flag):
self.is_modifiable = flag
def isModifiable(self):
return self.is_modifiable
It behaves just like a normal QCheckBox, being modifiable by default. However, when you call setModifiable(False), everytime you click it, it keeps the current state of the widget. The trick was to catch the clicked signal, not toggled neither stateChanged.
Try to disable the checkbox widget, but override its look using widget palette or style
Related
I am working on a project with PyQt5 which has QFrames. I am using mouse press event to trigger a function on clicking frame as below :
frame.mousePressEvent = lambda x: print_name(x, name)
Above line is not executed at the start, It is executed after user has done some work in UI.
I am getting the behaviour I want but here is the problem:
If the user clicks the frame after the above line of code is executed, it works fine but if the user clicks on the frame before the above line of the code is executed and later again clicks the frame (after code is executed), I am not getting the same behaviour. Basically nothing happens.
I want to know where is the problem and how do I solve it?
The problem is caused because PyQt5 caches the methods so if the method is assigned then it cannot be changed. Instead of following the bad practice of assigning methods to mousePressEvent there are other better alternatives such as:
Implement inheritance
class Frame(QFrame):
def mousePressEvent(self, event):
super().mousePressEvent(event)
print(event)
Use an event filter
class MouseObserver(QObject):
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, obj, event):
if obj is self.widget and event.type() == QEvent.MouseButtonPress:
print(event)
return super().eventFilter(obj, event)
Then
observer = MouseObserver(frame)
The second seems the most appropriate for your case.
First off -- thanks for this group! I started delving into PyQt a month or so ago. In that time, I've bumped up against many questions, and virtually always found an answer here.
Until now.
I have a workaround for this, but I think it's a kluge and there probably is a proper way. I'd like to understand better what's going on.
Here's the code:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class FormWidget(QWidget):
def __init__(self, parent):
super(FormWidget, self).__init__(parent)
# Create view with image in it
self.image = QGraphicsPixmapItem(QPixmap())
self.scene = QGraphicsScene()
self.scene.addItem(self.image)
self.view = QGraphicsView(self.scene)
self.hlayout = QHBoxLayout()
self.hlayout.addWidget(self.view)
self.setLayout(self.hlayout)
# self.view.keyPressEvent = self.keyPressEvent
def keyPressEvent(self, event):
key = event.key()
mod = int(event.modifiers())
print(
"<{}> Key 0x{:x}/{}/ {} {} {}".format(
self,
key,
event.text(),
" [+shift]" if event.modifiers() & Qt.SHIFT else "",
" [+ctrl]" if event.modifiers() & Qt.CTRL else "",
" [+alt]" if event.modifiers() & Qt.ALT else ""
)
)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
form = FormWidget(self)
self.setCentralWidget(form)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())
As is, all keyboard input is detected by the overloaded keyPressEvent() function, except arrow keys. I've found enough posts talking about this to have a sense that it is because the child widget (self.view) is receiving them. I presume the child widget is, in fact, receiving all the keystrokes, but ignoring the ones that are getting through, and sucking up the arrow keys, which is why they aren't getting to the parent's keyPressEvent() function. That seems to be so, because if I uncomment the line in the middle:
self.view.keyPressEvent = self.keyPressEvent
It behaves as I expect -- the parent's keyPressEvent() gets all the keystrokes, arrows included.
How would I tell the child widget to ignore all keystrokes? I thought maybe this:
self.view.setFocusPolicy(Qt.NoFocus)
When I add that, keyPressEvent() doesn't see any keystrokes at all.
I suppose I could overload keyPressEvent() for the child as well, and just explicitly pass everything up to the parent. But that doesn't seem better than my kluge.
I think I must be misunderstanding something here.
Thanks. Just looking to learn ...
By default, a QWidget does not accept the keyboard focus, so you need to enable it explicitly:
class FormWidget(QWidget):
def __init__(self, parent):
...
self.setFocusPolicy(Qt.StrongFocus)
Rather than subclassing the child widget or attempting to prevent keystrokes from reaching it, you should consider using an eventFilter to capture events on the child widget. You will see all events before the child widget, and you can suppress or transform them.
http://doc.qt.io/qt-5.5/qobject.html#eventFilter
I understand that QEvent::ShortcutOverride occurs when there is a shortcut registered in the parent and the child wants to "go against the rule". The example given on the Qt Wiki is of a media player which pauses with Space but a QLineEdit might want to use the Space, which makes a lot of sense.
Furthermore, if the event is accepted then a QEvent::KeyPress is generated to the child widget so you can treat your particular case.
Now, my question is, why does it seem that the default action is to actually accept the QEvent::ShortcutOverride when a standard shortcut is used? This seems to me like the opposite of what the name suggest, i.e., it's overriden by default and you have to treat the event to let the shortcut pass.
In the code below, if you don't install the event filter you don't see the message.
from PySide.QtGui import QApplication
from PySide import QtGui, QtCore
app = QApplication([])
class Test(QtGui.QWidget):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
self.setLayout(QtGui.QVBoxLayout())
self.w_edit = QtGui.QLineEdit(parent=self)
self.layout().addWidget(self.w_edit)
# If we install the event filter and ignore() the ShortcutOverride
# then the shortcut works
self.w_edit.installEventFilter(self)
# Ctrl+Left is already in use (jump to previous word)
shortcut = QtGui.QShortcut(QtGui.QKeySequence('Ctrl+Left'), self)
shortcut.setContext(QtCore.Qt.ApplicationShortcut)
shortcut.activated.connect(self.test_slot)
def test_slot(self):
print('ctrl+left pressed!')
def eventFilter(self, obj, event):
if obj is self.w_edit and event.type() == QtCore.QEvent.ShortcutOverride:
# Send the event up the hierarchy
event.ignore()
# Stop obj from treating the event itself
return True
# Events which don't concern us get forwarded
return super(Test, self).eventFilter(obj, event)
widget = Test()
widget.show()
if __name__ == '__main__':
app.exec_()
My actual scenario is a tab widget for which I want to use Ctrl+Left/Right to cycle through the tabs, which works unless something like a QLineEdit has focus. I feel that there should be a better way other than calling event->ignore(); return true on all QLineEdits and any other widgets which could use the key combo, am I missing something here?
Thanks!
You can set one event filter on the application instance and then filter accordingly:
QtGui.qApp.installEventFilter(self)
# self.w_edit.installEventFilter(self)
...
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ShortcutOverride:
# filter by source object, source.parent(), or whatever...
if isinstance(source, QtGui.QLineEdit):
event.ignore()
return True
return super(Test, self).eventFilter(source, event)
I have an eventFilter on my custom label and I would like to sniff double clicks with it. Is this possible?
self.installEventFilter(self)
# Handles mouse events
def eventFilter(self, object, event):
try:
if event.buttons() == QtCore.Qt.LeftButton:
#LeftButton event
else:
# nothing is being pressed
except:
pass
Yes it is possible but for some strange reason it is not that simple. Surely you never know if a single click might be followed by another single click effectively resulting in a double click. That's why there must be some inbuilt waiting time. Qt does it and delivers events for double clicks (QEvent.MouseButtonDblClick). On the other hand Qt still delivers events for single clicks (QEvent.MouseButtonPress) even in the case of a double click, but only one. This might not be the best design.
We must differentiate them correctly. I do it with an additional timer that needs to be a bit longer than the inbuilt Qt timer for detecting double clicks. The code then goes:
from PySide import QtCore, QtGui
class MyLabel(QtGui.QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.single_click_timer = QtCore.QTimer()
self.single_click_timer.setInterval(200)
self.single_click_timer.timeout.connect(self.single_click)
def single_click(self):
self.single_click_timer.stop()
print('timeout, must be single click')
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
self.single_click_timer.start()
return True
elif event.type() == QtCore.QEvent.MouseButtonDblClick:
self.single_click_timer.stop()
print('double click')
return True
return False
app = QtGui.QApplication([])
window = MyLabel('Click me')
window.resize(200, 200)
window.show()
app.exec_()
See also Distinguish between single and double click events in Qt.
I have a QDialog, and when the user closes the QDialog, and reopens it later, I want to remember the location and open the window at the exact same spot. How would I exactly remember that location?
For that, you can use the saveState(), saveGeometry() resize() and move() methods, in conjunction with the closeEvent() and QSettings mentioned by the other answers. Here is some example, to get the idea:
class MyWindow(QMainWindow):
def __init__(self, parent):
QMainWindow.__init__(self, parent)
self.settings = QSettings("MyCompany", "MyApp")
self.restoreGeometry(self.settings.value("geometry", ""))
self.restoreState(self.settings.value("windowState", ""))
def closeEvent(self, event):
self.settings.setValue("geometry", self.saveGeometry())
self.settings.setValue("windowState", self.saveState())
QMainWindow.closeEvent(self, event)
EDIT:
Updated answer to use PyQt API v2. If using API v1, you have to manually cast the result of settings.value() to a ByteArray like
self.restoreState(self.settings.value("windowState").toByteArray())
I also used the window's own size() and pos(), since I'm already loading the windows from a .ui file. You may set it to defaults before those lines if coding the window from scratch. For the state, I'm defaulting to an empty string, which the function happily accepts as an empty ByteArray and does nothing on the first run.
Ronan Paixão's answer is almost correct.
When attempting this a got the error:
AttributeError: 'NoneType' object has no attribute 'toByteArray'
this is because there is, at first, no saved geometry and state. Additionally the return value is already a QByteArray. This code works for me:
class MyWindow(QMainWindow):
def __init__(self, parent):
QMainWindow.__init__(self, parent)
self.settings = QSettings("MyCompany", "MyApp")
if not self.settings.value("geometry") == None:
self.restoreGeometry(self.settings.value("geometry"))
if not self.settings.value("windowState") == None:
self.restoreState(self.settings.value("windowState"))
def closeEvent(self, event):
self.settings.setValue("geometry", self.saveGeometry())
self.settings.setValue("windowState", self.saveState())
QMainWindow.closeEvent(self, event)
You could reimplement the CloseEvent of the dialog (found here in the Qt documentation), and save the appropriate settings using QSettings (docs here).
class MyDialog(QDialog):
def closeEvent(event):
settings = QSettings()
settings.setValue('value1', 1)
event.accept()
It looks like you can use QSettings for this. If you look at the section of the documentation titled Restoring the State of a GUI Application you'll find an example for a main window.
In other words, save the size and location when the user closes the dialog, then next time they open it reload those settings.
_windowStatesEnum = {
0x00000000 : Qt.WindowNoState, # The window has no state set (in normal state).
0x00000001 : Qt.WindowMinimized, # The window is minimized (i.e. iconified).
0x00000002 : Qt.WindowMaximized, # The window is maximized with a frame around it.
0x00000004 : Qt.WindowFullScreen, # The window fills the entire screen without any frame around it.
0x00000008 : Qt.WindowActive, # The window is the active window, i.e. it has keyboard focus.
}
def __setstate__(self, data):
self.__init__()
self.setGeometry(data['geometry'])
self.setWindowState(self._windowStatesEnum[data['window state']])
def __getstate__(self):
return {
'geometry' : self.geometry(),
'window state' : int(self.windowState()),
}