Qt windows created within imported functions won't show() #505

Closed
mspacek opened this Issue Jun 6, 2011 · 11 comments

Comments

Projects
None yet
2 participants
Contributor

mspacek commented Jun 6, 2011

I've written a library that generates the occasional plot, sometimes via matplotlib, sometimes in its own custom Qt window. When I use this library from within ipython (whether terminal or qtconsole), the mpl windows come up fine, but the custom Qt windows don't show, and no error is raised.

Perhaps this is a known limitation. It almost seems like a scope problem. Is there some kind of hook thing I need to do to mimic mpl's behaviour? I'm not at all well enough acquainted with the zmq and multiprocess stuff to say. Here's a minimal example that demonstrates the issue:

"""
test_window.py

run 'ipython-qtconsole --pylab=qt',
or 'ipython --pylab=qt',
then type 'import test_window'
"""
from PyQt4 import QtGui
from pylab import plot

class TestWindow(QtGui.QMainWindow):
    def __init__(self, parent=None, n=0):
        QtGui.QMainWindow.__init__(self)
        self.setWindowTitle('Test window %d' %n)

def test():
    testwin1 = TestWindow(n=1)
    testwin1.show()
    plot(range(10))
test() # testwin1 isn't shown, no error, plot is shown

testwin2 = TestWindow(n=2)
testwin2.show() # testwin2 is shown
Contributor

mspacek commented Jun 6, 2011

I think I've found a solution. It seems that a function (in this case, test()) must return the window object for it to actually show up when run from within ipython. And, if you've created multiple windows, you need to return all of them. Here's an example that shows this:

"""
test_window.py

run 'ipython-qtconsole --pylab=qt',
or 'ipython --pylab=qt',
then type 'from test_window import test; test()'
"""
from PyQt4 import QtGui

class TestWindow(QtGui.QMainWindow):
    def __init__(self, parent=None, n=0):
        QtGui.QMainWindow.__init__(self)
        self.setWindowTitle('Test window %d' %n)

def test():
    testwin1 = TestWindow(n=1)
    testwin1.show()
    testwin2 = TestWindow(n=2)
    testwin2.show()
    #return testwin1 # only testwin1 will be shown
    return testwin1, testwin2 # both will be shown

I guess this is some sort of a return payload thing from the kernel. If this is expected behaviour, is it documented somewhere? Sorry if this is noise, but I'm quite surprised by this behaviour, and glad that I've figured it out.

mspacek closed this Jun 6, 2011

mspacek reopened this Jun 6, 2011

Contributor

mspacek commented Jun 7, 2011

On second thought, this might be a reference counting issue. It looks like if the window object isn't returned or otherwise bound to some other long lived object, it's garbage collected before ipython has a chance to actually display it. Perhaps this has to do with the QApplication object living in the frontend (I think?) while the window is created and subsequently immediately destroyed on test() function exit in the kernel. Another example:

"""
test_window.py

run 'ipython-qtconsole --pylab=qt',
or 'ipython --pylab=qt',

then type:

from test_window import test, Container
c = Container()
test(c)

"""
from PyQt4 import QtGui

class TestWindow(QtGui.QMainWindow):
    def __init__(self, parent=None, n=0):
        QtGui.QMainWindow.__init__(self)
        self.setWindowTitle('Test window %d' %n)

class Container(object):
    def __init__(self):
        pass

def test(c):
    testwin1 = TestWindow(n=1)
    testwin1.show()
    testwin2 = TestWindow(n=2)
    testwin2.show()
    c.testwin1 = testwin1
    return testwin2
    # both testwin1 and testwin2 will be shown
Owner

minrk commented Jun 12, 2011

The kernel backend has no relationship with Qt (unless you are using the Qt matplotlib backend). The frontend is not in the same process as any user code, so there won't be a QApplication in the process where your code is executed unless you create it.

I think you are correct that the windows are not drawn because their references are discarded and they get destroyed on garbage collection. You should probably create the whole Application object if you want to launch Qt things from your code.

Owner

minrk commented Jun 30, 2011

Closing this as 'not an IPython issue'. There is some confusion about the fact that since the GUI is Qt, a QtApplication is running in the kernel, which is not the case. The only connection between the process where user code executes and the GUI frontend is zeromq communication. You can happily have a Wx backend with a Qt frontend if you felt like it.

Contributor

mspacek commented Jun 30, 2011

Sorry, I meant to reply to this sooner. I am indeed using the mpl Qt backend, and that's how the above examples were tested. In my ipython embedding Qt app, I call kernel_manager.start_kernel(pylab='qt') in the main qt window init, and in the if __name__ == "__main__" section of my main.py I create a QApplication. Then, via the ipython qt widget, the user imports other modules as needed and creates objects/calls functions. For those imported objects/functions that create a Qt window, it seems the window handle itself must be returned or bound to some long lived object that already exists in the kernel, otherwise the window never appears. Originally, I thought this meant that the window created in the imported module simply didn't have access to the QApplication running the main window with its ipython widget. So I tried creating another QApplication within the imported module, which didn't work, raising some kind of error (I forget the details, perhaps a complaint that a QApplication was already running).

What I wanted to do didn't seem possible, when really I think all that's required is to ensure the kernel gets a reference to the window in some way, to keep it from getting garbage collected on function exit.

I found all of this quite surprising, and it took me a long long time to figure out. Can this be added to the documentation somewhere, perhaps as an embedding example?

Owner

minrk commented Jun 30, 2011

The kernel cannot get a reference to the Console window, because it is not in the same process. That is impossible. Your Qt universe that you might create in the kernel (which would have to play nice with matplotlib if you are using it) cannot ever know anything about the Qt universe that is running the Console widget.

Unless I'm misunderstanding what you are trying to do.

Contributor

mspacek commented Jun 30, 2011

Well, I guess I don't really know what is and isn't running in the kernel. Just to clarify, I'm not accessing the Qt console window itself from within my user imported code. I'm creating new windows and calling show() in the user imported code, and returning the window object, so the user has a reference to it (is this equivalent to saying the kernel has a reference to it?). BTW, by user imported code, I mean the user typing something in the ipython widget like:

from foo import make_and_show_window
win = make_and_show_window() # window pops up
win.hide() # window disappears
win.show() # window reappears

Where foo.py merely defines and instantiates a QtWidget of some kind, but doesn't create a QApplication. Again, just to be clear, this is typed into an ipython qt widget which starts a kernel with pylab=qt, embedded in some kind of custom QtMainWindow (aka qt console window), in a main.py which creates its own QApplication. The above code works as expected. Should it not work?

Is there anything surprising about the first 3 examples I posted? Can you maybe explain why or how they work?

Owner

minrk commented Jun 30, 2011

Ah, sorry, I guess I did misunderstand. You aren't trying to get access to the same Qt environment in both the frontend app and the kernel. That's what can't work. The important point about the two-process QtConsole is that the Qt-ness of the frontend has exactly nothing to do with an Qt-code in the kernel. The kernel, under most circumstances, has no Qt code running at all. The pylab qt backend is the principle exception.

It does appear to be true that when you write a Qt app, you need to have references to Qt objects, because Python garbage collection of the reference causes destruction of the Qt object itself.

So if you have a function that creates a Window, you need to either attach it to something (e.g. the QApplication instance you have created) or return it so the user keeps track of the references however they like.

To get access to the current application, you can do:

from PyQt4 import QtCore
app = QtCore.QCoreApplication.instance()

and you can tack all your windows on to it so they don't get cleaned up with:

app.windows = []
# make a window
win = TestWindow()
app.windows.append(win)
Contributor

mspacek commented Jun 30, 2011

OK, so when running the qt frontend with pylab=qt, two QApplications are running: one in the frontend process, and one in the kernel process.

Thanks for the app.windows.append() tip. I might just do that. Can it be added to the docs somewhere before closing this issue?

minrk closed this in a0eb0c0 Jun 30, 2011

Contributor

mspacek commented Jun 30, 2011

Wow, that's fantastic! Thanks!

@jenshnielsen jenshnielsen pushed a commit to jenshnielsen/ipython that referenced this issue Jul 1, 2011

@minrk minrk add note about working with qt in two-process qtconsole
closes gh-505
bb94981

@mattvonrocketstein mattvonrocketstein pushed a commit to mattvonrocketstein/ipython that referenced this issue Nov 3, 2014

@minrk minrk add note about working with qt in two-process qtconsole
closes gh-505
4eff78f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment