Posts: 262
Threads: 115
Joined: Aug 2018
Jul-03-2025, 07:25 AM
(This post was last modified: Aug-08-2025, 07:34 AM by Winfried.)
Hello,
I use a wxPython GUI to run a CLI application. It's just a listbox and a button to trigger the action.
Is it possible to hide the CLI console while still being able to grab its stdout output and display it in the GUI?
Thank you.
import wx, subprocess
CMD = fr'C:\blah.exe'
class ListBoxFrame(wx.Frame):
def __init__(self, *args, **kwargs):
super().__init__(None, -1,title='Bulk Download')
panel = wx.Panel(self, -1)
sizer = wx.BoxSizer(wx.VERTICAL)
self.lb1 = wx.ListBox(panel, -1, style=(wx.LB_SINGLE | wx.LB_ALWAYS_SB))
sizer.Add(self.lb1,1, wx.ALL | wx.EXPAND ,5)
self.dload_btn = wx.Button(panel, -1, "Download")
sizer.Add(self.dload_btn,0, wx.EXPAND)
self.dload_btn.Bind(wx.EVT_BUTTON, self.OnDloadButtonClick)
panel.SetSizer(sizer)
def OnDloadButtonClick(self,event):
for i in range(self.lb1.GetCount()):
URL = self.lb1.GetString(i)
my_command = fr'{CMD} {URL}'
p = subprocess.Popen(my_command, stdout=subprocess.PIPE, text=True)
while (line := p.stdout.readline()) != "":
self.statusbar.SetStatusText(line)
wx.Yield()
self.statusbar.SetStatusText("Done.")
app = wx.App()
ListBoxFrame().Show()
app.MainLoop()
Posts: 4,900
Threads: 79
Joined: Jan 2018
See this thread perhaps. Also it seems that you can use subprocess.STARTUPINFO. See also this answer.
« We can solve any problem by introducing an extra level of indirection »
Posts: 262
Threads: 115
Joined: Aug 2018
Jul-28-2025, 11:24 AM
(This post was last modified: Jul-28-2025, 11:24 AM by Winfried.)
Thanks. I tried a few things, but nothing worked.
The requirements are:
1. Let the user double-click the script through a .pyw file so no parent console is displayed
2. Launch a CLI application without displaying a console while still reading its stdoud in a loop to show progress; Apparently, this requires subprocess.Popen() and a while loop.
So far, it seems like the two are mutually incompatible :-/
import sys,os, wx
import subprocess
class ListBoxFrame(wx.Frame):
def __init__(self, *args, **kwargs):
super().__init__(None, -1,title='Bulk Youtube Download')
self.Centre()
self.statusbar = self.CreateStatusBar()
self.dload_btn = wx.Button(self, -1, "Test")
self.dload_btn.Bind(wx.EVT_BUTTON, self.OnTestButtonClick)
def OnTestButtonClick(self,event):
CMD="ping -n 10 www.gooogle.com"
"""
#Doucle-click on script, dlick on button: Still shows console
process = subprocess.Popen(CMD, stdout=subprocess.PIPE)
for line in iter(process.stdout.readline, b""):
self.statusbar.SetStatusText(line)
wx.Yield()
"""
"""
#Doucle-click on script, dlick on button: Doesn't show progress
loProc = subprocess.run(CMD, capture_output=True, text=True)
print(self.statusbar.SetStatusText(loProc.stdout))
"""
#How to grab stdout in loop and display output in statusbar?
"""
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
#si.wShowWindow = subprocess.SW_HIDE # default
subprocess.call(CMD, startupinfo=si)
"""
"""
subprocess.call(CMD, creationflags=subprocess.CREATE_NO_WINDOW)
"""
"""
#How to grab stdout in loop and display output in statusbar?
#why the trailing comma?
#TypeError: StatusBar.SetStatusText(): argument 1 has unexpected type 'CompletedProcess'
self.statusbar.SetStatusText(subprocess.run(CMD,capture_output=True,creationflags=subprocess.CREATE_NO_WINDOW,))
"""
#.pyw: CLI still shows window
si = subprocess.STARTUPINFO()
#si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
si.wShowWindow = subprocess.SW_HIDE # default
process = subprocess.Popen(CMD, stdout=subprocess.PIPE, startupinfo=si)
for line in iter(process.stdout.readline, b""):
self.statusbar.SetStatusText(line)
wx.Yield()
self.statusbar.SetStatusText("Done.")
app = wx.App()
ListBoxFrame().Show()
app.MainLoop()
Posts: 6,979
Threads: 22
Joined: Feb 2020
Jul-30-2025, 02:16 AM
(This post was last modified: Jul-30-2025, 03:08 PM by deanhystad.)
This works for me. I had to add the CREATE_NO_WINDOW flag to prevent a window from popping up when using double click to launch the program. The flag was not needed when running the program from the command line or from VSCode.
import tkinter as tk
from subprocess import Popen, PIPE, CREATE_NO_WINDOW
class Window(tk.Tk):
def __init__(self):
super().__init__()
self.text = tk.Text(self, width=60, height=24, wrap=tk.WORD)
self.text.pack(expand=True, fill=tk.BOTH)
button = tk.Button(self, text="Press me", command=self.do_work)
button.pack(expand=True, fill=tk.X)
self.process = None
def do_work(self):
"""Launch ping as subprocess."""
self.text.delete("0.0", tk.END)
self.process = Popen(
["ping", "-n", "10", "www.gooogle.com"],
stdout=PIPE,
bufsize=1,
universal_newlines=True,
creationflags=CREATE_NO_WINDOW,
)
self.capture_output()
def capture_output(self):
"""Display process output as it becomes available."""
if self.process is None or self.process.stdout is None:
return
if self.process.poll() is None:
# Process is still active.
line = self.process.stdout.readline()
if line:
self.text.insert(tk.END, line)
self.after(100, self.capture_output) # Keep looking
else:
# Process finished. Fetch remaining stdout.
for line in self.process.stdout.readlines():
self.text.insert(tk.END, line)
Window().mainloop()Use wx CallLater, CallAfter or Timer to periodically check for output instead of using a while loop. Even using wx.Yield in the loop results in GUI being unresponsive while waiting for the next line of output.
Gribouillis likes this post
Posts: 262
Threads: 115
Joined: Aug 2018
Thanks for the code.
When I click on the button, it just jumps directly to "Done" without running ping, and the console displays "poll None":
import sys,os, wx
import subprocess
from subprocess import Popen, PIPE, CREATE_NO_WINDOW
import time
CMD="ping -n 10 www.gooogle.com"
class ListBoxFrame(wx.Frame):
def capture_output(self):
"""Display process output as it becomes available."""
if self.process is None or self.process.stdout is None:
print("None")
return
if self.process.poll() is None:
# Process is still active.
line = self.process.stdout.readline()
if line:
#self.text.insert(tk.END, line)
self.statusbar.SetStatusText(line)
#wait 100ms, and loop back
#self.after(100, self.capture_output) # Keep looking
time.sleep(0.1)
print("poll None")
self.capture_output
else:
# Process finished. Fetch remaining stdout.
for line in self.process.stdout.readlines():
#self.text.insert(tk.END, line)
self.statusbar.SetStatusText(line)
print("poll else")
def __init__(self, *args, **kwargs):
super().__init__(None, -1,title='Bulk Youtube Download')
self.statusbar = self.CreateStatusBar()
self.dload_btn = wx.Button(self, -1, "Test")
self.dload_btn.Bind(wx.EVT_BUTTON, self.OnTestButtonClick)
def OnTestButtonClick(self,event):
self.process = None
self.process = Popen(CMD,stdout=PIPE,bufsize=1,universal_newlines=True,creationflags=CREATE_NO_WINDOW,)
self.capture_output()
self.statusbar.SetStatusText("Done.")
app = wx.App()
ListBoxFrame().Show()
app.MainLoop()
Posts: 6,979
Threads: 22
Joined: Feb 2020
Aug-01-2025, 04:13 PM
(This post was last modified: Aug-01-2025, 04:14 PM by deanhystad.)
In the tkinter example, I used .after(time, function) to periodically call capture_output(). I mentioned you could use CallLater, CallAfter or Timer in wx to do the same thing. The code below uses Timer.
import wx
from subprocess import Popen, PIPE, CREATE_NO_WINDOW
CMD = ["ping", "-n", "10", "www.gooogle.com"]
class ListBoxFrame(wx.Frame):
def __init__(self, *args, **kwargs):
super().__init__(None, -1, title="Bulk Youtube Download")
self.statusbar = self.CreateStatusBar()
self.dload_btn = wx.Button(self, -1, "Test")
self.dload_btn.Bind(wx.EVT_BUTTON, self.do_work)
self.update_timer = wx.Timer(self, 1)
self.Bind(wx.EVT_TIMER, self.update_status)
self.linenum = 0
def do_work(self, event):
self.process = Popen(
CMD,
stdout=PIPE,
bufsize=1,
universal_newlines=True,
creationflags=CREATE_NO_WINDOW,
)
self.update_timer.Start(100)
def update_status(self, event):
"""Display process output as it becomes available."""
if self.process.poll() is None:
# Process is still active.
line = self.process.stdout.readline()
if line:
self.linenum += 1 # Added a line number so you can see the pings are not all the same line.
self.statusbar.SetStatusText(f"{self.linenum}: {line}")
else:
# Process finished. Fetch remaining stdout.
for line in self.process.stdout.readlines():
self.linenum += 1
self.statusbar.SetStatusText(f"{self.linenum}: {line}")
self.update_timer.Stop()
app = wx.App()
ListBoxFrame().Show()
app.MainLoop()Your code doesn't work because capture_output() called just the one time and you stopped checking the process and displaying output long before the process had finished.
Posts: 262
Threads: 115
Joined: Aug 2018
Thanks for the code + explanation.
Posts: 262
Threads: 115
Joined: Aug 2018
After a bit more experimenting, the following settings work after debugging and renaming the script as .pyw to hide the console (on Windows):
p = subprocess.Popen("blah", text=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
while (line := p.stdout.readline()) != "":
self.statusbar.SetStatusText(line.strip())
wx.Yield()
output = f"End of output. Return code: {p.wait()}"
self.statusbar.SetStatusText(output)
|