[PyQt] Non-Modal Dialog

Dennis Jensen djensen at pgcontrols.com
Wed Oct 9 14:47:08 BST 2019


I tried to find that official documentation but I am not sure where it 
is buried and frankly yes its buried somewhere within the official 
documentation.  The reference verbatim stated that you cannot put a 
object that inherits from QtWidgets into a sub-Thread as well as some 
objects that inherit from QtGui they can only be put within the Main 
Thread (aka QApplication) however they did not state which QtGui objects 
which I found a bit annoying.  I found this because I was trying to put 
a QWidget into a QThread but it was not working and I was trying to find 
out what I was doing wrong and then I stumbled across a comment by 
someone somewhere saying you cannot put a QWidget into a sub-Thread and 
then while researching that I stumbled across the official Qt 
documentation that collaborated it and a bit more.  If I happen to 
stumble across this documentation again I will post it here.

On a similar but different note : Sub-Classing a QThread became wrong in 
version 4.4 prior to that it was actually necessary to sub-class a 
QThread thus the confusion around how this is supposed to be 
implemented.  So like with many features prior to a certain version it 
is done one way and after it is done differently.  Just like you have to 
use print without ( ) in PyQt4 but in PyQt5 you have to use print with ( )

On 10/8/2019 8:48 PM, Kyle Altendorf wrote:
>
> On October 7, 2019 6:08:29 PM EDT, Dennis Jensen <djensen at pgcontrols.com> wrote:
>> Actually QPixMap is not only not Thread safe per-sae but elements of it
>>
>> are Thread prohibited as are all QtWidgets
> I know you shouldn't access GUI elements from other threads.  I wasn't aware of this.  Do you have a reference?
>
>> On 10/6/2019 2:59 PM, Maurizio Berti wrote:
>>> After some tests, I found that the problem is not about modal windows
>>> at all.
>>> If you check carefully, even after the QThread.sleep, the dialog
>>> interaction is blocked until the image is saved.
>>>
>>> It turns out that the issue here is that QPixmap is *not* thread safe
>>> (at least, not in all platforms).
>>> It does not seem to be any official documentation about this, but I
>>> found some information on this thread:
>>>
>> https://forum.qt.io/topic/52397/not-safe-to-use-qpixmap-outside-the-gui-thread/4
>>> 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')
>>>
>>> A couple of slightly unrelated suggestions, if I may.
>>> - 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
>>> - Avoid using object names that already are existing properties or
>>> methods (like self.thread)
>>> - You can connect the finished signal directly to the close (or,
>>> better, accept) slot of the popup and delete the thread itelf:
>>>
>>> class WaitMessage(QMessageBox):
>>>      ''' a message box that can't be closed by the user
>>>      '''
>>>      def __init__(self, parent):
>>> super(WaitMessage, self).__init__(QMessageBox.Information, 'Wait',
>>>              'This is a test, please wait', parent=parent)
>>>          # setting NoButton in the constructor won't be enough, it
>> must
>>> be set explicitly
>>>          # in this way the Escape key won't hide the dialog
>>> self.setStandardButtons(QMessageBox.NoButton)
>>>
>>>      def closeEvent(self, event):
>>>          # ignore any attempt to close the dialog via the title bar
>> buttoni
>>> event.ignore()
>>>
>>> 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()
>>>
>>> With this approach you won't need another function to delete the
>> popup
>>> nor the thread, as they will be deleted in Qt "scope" with
>>> deleteLater, and will be deleted by python followingly, since they're
>>> not instance attributes.
>>> Just ensure that both the popup and the worker have a parent,
>>> otherwise they will be garbage collected as soon as the function
>> returns.
>>> Cheers,
>>> Maurizio
>>>
>>>
>>> Il giorno dom 6 ott 2019 alle ore 18:20 Chuck Rhode
>>> <CRhode at lacusveris.com <mailto:CRhode at lacusveris.com>> ha scritto:
>>>
>>>      -----BEGIN PGP SIGNED MESSAGE-----
>>>      Hash: SHA1
>>>
>>>      In the deep past, I've worked with VB, Tk, Pascal Delphi, and
>> Gtk.  I
>>>      seem to recall that all had non-modal dialog boxes for situations
>>>      where
>>>      you wanted to inform the user through the user interface that a
>>>      long-running task was in progress.  Is this not done anymore?
>>>
>>>      I assume there are still long running tasks such as QPixmap
>> *save*s.
>>>      These tasks cannot emit progress signals for powering a progress
>> bar.
>>>      What alternatives are there?
>>>
>>>      I can't get a cursor change to show up.
>>>
>>>      Does one nowadays throw up a semitransparent overlay with a
>> spinner?
>>>      That is not so simple or informative as a non-modal dialog, I
>> think.
>>>      I've pored over Stackoverflow posts about Qt from the last
>> decade, and
>>>      I don't see a lot that I can use.  Most say its as simple as
>>>      QMessageBox *open* instead of *exec_*, but this has not been my
>>>      experience with PyQt.  Here is code that works, however:
>>>
>>>      #!/usr/bin/python
>>>      # -*- coding: utf-8 -*-
>>>
>>>      # nonmodal_example.py
>>>      # 2019 Oct 06 . ccr
>>>
>>>      """Demonstrate how to show a non-modal dialog box before starting
>> a
>>>      long-running task.
>>>
>>>      """
>>>
>>>      from __future__ import division
>>>      import sys
>>>      from PyQt5.QtWidgets import (
>>>          QApplication,
>>>          QMainWindow,
>>>          QWidget,
>>>          QGridLayout,
>>>          QPushButton,
>>>          QMessageBox,
>>>          )
>>>      from PyQt5.QtGui import (
>>>          QPixmap,
>>>          QTransform,
>>>          )
>>>      from PyQt5.QtCore import(
>>>          QThread,
>>>          )
>>>
>>>      ZERO = 0
>>>      SPACE = ' '
>>>      NULL = ''
>>>      NUL = '\x00'
>>>      NA = -1
>>>
>>>
>>>      class ThdWorker(QThread):
>>>
>>>          """An (arbitrarily) long-running task.
>>>
>>>          """
>>>
>>>          def run(self):
>>>              QThread.sleep(1)
>>>              WIN.pixmap.save('/tmp/nonmodal_test.png')
>>>              return
>>>
>>>
>>>      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 Next Test Scenario',
>>>      self.central_widget)
>>>              self.btn_run.setMaximumSize(200, 30)
>>>              self.btn_run.clicked.connect(self.test_part_1)
>>>              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_part_1(self):
>>>
>>>              """Fork a thread.
>>>
>>>              """
>>>
>>>              self.popup = QMessageBox(QMessageBox.Information, None,
>>>      'This is a test.  Please wait.')
>>>              self.popup.show()
>>>              self.thread = ThdWorker(self)
>>>              self.thread.finished.connect(self.test_part_2)
>>>              self.thread.start()
>>>              return
>>>
>>>          def test_part_2(self):
>>>              self.popup.close()
>>>              del self.popup
>>>              del self.thread
>>>              return
>>>
>>>
>>>      if __name__ == "__main__":
>>>          APP = QApplication(sys.argv)
>>>          WIN = WinMain()
>>>          WIN.show()
>>>          result = APP.exec_()
>>>          sys.exit(result)
>>>
>>>
>>>      # Fin
>>>
>>>      What really gripes me about this example, despite the fact that
>> it
>>>      works for me, is the sleep at the beginning of the thread that
>> starts
>>>      the long running thread.  The sleep is essential.  Although the
>>>      QMessageBox *show* paints its frame, it doesn't have time to
>> paint its
>>>      contents unless the thread pauses before it even gets going.
>>>
>>>      There HAS TO BE a more elegant way to allow non-modal dialogs to
>> paint
>>>      completely.  I've tried lots of different Stackoverflow
>>>      recommendations, and nothing works.  I have a more complete (and
>>>      considerably longer) test suite ready to show you that is
>>>      available upon
>>>      request.
>>>
>>>      - --
>>>      .. Be Seeing You,
>>>      .. Chuck Rhode, Sheboygan, WI, USA
>>>      .. Weather: http://LacusVeris.com/WX
>>>      .. 55° — Wind WSW 10 mph
>>>
>>>      -----BEGIN PGP SIGNATURE-----
>>>      Version: GnuPG v2
>>>
>>>      iEYEARECAAYFAl2aE+IACgkQYNv8YqSjllJHuwCfW+tQv04X3s8e6jE5gWZPqbeN
>>>      kZgAn2nbhXFERp5rmIcEuO6yEvC8+HVF
>>>      =bE0d
>>>      -----END PGP SIGNATURE-----
>>>      _______________________________________________
>>>      PyQt mailing list PyQt at riverbankcomputing.com
>>>      <mailto:PyQt at riverbankcomputing.com>
>>>      https://www.riverbankcomputing.com/mailman/listinfo/pyqt
>>>
>>>
>>>
>>> -- 
>>> È difficile avere una convinzione precisa quando si parla delle
>>> ragioni del cuore. - "Sostiene Pereira", Antonio Tabucchi
>>> http://www.jidesk.net
>>>
>>> _______________________________________________
>>> PyQt mailing list    PyQt at riverbankcomputing.com
>>> https://www.riverbankcomputing.com/mailman/listinfo/pyqt


More information about the PyQt mailing list