PyQt4: QGraphicsItem mousePressEvent() disables flag ItemIsMovable - mouseevent

Here's a bug which I accidently solved it and have no idea why that works. I hope that someone could explain to me the logics behind it.
I've reimplmented QGraphicsItem and its mousePressEvent.
By doing that the item was no longer movable.
Even when trying to call QGraphicsItem.mousePressEvent(self, event) it didn't work.
Only when I reimplmented mouseMoveEvent() and mouseReleaseEvent() it finally worked.
Code:
class LWResizeableItem(QtGui.QGraphicsItem):
def __init__(self):
super(LWResizeableItem, self).__init__()
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable)
def mousePressEvent(self, event):
QtGui.QGraphicsItem.mousePressEvent(self, event)
< some code.... >
def mouseMoveEvent(self, event):
QtGui.QGraphicsItem.mouseMoveEvent(self, event)
def mouseReleaseEvent(self, event):
QtGui.QGraphicsItem.mouseReleaseEvent(self, event)

A mouse event is propagated up the parent widget chain until a widget
accepts it with accept(), or an event filter consumes it.
My guess, since you did not show relevant code, is that your mousePressEvent accepted the event. That prevented QtGui from handling it (Your code did all the handling).
You solved the "bug" by calling QtGui.QGraphicsItem.mousePressEvent which performs the default function (in addition to your own).
Adding the other two functions (mouseMoveEvent and mouseReleaseEvent) must have coincided with your adding the
QtGui.QGraphicsItem.mousePressEvent(self, event)
line to your mousePressEvent - that is why it seemed to have solved the problem.

Related

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;

QBasicTimer can be used with threads started with QThread when selecting widget.setFocus(True)

I always get this error message.
QBasicTimer can be used with threads started with QThread when widget.setFocus()
Basically, I have a class Rotary which is listening to GPIO.
it has myForm.dial_on_value_changed() registered as a callback to Rotary which will call dial_on_value_changed() when rotator is rotated.
If myForm.dial_on_value_changed() only prints a text or string then it is fine. But if it selected one of myForm element and change its state such as setFocus(True), it will throw the above error, smth related with QThread.
My Application is a single thread and Rotari itself works based on listening to an interrupt in a GPIO pin which will call the registered callback function.
can someone tell me or guide me what I have done wrong? Or maybe a better way to solve this problem which is "A knob which will rotate and set all GUI element to be focused"
thanks in advance.
This is my snippet code
class MyForm(QDialog):
### creating a circular list
self._vt_dial_cycle = cycle([
# tuple UI element with a function
(self.vt_textedit, self.dial_to_vt),
(self.rr_textedit, self.dial_to_rr),
(self.stop_button, None),
(self.start_button, None),
(self.ie_textedit, self.dial_to_ie)
])
def dial_to_vt(self):
print("Dial on vt")
def dial_to_rr(self):
print("Dial on rr")
def dial_to_ie(self):
print("Dial on ie")
def dial_on_value_changed(self, direction):
# get next element from circular list.
(active_widget, action) = next(self._vt_dial_cycle)
self._current_widget = (active_widget, action)
#execute action manually registered with widget
action()
if isinstance(active_widget, QTextEdit):
self.move_cursor_to_end(active_widget)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyForm()
w.show()
# ... a rotari with a knop
ky040 = KY040(CLOCKPIN, DATAPIN, SWITCHPIN, w.dial_on_value_changed, w.dial_on_pressed)
ky040.start()

Understanding Qt5 / PyQt5 focus behavior

I have a very simple test application:
from PyQt5.QtWidgets import *
import sys
import time
class Example(QWidget):
def __init__(self):
super().__init__()
self.pbar = QProgressBar(self)
self.pbar.setGeometry(30, 40, 200, 25)
self.btn = QPushButton('Start', self)
self.btn.move(40, 80)
self.btn.clicked.connect(self.do_action)
self.txt = QLineEdit('Some info goes here', self)
self.txt.setReadOnly(True)
self.txt.move(40, 120)
self.setGeometry(300, 300, 280, 170)
self.setWindowTitle("Python")
self.show()
def do_action(self):
# setting for loop to set value of progress bar
self.btn.setDisabled(True)
for i in range(101):
time.sleep(0.05)
self.pbar.setValue(i)
self.btn.setEnabled(True)
if __name__ == '__main__':
App = QApplication(sys.argv)
window = Example()
sys.exit(App.exec())
I have two, possibly related, problems with this code:
If I click on btn it dutifully starts QProgressBar updating and disables itself, but if I click on it twice I will see two iterations of the pbar; shouldn't clicks on a disabled widget be ignored?
As soon as btn is disabled txt contents are selected, in spite of it being ReadOnly; is there some way to prevent this "focusing next" behavior? Leaving everything unfocused till next click would be best.
Is this behavior considered "normal"?
The first issue is due to the fact that you used a blocking function (which should never happen) in event-driven systems like ui frameworks. What happens is that the for loop with the sleep function prevents Qt to correctly process events, including input events: your second click gets "queued" and can only be processed as soon as the control is returned to the event manager (when the function finally returns), and since, at that point, you've re-enabled the button, only then the previously queud event gets finally processed (and the function is called again).
The solution is simple: avoid any situation like this, if you need a time-based iteration, use a QTimer, if you need parallel and non blocking processing, use a QThread.

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.

How to launch the QMainwindow from QDialog after the interval selected by user on QDialog combobox using QTimer

I have a QMainWIndow called Main which calls QDialog called popup_on_waiver. QDialog has a combobox to select number of hours. Once user selects hours and clicks Ok, I want to close the popup, hide the QMainwindow and launch the QMainwindow after selected number of hours from combobox. Program works until user selects hours and cliks ok. It closes popup and hides main window.(Requirement is that app has to be running in hidden forever, so hiding the main window). When it calls launch_after_interval, its failing with error "Process finished with exit code 1073741845". Please advise on the correct steps.
I am launching the Main window on certain other conditions that are not provided below so I am writing a separate block for launching the main window again after waiver hours selected by the user. Also, I tried to fetch the result of popup window, accepted or rejected but it didnt return anything.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import QUrl, Qt, QTimer, QSize, QRect
import sys
class popup_on_waiver(QDialog):
#pop up window
def __init__(self, parent=None):
super(QDialog,self).__init__(parent)
self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.setMinimumSize(QSize(660, 340))
self.setWindowTitle("Waiver")
self.cb = QComboBox() #combobox
self.cb.setGeometry(QRect(40, 40, 100, 30))
self.cb.addItems(["1", "2", "3", "4"])
self.cb.currentIndexChanged[str].connect(self.returnInterval)
self.cb.setObjectName("combobox")
self.cb.move(80, 80)
self.buttons = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self)
self.buttons.accepted.connect(self.hide_main)
self.buttons.rejected.connect(self.reject) #buttons
vbox = QVBoxLayout(self) #layout
vbox.addWidget(self.cb)
vbox.addWidget(self.buttons)
self.setLayout(vbox)
def hide_main(self, hours):
self.accept
self.parent().hide()
launch_after_interval(self.interval) #calling timer function
def returnInterval(self, hours): #combobox value that is number of hours
self.interval = int(hours) * 3600 * 1000
#QMainwindow
class Main(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.initUI()
def initUI(self):
self.centralwidget = QWidget(self)
self.Waiver = QPushButton('Waiver')
self.Waiver.clicked.connect(lambda: self.popup())
hbox = QHBoxLayout()
hbox.addWidget(self.Waiver)
self.centralwidget.setLayout(hbox)
self.setGeometry(50, 50, 1200, 600)
self.setWindowTitle("Timesheet")
self.setWindowIcon(QIcon(""))
self.setStyleSheet("background-color:")
self.setCentralWidget(self.centralwidget)
self.show()
def popup(self):
self.p = popup_on_waiver()
self.p.exec_()
def launch_after_interval(interval):
timer = QTimer()
timer.setSingleShot(True)
timer.setInterval(interval)
timer.timeout().connect(lambda: Main())
timer.start()
There are various problems with your code:
you create the dialog without setting the parent, so when you try to call self.parent().hide() it won't work because parent() returns None, which obviously doesn't have a hide attribute;
you have connected the accepted signal to hide_main, which requires an argument, but the accepted signal doesn't have any;
you missed the parentheses of accepted in hide_main, so it wouldn't be called;
self.interval is set only whenever the index of the combo is changed, but if the user doesn't change it (by leaving the default value), there won't be any self.interval set;
you are setting the WindowStaysOnTopHint flag only, which will reset any other window flags; the result will be that you won't have a new window, but a widget that is "embedded" in the parent; to correctly set the flag you should use self.setWindowFlags(self.flags() | Qt.WindowStaysOnTopHint);
signals cannot be "called", so there should be no parentheses in timer.timeout().connect;
the timer object has no reference outside the scope of launch_after_interval, nor it has no parent object set, so it will be deleted as soon as the function returns and will never be fired;
Revised code (modifications are in bold):
class popup_on_waiver(QDialog):
def __init__(self, parent=None):
super(QDialog,self).__init__(parent)
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
self.setMinimumSize(QSize(660, 340))
self.setWindowTitle("Waiver")
self.cb = QComboBox() #combobox
self.cb.setGeometry(QRect(40, 40, 100, 30))
self.cb.addItems(["1", "2", "3", "4"])
self.cb.currentIndexChanged[str].connect(self.returnInterval)
self.cb.setObjectName("combobox")
self.cb.move(80, 80)
self.buttons = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self)
self.buttons.accepted.connect(self.hide_main)
self.buttons.rejected.connect(self.reject)
vbox = QVBoxLayout(self)
vbox.addWidget(self.cb)
vbox.addWidget(self.buttons)
self.setLayout(vbox)
# set the default interval
self.interval = 3600000
# no arguments here!
def hide_main(self):
self.accept() # <-- the parentheses!
self.parent().hide()
launch_after_interval(self.interval)
def returnInterval(self, hours):
self.interval = int(hours) * 3600 * 1000
class Main(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
self.initUI()
def initUI(self):
self.centralwidget = QWidget(self)
self.Waiver = QPushButton('Waiver')
# if the function does not have arguments, lambda is usually not required
self.Waiver.clicked.connect(self.popup)
hbox = QHBoxLayout()
hbox.addWidget(self.Waiver)
self.centralwidget.setLayout(hbox)
self.setGeometry(50, 50, 1200, 600)
self.setWindowTitle("Timesheet")
self.setWindowIcon(QIcon(""))
self.setStyleSheet("background-color:")
self.setCentralWidget(self.centralwidget)
self.show()
def popup(self):
# the parent is required to make the dialog modal *and* allow it
# to call parent().hide()!
self.p = popup_on_waiver(self)
self.p.exec_()
def launch_after_interval(interval):
# set a parent QObject for the timer, so that it's not removed
# when the function returns
timer = QTimer(QApplication.instance())
timer.setSingleShot(True)
timer.setInterval(interval)
timer.timeout.connect(lambda: Main())
timer.start()
Other relatively minor issues:
similar attribute names (like centralwidget, which is too similar to QMainWindow's centralWidget()) should be avoided, as they can create confusion and lead to hard to find bugs and issues;
a timer that acts on an object should not be created outside of the object that will eventually call/access/show it (even if indirectly); while technically there's nothing wrong with it, it's usually better to keep objects "organized", so that they can be accessed if required (for example, showing the window and stopping the timer before it times out);
creating a new instance of the main window is not suggested, as one already exists; this is related to the previous point: if you have a direct reference to the timer and the window, you can also call self.someWindow.show();
avoid mixing and confusing naming styles: you've used upper case names for attributes (Waiver) and lower for classes (popup_on_waiver), while it should be the opposite; then there's also mixedCase (returnInterval) and under_score (hide_main); choose a style and keep that one (read more about it in the style guide for Python code, aka PEP-8);
I preferred to edit only the parts of your code that prevented the program to work, but you should really keep the aspects above in mind, even if they are "relatively minor" (emphasis on relatively).
Finally, (trivial, but not irrelevant): mixing import modes from the same modules should be avoided: you either use wildcard imports like from module import * (but you normally shouldn't) or explicit ones like from module import ClassA, ClassB, [...]; for big modules like PyQt5, it's common to import the submodule, like from PyQt5 import QtWidgets:
Good:
from PyQt5 import QtWidgets
class SomeWidget(QtWidgets.QWidget):
# ...
Also good, but tends to be very obnoxious as you have to remember to add classes each time you need a new one and you might end up with a really long list of imports, possibly resulting in unnecessary classes as you ended up in not using some (also, I doubt there's a substantial benefit, at least on Qt):
from PyQt5.QtWidgets import QWidget, QHBoxLayout # etc...
class SomeWidget(QWidget):
# ...
Not so good, but it works (keeping submodule names can be useful to keep also in mind their "scope") and behaves as the previous one:
from PyQt5.QtWidgets import *
And this, this is simply wrong (I mean, it works, but it doesn't make any sense):
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QWidget