Embed Python console in GUI application - Printable Version +- Python Forum (https://python-forum.io) +-- Forum: Python Coding (https://python-forum.io/forum-7.html) +--- Forum: GUI (https://python-forum.io/forum-10.html) +--- Thread: Embed Python console in GUI application (/thread-25117.html) |
Embed Python console in GUI application - deanhystad - Mar-20-2020 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. RE: Embed Python console in GUI application - deanhystad - Mar-22-2020 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. RE: Embed Python console in GUI application - deanhystad - Mar-23-2020 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_()) RE: Embed Python console in GUI application - kiyoshi7 - Jun-03-2021 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_()) RE: Embed Python console in GUI application - Axel_Erfurt - Jun-03-2021 If you can use Gtk 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() RE: Embed Python console in GUI application - deanhystad - Jun-04-2021 You should also look at qtconsole. It gives you a very pretty, feature rich console. |