1

I'm working on an application with Ruby Tkinter and I've run into an issue defining events for buttons in a loop.

Take for example the code below:

array = ["one","two","three","four","five","six","seven","eight","nine","ten","eleven","twelve"]
new_array = []

for i in 0..(array.length - 1)
    new_array.append [i,Proc.new {puts array[i]}]
end

for i in 0..(new_array.length - 1)
    new_array[i][1].call
end

When the code above is run I get the following output as expected:

one
two
three
four
five
six
seven
eight
nine
ten
eleven
twelve

But if I want to create a set of buttons in a loop for my Tkinter application applying the same concept:

require 'tk'
root = TkRoot.new()
root.title("Test")

list = ["one","two","three","four","five","six","seven","eight","nine","ten","eleven","twelve"]

for i in (0..list.length - 1)
    button = Tk::Tile::Button.new(root).pack :side => "top", :expand => false, :fill => "x"
    button.text = (i + 1).to_s
    button.command = Proc.new {puts list[i]}
end

Tk.mainloop

I get this output if I press all the buttons in the window from the code above:

twelve
twelve
twelve
twelve
twelve
twelve
twelve
twelve
twelve
twelve
twelve
twelve

What is going on? Why are my button events all the same? I've seen something somewhere about "late event binding" issues for Tk in Python, but I haven't been able to find many solutions. Especially not for Ruby.

pocketonion
  • 13
  • 1
  • 5

1 Answers1

0

This is a common closure problem: the procs end up referencing i itself rather than its value when Proc.new is called. This is more common in JavaScript than Ruby because loops are rarely used in Ruby.

The easiest Ruby solution is to not use a loop at all:

list.each_with_index do |item, i|
  button         = Tk::Tile::Button.new(root).pack side: 'top', expand: false, fill: 'x'
  button.text    = (i + 1).to_s
  button.command = Proc.new { puts item }
end

You could also say:

list.length.times do |i|
  button         = Tk::Tile::Button.new(root).pack side: 'top', expand: false, fill: 'x'
  button.text    = (i + 1).to_s
  button.command = Proc.new { puts list[i] }
end

but each_with_index is more idiomatic.

mu is too short
  • 426,620
  • 70
  • 833
  • 800