在 GTKMM 中,on_draw 方法在分离线程中发生无效后停止调用
In GTKMM, on_draw method stops being called after invalidate occurs in separated thread
使用 GTKMM,我正在扩展 DrawingArea 小部件,其想法是外部进程为其提供图像。然后,我的 CameraDrawingArea 将使用 Cairo.
以正确的大小显示图像
每次图像到达时,我都会存储它并调用 invalidate
方法,最终调用 on_draw
,我可以在其中调整大小并显示图像。
我的问题如下:
- 前 10 或 20 个图像如我所料显示。
- 一段时间后,图像不断从提供者进程中传来,我不断调用
invalidate
- 但
on_draw
不再被调用。
为了在此处展示它,我简化了代码,以便 class 没有任何外部内容,并且 link 没有其他库。我已经用 for 循环的方法替换了提供图像的过程,并通过在小部件区域中间打印一个简单的文本来显示图像:
- 在构造函数中,我启动了一个新的
std::thread
以在同一实例中调用 doCapture
方法。我还设置了一个字体说明,以后用到。
doCapture
方法是一个愚蠢的 CPU 吃货,除了不时调用 refreshDrawing
方法外什么都不做,只要 keepCapturing
不是 false
.
refreshDrawing
通过调用 invalidate
. 使整个 window 的矩形无效
- Gtk 的魔法假设调用
on_draw
并提供 Cairo 上下文来绘制任何内容。在我的例子中,出于测试目的,我画了一个褐色居中的整数。
- class 析构函数通过将
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
我很确定我做错了什么,但不知道是什么。任何帮助将不胜感激。
我也检查了以下问题,但它们似乎与我的案例无关:
除了您启动 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();
}
}
现在,这没有其他问题了。
使用 GTKMM,我正在扩展 DrawingArea 小部件,其想法是外部进程为其提供图像。然后,我的 CameraDrawingArea 将使用 Cairo.
以正确的大小显示图像每次图像到达时,我都会存储它并调用 invalidate
方法,最终调用 on_draw
,我可以在其中调整大小并显示图像。
我的问题如下:
- 前 10 或 20 个图像如我所料显示。
- 一段时间后,图像不断从提供者进程中传来,我不断调用
invalidate
- 但
on_draw
不再被调用。
为了在此处展示它,我简化了代码,以便 class 没有任何外部内容,并且 link 没有其他库。我已经用 for 循环的方法替换了提供图像的过程,并通过在小部件区域中间打印一个简单的文本来显示图像:
- 在构造函数中,我启动了一个新的
std::thread
以在同一实例中调用doCapture
方法。我还设置了一个字体说明,以后用到。 doCapture
方法是一个愚蠢的 CPU 吃货,除了不时调用refreshDrawing
方法外什么都不做,只要keepCapturing
不是false
.refreshDrawing
通过调用invalidate
. 使整个 window 的矩形无效
- Gtk 的魔法假设调用
on_draw
并提供 Cairo 上下文来绘制任何内容。在我的例子中,出于测试目的,我画了一个褐色居中的整数。 - class 析构函数通过将
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
我很确定我做错了什么,但不知道是什么。任何帮助将不胜感激。
我也检查了以下问题,但它们似乎与我的案例无关:
除了您启动 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();
}
}
现在,这没有其他问题了。