无法初始化 window 并等待进程在 Python 3 + GTK+ 3 中结束

Unable to initialize a window and wait for a process to end in Python 3 + GTK+ 3

我是面向对象编程、Python 和 GTK+3 的新手,尽管我对过程编程(主要是 C)有一定的了解。

我正在尝试构建一个简单的 Python + GTK+ 3 脚本到 运行 pkexec apt-get update under Linux.

我有一个 mainWindow class(基于 Gtk.Window class),其中包含一个名为 button 的按钮对象(基于 Gtk.Button class) 在 clicked 事件时触发 mainWindow 中定义的 new_update_window() 方法;

new_update_window() 方法从 updateWindow class(基于 Gtk.Window class)初始化一个 updateWindow 对象,其中包含名为 label 的标签对象(基于 Gtk.Label class)并调用 updateWindow;[=69= 中定义的方法 show_all()update() ]

update()方法应该改label,运行pkexec apt-get update再改label

问题是无论我做什么都会发生以下情况之一:

Tl;博士

我不知道如何完成我所追求的,即:

  1. 显示 updateWindow (Gtk.Window)
  2. 正在 updateWindow
  3. 中更新 labelGtk.Label
  4. 运行 pkexec apt-get update
  5. 正在 updateWindow
  6. 中更新 label

这是代码;

不使用线程(情况 1,在本例中使用 subprocess.Popen()):

#!/usr/bin/python3
from gi.repository import Gtk
import subprocess

class mainWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title = "Updater")

        button = Gtk.Button()
        button.set_label("Update")
        button.connect("clicked", self.new_update_window)
        self.add(button)

    def new_update_window(self, button):
        update = updateWindow()
        update.show_all()
        update.update()

class updateWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title = "Updating...")

        self.label = Gtk.Label()
        self.label.set_text("Idling...")
        self.add(self.label)

    def update(self):
        self.label.set_text("Updating... Please wait.")
        subprocess.call(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
        self.label.set_text("Updated.")

    def run_update(self):

main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()

使用线程(案例 3,在本例中使用 subprocess.Popen()):

#!/usr/bin/python3
from gi.repository import Gtk
import threading
import subprocess

class mainWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title = "Updater")

        button = Gtk.Button()
        button.set_label("Update")
        button.connect("clicked", self.new_update_window)
        self.add(button)

    def new_update_window(self, button):
        update = updateWindow()
        update.show_all()
        update.update()

class updateWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title = "Updating...")

        self.label = Gtk.Label()
        self.label.set_text("Idling...")
        self.add(self.label)

    def update(self):
        self.label.set_text("Updating... Please wait.")
        thread = threading.Thread(target=self.run_update)
        thread.start()
        thread.join()
        self.label.set_text("Updated.")

    def run_update(self):
        subprocess.Popen(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])

main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()

def run_update(self):
    subprocess.Popen(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])

您没有等待进程终止,请尝试

def run_update(self):
    proc = subprocess.Popen(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
    proc.wait()

相反。这应该正确地等待完成,但它不会有太大帮助,因为 updateWindow.update 将从 mainWindow.new_update_window 调用并且 GUI 线程将等待进程完成。

自定义信号可用于在 subprocess.Popensubprocess.call 启动的进程完成时进行通信:

#!/usr/bin/python3

from gi.repository import Gtk, GObject
import threading
import subprocess

class mainWindow(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, title = "Updater")

        button = Gtk.Button()
        button.set_label("Update")
        button.connect("clicked", self.new_update_window)
        self.add(button)

    def new_update_window(self, button):
        update = updateWindow(self)
        update.show_all()
        update.start_update()

class updateWindow(Gtk.Window):
    def __init__(self, parent):
        Gtk.Window.__init__(self, title = "Updating...")

        self.label = Gtk.Label()
        self.label.set_text("Idling...")
        self.add(self.label)
        self.parent = parent

        GObject.signal_new('update_complete', self, GObject.SIGNAL_RUN_LAST,
                           None, (int,))
        self.connect('update_complete', self.on_update_complete)

    def on_update_complete(self, widget, rc):
        self.label.set_text("Updated {:d}".format(rc))
        # emit a signal to mainwindow if needed, self.parent.emit(...)

    def start_update(self):
        self.label.set_text("Updating... Please wait.")
        thread = threading.Thread(target=self.run_update)
        thread.start()

    def run_update(self):
        rc = subprocess.call(["/usr/bin/pkexec", "apt-get", "update"],
                                shell=False)
        self.emit('update_complete', rc)

main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()

你快到了...
解决方案非常简单:)

您遇到的问题是 subprocess.call() 会冻结 GUI(循环),从而阻止 window 出现,而​​ subprocess.Popen() 会抛出命令并跳转至 self.label.set_text("Updated.")

如何解决

您可以通过 运行创建一个单独的线程来简单地解决它,调用您的命令:

subprocess.call(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])

并移动标签更改命令

self.label.set_text("Updated.")

进入线程,在第一个命令之后定位。然后线程不会冻结界面,而 label 不会过早更改,因为 subprocess.call() 会阻止这种情况。

代码则变为:

#!/usr/bin/python3
from gi.repository import Gtk
from threading import Thread
import subprocess

class mainWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title = "Updater")

        button = Gtk.Button()
        button.set_label("Update")
        button.connect("clicked", self.new_update_window)
        self.add(button)

    def new_update_window(self, button):
        update = updateWindow()
        update.show_all()
        update.update()

class updateWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title = "Updating...")

        self.label = Gtk.Label()
        self.label.set_text("Idling...")
        self.add(self.label)

    def update(self):
        self.label.set_text("Updating... Please wait.")
        Thread(target = self.run_update).start()

    def run_update(self):
        subprocess.call(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
        self.label.set_text("Updated.")

main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()

或者

如果您想避免使用 Thread,您可以使用 Gtk.main_iteration() 来防止在进程 运行s:

时界面冻结
#!/usr/bin/python3
from gi.repository import Gtk
import subprocess

class mainWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title = "Updater")

        button = Gtk.Button()
        button.set_label("Update")
        button.connect("clicked", self.new_update_window)
        self.add(button)

    def new_update_window(self, button):
        update = updateWindow()
        update.show_all()
        update.update()

class updateWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title = "Updating...")

        self.label = Gtk.Label()
        self.label.set_text("Idling...")
        self.add(self.label)

    def update(self):
        self.label.set_text("Updating... Please wait.")
        subprocess.Popen(["gedit"])
        self.hold()
        self.label.set_text("Updated.")

    def run_update(self):
        subprocess.Popen(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])

    def hold(self):
        while True:
            Gtk.main_iteration()
            try:
                subprocess.check_output(["pgrep", "apt-get"]).decode("utf-8")
            except subprocess.CalledProcessError:
                break

main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()

编辑

不断深入了解有更好的线程使用方法,然后在我的回答中发布。

您可以在 Gtk GUI 中使用线程,使用

GObject.threads_init() 

然后,要从线程更新接口,使用

GObject.idle_add()

来自 this (slgihtly outdated) link:

...在应用程序初始化时调用 gobject.threads_init()。然后您正常启动线程,但要确保线程从不直接执行任何 GUI 任务。相反,您使用 gobject.idle_add 安排 GUI 任务在主线程中执行

当我们将 gobject.threads_init() 替换为 GObject.threads_init() 并将 gobject.idle_add 替换为 GObject.idle_add() 时,我们几乎已经有了如何在一个线程中 运行 线程的更新版本Gtk 应用程序。

在您的代码中应用(使用第二个示例,使用线程):

#!/usr/bin/python3
from gi.repository import Gtk, GObject
from threading import Thread
import subprocess

class mainWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title = "Updater")

        button = Gtk.Button()
        button.set_label("Update")
        button.connect("clicked", self.new_update_window)
        self.add(button)

    def new_update_window(self, button):
        update = updateWindow()
        update.show_all()
        update.update()

class updateWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title = "Updating...")

        self.label = Gtk.Label()
        self.label.set_text("Idling...")
        self.add(self.label)

    def update(self):
        # self.thread = threading.Thread(target=self.run_update)
        thread = Thread(target=self.run_update)
        thread.start()

    def run_update(self):
        GObject.idle_add(
            self.label.set_text, "Updating... Please wait.",
            priority=GObject.PRIORITY_DEFAULT
            )
        subprocess.call(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
        GObject.idle_add(
            self.label.set_text, "Updated.",
            priority=GObject.PRIORITY_DEFAULT
            )

GObject.threads_init()
main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()

不使用 Python 的 subprocess 模块,您可以使用 Gio.Subprocess,它集成了 GTK 的主循环:

#!/usr/bin/python3
from gi.repository import Gtk, Gio

# ...

class updateWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title="Updating...")

        self.label = Gtk.Label()
        self.label.set_text("Idling...")
        self.add(self.label)

    def update(self):
        self.label.set_text("Updating... Please wait.")
        subprocess = Gio.Subprocess.new(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"], 0)
        subprocess.wait_check_async(None, self._on_update_finished)

    def _on_update_finished(self, subprocess, result):
        subprocess.wait_check_finish(result)
        self.label.set_text("Updated.")