[PyQt] PyQt4 interactive on Windows and Unix without readline
Michiel De Hoon
mdehoon at c2b2.columbia.edu
Sat Jul 7 09:23:15 BST 2007
Hi everybody,
I have been happily using PyQt4 for scientific visualization. For this
purpose, the recent ability of PyQt to handle Python commands interactively,
as described in
http://www.riverbankcomputing.com/Docs/PyQt4/pyqt4ref.html#using-pyqt-from-th
e-python-shell
is absolutely marvelous. Unfortunately, interactivity is broken if Python
does not have the readline module (for example on Windows), causing PyQT
windows to freeze. Below I suggest a modification to PyQt that fixes this bug
on Windows, as well as on Unix-like systems without readline. The suggested
code also improves the performance on Unix-like systems that do have the
readline module. From reading the mailing list, I believe it is the
appropriate place to submit PyQt patches; please let me know if it is not.
PyQt4 handles GUI messages while Python is waiting for user commands by
letting PyOS_InputHook point to the qtcore_input_hook function in
QtCore/sipQtCorecmodule.cpp. The qtcore_input_hook function handles all
existing GUI events by calling QCoreApplication::processEvents(), and returns
immediately after that:
static int qtcore_input_hook()
{
QCoreApplication::processEvents();
return 0;
}
On Python *with* the readline module, PyOS_InputHook and therefore
QCoreApplication::processEvents() is called periodically (ten times per
second) as long as no new input is available on stdin. See Modules/readline.c
in the Python distribution for details; the exact code depends on which
version of the readline library is used. So in the current implementation,
interactivity works as follows:
Handle events with QCoreApplication::processEvents()
Wait 0.1 seconds
Handle events with QCoreApplication::processEvents()
Wait 0.1 seconds
...
breaking out of this loop once the next Python command is available on stdin.
On Python *without* the readline module, for example on Windows,
PyOS_InputHook is called only once, just before reading input from stdin. So
now we have:
Handle events with QCoreApplication::processEvents()
Wait for input on stdin
While waiting for input on stdin, no GUI events are handled. The PyQT windows
then freeze until the user types in the next Python command. If you want to
see this behavior on a Python with the readline module, temporarily rename
readline.so in /usr/lib/python2.5/lib-dynload/ (exact file name and path may
be different on your system) and restart Python. You will have a Python
without readline. If you then create a QPushButton in a PyQT window, it will
not react to mouse clicks (until you enter the next Python command).
The cleanest solution to this bug would be to use the same approach as
Tkinter, which is to stay in the GUI event loop until the next Python command
is available on stdin. Ideally, one would create a QFile for stdin, and use
the readyRead signal. Unfortunately, currently in Qt, QFile does not emit the
readyRead signal (see http://doc.trolltech.com/4.3/qfile.html).
On Unix-like systems, as a workaround we can use a QSocketNotifier on file
descriptor 0 (stdin); on Unix-like systems, QSocketNotifier doesn't care if
the file descriptor is actually a socket. The following replacement for
qtcore_input_hook then fixes the bug:
static int qtcore_input_hook()
{
QCoreApplication* app = QCoreApplication::instance();
if (!app) return 0;
QSocketNotifier notifier(0, QSocketNotifier::Read, 0);
QObject::connect(¬ifier, SIGNAL(activated(int)), app, SLOT(quit()));
QCoreApplication::exec();
QObject::disconnect(¬ifier, SIGNAL(activated(int)), app,
SLOT(quit()));
return 0;
}
So, on the first call to PyOS_InputHook, we enter QCoreApplication::exec()
and stay there until we get the SIGNAL(activated) for stdin. At that point,
we quit QCoreApplication::exec() and return to Python to handle the next
Python command. This guarantees that Qt events continue to be handled.
Note that this replacement for qtcore_input_hook also gives better
performance on Python with the readline module, as it avoids the
ten-times-per-second loop above.
On Windows, unfortunately, this won't work, since on Windows sockets are
inherently different from the console's stdin. So on Windows, we have to do
use a loop, for example as follows:
static int qtcore_input_hook()
{
QCoreApplication::processEvents();
QCoreApplication* app = QCoreApplication::instance();
if (!app) return 0;
while (1)
{
QTimer timer;
QObject::connect(&timer, SIGNAL(timeout()), app, SLOT(quit()));
timer.start(100);
QCoreApplication::exec();
timer.stop();
QObject::disconnect(&timer, SIGNAL(timeout()), app, SLOT(quit()));
if (_kbhit()) return 0;
}
}
The _kbhit function is a Windows-specific function that returns TRUE if input
is available for reading from stdin. The _kbhit function is in conio.h, which
should be #included. This code essentially does the ten-times-per-second loop
that is currently being used with readline. If in some future version of Qt
we can get a SIGNAL on stdin using QFile, then we can use that instead of
this loop to get better performance.
Unfortunately, I am not very familiar with sip, and I don't feel confident
that I can write a patch against sip/QtCore/qcoreapplication.sip without
making errors due to my inexperience with sip. I hope that the suggested code
as replacement for qtcore_input_hook() will nevertheless be considered as a
solution to get interactivity to work appropriately on Windows and
readline-less Unices.
Best wishes, and thank you for PyQT,
--Michiel.
Michiel de Hoon
Center for Computational Biology and Bioinformatics
Columbia University
1150 St Nicholas Avenue
New York, NY 10032
More information about the PyQt
mailing list