(Gtk.jl) Why does this MWE not produce an animation and only show the last updated draw call?

Using Gtk.jl I would like to be able to produce an animation within a canvas widget. I have this MWE, which when run, produces a window with 2 buttons and a canvas. The first button each time pressed changes the canvas to present a rectangle of a randomly chosen color. The second button uses the Timer object to call a function multiple times with a delay to again produce on the canvas a new random color, but there is strange behavior in that only the last iteration produces a visible change in the color. I have a println statement showing that the timer is calling the draw function multiple times, but only upon the final iteration is an effect seen. Is this to do with ‘double-buffering’? Is there a solution similar to plots with the display(plt) function? Is there a queue function to flush a stack?

using Gtk
using Cairo

global canvasWidget

function b_handler1(Widget)
    println("button1")
    global canvasWidget
    Gtk.draw(canvasWidget)
end
function b_handler2(Widget)
    println("button2")
    global canvasWidget

    Gtk.draw(canvasWidget)
    callFive(canvasWidget)
    
end

function callFive(canvasWidget)
    i=0
    cb(timer) = begin
        (Gtk.draw(canvasWidget))
        i+=1
        println("i=$(i)")
    end
    t = Timer(cb, 1, interval=0.5)
    wait(t)
    sleep(5)
    close(t)
    
end

function drawColor(Widget)
    println("color")
    ctx = getgc(Widget)
    h = height(Widget)
    w = width(Widget)
    rectangle(ctx, 0, 0, w, h)
    set_source_rgb(ctx, rand(), rand(), rand())
    fill(ctx)
end
win = Gtk.Window("random colors")

boxV = Gtk.Box(:v)
push!(win,boxV)

button1 = Gtk.Button("change color once")
button2 = Gtk.Button("change color 5x")

signal_connect(b_handler1,button1, "clicked")
signal_connect(b_handler2,button2, "clicked")

push!(boxV,button1)
push!(boxV,button2)

canvasWidget = Gtk.Canvas(200,200)
canvasWidget.draw = drawColor
push!(boxV,canvasWidget)

Gtk.showall(win)

Running this code (self contained) should display the effect I am referring to with the second button, which I would like to run for 5 seconds, and update the colors every half a second.

This recent post by @tobias.knopp, https://discourse.julialang.org/t/threading-1-3-success-story/27111, shows an updating bar by utilizing the new threads capabilities in Julia1.3

signal_connect(button, "clicked") do widget
  Threads.@spawn doWork()
end

Would upgrading let me use this to solve the task, or is there another approach that can be used to update upon each draw call?

Hi @mantzaris,

this is doable without threading. Just change your draw function with

function callFive(canvasWidget)
    i=0
    t0 = now()
    function cb(timer)
        Gtk.draw(canvasWidget)
        
        i+=1
        println("i=$(i)")

        if (now()-t0).value > 5000
          close(timer)
        end
    end
    t = Timer(cb, 1, interval=0.5)
end

The essential point here is that you need to leave the callback so that Gtk can draw again. In your original code the entire UI is frozen until you left the callback.

Using this pattern, I have successfully developed some async applications. This works well in case your callbacks are fast. Background threads can be simulated by repeatedly sleeping, so that the UI can be redrawn.

2 Likes

Thank you, this worked! It is great to see it operate.

1)so the callback leaving must be explicitly done from what I understand. Is there another route where a Timer is not used? I cannot find a route that does not use it.

  1. I have tried using sleep as well, and not found how it can be used
    eg. sleep(1); close(timer) does not work.

A Timer is not necessary, but a Task is. Here is a more advanced solution that I use in some real code

using Gtk
using Dates

global canvasWidget
global state 

mutable struct CallbackState
  task::Union{Task,Nothing}
  cancelled::Bool
  startTime::DateTime
  canvas::GtkCanvas
end

function cancel(state::CallbackState)
  measState.cancelled = true
end

function callback(canvasWidget)
  state = CallbackState(nothing, false, now(), canvasWidget)
  state.task = Task(()->callbackInner(state))
  schedule(state.task)
  return state
end

function callbackInner(state::CallbackState)
    i = 0
    while !state.cancelled
        Gtk.draw(state.canvas)
        
        i+=1
        println("i=$(i)")

        if (now() - state.startTime).value > 10000
          state.cancelled = true
        end
        sleep(0.5)
    end
end


function drawColor(Widget)
    println("color")
    ctx = getgc(Widget)
    h = height(Widget)
    w = width(Widget)
    rectangle(ctx, 0, 0, w, h)
    set_source_rgb(ctx, rand(), rand(), rand())
    fill(ctx)
end

win = Gtk.Window("random colors")

boxV = Gtk.Box(:v)
push!(win,boxV)

button1 = Gtk.Button("change color 5x")
button2 = Gtk.Button("cancel")

function b_handler1(widget)
    println("button1")
    global canvasWidget
    global state
    state = callback(canvasWidget)
end

function b_handler2(widget)
    global state
    state.cancelled = true
end

signal_connect(b_handler1, button1, "clicked")
signal_connect(b_handler2, button2, "clicked")

push!(boxV,button1)
push!(boxV,button2)

canvasWidget = Gtk.Canvas(200,200)
canvasWidget.draw = drawColor
push!(boxV, canvasWidget)

Gtk.showall(win)

It allows you to spawn a Task and communicate with it using a struct. As an example I have implemented a cancel button to stop the task. In real code one should wrap the globals into a Ref so that things keep typestable.

1 Like

So the Task here still runs in a thread of its own? And how does the schedule(state.task) allow the Gtk.draw(state.canvas) to occur in a loop and not become frozen on its own even if the rest of the UI is not locked up on that thread?

It is interesting how you now can pass arguments to the Task function callback which in the docs does not allow for arguments to be passed: Task(()->callbackInner(state). The syntax, means that there is a lambda associated with that function call to Task?

It does not run on a thread but on a Task, which currently is a so-called “green-thread”. Green threads are running all on the same thread and are cooperating with each other by yielding, which means they need to go sleeping from time to time.

schedule actually just says that the Task can be started but it will not do so immediately. You can have a look at the manual where @async is described. What I am doing here is the same but the Task creation is not hidden in a macro.

Yes, Task wants a function without arguments. () ->callbackInner(state) creates such a function. You can also write tmp_func() = callbackInner(state) and pass that to the Task.

1 Like