Re: Non-Modal Dialog

Chuck Rhode <[email protected]>
Newsgroups gmane.comp.python.pyqt-pykde
Message-ID <[email protected]>
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On Sun, 6 Oct 2019 21:59:48 +0200
Maurizio Berti <[email protected]> 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-----
_______________________________________________
PyQt mailing list    [email protected]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.