I'm trying to create a basic PyGTK app to embed MPlayer inside a window (since it otherwise doesn't work well with tiling WM's, which I like).
I'll put my code so far at the end of this post, but basically my setup currently involves a Window containing a DrawingArea which I embed MPlayer into using the `-wid' command-line option.
The problem I'm having is that, when resizing, I get the following sorts of visual artifacts (see inside the red box):
I've tried calling queue_draw() on the DrawingArea when a configure-event happens, but this seems to have no effect. Anyone have any ideas?
My complete code is as follows: (command-line usage is `$0 [ vid ]')
#!/usr/bin/env python2
import sys
import os
import subprocess
import time
import string
import gtk
import gobject
import pygtk
pygtk.require('2.0')
class MPlayer:
def __init__(self, path, draw, show_output=True):
self.path = path
self.draw = draw
self.fifo = "/tmp/%s.%d" % (os.path.basename(__file__), time.time())
# Start mplayer in draw
cmd = string.split("mplayer -slave -wid %d -input file=%s" % \
(self.draw.window.xid, self.fifo))
cmd.append(self.path)
if show_output:
process = subprocess.Popen(cmd)
else:
self.devnull = open(os.devnull)
process = subprocess.Popen(cmd, stdout=self.devnull, \
stderr=self.devnull)
self.pid = process.pid
def __enter__(self):
os.mkfifo(self.fifo)
return self
def __exit__(self, ext_type, exc_value, traceback):
if hasattr(self, "devnull"):
self.devnull.close()
os.unlink(self.fifo)
# Send cmd to mplayer via fifo
def exe(self, cmd, *args):
if not self.pid: return
full_cmd = "%s %s\n" % (cmd, string.join([str(arg) for arg in args]))
with open(self.fifo, "w+") as fifo:
fifo.write(full_cmd)
fifo.flush()
class MPlayerWrapper:
def __init__(self):
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.draw = gtk.DrawingArea()
self.mplayer = None
self.setup_widgets()
def setup_widgets(self):
self.window.connect("destroy", gtk.main_quit)
self.window.connect("key_press_event", self.key_press_event)
self.draw.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("black"))
self.draw.connect("configure_event", self.redraw)
self.window.add(self.draw)
self.window.show_all()
def mplayer_exe(self, cmd, *args):
if self.mplayer:
self.mplayer.exe(cmd, *args)
def key_press_event(self, widget, event, data=None):
self.mplayer_exe("key_down_event", event.keyval)
def redraw(self, draw, event, data=None):
self.draw.queue_draw()
def play(self, path):
with MPlayer(path, self.draw, True) as self.mplayer:
gobject.child_watch_add(self.mplayer.pid, gtk.main_quit)
gtk.main()
if __name__ == "__main__":
wrapper = MPlayerWrapper()
wrapper.play(sys.argv[1])
Solved it -- the solution was to add
-vo gl
to the call to mplayer. That is, I replaced
cmd = string.split("mplayer -slave -wid %d -input file=%s" % \
(self.draw.window.xid, self.fifo))
with
cmd = string.split("mplayer -slave -vo gl -wid %d -input file=%s" % \
(self.draw.window.xid, self.fifo))
in the above code, and now it resizes correctly.
That command line option has actually removed the whole reason I wanted to create this wrapper script in the first place (i.e. to embed mplayer into a window with black borders taking up the space not required by the movie itself, in order to use it in dwm), but I imagine this fix would still be useful for other people wanting to embed mplayer for other reasons.
Related
It's been a while now that Gtk4 shipped out, with a new Gtk.Video() component that allows to display a video in a Gtk window without resorting to using Gstreamer.
It's a very simple class with only a handful of subclasses (4) methods (10) & attributes (2).
But no matter how hard I try, I can't find a single example (? not even in C) ; I got this far :
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio
class MainWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(title="Mini Player", *args, **kwargs)
player = Gtk.Video.new()
player.set_autoplay(True)
print('file: ', player.props.file) # => None
file_to_play = Gio.File.new_for_path('/my/valid/video/file.mp4')
player.set_file(file_to_play) # => this is supposed to start the playing
self.set_child(player)
print('file: ', player.props.file) # => file: __gi__.GLocalFile object
print('autoplay: ', player.props.autoplay) # => True
# self.show() # I tried this too, it does nothing
class MyApp(Adw.Application):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.connect('activate', self.on_activate)
self.connect('open', self.on_open)
self.set_flags(Gio.ApplicationFlags.HANDLES_OPEN) # Need to tell GApplication we can handle this
self.win = None
def on_activate(self, app):
self.win = MainWindow(application=app)
self.win.present()
# This is to avoid the error "Your application claims to support opening files but does not implement g_application_open() and has no handlers connected to the 'open' signal." while I learn how to pass this file to the Mainwindow class
def on_open(self, app, files, n_files, hint):
self.on_activate(app)
for file in files:
print("File to open: " + file.get_path())
app = MyApp(application_id="com.example.GtkApplication")
app.run(sys.argv)
The window shows up, with a neat play button in the lower left corner, all signals are good, no warnings or messages in the console, but the video doesn't play, the play button does nothing and the window stays black.
Am I missing something obvious?
Ok, I found out that I missed a library:
sudo apt install libgtk-4-media-gstreamer
Dit the trick, it works now. It's very slow but it works.
I made two tkinter text boxes one of which takes your python script as input and the other one shows results of the execution of your script but when I used input() command I got an error. Below given is the class for stdout redirector and also the execute function which executes after reading the script, which works fine. I have not included Text, tkinter, etc because I use all the general methods that work with the code like Text.get(), Text.mark_set(), Text.replace(), etc and also some of the functions are not included here. Other than the script and the output boxes I also tried to embed whole of the console in a textbox with InteractiveConsole but the problem was same in the case of receiving input or stdin but in both the cases stdout and stderr works fine.
from code import InteractiveConsole, InteractiveInterpreter
class StdoutRedirector(object):
def __init__(self, text_widget):
self.text_space = text_widget
def write(self, string):
self.text_space.insert('end', string)
self.text_space.see('end')
##class StdinRedirector(object):
## def __init__(self, text_widget):
## self.text_space = text_widget
##
## def readline(self) -> str:
## t = self.text_space.get(INSERT, f"{int(text.index(INSERT).split('.')[0])}.{int(text.index(INSERT).split('.')[1])}")
## return t
def execute(event=None):
save()
code = text.get('1.0',END+'-1c')
stdin = sys.stdin
stdout = sys.stdout
stderr = sys.stderr
output.delete('1.0',END)
## def a():
## sys.stdin = StdinRedirector(output)
## output.bind('<Return>', lambda: a)
sys.stdout = StdoutRedirector(output)
sys.stderr = StdoutRedirector(output)
interp = InteractiveInterpreter()
interp.runcode(code)
sys.stdout = stdout
sys.stderr = stderr
## sys.stdin = stdin
After which I tried Redirecting stdin, which obviously didn't work, and instead the application hung and the window stopped responding even after trying again and again.
Please help me with this... I don't know if its impossible but PyCharm and others have I/O Streams inside them so maybe the console or the execution window CAN be wholly embedded in a text box.
Ok so after researching on the web, in docs, and inside the code of the queue, idlelib and subprocess modules, I figured out the simplest way to make tkinter Textbox interact with python console as stdin, stdout, and stderr receiver. Here's the code:
import tkinter as tk
import subprocess
import queue
import os
from threading import Thread
class Console(tk.Frame):
def __init__(self, parent=None, **kwargs):
tk.Frame.__init__(self, parent, **kwargs)
self.parent = parent
# create widgets
self.ttytext = tk.Text(self, wrap=tk.WORD)
self.ttytext.pack(fill=tk.BOTH, expand=True)
self.ttytext.linenumbers.pack_forget()
self.p = subprocess.Popen(["jupyter", "qtconsole"], stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)
# make queues for keeping stdout and stderr whilst it is transferred between threads
self.outQueue = queue.Queue()
self.errQueue = queue.Queue()
# keep track of where any line that is submitted starts
self.line_start = 0
# a daemon to keep track of the threads so they can stop running
self.alive = True
# start the functions that get stdout and stderr in separate threads
Thread(target=self.readfromproccessout).start()
Thread(target=self.readfromproccesserr).start()
# start the write loop in the main thread
self.writeloop()
# key bindings for events
self.ttytext.bind("<Return>", self.enter)
self.ttytext.bind('<BackSpace>', self.on_bkspace)
self.ttytext.bind('<Delete>', self.on_delete)
self.ttytext.bind('<<Copy>>', self.on_copy)
self.ttytext.bind('<<Paste>>', self.on_paste)
self.ttytext.bind('<Control-c>', self.on_copy)
self.ttytext.bind('<Control-v>', self.on_paste)
def destroy(self):
"""This is the function that is automatically called when the widget is destroyed."""
self.alive = False
# write exit() to the console in order to stop it running
self.p.stdin.write("exit()\n".encode())
self.p.stdin.flush()
# call the destroy methods to properly destroy widgets
self.ttytext.destroy()
tk.Frame.destroy(self)
def enter(self, event):
"""The <Return> key press handler"""
cur_ind = str(self.ttytext.index(tk.INSERT))
if int(cur_ind.split('.')[0]) < int(self.ttytext.search(': ', tk.END, backwards=True).split('.')[0]):
try:
selected = self.ttytext.get('sel.first', 'sel.last')
if len(selected) > 0:
self.ttytext.insert(tk.END, selected)
self.ttytext.mark_set(tk.INSERT, tk.END)
self.ttytext.see(tk.INSERT)
return 'break'
except:
selected = self.ttytext.get(
self.ttytext.search(': ', tk.INSERT, backwards=True), tk.INSERT)
self.ttytext.insert(tk.END, selected.strip(': '))
self.ttytext.mark_set(tk.INSERT, tk.END)
self.ttytext.see(tk.INSERT)
return 'break'
string = self.ttytext.get(1.0, tk.END)[self.line_start:]
self.line_start += len(string)
self.p.stdin.write(string.encode())
self.p.stdin.flush()
def on_bkspace(self, event):
pass
def on_delete(self, event):
pass
def on_key(self, event):
"""The typing control (<KeyRelease>) handler"""
cur_ind = str(self.ttytext.index(tk.INSERT))
try:
if int(cur_ind.split('.')[0]) < int(self.ttytext.search(r'In [0-9]?', tk.END, backwards=True).split('.')[0]):
return 'break'
except:
return
def on_copy(self, event):
"""<Copy> event handler"""
self.ttytext.clipboard_append(self.ttytext.get('sel.first', 'sel.last'))
# I created this function because I was going to make a custom textbox
def on_paste(self, event):
"""<Paste> event handler"""
self.ttytext.insert(tk.INSERT, self.ttytext.clipboard_get())
# I created this function because I was going to make a custom textbox
def readfromproccessout(self):
"""To be executed in a separate thread to make read non-blocking"""
while self.alive:
data = self.p.stdout.raw.read(1024).decode()
self.outQueue.put(data)
def readfromproccesserr(self):
"""To be executed in a separate thread to make read non-blocking"""
while self.alive:
data = self.p.stderr.raw.read(1024).decode()
self.errQueue.put(data)
def writeloop(self):
"""Used to write data from stdout and stderr to the Text widget"""
# if there is anything to write from stdout or stderr, then write it
if not self.errQueue.empty():
self.write(self.errQueue.get())
if not self.outQueue.empty():
self.write(self.outQueue.get())
# run this method again after 10ms
if self.alive:
self.after(10, self.writeloop)
def write(self, string):
self.ttytext.insert(tk.END, string)
self.ttytext.see(tk.END)
self.line_start += len(string)
self.ttytext.inst_trigger()
if __name__ == '__main__':
root = tk.Tk()
main_window = Console(root)
main_window.pack(fill=tk.BOTH, expand=True)
main_window.ttytext.focus_force()
root.mainloop()
The code above uses jupyter qtconsole (because it is very handy), otherwise simple python shell can also be used using the InteractiveShell() in code module.
I have not completely made functions for Enter key, Up and Down arrow keys. These can be made by the user as per their choice.
This can also be found in Oli's answer here and it is customizable.
I am trying to write a program that finds a window by searching for its title. Once it has found the window, it will attempt to bring it to front. I am using win32gui API to achieve this. I am able to get it to work for the most part, but for some reason it does not work if the taskmanager is in front. I have the follow sample code.
import win32gui, win32con
import re, traceback
from time import sleep
class cWindow:
def __init__(self):
self._hwnd = None
def BringToTop(self):
win32gui.BringWindowToTop(self._hwnd)
def SetAsForegroundWindow(self):
win32gui.SetForegroundWindow(self._hwnd)
def Maximize(self):
win32gui.ShowWindow(self._hwnd, win32con.SW_MAXIMIZE)
def setActWin(self):
win32gui.SetActiveWindow(self._hwnd)
def _window_enum_callback(self, hwnd, wildcard):
'''Pass to win32gui.EnumWindows() to check all the opened windows'''
if re.match(wildcard, str(win32gui.GetWindowText(hwnd))) != None:
self._hwnd = hwnd
def find_window_wildcard(self, wildcard):
self._hwnd = None
win32gui.EnumWindows(self._window_enum_callback, wildcard)
def main():
sleep(5)
try:
wildcard = ".*Building Operation WorkStation.*"
cW = cWindow()
cW.find_window_wildcard(wildcard)
cW.Maximize()
cW.BringToTop()
cW.SetAsForegroundWindow()
except:
f = open("log.txt", "w")
f.write(traceback.format_exc())
print traceback.format_exc()
main()
I pieced this together from multiple online sources. It seems to work for the most part but for some windows like the task manager, it'll work sometimes but fails the rest. When it doesnt work properly, all I notice is the application icon blinks yellow. Is there a proper way of doing this to make sure the window that I am interested in is set to foreground 100% of the times? I am not sure if this is relevant but I am using Windows 7 Professional (32-bit) with Service Pack 1.
I found a solution: if taskmanager, then kill it. I added a method to cWindow:
def kill_task_manager(self):
# Here I use your method to find a window because of an accent in my french OS,
# but you should use win32gui.FindWindow(None, 'Task Manager complete name').
wildcard = 'Gestionnaire des t.+ches de Windows'
self.find_window_wildcard(wildcard)
if self._hwnd:
win32gui.PostMessage(self._hwnd, win32con.WM_CLOSE, 0, 0) # kill it
sleep(0.5) # important to let time for the window to be closed
Call this method just after cW = cWindow().
Another bug trap is to prevent this exception in SetAsForegroundWindow:
error: (0, 'SetForegroundWindow', 'No error message is available')
just send an alt key before the win32gui call:
# Add this import
import win32com.client
# Add this to __ini__
self.shell = win32com.client.Dispatch("WScript.Shell")
# And SetAsForegroundWindow becomes
def SetAsForegroundWindow(self):
self.shell.SendKeys('%')
win32gui.SetForegroundWindow(self._hwnd)
Last, if I may, do not compare != None but is not None. More pythonic ;)
This is the full code:
# coding: utf-8
import re, traceback
import win32gui, win32con, win32com.client
from time import sleep
class cWindow:
def __init__(self):
self._hwnd = None
self.shell = win32com.client.Dispatch("WScript.Shell")
def BringToTop(self):
win32gui.BringWindowToTop(self._hwnd)
def SetAsForegroundWindow(self):
self.shell.SendKeys('%')
win32gui.SetForegroundWindow(self._hwnd)
def Maximize(self):
win32gui.ShowWindow(self._hwnd, win32con.SW_MAXIMIZE)
def setActWin(self):
win32gui.SetActiveWindow(self._hwnd)
def _window_enum_callback(self, hwnd, wildcard):
'''Pass to win32gui.EnumWindows() to check all the opened windows'''
if re.match(wildcard, str(win32gui.GetWindowText(hwnd))) is not None:
self._hwnd = hwnd
def find_window_wildcard(self, wildcard):
self._hwnd = None
win32gui.EnumWindows(self._window_enum_callback, wildcard)
def kill_task_manager(self):
wildcard = 'Gestionnaire des t.+ches de Windows'
self.find_window_wildcard(wildcard)
if self._hwnd:
win32gui.PostMessage(self._hwnd, win32con.WM_CLOSE, 0, 0)
sleep(0.5)
def main():
sleep(5)
try:
wildcard = ".*Building Operation WorkStation.*"
cW = cWindow()
cW.kill_task_manager()
cW.find_window_wildcard(wildcard)
cW.BringToTop()
cW.Maximize()
cW.SetAsForegroundWindow()
except:
f = open("log.txt", "w")
f.write(traceback.format_exc())
print(traceback.format_exc())
if __name__ == '__main__':
main()
Sources: how do I close window with handle using win32gui in Python and win32gui.SetActiveWindow() ERROR : The specified procedure could not be found.
Note: The following deals only with making sure that always-on-top windows such as Task Manager are hidden before activating the window - it assumes that the activation part itself works fine, which may not be the case. The conditions under which a process is allowed to call the SetForegroundWindow Windows API function are listed here.
Task Manager is special in two respects:
By default, it is set to display always on top, i.e., above all other windows.
Even when that is turned off (Options > Always on Top unchecked), you can still make it display on top of other always-on-top windows (something that ordinary windows seemingly cannot do).
Your code:
is working - in my tests - in the sense that the target window does become the active window.
is not working in the sense that the Task Manager window still stays on top of the (maximized) window.
even trying to make your window an always-on-top window as well wouldn't help, unfortunately.
Specifically checking for the presence of the Task Manager window and minimizing it is an option, but note that there may be other always-on-top windows, so for a robust solution you have to identify all open always-on-top windows and minimize them:
The following tries hard to identify all always-on-top windows except the Task Bar and Start button, and minimizes (effectively hides) any such windows.
The new methods are hide_always_on_top_windows and _window_enum_callback_hide.
import win32gui, win32con
import re, traceback
from time import sleep
class cWindow:
def __init__(self):
self._hwnd = None
def SetAsForegroundWindow(self):
# First, make sure all (other) always-on-top windows are hidden.
self.hide_always_on_top_windows()
win32gui.SetForegroundWindow(self._hwnd)
def Maximize(self):
win32gui.ShowWindow(self._hwnd, win32con.SW_MAXIMIZE)
def _window_enum_callback(self, hwnd, regex):
'''Pass to win32gui.EnumWindows() to check all open windows'''
if self._hwnd is None and re.match(regex, str(win32gui.GetWindowText(hwnd))) is not None:
self._hwnd = hwnd
def find_window_regex(self, regex):
self._hwnd = None
win32gui.EnumWindows(self._window_enum_callback, regex)
def hide_always_on_top_windows(self):
win32gui.EnumWindows(self._window_enum_callback_hide, None)
def _window_enum_callback_hide(self, hwnd, unused):
if hwnd != self._hwnd: # ignore self
# Is the window visible and marked as an always-on-top (topmost) window?
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE) & win32con.WS_EX_TOPMOST:
# Ignore windows of class 'Button' (the Start button overlay) and
# 'Shell_TrayWnd' (the Task Bar).
className = win32gui.GetClassName(hwnd)
if not (className == 'Button' or className == 'Shell_TrayWnd'):
# Force-minimize the window.
# Fortunately, this seems to work even with windows that
# have no Minimize button.
# Note that if we tried to hide the window with SW_HIDE,
# it would disappear from the Task Bar as well.
win32gui.ShowWindow(hwnd, win32con.SW_FORCEMINIMIZE)
def main():
sleep(5)
try:
regex = ".*Building Operation WorkStation.*"
cW = cWindow()
cW.find_window_regex(regex)
cW.Maximize()
cW.SetAsForegroundWindow()
except:
f = open("log.txt", "w")
f.write(traceback.format_exc())
print(traceback.format_exc())
main()
I want to add a control terminal widget to my pure python+tkinter application similar to the python interpreter provided in Blender. It should be running within the same context (process) so the user can add features and control the application that is currently running from the control widget. Ideally I'd like it to also "hijack" stdout and stderr of the current application so it will report any problems or debugging information within the running application.
This is what I have come up with so far. The only problems are that it isn't responding to commands, and the thread doesn't stop when the user closes the window.
import Tkinter as tk
import sys
import code
from threading import *
class Console(tk.Frame):
def __init__(self,parent=None):
tk.Frame.__init__(self, parent)
self.parent = parent
sys.stdout = self
sys.stderr = self
self.createWidgets()
self.consoleThread = ConsoleThread()
self.after(100,self.consoleThread.start)
def write(self,string):
self.ttyText.insert('end', string)
self.ttyText.see('end')
def createWidgets(self):
self.ttyText = tk.Text(self.parent, wrap='word')
self.ttyText.grid(row=0,column=0,sticky=tk.N+tk.S+tk.E+tk.W)
class ConsoleThread(Thread):
def __init__(self):
Thread.__init__(self)
def run(self):
vars = globals().copy()
vars.update(locals())
shell = code.InteractiveConsole(vars)
shell.interact()
if __name__ == '__main__':
root = tk.Tk()
root.config(background="red")
main_window = Console(root)
main_window.mainloop()
try:
if root.winfo_exists():
root.destroy()
except:
pass
I have the answer in case anyone still cares! (I have also changed to python 3, hence the import tkinter rather than import Tkinter)
I have changed the approach slightly from the original by using a separate file to run the InteractiveConsole, and then making the main file open this other file (which I have called console.py and is in the same directory) in a subprocess, linking the stdout, stderr, and stdin of this subprocess to the tkinter Text widget programatically.
Here is the code in the for the console file (if this is run normally, it acts like a normal console):
# console.py
import code
if __name__ == '__main__':
vars = globals().copy()
vars.update(locals())
shell = code.InteractiveConsole(vars)
shell.interact()
And here is the code for the python interpreter, that runs the console inside the Text widget:
# main.py
import tkinter as tk
import subprocess
import queue
import os
from threading import Thread
class Console(tk.Frame):
def __init__(self,parent=None):
tk.Frame.__init__(self, parent)
self.parent = parent
self.createWidgets()
# get the path to the console.py file assuming it is in the same folder
consolePath = os.path.join(os.path.dirname(__file__),"console.py")
# open the console.py file (replace the path to python with the correct one for your system)
# e.g. it might be "C:\\Python35\\python"
self.p = subprocess.Popen(["python3",consolePath],
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE)
# make queues for keeping stdout and stderr whilst it is transferred between threads
self.outQueue = queue.Queue()
self.errQueue = queue.Queue()
# keep track of where any line that is submitted starts
self.line_start = 0
# make the enter key call the self.enter function
self.ttyText.bind("<Return>",self.enter)
# a daemon to keep track of the threads so they can stop running
self.alive = True
# start the functions that get stdout and stderr in separate threads
Thread(target=self.readFromProccessOut).start()
Thread(target=self.readFromProccessErr).start()
# start the write loop in the main thread
self.writeLoop()
def destroy(self):
"This is the function that is automatically called when the widget is destroyed."
self.alive=False
# write exit() to the console in order to stop it running
self.p.stdin.write("exit()\n".encode())
self.p.stdin.flush()
# call the destroy methods to properly destroy widgets
self.ttyText.destroy()
tk.Frame.destroy(self)
def enter(self,e):
"The <Return> key press handler"
string = self.ttyText.get(1.0, tk.END)[self.line_start:]
self.line_start+=len(string)
self.p.stdin.write(string.encode())
self.p.stdin.flush()
def readFromProccessOut(self):
"To be executed in a separate thread to make read non-blocking"
while self.alive:
data = self.p.stdout.raw.read(1024).decode()
self.outQueue.put(data)
def readFromProccessErr(self):
"To be executed in a separate thread to make read non-blocking"
while self.alive:
data = self.p.stderr.raw.read(1024).decode()
self.errQueue.put(data)
def writeLoop(self):
"Used to write data from stdout and stderr to the Text widget"
# if there is anything to write from stdout or stderr, then write it
if not self.errQueue.empty():
self.write(self.errQueue.get())
if not self.outQueue.empty():
self.write(self.outQueue.get())
# run this method again after 10ms
if self.alive:
self.after(10,self.writeLoop)
def write(self,string):
self.ttyText.insert(tk.END, string)
self.ttyText.see(tk.END)
self.line_start+=len(string)
def createWidgets(self):
self.ttyText = tk.Text(self, wrap=tk.WORD)
self.ttyText.pack(fill=tk.BOTH,expand=True)
if __name__ == '__main__':
root = tk.Tk()
root.config(background="red")
main_window = Console(root)
main_window.pack(fill=tk.BOTH,expand=True)
root.mainloop()
The reason that reading from stdout and stderr is in separate threads is because the read method is blocking, which causes the program to freeze until the console.py subprocess gives more output, unless these are in separate threads. The writeLoop method and the queues are needed to write to the Text widget since tkinter is not thread safe.
This certainly still has problems to be ironed out, such as the fact that any code on the Text widget is editable even once already submitted, but hopefully it answers your question.
EDIT: I've also neatened some of the tkinter such that the Console will behave more like a standard widget.
it isn't responding to commands
The reason it isn't responding to commands is because you haven't linked the Text widget (self.ttyText) into stdin. Currently when you type it adds text into the widget and nothing else. This linking can be done similarly to what you've already done with stdout and stderr.
When implementing this, you need to keep track of which part of the text in the widget is the text being entered by the user - this can be done using marks (as described here).
the thread doesn't stop when the user closes the window.
I don't think there is a "clean" way to solve this issue without a major code re-write, however a solution that seems to work well enough is it simply detect when the widget is destroyed and write the string "\n\nexit()" to the interpreter. This calls the exit function inside the interpreter, which causes the call to shell.interact to finish, which makes the thread finish.
So without further ado, here is the modified code:
import tkinter as tk
import sys
import code
from threading import Thread
import queue
class Console(tk.Frame):
def __init__(self, parent, _locals, exit_callback):
tk.Frame.__init__(self, parent)
self.parent = parent
self.exit_callback = exit_callback
self.destroyed = False
self.real_std_in_out = (sys.stdin, sys.stdout, sys.stderr)
sys.stdout = self
sys.stderr = self
sys.stdin = self
self.stdin_buffer = queue.Queue()
self.createWidgets()
self.consoleThread = Thread(target=lambda: self.run_interactive_console(_locals))
self.consoleThread.start()
def run_interactive_console(self, _locals):
try:
code.interact(local=_locals)
except SystemExit:
if not self.destroyed:
self.after(0, self.exit_callback)
def destroy(self):
self.stdin_buffer.put("\n\nexit()\n")
self.destroyed = True
sys.stdin, sys.stdout, sys.stderr = self.real_std_in_out
super().destroy()
def enter(self, event):
input_line = self.ttyText.get("input_start", "end")
self.ttyText.mark_set("input_start", "end-1c")
self.ttyText.mark_gravity("input_start", "left")
self.stdin_buffer.put(input_line)
def write(self, string):
self.ttyText.insert('end', string)
self.ttyText.mark_set("input_start", "end-1c")
self.ttyText.see('end')
def createWidgets(self):
self.ttyText = tk.Text(self.parent, wrap='word')
self.ttyText.grid(row=0, column=0, sticky=tk.N + tk.S + tk.E + tk.W)
self.ttyText.bind("<Return>", self.enter)
self.ttyText.mark_set("input_start", "end-1c")
self.ttyText.mark_gravity("input_start", "left")
def flush(self):
pass
def readline(self):
line = self.stdin_buffer.get()
return line
if __name__ == '__main__':
root = tk.Tk()
root.config(background="red")
main_window = Console(root, locals(), root.destroy)
main_window.mainloop()
This code has few changes other than those that solve the problems stated in the question.
The advantage of this code over my previous answer is that it works inside a single process, so can be created at any point in the application, giving the programmer more control.
I have also written a more complete version of this which also prevents the user from editing text which shouldn't be editable (e.g. the output of a print statement) and has some basic coloring: https://gist.github.com/olisolomons/e90d53191d162d48ac534bf7c02a50cd
Is there a way that I can run a script from another while getting output as readily as I get by executing it by itself.
For example:
I use the os.popen3 command to execute abc.py, but I am unable to get output from abc.py as readily as I would with doing python abc.py; it seems that I need to wait for os.popen3 command to finish:
fin, fout, ferr=os.popen3("abc.py")
out = fout.read()
err = ferr.read()
fo.write(out)
fe.write(err)
print out
print err
[EDIT]:fo and fe here are file handles to the output and error logs, respectively.
Also, what widget do I use to populate the output in, in pygtk?
import subprocess
pro = subprocess.Popen('abc.py')
Is a much better way of fiddling with another scripts ins, outs, and errors.
The subprocess module is the option, but the tricky part is to follow the output un parallel with your main loop of gtk, to accomplish that goal you must have to consider the platform that you are dealing, if you are in linux you can easily run another thread and use gtk.gdk.threads_init to use threads in pygtk, but if you are planing to run your application on windows, then you should use generators and gobject.idle_add.
About the widget, use gtk.TextBuffer associated with a gtk.TextView
Here is an example with threads
import gtk
import subprocess
import threading
gtk.gdk.threads_init()
class FollowProcess(threading.Thread):
def __init__(self, cmd, text_buffer):
self.tb = text_buffer
self.child = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
super(FollowProcess, self).__init__()
def run(self):
while not self.child.poll():
out = self.child.stdout.read(1)
if out != '':
gtk.gdk.threads_enter()
self.tb.insert_at_cursor(out)
gtk.gdk.threads_leave()
def destroy(w, cmd):
cmd.child.terminate()
gtk.main_quit()
i = 0
def click_count(btn):
global i
message.set_text('Calling button %d' %i)
i += 1
other_command = 'python extranger.py'
w = gtk.Window()
w.resize(400, 400)
message = gtk.Label('Nothing')
tb = gtk.TextBuffer()
tv = gtk.TextView(tb)
scroll = gtk.ScrolledWindow()
scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
scroll.add(tv)
box = gtk.VBox()
button = gtk.Button('Active button')
cmd = FollowProcess('python extranger.py', tb)
button.connect('clicked', click_count )
w.connect('destroy', destroy, cmd)
box.pack_start(button, False)
box.pack_start(message, False)
box.pack_start(scroll)
w.add(box)
w.show_all()
cmd.start()
gtk.main()
And in extranger.py
import time
import sys
i = 0
while True:
print 'some output %d' % i
sys.stdout.flush() # you need this to see the output
time.sleep(.5)
i += 1
Note how the button stills responsive even with the update in parallel.
You mentioned PyGtk but you can give a try to PyQt and particularly QProcess class wich has some nice signals like :
readyReadStandardError
readyReadStandardOutput
Look for a similar tool with PyGtk.