Gtk3 和 cairo 动画 twitch

Gtk3 and cairo animation twitch

我用 gtk3 和 cairo 制作了一个非常简单的动画。每隔一秒钟它就会有点抽搐。真的很烦人,也不好看。为什么会发生这种情况,我该如何解决?

#include <gtk/gtk.h>
#include <cairo.h>

static int width, height,
           posX = 0,
           vX = 2;
gboolean draw(GtkWidget* widget, cairo_t* cr)
{
    GtkWidget* window = gtk_widget_get_toplevel(widget);
    gtk_window_get_size(GTK_WINDOW(window), &width, &height);

    cairo_set_source_rgb(cr, 0, 0, 0);
    cairo_set_line_width(cr, 100);

    cairo_rectangle(cr, posX, height/2, 100, 100);
    cairo_stroke(cr);

    if(posX + vX >= width || posX + vX == 0)
        vX = -vX;
    posX += vX;

    gtk_widget_queue_draw(widget);
    return TRUE;
}
int main(int argc, char** argv)
{
    GtkWidget* window;
    GtkWidget* darea;

    gtk_init(&argc, &argv);
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    darea = gtk_drawing_area_new();

    gtk_container_add(GTK_CONTAINER(window), darea);
    gtk_window_set_default_size(GTK_WINDOW(window), 500, 400);

    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    g_signal_connect(G_OBJECT(darea), "draw", G_CALLBACK(draw), NULL);

    g_timeout_add(16, (GSourceFunc)draw, window);

    gtk_widget_show_all(window);
    gtk_main();
}

首先请考虑您的原始程序在我的系统上完美运行,因此这可能是您系统上的一个问题。您可以尝试提高程序的优先级,但基本上您的代码容易受到此类问题的影响:请参阅 g_timeout_add 了解基本原理。

总之存在一个概念问题:您正在使用 draw() 做两件事。它用作 draw 信号和超时回调。这是错误的:绘图信号必须是幂等的,因为您不知道它何时以及被调用了多少次。

这是您的示例,原始函数拆分为 move()draw():

#include <gtk/gtk.h>
#include <cairo.h>

static int width, height,
           posX = 0,
           vX = 2;

static gboolean move(GtkWidget* widget)
{
    GtkWidget* window = gtk_widget_get_toplevel(widget);
    gtk_window_get_size(GTK_WINDOW(window), &width, &height);

    if(posX + vX >= width || posX + vX == 0)
        vX = -vX;
    posX += vX;

    gtk_widget_queue_draw(widget);
    return TRUE;
}

static gboolean draw(GtkWidget* widget, cairo_t* cr)
{
    cairo_set_source_rgb(cr, 0, 0, 0);
    cairo_set_line_width(cr, 100);

    cairo_rectangle(cr, posX + 0.5, height/2 + 0.5, 100, 100);
    cairo_stroke(cr);

    return FALSE;
}

int main(int argc, char** argv)
{
    GtkWidget* window;
    GtkWidget* darea;

    gtk_init(&argc, &argv);
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    darea = gtk_drawing_area_new();

    gtk_container_add(GTK_CONTAINER(window), darea);
    gtk_window_set_default_size(GTK_WINDOW(window), 500, 400);

    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    g_signal_connect(G_OBJECT(darea), "draw", G_CALLBACK(draw), NULL);

    g_timeout_add(16, (GSourceFunc)move, window);

    gtk_widget_show_all(window);
    gtk_main();
}

不确定您的问题是否会得到解决,但无论如何都必须这样做。

我认为您遇到的问题是您使用 g_timeout_add 就好像它是一个精确的时间源,这不是文档所述:

Note that timeout functions may be delayed, due to the processing of other event sources. Thus they should not be relied on for precise timing. After each call to the timeout function, the time of the next timeout is recalculated based on the current time and the given interval (it does not try to 'catch up' time lost in delays).

这意味着您在 draw 回调中的代码可能会在每次调用时稍晚(或更晚)被调用。由于未重新计算超时,错误加起来。你会失去同步并在错误的位置绘制。例如,当解码帧时,视频播放器会发生这种情况:如果帧解码时间太长,它可能会被丢弃,因为我们可能已经太晚了,需要显示下一帧。

我不确定什么是正确的解决方案,也许 GTK+(或真正为动画制作的 Clutter)开发人员可以给你一些提示,所以通过他们的 IRC 询问他们是个好主意频道。

但是,我自己在编写节拍器时遇到了这个问题。如果您尝试与 g_timeout_add 同步,错误会累积起来,您就会失去同步。这是我为我所做的和为我工作的。

首先,我在一开始就启动了一个GTimer,所以我有一个可靠、精确和绝对的时间参考。然后当我的回调被调用时,我:

  • 计算自上次刻度以来经过的时间(自您的案例中的最后一帧)
  • 播放我的滴答声(在你的情况下在正确的位置显示对象)
  • 计算下一个刻度(下一帧)之前还剩多少时间
  • 使用该值调用 g_timeout_add(它创建一个新的事件源)
  • return G_SOURCE_REMOVE 这样就不会再调用当前的事件源了(我基本上换成新的了)

这是我的节拍器代码供参考:https://github.com/liberforce/metrognome/blob/master/metronome.c

您也可以使用 g_timeout_add_full 而不是 g_timeout_add,这样您就可以使用更高的优先级。

我还推荐阅读 Owen Taylor 关于动画同步的系列文章 gnome-shell: