在 GTKMM 中,on_draw 方法在分离线程中发生无效后停止调用

In GTKMM, on_draw method stops being called after invalidate occurs in separated thread

使用 GTKMM,我正在扩展 DrawingArea 小部件,其想法是外部进程为其提供图像。然后,我的 CameraDrawingArea 将使用 Cairo.

以正确的大小显示图像

每次图像到达时,我都会存储它并调用 invalidate 方法,最终调用 on_draw,我可以在其中调整大小并显示图像。

我的问题如下:

为了在此处展示它,我简化了代码,以便 class 没有任何外部内容,并且 link 没有其他库。我已经用 for 循环的方法替换了提供图像的过程,并通过在小部件区域中间打印一个简单的文本来显示图像:

    #include "camera-drawing-area.hpp"

    #include <iostream>

    CameraDrawingArea::CameraDrawingArea():
    captureThread(nullptr) {
        fontDescription.set_family("Monospace");
        fontDescription.set_weight(Pango::WEIGHT_BOLD);
        fontDescription.set_size(30 * Pango::SCALE);

        keepCapturing = true;
        captureThread = new std::thread([this] { 
                doCapture(); 
                });
    }

    void CameraDrawingArea::doCapture() {
        while (keepCapturing) {
            float f = 0.0;
            for (int n = 0; n < 1000; n++) {
                for (int m = 0; m < 1000; m++) {
                    for (int o = 0; o < 500; o++) {
                        f += 1.2;
                    }
                }
            }
            std::cout << "doCapture - " << f << std::endl; 
            refreshDrawing();
        }
    }

    void CameraDrawingArea::refreshDrawing() {
        auto win = get_window();
        if (win) {
            win->invalidate(false);
            std::cout << "refreshDrawing" << std::endl; 
        }
    }

    bool CameraDrawingArea::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) {
        std::cout << "on_draw" << std::endl; 

        static char buffer[50];
        static int n = 0;
        sprintf(buffer, "-%d-", n++);

        Gtk::Allocation allocation = get_allocation();
        const int width = allocation.get_width();
        const int height = allocation.get_height();


        auto layout = create_pango_layout(buffer);
        layout->set_font_description(fontDescription);

        int textWidth, textHeight;
        layout->get_pixel_size(textWidth, textHeight);
        cr->set_source_rgb(0.5, 0.2, 0.1);
        cr->move_to((width - textWidth) / 2, (height - textHeight) / 2);
        layout->show_in_cairo_context(cr);
        cr->stroke();

        return true;
    }

    CameraDrawingArea::~CameraDrawingArea() {
        keepCapturing = false;
        captureThread->join();
        free(captureThread);
    }

这是我的头文件:

    #ifndef CAMERA_DRAWING_AREA_HPP
    #define CAMERA_DRAWING_AREA_HPP

    #include <gtkmm.h>
    #include <thread>

    class CameraDrawingArea : public Gtk::DrawingArea {
    public:
        CameraDrawingArea();
        virtual ~CameraDrawingArea();

    protected:
        bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;

    private:
        bool keepCapturing;
        void doCapture();
        void refreshDrawing();
        std::thread* captureThread;
        Pango::FontDescription fontDescription;
    };
    #endif

问题表现如下:

此外,如果我在应用程序停止刷新之前停止它,那么它会很好地结束。但是,如果我在应用程序停止刷新后停止它,那么我会在下面看到这条消息(ID 值不同):

GLib-CRITICAL **: 10:05:04.716: Source ID 25 was not found when attempting to remove it

我很确定我做错了什么,但不知道是什么。任何帮助将不胜感激。

我也检查了以下问题,但它们似乎与我的案例无关:

除了您启动 GTK 主循环的线程之外,您不能从任何其他线程使用 GTK 方法。可能 win->invalidate() 调用导致此处出现问题。

相反,使用 Glib::Dispatcher 与主线程通信,或使用 gdk_threads_add_idle() 以获得更 C 风格的解决方案。

根据@ptomato 的回答,我重写了我的示例代码。黄金法则是不要从另一个线程调用 GUI 函数,但如果这样做,则首先获取一些特定的 GDK 锁。这就是 Glib::Dispatcher 的目的:

If a Glib::Dispatcher object is constructed in the main GUI thread (which will therefore be the receiver thread), any worker thread can emit on it and have the connected slots safely execute gtkmm functions.

基于此,我添加了一个新的私有成员 Glib::Dispatcher refreshDrawingDispatcher,它将允许线程安全地进入 invalidate windows 区域:

    #ifndef CAMERA_DRAWING_AREA_HPP
    #define CAMERA_DRAWING_AREA_HPP

    #include <gtkmm.h>
    #include <thread>

    class CameraDrawingArea :
    public Gtk::DrawingArea {
    public:
        CameraDrawingArea();
        virtual ~CameraDrawingArea();

    protected:
        bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override;

    private:
        bool keepCapturing;
        void doCapture();
        void refreshDrawing();
        Glib::Dispatcher refreshDrawingDispatcher;
        std::thread* captureThread;
        Pango::FontDescription fontDescription;
    };
    #endif

然后,我将调度程序连接到 refreshDrawing 方法。我在 class 构造函数中执行此操作,该构造函数在 GUI 启动期间调用,因此在主 GUI 线程中调用:

    CameraDrawingArea::CameraDrawingArea():
    refreshDrawingDispatcher(),
    captureThread(nullptr) {
        fontDescription.set_family("Monospace");
        fontDescription.set_weight(Pango::WEIGHT_BOLD);
        fontDescription.set_size(30 * Pango::SCALE);

        keepCapturing = true;
        captureThread = new std::thread([this] {
                doCapture(); 
                });

        refreshDrawingDispatcher.connect(sigc::mem_fun(*this, &CameraDrawingArea::refreshDrawing));
    }   

最后,线程必须调用调度程序:

    void CameraDrawingArea::doCapture() {
        while (keepCapturing) {
            float f = 0.0;
            for (int n = 0; n < 1000; n++) {
                for (int m = 0; m < 1000; m++) {
                    for (int o = 0; o < 500; o++) {
                        f += 1.2;
                    }
                }
            }
            std::cout << "doCapture - " << f << std::endl; 
            refreshDrawingDispatcher.emit();
        }
    }

现在,这没有其他问题了。