[PyQt] Non-Modal Dialog
Chuck Rhode
CRhode at LacusVeris.com
Mon Oct 7 22:15:41 BST 2019
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On Sun, 6 Oct 2019 21:59:48 +0200
Maurizio Berti <maurizio.berti at gmail.com> wrote:
> After some tests, I found that the problem is not about modal
> windows at all. <snip> A simple solution is to convert the pixmap to
> a QImage and use its save() function, which seems to be thread safe
> and doesn't block the GUI:
> WIN.pixmap.toImage().save('/tmp/nonmodal_test.png')
That didn't change things for me.
... which is good. For my test, I require a long-running task that
locks the GUI. Both QPixmap *save* and QImage *save* seem to fill the
bill.
I changed QMessageBox to *exec_* from *show* and fired it from the
*started* signal of the thread per your example, which seems to be a
straightforward approach, but that couldn't manage to display anything
unless the thread paused with a *sleep* first.
Returning to your kind reply....
> - You don't need to return every function if the returned value is
> not required, as Python implicitly returns None if no explicit
> return exists
Hah! You're not the first to be bugged by this habit of mine. I am
more comfortable making the exit from the function explicit AND
TRAILING although doing so is rundundant. I mightily resist returning
from the middle. I'm sure this has to do with my tenure as a COBOL
'68 programmer where many (of those who were influential) considered
it best practice, although it was (usually) redundant, to insert empty
exit-paragraph labels explicitly to delimit the trailing edge of
*perform*ed code. In Python, where everything is a function with a
return value (which is usually None), I try to consider what to return
and make the choice explicit -- even when IT IS nothing. When I'm
writing my own classes, I'll usually make the *set* methods return
*self*, for example.
> - Avoid using object names that already are existing properties or
> methods (like self.thread)
Oh, heck!
I remember, writing Pascal, where it was fairly common to prefix
labels to indicate their type: x-strings, i-integers, f-functions, and
xxx-methods, where xxx was the abbreviated name of the class. This
had the virtue of avoiding tracking-over implementor's names during
subclassing and having your own tracked-over in turn. In Python, it's
much more chic to rely on implicit typing, and best practice is to
avoid such prefixing, and I get into trouble.
> - You can connect the finished signal directly to the close (or,
> better, accept) slot of the popup and delete the thread itelf:
<snip>
> class WinMain(QMainWindow):
> def test_part_1(self):
> popup = WaitMessage(self)
> worker = ThdWorker(self)
> worker.started.connect(popup.exec_)
> worker.finished.connect(worker.deleteLater)
> worker.finished.connect(popup.deleteLater)
> worker.start()
That's cool! Didn't think of that. The modal dialog can be harnessed
for non-modal duty.
I dithered about whether to *del* the thread and popup structures. In
my extended test suite, they are replaced by new ones for each
scenario, and presumably the old ones are garbage-collected soon
thereafter. Not bothering about garbage collection is one of the
principal virtues of coding in Python, and I shouldn't have worried
about trying to show their destruction explicitly. Aren't explicit
deletes like *del* or *deleteLater* truely rundundant?
Anyway here's a revised example. First is a module containing a
"wOrker" object:
> #!/usr/bin/python
> # -*- coding: utf-8 -*-
> # uidgets.py
> # 2019 Oct 07 . ccr
> """Local extensions to Qt widgets.
>
> """
> from __future__ import division
> from PyQt5.QtWidgets import (
> QMessageBox,
> )
> from PyQt5.QtCore import(
> QThread,
> )
> from PyQt5.QtCore import(
> Qt,
> )
> ZERO = 0
> SPACE = ' '
> NULL = ''
> NUL = '\x00'
> NA = -1
> class UiOrker(object):
> """Fork a long-running task into its own thread after displaying a wait
> box.
>
> *title* is the window title of the wait box.
>
> *text* is the text in the wait box.
>
> *parent* is the window that owns the wait box and the forked thread.
>
> *process* is the long-running task.
>
> *kill* is the epilog performed after the wait box is canceled or the
> forked thread finishes, if any.
>
> *run()* starts the process.
>
> """
> def __init__(
> self,
> icon=QMessageBox.Information,
> title=None,
> text='Please wait.',
> parent=None,
> process=None,
> kill=None,
> ):
> self.icon = icon
> self.title = title
> self.text = text
> self.parent = parent
> self.process = process
> self.kill = kill
> self.parms = None
> return
> def get_popup(self):
> result = QMessageBox(self.icon, self.title, self.text, parent=self.parent)
> result.setStandardButtons(QMessageBox.Cancel)
> result.setWindowFlags(Qt.Dialog)
> result.finished.connect(self.epilog)
> return result
> def get_fork(self, popup):
> result = QThread(self.parent)
> result.run = self.run_threaded
> result.started.connect(popup.exec_)
> result.finished.connect(popup.close)
> return result
> def run(self, *parms):
> self.parms = parms
> popup = self.get_popup()
> self.fork = self.get_fork(popup)
> self.fork.start()
> return self
> def run_threaded(self):
> QThread.msleep(100)
> self.process(*self.parms)
> return
> def epilog(self):
> self.fork.exit(NA)
> if self.kill:
> self.kill(*self.parms)
> return
> # Fin
And here is the UI:
> #!/usr/bin/python
> # -*- coding: utf-8 -*-
> # nonmodal_example_2.py
> # 2019 Oct 07 . ccr
> """Demonstrate a class showing a non-modal dialog box while starting a
> long-running task.
>
> """
> from __future__ import division
> import sys
> from PyQt5.QtWidgets import (
> QApplication,
> QMainWindow,
> QWidget,
> QGridLayout,
> QPushButton,
> )
> from PyQt5.QtGui import (
> QPixmap,
> QTransform,
> )
> import uidgets
> ZERO = 0
> SPACE = ' '
> NULL = ''
> NUL = '\x00'
> NA = -1
> class WinMain(QMainWindow):
> """The (trivial) main window of the graphical user interface.
>
> """
> def __init__(self):
> super(WinMain, self).__init__()
> self.resize(800, 600)
> self.central_widget = QWidget(self)
> self.setCentralWidget(self.central_widget)
> self.layout = QGridLayout()
> self.central_widget.setLayout(self.layout)
> self.btn_run = QPushButton('Run Test Scenario', self.central_widget)
> self.btn_run.setMaximumSize(200, 30)
> self.btn_run.clicked.connect(self.test)
> self.layout.addWidget(self.btn_run)
> figure = QPixmap()
> result = figure.load('/usr/share/qt5/doc/qtdesigner/images/designer-screenshot.png')
> if result:
> pass
> else:
> raise NotImplementedError
> transformation = QTransform()
> transformation.scale(5.0, 5.0)
> self.pixmap = figure.transformed(transformation)
> return
> def test(self):
> """Fork a thread.
>
> """
> def long_running_task(*parms):
> print 'process', parms
> self.pixmap.toImage().save('/tmp/nonmodal_test.png')
> return
> def kill(*parms):
> print 'kill', parms
> return
> uidgets.UiOrker(parent=self, process=long_running_task, kill=kill).run('Ui', ZERO)
> return
> if __name__ == "__main__":
> APP = QApplication(sys.argv)
> WIN = WinMain()
> WIN.show()
> result = APP.exec_()
> sys.exit(result)
> # Fin
Still, it'd be nice not to have that niggling *sleep* at the beginning
of the thread, you know.
- --
.. Be Seeing You,
.. Chuck Rhode, Sheboygan, WI, USA
.. Weather: http://LacusVeris.com/WX
.. 53° — Wind W 8 mph
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2
iEYEARECAAYFAl2bqv0ACgkQYNv8YqSjllLimgCfVwkKprODT8wMme4pNfM8lk+R
Eh8An2IxHZQRyU5F59eXHMCk1k6fFdG8
=SOSB
-----END PGP SIGNATURE-----
More information about the PyQt
mailing list