rubymultithreadinguser-interfacegtkgraphical-programming

How to update Ruby GTK window label in real time and communicate with other threads


I am building a Ruby application which consists of the code responsible for the logic of the program and the one for GUI. Both parts of the code are split in classes and run in separate threads.

Ruby Gtk library is very poorly documented. I want to know how to update specific Gtk elements in real time (for instance, text in a label, which is in a window). I want to update a specific element every second.

I also want to find out how can threads exchange data. I have tried using Queue library. When I use it, I run into an error in the console:

undefined local variable or method `queue' for #<TimerWindow:0xa36d634 ptr=0xb4201178>

Program code:

require_relative 'notifications'
require_relative 'settings_reader'


require 'gtk3'
require "thread"

q = Queue.new

class TimerWindow < Gtk::Window

  def initialize
    @label = ""

        super
        init_ui
  end

    def init_ui

    fixed = Gtk::Fixed.new
    add fixed

    button = Gtk::Button.new :label => "Quit"
        button.set_size_request 80, 35      
        button.signal_connect "clicked" do 
      @label.set_text q.pop
        end

    fixed.put button, 50, 50

        set_title  "Tomatono"
        signal_connect "destroy" do 
            Gtk.main_quit 
        end

        set_border_width 10
        @label = Gtk::Label.new "HEY"
        fixed.put @label, 20, 20


        set_default_size 250, 200
        set_window_position :center



        show_all
    end

end


class Timer

  def initialize
    # Current time in seconds
    @time = 0

    settings = Settings_Reader.new
    @work_time = Integer(settings.work_time) * 60
    @break_time = Integer(settings.break_time) * 60
    @work_text = settings.work_text
    @return_text = settings.return_text
    @break_text = settings.break_text
    @work_notif_header = settings.work_notif_header
    @break_notif_header = settings.break_notif_header
    @status_notif_header = settings.status_notif_header
    @work_status = settings.work_status
    @break_status = settings.break_status
  end

  def launch

    while true
      work_time()
      break_time()
    end

  end

  def work_time()
    puts @work_text

    notification = Notif.new(@work_notif_header, @work_text)
    notification.post

    @time = 0
    sleep(1)

      while @time < @work_time
    @time += 1
    puts "#{min_remaining()} minutes remaining" if (@time % 60) == 0

    if (@time % 60) == 0
      notification = Notif.new(@work_notif_header, "#{@work_status} #{@time / 60} minutes.")
      notification.post
    end
    q << @time
    sleep(1)
      end

  end

  def break_time
    puts @break_text
    @time = 0
    sleep(1)

    while @time < @break_time
    @time += 1
    puts "#{min_remaining()} minutes remaining" if (@time % 60) == 0
    notification = Notif.new(@break_notif_header, "#{@break_status} #{@time / 60} minutes.")
    notification.post

    q << @time
    sleep(1)
    end

  end

  def reset
  end

  def stop_time
  end

  def min_remaining()
    (1500 - @time) / 60
  end


end



app = Thread.new {
  timer = Timer.new
  timer.launch
}

gui = Thread.new {
  Gtk.init
    window = TimerWindow.new
    #window.update
  Gtk.main
}

app.join
gui.join

Whenever I press the "Quit" button, I want the label text to change to the value set in the q variable, set in the Timer class (in the while loop). But it throws out an error that variable does not exist. Should it not be global?


Solution

  • No It is a local variable:

    myglobal = "toto"
    
    class Myclass
      def initialize
        @myvar = myglobal
      end
      def print
        puts @myvar
      end
    end
    
    an_instance = Myclass.new
    
    an_instance.print
    

    throw this error:

    global_and_class.rb:5:in `initialize': undefined local variable or method `myglobal' for #<Myclass:0x00000000a74ce8> (NameError)
    

    But it works if you specify myglobal as a global variable:

    $myglobal = "toto"
    
    class Myclass
      def initialize
        @myvar = $myglobal
      end
      def print
        puts @myvar
      end
    end
    
    an_instance = Myclass.new
    
    an_instance.print
    

    But you should be carefull with the use of global variable. Why not use the Queue instance as an argument for the initialize method ?

    ** Edit **

    First of all here is a simple example that works with just a local variable:

    #!/usr/bin/env ruby
    
    require "gtk3"
    label = Gtk::Label.new("test")
    othert = Thread.new {
      loop { 
        puts 'thread running';
         label.text = Time.now.to_s; sleep 1 }
    }
    maint = Thread.new {
      win = Gtk::Window.new
      win.set_default_size 100, 30
      win.add(label)
    
      win.show_all
      win.signal_connect("destroy") {othert.kill;Gtk.main_quit}
      Gtk.main
    }
    maint.join
    othert.join
    

    Maybe you should start from this and see how to create you classes.

    Edit 2

    class TimerWindow < Gtk::Window
      def initialize(label)
        super()
        add(label)
      end
    end
    
    alabel = Gtk::Label.enw("test")
    othert = Thread.new {
      loop { 
        puts 'thread running';
         label.text = Time.now.to_s; sleep 1 }
    }
    maint = Thread.new {
      win = TimerWindow.new(alabel)
      win.set_default_size 100, 30
      win.show_all
      win.signal_connect("destroy") {othert.kill;Gtk.main_quit}
      Gtk.main
    }
    maint.join
    othert.join