[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