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

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

本文关键字:draw 方法 调用 on 失效 分离 GTKMM 线程      更新时间:2023-10-16

使用GTKMM,我扩展了DrawingArea小部件,认为外部进程为其提供图像。然后,我的CameraDrawingArea将使用Cairo以正确的大小显示图像。

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

我的问题如下:

  • 前10或20个图像将按我的预期显示
  • 过了一段时间,图像不断来自提供者进程,我一直在调用invalidate
  • 但是CCD_ 4不再被调用

为了在这里展示它,我简化了代码,这样类就没有外部的东西,也没有与其他库的链接。我已经用for循环的方法代替了提供图像的过程,并通过在小部件区域的中间打印一个简单的文本来显示图像:

  • 在构造函数中,我启动一个新的std::thread来调用同一实例中的doCapture方法。我还设置了一个字体描述,以便以后使用
  • doCapture方法是一个愚蠢的CPU食客,它除了不时调用refreshDrawing方法之外什么都不做,只要keepCapturing不是false
  • CCD_ 11通过对CCD_ 12的调用使整个窗口的矩形无效
  • Gtk的魔术是假设调用on_draw并提供Cairo上下文来绘制任何内容。在我的例子中,为了测试的目的,我画了一个以褐色为中心的整数
  • 类析构函数通过将keepCapturing设置为false来停止线程,并等待使用join终止
#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

问题表现如下:

  • 启动应用程序时,它会忠实地显示1、2、3
  • 在第5次和第20次迭代之间(它是随机的,但很少超出这些范围),它停止刷新
  • 由于cout,我可以看到refreshDrawing被调用了——确保invalidate也被调用了,但on_draw没有

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

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

我很确定我做错了什么,但对什么一无所知。如有任何帮助,我们将不胜感激。

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

  • 绘制信号不';当派生类没有';t调用超类';s构造函数

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

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

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

如果在主GUI线程(因此将成为接收器线程)中构造Glib::Dispatcher对象,则任何工作线程都可以在其上发出,并使连接的插槽安全地执行gtkmm函数。

在此基础上,我添加了一个新的私有成员Glib::Dispatcher refreshDrawingDispatcher,它将允许线程安全地访问invalidate窗口区域:

#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方法。我在类构造函数中这样做,它在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();
}
}

现在,这项工作没有进一步的问题。