qcha
qcha

Reputation: 583

How to control/drive/interact a PyQt GUI from command line in Python

I have a GUI application created with PyQt and I would like to be able to control it also from the python terminal through a kind of internal API.

Ideas :

I don't want an 'in-app' terminal.

Do you have any other ideas ?

Upvotes: 2

Views: 2959

Answers (2)

titusjan
titusjan

Reputation: 5546

Using the main terminal : impossible since that it is blocked by the QApplication (by app.exec_())

You can omit app.exec() if you are within a Python terminal and it will not be blocking. As explained here this works because...

PyQt5 installs an input hook (using PyOS_InputHook) that processes events when an interactive interpreter is waiting for user input. This means that you can, for example, create widgets from the Python shell prompt, interact with them, and still being able to enter other Python commands.

For example, enter the following in the Python shell to have a working Qt widget and a non-blocking REPL simultaniously.

$> python
Python 3.7.6 | packaged by conda-forge | (default, Jan  7 2020, 22:05:27)
[Clang 9.0.1 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from PyQt5.QtWidgets import QApplication, QWidget
>>> a = QApplication([])
>>> w = QWidget()
>>> w.show()
>>> w.raise_()

IPython has similar functionality. If you start it with ipython --gui=qt, or type %gui qt in the terminal you get the same effect...

$> ipython
Python 3.7.6 | packaged by conda-forge | (default, Jan  7 2020, 22:05:27)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.11.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: %gui qt

In [2]: from PyQt5 import QtWidgets

In [3]: win = QtWidgets.QPushButton("click me")

In [4]: win.show()

In [5]: win.raise_()

I recommend to use IPython because it's better suited for interactive work and it will work with PySide (perhaps regular Python and PySide will also work; I didn't check).

See also my earlier answer here

Finally, even though this works I don't know how good the performance is. It's a good solution for a hobby project but if you have many users I would consider to implement an 'in-app' terminal or some form of inter-process communication.

Upvotes: 3

eyllanesc
eyllanesc

Reputation: 244282

Depending on what you seem to ask, you want to implement something similar to Native Messaging Protocol(Chrome, Mozilla), if so, then you should use QWinEventNotifier or QSocketNotifier depending on the OS to detect if it was written on the console.

Based on my previous answer I created the following example where the user writes in the console some phrase and press Enter then that phrase is shown in the QLabel (I have only tested my example in Linux).

import platform
import sys

from PyQt5 import QtCore, QtGui, QtWidgets


class NativeMessenger(QtCore.QObject):
    messageChanged = QtCore.pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)

        self.m_qin = QtCore.QFile()

        self.m_qin.open(
            sys.stdin.fileno(), QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Unbuffered
        )

        if platform.system() == "Windows":
            import win32api

            if sys.platform == "win32":
                import os
                import msvcrt

                if platform.python_implementation() == "PyPy":
                    os.fdopen(fh.fileno(), "wb", 0)
                else:
                    msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)

            self.m_notifier = QtCore.QWinEventNotifier(
                win32api.GetStdHandle(win32api.STD_INPUT_HANDLE)
            )

        else:
            self.m_notifier = QtCore.QSocketNotifier(
                sys.stdin.fileno(), QtCore.QSocketNotifier.Read, self
            )

        self.m_notifier.activated.connect(self.readyRead)

    @QtCore.pyqtSlot()
    def readyRead(self):
        line = self.m_qin.readLine().data().decode().strip()
        self.messageChanged.emit(line)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)

    w = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
    w.resize(640, 480)
    w.show()

    messenger = NativeMessenger()
    messenger.messageChanged.connect(w.setText)

    sys.exit(app.exec_())

Output:

Stack Overflow

enter image description here

The above can be taken as the basis for implementing your API.


Although another approach is to have 2 applications where a CLI controls the GUI by communicating through sockets and other protocols such as IPC (dbus, etc.), ZeroMQ, MQTT, etc.

Upvotes: 2

Related Questions