0

I have a PyQt5 GUI class that I want to be able to create multiple instances of either from an interactive console or normal run. I need these GUIs to be non-blocking so that they can be used while subsequent code runs.

I've tried calling app.exec__() in separate threads for each GUI like this answer, but the program sometimes crashes as the comment on the answer warned it would:
Run pyQT GUI main app in seperate Thread

And now I'm trying to get the code below working which I made based on this answer:
Run Pyqt GUI main app as a separate, non-blocking process

But when I run it the windows pop and and immediately disappear

import sys
from PyQt5 import QtWidgets, QtGui, QtCore
import time

class MainWindow(QtWidgets.QWidget):
    def __init__(self):
        # call super class constructor
        super(MainWindow, self).__init__()
        # build the objects one by one
        layout = QtWidgets.QVBoxLayout(self)
        self.pb_load = QtWidgets.QPushButton('Load')
        self.pb_clear= QtWidgets.QPushButton('Clear')
        self.edit = QtWidgets.QTextEdit()
        layout.addWidget(self.edit)
        layout.addWidget(self.pb_load)
        layout.addWidget(self.pb_clear)
        # connect the callbacks to the push-buttons
        self.pb_load.clicked.connect(self.callback_pb_load)
        self.pb_clear.clicked.connect(self.callback_pb_clear)

    def callback_pb_load(self):
        self.edit.append('hello world')
    def callback_pb_clear(self):
        self.edit.clear()

def show():
    app = QtWidgets.QApplication.instance()
    if not app:
        app = QtWidgets.QApplication(sys.argv)
    win = MainWindow()
    win.show()

if __name__ == '__main__':
    show()
    show()

EDIT - I don't see how this question is a duplicate. The 'duplicate' questions are only slightly related and don't provide solutions to my problem at all.

I want to be able to create multiple instances of a GUI (MainWindow in my example) by calling the show() function from either an interactive session or script, and I want those windows to stay on my screen while subsequent code is running.

EDIT2 - When I run the code as a script I can do what I want by using multiprocessing, see this demo:
https://www.screencast.com/t/5WvJNVSLm9OR

However I still need help because I want it to also work in interactive Python console sessions, and multiprocessing does not work in that case.

pyjamas
  • 4,608
  • 5
  • 38
  • 70
  • 2
    You don't need separate threads or processes. Just create ***one*** `QApplication`, and then open multiple windows. Obviously you must also call `app.exec_()` when running outside of a console, otherwise the script will end immediately. – ekhumoro Jan 18 '19 at 22:53
  • @ekhumoro Where do I call app.exec__() ? If I add app.exec__() to show() then it blocks until I close the window. I want to be able to call show() twice to open 2 GUI windows and have them both usable while subsequent code runs – pyjamas Jan 18 '19 at 23:02
  • @ekhumoro I want to be able to create as many GUI instances as I want during a console session by repeatedly calling show(), not just exactly 2. And your code still blocks as soon as you call app.exec_(), right? I want subsequent code to run without having to close the GUI first. – pyjamas Jan 18 '19 at 23:16
  • @eyllanesc I don't want to just continue creating widgets. My GUI class is a spreadsheet and the point of it is to let you view Pandas DataFrames in a GUI window while doing data analysis with Pandas. So the GUI needs to be open for me to look at while I'm typing commands into the console. I want my module to be imported, and then I want to be able to call show(df) to pop up a spreadsheet or many and still have the code after it run, whether in an interactive session or a single script. – pyjamas Jan 18 '19 at 23:38
  • @eyllanesc Sorry I don't understand. Your code is all showing up on one line making it hard to read in the comments and I also don't understand how I would incorporate it into my original program. It sounds like you're saying what I want is impossible without running the GUI as a completely separate program, but then you're giving me code anyways? Regardless, I don't think this is a duplicate question. – pyjamas Jan 18 '19 at 23:59
  • @eyllanesc I execute it inside Python scripts and inside the Python interactive terminal within PyCharm and other IDEs. As I described in a comment above, it is a spreadsheet and I import the class then use it to view Pandas DataFrames while I am doing data analysis. Sometimes my data analysis is a premade script that I run, sometimes I am typing one line at a time in the Python console. I left this out because the GUI itself is very complicated and not relevant so I instead gave a minimal working example to simplify my post. Also please see my recent edit – pyjamas Jan 19 '19 at 00:16
  • 1
    I have added an answer which should do what you want. It works fine for me, but I only used a standard console for testing, so I can't guarantee that it will work in all python IDEs. – ekhumoro Jan 19 '19 at 01:21

2 Answers2

2

It isn't necessary to use separate threads or processes for this. You just need a way to maintain a reference to each new window when importing the script in a python interactive session. A simple list can be used for this. It is only necessary to explictly start an event-loop when running the script from the command-line; in an interactive session, it will be handled automatically by PyQt.

Here is an implementation of this approach:

...
_cache = []

def show(title=''):
    if QtWidgets.QApplication.instance() is None:
        _cache.append(QtWidgets.QApplication(sys.argv))
    win = MainWindow()
    win.setWindowTitle(title)
    win.setAttribute(QtCore.Qt.WA_DeleteOnClose)
    win.destroyed.connect(lambda: _cache.remove(win))
    _cache.append(win)
    win.show()

if __name__ == '__main__':

    show('Foo')
    show('Bar')

    sys.exit(QtWidgets.QApplication.instance().exec_())
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • I don't think this solves the problem since still no other code can run while the GUI instances are running. The line `sys.exit(QtWidgets.QApplication.instance().exec_())` is blocking. If I use your code in an iPython session the windows freeze immediately. If I import to a normal console session they don't freeze and allow me to continue typing, but they freeze during processing. I need the GUIs to remain functional while other code is running – pyjamas Jan 19 '19 at 09:38
  • Here is what happens with yours: https://www.screencast.com/t/cqU8fH6w And this is what I want it to do (but this solution uses multiprocessing which doesn't work in interactive): https://www.screencast.com/t/rM0usLfc – pyjamas Jan 19 '19 at 09:38
  • 2
    @Esostack It's really unacceptable for you to keep adding extra requirements like this. My answer works perfectly according to what you asked for in your question: "I want to be able to create multiple instances of a GUI (MainWindow in my example) by calling the show() function from either an interactive session or script, and I want those windows to stay on my screen while subsequent code is running". Please rewrite your whole question so that it specifies ***all*** your requirements in detail. People shouldn't have to wade through a bunch of long comments to understand what you want. – ekhumoro Jan 19 '19 at 18:01
  • I'm not adding additional requirements or being unclear. I even gave a video demo of exactly what I want. You really think I'm moving the goalposts because I don't consider THIS to be "non-blocking" and "staying on my screen"? https://www.screencast.com/t/aUjAW9MupynQ There's nothing in the comments that isn't stated in my question or title. Anyways... I've solved it now using the multiprocess library from pathos but I'm not sure how stable it is, I'll post it if nobody has a better solution. – pyjamas Jan 19 '19 at 19:27
  • Hey so I'm coming back to this 7 months later because I just learned that windows becoming unresponsive during interactive sessions was a problem specific to IPython. My confusion was that I thought you were implying them freezing was expected and that it met my requirement of "staying on the screen"! – pyjamas Sep 15 '19 at 03:02
1

This is a minor addendum to @ekhumoro's answer. I don't have enough reputation to only add a comment so I had to write this as an answer.

@ekhumoro's answer almost fully answers @Esostack's question, but doesn't work in the Ipython console. After many hours of searching for the answer to this question myself, I came across a comment from @titusjan in a three year old thread (here) also responding to a good answer from @ekhumoro. The missing part to @ekhumoro's answer which results in the gui windows freezing for Ipython specifically is that Ipython should be set to use the qt gui at launch or once running.


To make this work with Ipython:

Launch Ipython with ipython --gui=qt5

In a running Ipython console run the magic command %gui qt5

To fix it from a Python script you can run this function

def fix_ipython():
    from IPython import get_ipython

    ipython = get_ipython()
    if ipython is not None:
        ipython.magic("gui qt5")
pyjamas
  • 4,608
  • 5
  • 38
  • 70
Tim Child
  • 339
  • 1
  • 11
  • Ah yes, I actually discovered this on my own project but forgot to post about it here. Thanks for the addition. – pyjamas Jul 31 '20 at 19:39
  • 1
    Thanks for the edit @Esostack, I think it's nice to see the full range of solutions in one go, and it actually answers another question I had (but hadn't asked yet). Because `from PyQt5 import QtWebEngineWidgets` has to be run *before* activating the `qt5` gui support in IPython (as it has to be imported before a `QCoreApplication` instance is created), so this is perfect to run after imports. As a side note - `ipython.enable_gui('qt5')` also works in place of 'ipython.magic("gui qt5")` but I don't know that there is any advantage of one over the other. – Tim Child Aug 01 '20 at 23:16