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

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

Related

How do I change the dropEvent behavior correctly?

I am trying to change the default behavior when dropping a file in QtextEdit
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(869, 499)
self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.textEdit = QtWidgets.QTextEdit(Dialog)
self.textEdit.setObjectName("textEdit")
self.horizontalLayout_2.addWidget(self.textEdit)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.textEdit.setAcceptDrops(True)
self.textEdit.dropEvent = self.dropEvent
def dropEvent(self, event):
event.setDropAction(QtCore.Qt.CopyAction)
if event.mimeData().hasText():
print(event.mimeData().text())
event.accept()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
ui = Ui_Dialog()
ui.setupUi(Dialog)
Dialog.show()
sys.exit(app.exec_())
But after performing dropEvent, the cursor in TextEdit stops moving.
What am I missing?
QTextEdit uses internal flags (not exposed to the API) that properly update the text cursor during drag and drop operations, mostly to allow pasting in the exact position within the text based on the mouse cursor, and in the meantime show the "cursor caret" to the user so that they will know where the content would be inserted.
This means that the default implementation of QTextEdit dropEvent() must always be called in order to properly update the cursor.
Now, proper drag&drop implementation of QTextEdit should always be done through insertFromMimeData() (and eventually canInsertFromMimeData() to prevent drop at all).
If you want to alter the behavior when dropping certain contents, then just override that function:
from PyQt5 import QtWidgets
class DropEdit(QtWidgets.QTextEdit):
def insertFromMimeData(self, data):
if data.hasUrls():
self.insertPlainText('%ONEFILE%')
else:
super().insertFromMimeData(data)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
test = DropEdit()
test.show()
sys.exit(app.exec_())
Now, the issue is that insertFromMimeData() is called no matter if the operation is done from clipboard (through Ctrl+V or via the context menu) or from drag&drop. Another issue is that drag&drop can also happen within the text edit, for instance to move a selected text somewhere else.
A basic solution, which would prevent pasting from d&d but not from clipboard, would be to use an internal flag that can be set in the dropEvent() and would be cleared in insertFromMimeData().
The following example will accept drops only if the dropped data has no urls in it, but will still accept pasting from clipboard if it contains urls (for instance, copying an object in the file browser):
class DropEdit(QtWidgets.QTextEdit):
acceptDrop = True
def insertFromMimeData(self, data):
if self.acceptDrop:
super().insertFromMimeData(data)
self.acceptDrop = True
def dropEvent(self, event):
self.acceptDrop = not event.mimeData().hasUrls()
super().dropEvent(event)

Fault in my resource file or resource import for PyQt5?

I'm new to working with PyQt for GUI development. As a tutorial for one of the trainings, I'm to develop a phone dialer, which basically just displays a number pad and makes the appropriate dial tones when pressed. This involves importing a resource file that contains the 12 tones. i downloaded the tones from the internet and tested them. They all work just fine. however after I create the resource file, and convert it using pyrcc5, the tones do not play. I've copied the resources.py from the tutorial and the dial pad works as expected. I'm wondering if anyone can help me debug this issue. I haven't seen anything online that shows a similar problem.
if someone can help me attach files, I can add the resource files to the question as well.
Here is a copy of my code although it doesn't appear to be the problem:
import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5 import QtMultimedia as qtmm
import resources1
class SoundButton(qtw.QPushButton):
def __init__(self, wav_file, *args, **kwargs):
super().__init__(*args, **kwargs)
self.wav_file = wav_file
self.player = qtmm.QSoundEffect()
self.player.setSource(qtc.QUrl.fromLocalFile(wav_file))
self.clicked.connect(self.player.play)
class MainWindow(qtw.QMainWindow):
def __init__(self):
"""MainWindow constructor.
This widget will be our main window.
We'll define all the UI components in here.
"""
super().__init__()
# Main UI code goes here
dialpad = qtw.QWidget()
self.setCentralWidget(dialpad)
dialpad.setLayout(qtw.QGridLayout())
for i, symbol in enumerate('123456789*0#'):
button = SoundButton(f':/dtmf/{symbol}.wav', symbol)
row = i // 3
column = i % 3
dialpad.layout().addWidget(button, row, column)
# End main UI code
self.show()
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
# it's required to save a reference to MainWindow.
# if it goes out of scope, it will be destroyed.
mw = MainWindow()
sys.exit(app.exec())
Thanks in advance for your help

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.

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