Python Forum
Embed Python console in GUI application
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Embed Python console in GUI application
#1
I am trying to figure out how best to provide access to a python interpreter from inside a GUI based program. The GUI is the interface for a process controller. Starting the GUI can take a long time because it involves booting multiple subsystems and turning on machinery in the proper sequence with lots of waiting for things to complete. If something goes wrong I would like a way to peek around inside the controller and maybe even try a patch before shutting things down and having to restart.

Looking for "embedded console", "gui console", "interactive interpreter", "python terminal" has pointed me to code.InteractiveConsole which is exactly what I want, except I don't see how I can hook this to a text editor widget inside the GUI. I am using Qt Widgets (PySide2), so QtConsole popped up, but that is about 100 times more powerful than I need and can I even use it outside of Jupyter?

I'd appreciate any suggestions or paths to pursue in my investigations. If you think my approach to solving this problem is off track I would appreciate hearing that too. I'm a C, C++, C# programmer and a lot of Python catches me off guard. Maybe I am completely backward (again) and I should be thinking of using a tool outside my GUI application instead of trying to shoehorn one into it.
Reply
#2
I made quite a lot of headway. I have a way of entering python code in a GUI control that works much line code.InteractiveConsole and I redirect stdout and stderr so they appear in my GUI:
import sys
import code
import PySide2.QtWidgets as QtWidgets
import PySide2.QtCore as QtCore
import PySide2.QtGui as QtGui
from contextlib import redirect_stdout, redirect_stderr

class Console(QtWidgets.QWidget):
<snip>
    def write(self, line: str):
        self.writeoutput(line, self.outfmt)

    def writeoutput(self, line: str, fmt: QtGui.QTextCharFormat=None) -> None:
        if fmt is not None:
            self.outdisplay.setCurrentCharFormat(fmt)
        self.outdisplay.appendPlainText(line.rstrip())

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    console = Console()
    <snip>
    with redirect_stdout(console), redirect_stderr(console):
        console.show()
        sys.exit(app.exec_())
The program creates a format for characters typed as input (black) and another for captured output (blue). I would like a third format (red) for captured stderr. How do I differentiate stdout from stderr?

One idea I have is creating a redirect class that calls another function when it's write method gets called. I added an errorwrite method to my console class and create a redirect object to call that method. Then I redirect stderr to my redirect object instead of the console

class Redirect():
    def __init__(self, func):
        self.func = func

    def write(self, line:str):
        self.func(line)

class Console(QtWidgets.QWidget):
<snip>
    def errorwrite(self, line: str):
        self.writeoutput(line, self.errfmt)

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    console = Console()
    redirect = Redirect(console.errorwrite)
    with redirect_stdout(console), redirect_stderr(redirect):
        console.show()
        sys.exit(app.exec_())
This works, but is there a better way?

Also, at what point is it polite to provide code snippets instead of enough code for a working example. My console.py file is 160 lines long and there isn't much fat to remove and still have it work.
Reply
#3
If anyone is interested here's a working solution. Happy to hear suggestions for improvement.
""" Console
Interactive console widget.  Use to add an interactive python interpreter
in a GUI application.
"""

import sys
import code
import re
from typing import Dict, Callable
import PySide2.QtWidgets as QtWidgets
import PySide2.QtCore as QtCore
import PySide2.QtGui as QtGui
from contextlib import redirect_stdout, redirect_stderr

class LineEdit(QtWidgets.QLineEdit):
    """QLIneEdit with a history buffer for recalling previous lines.
    I also accept tab as input (4 spaces).
    """
    newline = QtCore.Signal(str)    # Signal when return key pressed

    def __init__(self, history: int=100) -> 'LineEdit':
        super().__init__()
        self.historymax = history
        self.clearhistory()
        self.promptpattern = re.compile('^[>\.]')
    
    def clearhistory(self) -> None:
        """Clear history buffer"""
        self.historyindex = 0
        self.historylist = []

    def event(self, ev: QtCore.QEvent) -> bool:
        """Intercept tab and arrow key presses.  Insert 4 spaces
        when tab pressed instead of moving to next contorl.  WHen
        arrow up or down are pressed select a line from the history
        buffer.  Emit newline signal when return key is pressed.
        """
        if ev.type() == QtCore.QEvent.KeyPress:
            if ev.key() == int(QtCore.Qt.Key_Tab):
                self.insert('    ')
                return True
            elif ev.key() == int(QtCore.Qt.Key_Up):
                self.recall(self.historyindex-1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Down):
                self.recall(self.historyindex+1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Home):
                self.recall(0)
                return True
            elif ev.key() == int(QtCore.Qt.Key_End):
                self.recall(len(self.historylist)-1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Return):
                self.returnkey()
                return True
        return super().event(ev)

    def returnkey(self) -> None:
        """Return key was pressed.  Add line to history and emit
        the newline signal.
        """
        text = self.text().rstrip()
        self.record(text)
        self.newline.emit(text)
        self.setText('')

    def recall(self, index: int) -> None:
        """Select a line from the history list"""
        length = len(self.historylist)
        if (length > 0):
            index = max(0, min(index, length-1))
            self.setText(self.historylist[index])
            self.historyindex = index

    def record(self, line:str) -> None:
        """Add line to history buffer"""
        self.historyindex += 1
        while len(self.historylist) >= self.historymax-1:
            self.historylist.pop()
        self.historylist.append(line)
        self.historyindex = min(self.historyindex, len(self.historylist))


class Redirect():
    """Map self.write to a function"""
    def __init__(self, func: Callable) -> 'Redirect':
        self.func = func

    def write(self, line:str) -> None:
        self.func(line)


class Console(QtWidgets.QWidget):
    """A GUI version of code.InteractiveConsole."""
    
    def __init__(
            self,
            context = locals(), # context for interpreter
            history: int=20,    # max lines in history buffer
            blockcount: int=500 # max lines in output buffer
            ) -> 'Console':
        super().__init__()
        self.setcontext(context)
        self.buffer = []
        
        self.content = QtWidgets.QGridLayout(self)
        self.content.setContentsMargins(0,0,0,0)
        self.content.setSpacing(0)

        # Display for output and stderr
        self.outdisplay = QtWidgets.QPlainTextEdit(self)
        self.outdisplay.setMaximumBlockCount(blockcount)
        self.outdisplay.setReadOnly(True)
        self.content.addWidget(self.outdisplay, 0, 0, 1, 2)

        # Use color to differentiate input, output and stderr
        self.inpfmt = self.outdisplay.currentCharFormat()
        self.outfmt = QtGui.QTextCharFormat(self.inpfmt)
        self.outfmt.setForeground(QtGui.QBrush(QtGui.QColor(0, 0, 255)))
        self.errfmt = QtGui.QTextCharFormat(self.inpfmt)
        self.errfmt.setForeground(QtGui.QBrush(QtGui.QColor(255, 0, 0)))

        # Display input prompt left of input edit
        self.promptdisp = QtWidgets.QLineEdit(self)
        self.promptdisp.setReadOnly(True)
        self.promptdisp.setFixedWidth(15)
        self.promptdisp.setFrame(False)
        self.content.addWidget(self.promptdisp, 1, 0)
        self.setprompt('> ')

        # Enter commands here
        self.inpedit = LineEdit(history = history)
        self.inpedit.newline.connect(self.push)
        self.inpedit.setFrame(False)
        self.content.addWidget(self.inpedit, 1, 1)

    def setcontext(self, context):
        """Set context for interpreter"""
        self.interp = code.InteractiveInterpreter(context)

    def resetbuffer(self) -> None:
        """Reset the input buffer."""
        self.buffer = []

    def setprompt(self, text: str):
        self.prompt = text
        self.promptdisp.setText(text)

    def push(self, line: str) -> None:
        """Execute entered command.  Command may span multiple lines"""
        if line == 'clear':
            self.inpedit.clearhistory()
            self.outdisplay.clear()
        else:
            lines = line.split('\n')
            for line in lines:
                if re.match('^[\>\.] ', line):
                    line = line[2:]
                self.writeoutput(self.prompt+line, self.inpfmt)
                self.setprompt('. ')
                self.buffer.append(line)
            # Built a command string from lines in the buffer
            source = "\n".join(self.buffer)
            more = self.interp.runsource(source, '<console>')
            if not more:
                self.setprompt('> ')
                self.resetbuffer()

    def setfont(self, font: QtGui.QFont) -> None:
        """Set font for input and display widgets.  Should be monospaced"""
        self.outdisplay.setFont(font)
        self.inpedit.setFont(font)

    def write(self, line: str) -> None:
        """Capture stdout and display in outdisplay"""
        if (len(line) != 1 or ord(line[0]) != 10):
            self.writeoutput(line.rstrip(), self.outfmt)

    def errorwrite(self, line: str) -> None:
        """Capture stderr and display in outdisplay"""
        self.writeoutput(line, self.errfmt)

    def writeoutput(self, line: str, fmt: QtGui.QTextCharFormat=None) -> None:
        """Set text formatting and display line in outdisplay"""
        if fmt is not None:
            self.outdisplay.setCurrentCharFormat(fmt)
        self.outdisplay.appendPlainText(line.rstrip())

      
if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    console = Console()
    console.setWindowTitle('Console')
    console.setfont(QtGui.QFont('Lucida Sans Typewriter', 10))

    # Redirect stdout to console.write and stderr to console.errorwrite
    redirect = Redirect(console.errorwrite)
    with redirect_stdout(console), redirect_stderr(redirect):
        console.show()
        sys.exit(app.exec_())
Gribouillis likes this post
Reply
#4
hey, I want to thank you for this awesome code, I have been banging my head on how to embed a console in pyqt for a while now. I modified it to use pyqt5, changed 1 line of code other than the imports and want to post it here for anyone else who wants to use pyqt5.

""" Console
Interactive console widget.  Use to add an interactive python interpreter
in a GUI application.

Original by deanhystad available here: https://python-forum.io/thread-25117.html
changes: PySide2 -> pyqt5
            + line 22: QtCore.Signal(str) -> QtCore.pyqtSignal(str)
"""

import sys
import code
import re
from typing import Callable
from PyQt5 import QtCore, QtGui, QtWidgets
from contextlib import redirect_stdout, redirect_stderr


class LineEdit(QtWidgets.QLineEdit):
    """QLIneEdit with a history buffer for recalling previous lines.
    I also accept tab as input (4 spaces).
    """
    newline = QtCore.pyqtSignal(str)  # Signal when return key pressed

    def __init__(self, history: int = 100) -> 'LineEdit':
        super().__init__()
        self.historymax = history
        self.clearhistory()
        self.promptpattern = re.compile('^[>\.]')

    def clearhistory(self) -> None:
        """Clear history buffer"""
        self.historyindex = 0
        self.historylist = []

    def event(self, ev: QtCore.QEvent) -> bool:
        """Intercept tab and arrow key presses.  Insert 4 spaces
        when tab pressed instead of moving to next contorl.  WHen
        arrow up or down are pressed select a line from the history
        buffer.  Emit newline signal when return key is pressed.
        """
        if ev.type() == QtCore.QEvent.KeyPress:
            if ev.key() == int(QtCore.Qt.Key_Tab):
                self.insert('    ')
                return True
            elif ev.key() == int(QtCore.Qt.Key_Up):
                self.recall(self.historyindex - 1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Down):
                self.recall(self.historyindex + 1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Home):
                self.recall(0)
                return True
            elif ev.key() == int(QtCore.Qt.Key_End):
                self.recall(len(self.historylist) - 1)
                return True
            elif ev.key() == int(QtCore.Qt.Key_Return):
                self.returnkey()
                return True
        return super().event(ev)

    def returnkey(self) -> None:
        """Return key was pressed.  Add line to history and emit
        the newline signal.
        """
        text = self.text().rstrip()
        self.record(text)
        self.newline.emit(text)
        self.setText('')

    def recall(self, index: int) -> None:
        """Select a line from the history list"""
        length = len(self.historylist)
        if length > 0:
            index = max(0, min(index, length - 1))
            self.setText(self.historylist[index])
            self.historyindex = index

    def record(self, line: str) -> None:
        """Add line to history buffer"""
        self.historyindex += 1
        while len(self.historylist) >= self.historymax - 1:
            self.historylist.pop()
        self.historylist.append(line)
        self.historyindex = min(self.historyindex, len(self.historylist))


class Redirect:
    """Map self.write to a function"""

    def __init__(self, func: Callable) -> 'Redirect':
        self.func = func

    def write(self, line: str) -> None:
        self.func(line)


class Console(QtWidgets.QWidget):
    """A GUI version of code.InteractiveConsole."""

    def __init__(
            self,
            context=locals(),  # context for interpreter
            history: int = 20,  # max lines in history buffer
            blockcount: int = 500  # max lines in output buffer
    ) -> 'Console':
        super().__init__()
        self.setcontext(context)
        self.buffer = []

        self.content = QtWidgets.QGridLayout(self)
        self.content.setContentsMargins(0, 0, 0, 0)
        self.content.setSpacing(0)

        # Display for output and stderr
        self.outdisplay = QtWidgets.QPlainTextEdit(self)
        self.outdisplay.setMaximumBlockCount(blockcount)
        self.outdisplay.setReadOnly(True)
        self.content.addWidget(self.outdisplay, 0, 0, 1, 2)

        # Use color to differentiate input, output and stderr
        self.inpfmt = self.outdisplay.currentCharFormat()
        self.outfmt = QtGui.QTextCharFormat(self.inpfmt)
        self.outfmt.setForeground(QtGui.QBrush(QtGui.QColor(0, 0, 255)))
        self.errfmt = QtGui.QTextCharFormat(self.inpfmt)
        self.errfmt.setForeground(QtGui.QBrush(QtGui.QColor(255, 0, 0)))

        # Display input prompt left of input edit
        self.promptdisp = QtWidgets.QLineEdit(self)
        self.promptdisp.setReadOnly(True)
        self.promptdisp.setFixedWidth(15)
        self.promptdisp.setFrame(False)
        self.content.addWidget(self.promptdisp, 1, 0)
        self.setprompt('> ')

        # Enter commands here
        self.inpedit = LineEdit(history=history)
        self.inpedit.newline.connect(self.push)
        self.inpedit.setFrame(False)
        self.content.addWidget(self.inpedit, 1, 1)

    def setcontext(self, context):
        """Set context for interpreter"""
        self.interp = code.InteractiveInterpreter(context)

    def resetbuffer(self) -> None:
        """Reset the input buffer."""
        self.buffer = []

    def setprompt(self, text: str):
        self.prompt = text
        self.promptdisp.setText(text)

    def push(self, line: str) -> None:
        """Execute entered command.  Command may span multiple lines"""
        if line == 'clear':
            self.inpedit.clearhistory()
            self.outdisplay.clear()
        else:
            lines = line.split('\n')
            for line in lines:
                if re.match('^[\>\.] ', line):
                    line = line[2:]
                self.writeoutput(self.prompt + line, self.inpfmt)
                self.setprompt('. ')
                self.buffer.append(line)
            # Built a command string from lines in the buffer
            source = "\n".join(self.buffer)
            more = self.interp.runsource(source, '<console>')
            if not more:
                self.setprompt('> ')
                self.resetbuffer()

    def setfont(self, font: QtGui.QFont) -> None:
        """Set font for input and display widgets.  Should be monospaced"""
        self.outdisplay.setFont(font)
        self.inpedit.setFont(font)

    def write(self, line: str) -> None:
        """Capture stdout and display in outdisplay"""
        if len(line) != 1 or ord(line[0]) != 10:
            self.writeoutput(line.rstrip(), self.outfmt)

    def errorwrite(self, line: str) -> None:
        """Capture stderr and display in outdisplay"""
        self.writeoutput(line, self.errfmt)

    def writeoutput(self, line: str, fmt: QtGui.QTextCharFormat = None) -> None:
        """Set text formatting and display line in outdisplay"""
        if fmt is not None:
            self.outdisplay.setCurrentCharFormat(fmt)
        self.outdisplay.appendPlainText(line.rstrip())


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    console = Console()
    console.setWindowTitle('Console')
    console.setfont(QtGui.QFont('Lucida Sans Typewriter', 10))

    # Redirect stdout to console.write and stderr to console.errorwrite
    redirect = Redirect(console.errorwrite)
    with redirect_stdout(console), redirect_stderr(redirect):
        console.show()
        sys.exit(app.exec_())
Gribouillis likes this post
Reply
#5
If you can use Gtk

[Image: pyterminal.png?raw=1]

import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
gi.require_version("Vte", "2.91")
from gi.repository import Gtk, Vte, Gdk
from gi.repository import GLib


class Terminal(Vte.Terminal):

    def __init__(self):
        super(Vte.Terminal, self).__init__()

        self.spawn_async(Vte.PtyFlags.DEFAULT, 
            "/tmp",
            ["/bin/bash"],
            None,
            GLib.SpawnFlags.DO_NOT_REAP_CHILD,
            None,
            None,
            -1,
            None,
            None
            )
            

        self.set_font_scale(0.9)
        self.set_scroll_on_output(True)
        self.set_scroll_on_keystroke(True)
        palette = [Gdk.RGBA(0.4, 0.8, 1.0, 1.0)] * 16
        self.set_colors(Gdk.RGBA(1.0, 1.0, 1.0, 1.0), Gdk.RGBA(0.3, 0.3, 0.3, 1.0), palette)
        self.connect("key_press_event", self.copy_or_paste)
        self.connect("current-directory-uri-changed", self.wd_changed)

        self.set_scrollback_lines(-1)
        self.set_audible_bell(0)

    def copy_or_paste(self, widget, event):
        control_key = Gdk.ModifierType.CONTROL_MASK
        shift_key = Gdk.ModifierType.SHIFT_MASK
        if event.type == Gdk.EventType.KEY_PRESS:
            if event.state == shift_key | control_key:
                if event.keyval == 67:
                    self.copy_clipboard()
                elif event.keyval == 86:
                    self.paste_clipboard()
                return True
                
    def wd_changed(self, *args):
        workingDir = self.get_current_directory_uri()
        print("workingDir changed to:", workingDir)

class MyWindow(Gtk.Window):
    def __init__(self, parent=None):
        super(MyWindow, self).__init__()

    def main(self):
        self.terminal = Terminal()
        self.cb = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY)   
        self.cb.wait_for_text()
        self.cb.set_text("python3", -1)        
        self.connect('delete-event', Gtk.main_quit)
        self.scrolled_win = Gtk.ScrolledWindow()
        self.scrolled_win.add(self.terminal)
        self.add(self.scrolled_win)
        self.set_title("Terminal")
        self.resize(800, 300)
        self.move(0, 0)
        self.show_all()
        self.terminal.paste_primary()
        self.terminal.grab_focus()
        self.terminal.feed_child([13])

        
if __name__ == "__main__":
    win = MyWindow()
    win.main()
    Gtk.main()
Gribouillis likes this post
Reply
#6
You should also look at qtconsole. It gives you a very pretty, feature rich console.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [PyGUI] Python Application Not Finding Excel File dseals26 2 475 Feb-17-2021, 01:45 AM
Last Post: thewolf
  Python Application in Taskbar constantin01 3 2,467 Jan-24-2020, 10:57 AM
Last Post: buran
  How to make cross platform gui python application ? Jayam 1 1,752 Feb-21-2018, 01:53 PM
Last Post: Larz60+
  Interacting with python console while program is running Murmele 2 1,678 Feb-10-2018, 05:43 PM
Last Post: kmcollins
  Python Wrapper Application panick1992 8 3,601 Mar-15-2017, 11:54 PM
Last Post: micseydel

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020