已解决:尝试将线程与 GTK 和 Pycairo 一起使用时出现错误(绘制 window 并从另一个线程发出信号)

SOLVED: I get an error when trying to use threading with GTK & Pycairo (to draw a window and signal it from another thread)

解决方案

解决方案的修改

抱歉,diff markdown 似乎没有正确显示,希望您仍然应该了解解决方案的工作原理

Window class

class Window(Gtk.Window):
-    __gsignals__ = {
-        'update_signal': (GObject.SIGNAL_RUN_FIRST, None,
-                      ())
-    }
-
-    def do_update_signal(self):
-        print("UPDATE SIGNAL CALLED")
-        self.shapes = self.shapes_channel.read()
-        print("Num new shapes:", len(self.shapes))
-        self.show_all()

在class方法中init_ui

         self.connect("delete-event", Gtk.main_quit)
+        self.show_all()
+        a = self.darea.get_allocation()
+        print (a.x, a.y, a.width, a.height)
+        self.img = cairo.ImageSurface(cairo.Format.RGB24, a.width, a.height)

一个新的class方法update_shapes

+    def update_shapes(self, shapes):
+        self.shapes = shapes
+        cr = cairo.Context(self.img)
+        self.draw_background(cr)
+        for shape in self.shapes:
+            shape.draw(cr)
+        self.darea.queue_draw()
+        return True

主要代码

- shapes_channel = Channel()
  iter_num = 0
- def optimize(chan, prob, signaller):
+ def optimize(prob, signaller):
      def print_iter_num(xk):
          global iter_num
          iter_num += 1
          prob.update_positions(xk)
          prob.update_grads(jacobian(xk))
          new_shapes = convert_grid(prob.grid, building_size=1.0/GRID_SIZE)
-         chan.write(new_shapes)
-         signaller.emit("update_signal")
+         GLib.idle_add(signaller.update_shapes, new_shapes)
          print("Iteration", iter_num, "complete...")
      try:
          sol = minimize(objective, x0, bounds = all_bounds, constraints=constraints, options={'maxiter': MAX_ITER, 'disp': True}, callback=print_iter_num, jac=jacobian)
          prob.update_positions(sol.x)
      except Exception as e:
          print("ran into an error", e)

- window = new_window(shapes_channel=shapes_channel)
+ window = new_window()
- x = threading.Thread(target=optimize, args=(shapes_channel, optim_problem, window))
+ x = threading.Thread(target=optimize, args=(optim_problem, window))
  x.start()
  window.run()

问题

Window class

import cairo
import gi
import math
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject

class Line():
    def __init__(self, start, end, thickness, colour):
        self.start = start
        self.end = end
        self.thickness = thickness
        self.colour = colour

    def draw(self, cr):
        cr.move_to(*self.start)
        cr.line_to(*self.end)
        cr.set_source_rgba(*self.colour)
        cr.set_line_width(self.thickness)
        cr.stroke()

class Polygon():
    def __init__(self, points, line_colour, line_thickness, fill_colour=None):
        self.points = points # points should be an iterable of points
        self.line_colour = line_colour
        self.line_thickness = line_thickness
        self.fill_colour = fill_colour

    def draw(self, cr):
        cr.move_to(*self.points[0])
        for point in self.points[1:]:
            cr.line_to(*point)
        cr.close_path()
        cr.set_source_rgba(*self.line_colour)
        cr.set_line_width(self.line_thickness)
        cr.stroke()
        if self.fill_colour is not None:
            cr.move_to(*self.points[0])
            for point in self.points[1:]:
                cr.line_to(*point)
            cr.close_path()
            cr.set_source_rgba(*self.fill_colour)
            cr.fill()

class Window(Gtk.Window):
    __gsignals__ = {
        'update_signal': (GObject.SIGNAL_RUN_FIRST, None,
                      ())
    }

    def do_update_signal(self):
        print("UPDATE SIGNAL CALLED")
        self.shapes = self.shapes_channel.read()
        print("Num new shapes:", len(self.shapes))
        self.show_all()

    def __init__(self, shapes_channel, window_size, background_colour=(1, 1, 1, 1), title="GTK window"):
        super(Window, self).__init__()
        self.width = window_size[0]
        self.height = window_size[1]
        self.background_colour = background_colour
        self.title = title
        self.shapes = []
        self.shapes_channel = shapes_channel
        self.init_ui()

    def init_ui(self):    
        darea = Gtk.DrawingArea()
        darea.connect("draw", self.on_draw)
        self.add(darea)
        self.set_title(self.title)
        self.resize(self.width, self.height)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", Gtk.main_quit)

    def draw_background(self, cr: cairo.Context):
        cr.scale(self.width, self.height)
        cr.rectangle(0, 0, 1, 1)  # Rectangle(x0, y0, x1, y1)
        cr.set_source_rgba(*self.background_colour)
        cr.fill()

    def on_draw(self, wid, cr: cairo.Context):
        self.draw_background(cr)
        for shape in self.shapes:
            shape.draw(cr)

    def run(self):
        Gtk.main()

def new_window(shapes_channel,
         window_size=(1000, 1000),
         background_colour=(1,1,1,1),
         title="3yp"):
    return Window(shapes_channel,
                    window_size=window_size,
                    background_colour=background_colour,
                    title=title)

我正在尝试 运行 可以绘制我定义的形状(直线和多边形)的 window。

在我向它提供形状列表并在我的应用程序结束时 运行 它工作之前它工作正常

但是,我正在尝试添加交互性并让它在 update_signal 被调用时重绘形状列表,并且新形状列表沿着 shapes_channel 传递,这是 shapes_channel 的一部分构造函数。

主要代码

这是我的主要代码中的相关位:

shapes_channel = Channel()
iter_num = 0

def optimize(chan, prob, signaller):
    def print_iter_num(xk):
        global iter_num
        iter_num += 1
        prob.update_positions(xk)
        prob.update_grads(jacobian(xk))
        new_shapes = convert_grid(prob.grid, building_size=1.0/GRID_SIZE)
        chan.write(new_shapes)
        signaller.emit("update_signal")
        print("Iteration", iter_num, "complete...")
    try:
        sol = minimize(objective, x0, bounds = all_bounds, constraints=constraints, options={'maxiter': MAX_ITER, 'disp': True}, callback=print_iter_num, jac=jacobian)
        prob.update_positions(sol.x)
    except Exception as e:
        print("ran into an error", e)

window = new_window(shapes_channel=shapes_channel)
x = threading.Thread(target=optimize, args=(shapes_channel, optim_problem, window))
x.start()
window.run()

如您所见:

  1. 创建了一个 Channel() 对象,命名为 shapes_channel
  2. 创建了一个新的 window,shapes_channel 通过中间函数 new_window 传递给构造函数。
  3. 这个window传递给另一个线程,让另一个线程 可以发出相关信号("update_signal")
  4. 另一个线程是运行
  5. 主线程中的 window 是 运行 我得到以下控制台输出:
UPDATE SIGNAL CALLED
Num new shapes: 31
Gdk-Message: 01:27:14.090: main.py: Fatal IO error 0 (Success) on X server :0.

从控制台输出,我们可以推断信号被成功调用,新的形状被传递到 window 并正确存储,但在 self.show_all() 行失败。

这是一个以前运行良好并生成图形输出的对象,从对象的角度来看,我只能想到 2 种可能发生变化的事情:

  1. Channel 对象按预期工作,但也许仅仅存在跨线程共享的对象就会使整个事情变得混乱
  2. 虽然在主线程,但不喜欢有其他线程

对于这起令人抓狂的事件,我非常希望能得到一些指导。

关于你的假设:

  1. 不清楚您的通道对象是否可以从两个线程安全访问。
  2. 信号处理程序在发出信号的线程中执行。

我的猜测是您从另一个线程发出信号导致了这个问题。

您可以使用 GLib.idle_add(your_update_func) 解决此问题。不是直接调用 your_update_func,而是将一个请求添加到 Gtk 主循环,当没有更多事件要处理时执行它,防止任何线程问题。

在此处阅读更多内容:https://wiki.gnome.org/Projects/PyGObject/Threading