【问题标题】:Run process with realtime output to a Tkinter GUI运行进程并实时输出到 Tkinter GUI
【发布时间】:2015-08-05 06:52:35
【问题描述】:

我试图在 Tkinter python 中创建一个 GUI。我想将工具的输出显示到我的 Tkinter 界面。该工具在命令行中运行良好,但它是一个连续扫描仪。有点像连续 ping(我的意思是 Linux 中没有选项的 ping 命令)。

现在的问题是由于 ping 的输出永远不会完成,因此我无法在 Tkinter 中打印输出。它也使我的应用程序冻结。几秒钟后我也无法停止命令以显示输出。 Run process with realtime output in PHP 我发现上面的这个链接对 php 很有帮助,但是如何在 python 中转换这段代码:

https://stackoverflow.com/a/6144213/4931414

这是我想在 tkinter 框架上显示的一些示例代码

#!/usr....

import subprocess
x = subprocess.call(["ping", "127.0.0.1"])
print x

这在命令行上效果很好,但我在 tkinter 界面上没有得到输出。

【问题讨论】:

    标签: python python-2.7 python-3.x tkinter


    【解决方案1】:

    首先,我必须承认我对模块subprocessthreading不是很熟悉,但是我尝试创建一个简单的控制台,可以接受您编写命令,其输出将显示在Text 小部件。

    基本思想是有一个新的运行并行线程,当您单击按钮Execute 时处理命令。我们不断迭代 stdout 的行并将它们插入到 Text 小部件中。

    它似乎适用于任何命令,但我很确定存在一些问题和错误。如果你们更熟悉我上面引用的模块,发现我的代码有任何严重的问题,或者有任何改进它的建议,我一定会听你的,以改进这个例子。

    现在,这是代码:

    import tkinter as tk
    from tkinter.scrolledtext import ScrolledText
    import threading
    from subprocess import Popen, PIPE
    
    
    class Console(tk.Frame):
    
        """Simple console that can execute bash commands"""
    
        def __init__(self, master, *args, **kwargs):
            tk.Frame.__init__(self, master, *args, **kwargs)
    
            self.text_options = {"state": "disabled",
                                 "bg": "black",
                                 "fg": "#08c614",
                                 "insertbackground": "#08c614",
                                 "selectbackground": "#f01c1c"}
    
            self.text = ScrolledText(self, **self.text_options)
    
            # It seems not to work when Text is disabled...
            # self.text.bind("<<Modified>>", lambda: self.text.frame.see(tk.END))
    
            self.text.pack(expand=True, fill="both")
    
            # bash command, for example 'ping localhost' or 'pwd'
            # that will be executed when "Execute" is pressed
            self.command = ""  
            self.popen = None     # will hold a reference to a Popen object
            self.running = False  # True if the process is running
    
            self.bottom = tk.Frame(self)
    
            self.prompt = tk.Label(self.bottom, text="Enter the command: ")
            self.prompt.pack(side="left", fill="x")
            self.entry = tk.Entry(self.bottom)
            self.entry.bind("<Return>", self.start_thread)
            self.entry.bind("<Command-a>", lambda e: self.entry.select_range(0, "end"))
            self.entry.bind("<Command-c>", self.clear)
            self.entry.focus()
            self.entry.pack(side="left", fill="x", expand=True)
    
            self.executer = tk.Button(self.bottom, text="Execute", command=self.start_thread)
            self.executer.pack(side="left", padx=5, pady=2)
            self.clearer = tk.Button(self.bottom, text="Clear", command=self.clear)
            self.clearer.pack(side="left", padx=5, pady=2)
            self.stopper = tk.Button(self.bottom, text="Stop", command=self.stop)
            self.stopper.pack(side="left", padx=5, pady=2)
    
            self.bottom.pack(side="bottom", fill="both")
    
        def clear_text(self):
            """Clears the Text widget"""
            self.text.config(state="normal")
            self.text.delete(1.0, "end-1c")
            self.text.config(state="disabled")
    
        def clear_entry(self):
            """Clears the Entry command widget"""
            self.entry.delete(0, "end")
    
        def clear(self, event=None):
            """Does not stop an eventual running process,
            but just clears the Text and Entry widgets."""
            self.clear_entry()
            self.clear_text()
    
        def show(self, message):
            """Inserts message into the Text wiget"""
            self.text.config(state="normal")
            self.text.insert("end", message)
            self.text.see("end")
            self.text.config(state="disabled")
    
        def start_thread(self, event=None):
            """Starts a new thread and calls process"""
            self.stop()
            self.running = True
            self.command = self.entry.get()
            # self.process is called by the Thread's run method
            threading.Thread(target=self.process).start()
    
        def process(self):
            """Runs in an infinite loop until self.running is False""" 
            while self.running:
                self.execute()
    
        def stop(self):
            """Stops an eventual running process"""
            if self.popen:
                try:
                    self.popen.kill()
                except ProcessLookupError:
                    pass 
            self.running = False
    
        def execute(self):
            """Keeps inserting line by line into self.text
            the output of the execution of self.command"""
            try:
                # self.popen is a Popen object
                self.popen = Popen(self.command.split(), stdout=PIPE, bufsize=1)
                lines_iterator = iter(self.popen.stdout.readline, b"")
    
                # poll() return None if the process has not terminated
                # otherwise poll() returns the process's exit code
                while self.popen.poll() is None:
                    for line in lines_iterator:
                        self.show(line.decode("utf-8"))
                self.show("Process " + self.command  + " terminated.\n\n")
    
            except FileNotFoundError:
                self.show("Unknown command: " + self.command + "\n\n")                               
            except IndexError:
                self.show("No command entered\n\n")
    
            self.stop()
    
    
    if __name__ == "__main__":
        root = tk.Tk()
        root.title("Console")
        Console(root).pack(expand=True, fill="both")
        root.mainloop()
    

    【讨论】:

    • 只是一个附录。您希望保留对 psutil 的引用,例如在使用 shell=True 时,您要杀死至少 2 个甚至可能 3 个进程。示例代码(适用于所有操作系统的 AFAIK)stackoverflow.com/questions/1230669/…
    • 请原谅我笨拙的解释。我只是想让你知道它的存在。如果您曾经处于想要关闭父进程和子进程的情况,它可以节省大量寻找解决方案的时间,并且会让人头疼。
    • 你不应该从不同的线程调用 tkinter 方法。这可能会导致 tkinter 崩溃。
    【解决方案2】:

    如果您将代码更改为以下内容,您将看到 ping,而不是控制台上显示的“打印 x”

    import subprocess
    x = subprocess.call(["ping", "127.0.0.1"])
    print "x is", x   ## or comment out this line
    

    您必须定期使用管道并刷新标准输出才能获得我认为您想要的内容。请参阅 Doug Hellmann 的本周 Python 模块http://pymotw.com/2/subprocess/index.html#module-subprocesspopen@

    【讨论】:

      【解决方案3】:

      @nbro 回答的改进:

      from tkinter.scrolledtext import ScrolledText
      from subprocess import Popen, PIPE
      from threading import Thread, Lock
      import tkinter as tk
      
      
      class Console(ScrolledText):
          """
          Simple console that can execute commands
          """
      
          def __init__(self, master, **kwargs):
              # The default options:
              text_options = {"state": "disabled",
                              "bg": "black",
                              "fg": "#08c614",
                              "selectbackground": "orange"}
              # Take in to account the caller's specified options:
              text_options.update(kwargs)
              super().__init__(master, **text_options)
      
              self.proc = None # The process
              self.text_to_show = "" # The new text that we need to display on the screen
              self.text_to_show_lock = Lock() # A lock to make sure that it's thread safe
      
              self.show_text_loop()
      
          def clear(self) -> None:
              """
              Clears the Text widget
              """
              super().config(state="normal")
              super().delete("0.0", "end")
              super().config(state="disabled")
      
          def show_text_loop(self) -> None:
              """
              Inserts the new text into the `ScrolledText` wiget
              """
              new_text = ""
              # Get the new text that needs to be displayed
              with self.text_to_show_lock:
                  new_text = self.text_to_show.replace("\r", "")
                  self.text_to_show = ""
      
              if len(new_text) > 0:
                  # Display the new text:
                  super().config(state="normal")
                  super().insert("end", new_text)
                  super().see("end")
                  super().config(state="disabled")
      
              # After 100ms call `show_text_loop` again
              super().after(100, self.show_text_loop)
      
          def run(self, command:str) -> None:
              """
              Runs the command specified
              """
              self.stop()
              thread = Thread(target=self._run, daemon=True, args=(command, ))
              thread.start()
      
          def _run(self, command:str) -> None:
              """
              Runs the command using subprocess and appends the output
              to `self.text_to_show`
              """
              self.proc = Popen(command, shell=True, stdout=PIPE)
      
              try:
                  while self.proc.poll() is None:
                      text = self.proc.stdout.read(1).decode()
                      with self.text_to_show_lock:
                          self.text_to_show += text
      
                  self.proc = None
              except AttributeError:
                  # The process ended prematurely
                  pass
      
          def stop(self, event:tk.Event=None) -> None:
              """
              Stops the process.
              """
              try:
                  self.proc.kill()
                  self.proc = None
              except AttributeError:
                  # No process was running
                  pass
      
          def destroy(self) -> None:
              # Stop the process if the text widget is to be destroyed:
              self.stop()
              super().destroy()
      
      
      if __name__ == "__main__":
          def run_command_in_entry(event:tk.Event=None):
              console.run(entry.get())
              entry.delete("0", "end")
              return "break"
      
          root = tk.Tk()
          root.title("Console")
      
          console = Console(root)
          console.pack(expand=True, fill="both")
      
          entry = tk.Entry(root, bg="black", fg="white",
                           insertbackground="white")
          entry.insert("end", "ping 8.8.8.8 -n 4")
          entry.bind("<Return>", run_command_in_entry)
          entry.pack(fill="x")
      
          root.mainloop()
      

      我们的答案之间的唯一区别是,我删除了除 ScrolledText 之外的所有类中的小部件,并确保我以线程安全的方式使用 tkinter。 tkinter 的某些部分不是线程安全的,也不意味着可以从不同的线程调用(可能会引发错误)。在更糟糕的情况下,tkinter 可能会崩溃而不会给出错误或回溯。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-08-25
        • 2021-08-27
        • 2013-01-17
        • 1970-01-01
        • 2011-03-20
        • 1970-01-01
        • 1970-01-01
        • 2021-09-16
        相关资源
        最近更新 更多