close
Python Forum
[WxPython] [SOLVED] Hide CLI while grabbing its stdout?
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[WxPython] [SOLVED] Hide CLI while grabbing its stdout?
#1
Question 
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()
Reply
#2
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 »
Reply
#3
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()
Reply
#4
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
Reply
#5
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()
Reply
#6
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.
Winfried likes this post
Reply
#7
Thanks for the code + explanation.
Reply
#8
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)
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [Tkinter] Tkinter popup no grabbing the selected keys - Event propagation Wehaveall 2 2,483 Aug-10-2024, 01:18 PM
Last Post: Wehaveall
  [Tkinter] Hide clicked buttons Rubberduck 6 10,545 Jun-02-2021, 12:44 PM
Last Post: Rubberduck
  [Tkinter] Redirecting stdout to TextBox in realtime Gilush 2 13,544 Jun-06-2020, 11:05 AM
Last Post: deanhystad
  [PyQt] Hide Dock Icon for QSystemTrayIcon App AeglosGreeenleaf 0 5,045 Jun-20-2019, 07:21 PM
Last Post: AeglosGreeenleaf
  General help with hide show status bar for a begineer in python ArakelTheDragon 0 4,091 Mar-17-2019, 11:58 AM
Last Post: ArakelTheDragon
  Hide button when clicked frequency 2 10,648 Dec-24-2018, 02:10 PM
Last Post: frequency
  [PyGUI] Hi All, how to hide/mask user input in PySimpleGUI nmrt 1 16,977 Sep-21-2018, 09:59 AM
Last Post: nmrt
  [PYQT5] Crashing when using .hide() and .show() aking76 0 9,284 Sep-18-2018, 02:09 PM
Last Post: aking76
  [Tkinter] How to show and hide tkinter entry box when select yes from drop down Prince_Bhatia 1 12,165 Jun-12-2018, 08:05 AM
Last Post: Larz60+
  [Tkinter] Hide text in entry box when i click on it. MeeranRizvi 1 12,990 Jan-25-2017, 10:38 AM
Last Post: MeeranRizvi

Forum Jump:
Image

User Panel Messages

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