Interface Gráfica com Ruby

Quando se pensa em Ruby, logo se imagina usando Rails. Na verdade, Ruby é uma linguagem completa com suporte a praticamente qualquer coisa. Todo programador Ruby sabe disso, mas esquecemos isso em nosso dia-a-dia.

Muitas pessoas usam várias GEMs para tentar automatizar qualquer coisa – e isso é bem errado. Por exemplo, há várias gems que tentam trazer o ActiveRecord para Sinatra, enquanto que na verdade, tudo o que temos que fazer é o código abaixo:

ActiveRecord::Base.establish_connection(
  adapter: 'sqlite3',
  database: 'some_database.sqlite3'
)

E é a mesma coisa com GUI (Graphical User Interface). Há uma série de gems que tentam trazer uma interface gráfica “fácil” para nós, que na verdade só acabam complicando tudo. Por isso, temos às vezes que simplificar o processo – que no caso de interfaces gráficas, para mim significa usar a gem qtbindings

A gem qtbindings na verdade faz um bind com a gem QT. Na prática, ela faz praticamente a mesma coisa que o JRuby faz com bibliotecas Java: traduz os métodos que seriam C++ para Ruby. Por exemplo, um método QMainWindow#setWindowTitle(QString title) será traduzido para ruby como Qt::MainWindow#window_title = “Some Ruby String”.

Basicamente, a tela é montada no Qt Designer, um aplicativo da própria QT e que provavelmente virá instalado na maior parte dos Linux. Ao salvar cada tela, teremos um arquivo .ui. Esse arquivo será convertido num código Ruby de interface usando o programa rbuic4, que já vem com a gem qtbindings. Depois, só precisamos fazer um “require” nesse arquivo gerado, e mapear nossos eventos. Na verdade, o processo todo é bem simples, então vou usar o espaço aqui do blog para falar da organização dos arquivos.

Eu costumo deixar todos os arquivos que eu gero com o rbuic4 numa pasta chamada ui. Dessa forma, eu posso só fazer require_relative ‘ui/main.rb’ e já possuo uma classe chamada Ui::Main, que eu posso usar. É importante mencionar que cada tela que criamos no designer deve ter seus nomes alterados para que reflitam um nome de uma classe, caso contrário o rbuic4 pode gerar arquivos inválidos. Para mapear uma classe, eu costumo fazer o seguinte (assumindo que criamos uma QMainWindow no designer chamada Main, por exemplo):

require 'qt'
require_relative 'ui/main'

class Main < Qt::MainWindow
  def initialize
    super("My main window's title")
    @ui = Ui::Main.new
    @ui.setup_ui(self)
  end
end

Isso já cria uma tela principal. Se quisermos mostrar essa tela, podemos usar o seguinte comando (digamos, num outro arquivo “start.rb”):

require 'qt'
require_relative 'main'

app = Qt::Application.new(ARGV)
main = Main.new
main.show
app.exec

Repare bem nas linhas 5 e 6: Perceba que eu atribuí uma variável “main”, e só depois eu rodei show. Ao contrário de Java, aonde poderíamos fazer algo como new Main().show(), isso não é válido em QT – e o motivo será explicado mais tarde. Por hora, entenda a seguinte regra: todo objeto de interface precisa manter-se referenciado enquanto está sendo exibido na tela. Ele nunca pode sair de escopo.

Mas nenhum aplicativo desktop está bom se não puder reagir a eventos. Então, digamos que na nossa UI temos um objeto chamado “button”, e que queremos reagir a eventos de click pra ele. Pela documentação, vemos que ele responde a alguns sinais, e um deles é clicked(bool checked=false). Conectamos usando um método especial – SIGNAL – no qtbindings:

require 'qt'
require_relative 'ui/main'

class Main < Qt::MainWindow
  def initialize
    super("My main window's title")
    @ui = Ui::Main.new
    @ui.setup_ui(self)

    @ui.button.connect SIGNAL('clicked(bool)') do |checked|
      puts "O botão foi pressionado!"
    end
  end
end

A partir daí, fazer interfaces gráficas em Ruby é um sossego – apenas conectamos eventos e usamos Ruby normalmente.

Sugestões

Conectar eventos dessa forma é mais “Java” e “C” do que “Ruby”. Podemos tentar minimizar o impacto de tais operações usando um Hash como representação da tela. Imagine, por exemplo, que temos um banco de dados com uma tabela chamada people, e com os campos: name, age, e birth_date. Se fizermos uma interface no QT Designer, podemos nomear um campo de texto como name, um campo do tipo QSpinBox do chamado age, e um do tipo QDateEdit chamado birth_date.

A primeira coisa que teríamos que fazer, nesse caso, seria mapear que todos esses objetos retornem uma API igual. Para facilitar, vamos dizer que todos precisam suportar os métodos text e text=. Podemos resolver isso com monkey-patch:

class Qt::DateEdit
  def text
    date.toString("d/M/yyyy")
  end

  def text=(value)
    if value.is_a?(Date)
      my_date = Qt::Date.new
      my_date.setDate(value.year, value.month, value.day)
      self.setDate(my_date)
    end
  end
end

class Qt::SpinBox
  def text
    value.to_s
  end

  def text=(value)
    self.setValue(value.to_i)
  end
end

Depois disso, podemos usar nosso próprio modelo Ruby para definir os campos que vamos usar da interface, e dessa forma promover a exibição automática dos dados na tela (no caso de uma consulta) ou salvar o registro (no caso de criação/modificação) sem precisar de muito código:

require 'qt'
require_relative 'ui/main'

class Main < Qt::MainWindow
  def initialize
    super("My main window's title")
    @ui = Ui::Main.new
    @ui.setup_ui(self)

    @ui.button.connect SIGNAL('clicked(bool)') do |checked|
      puts "O botão foi pressionado!"
    end
    
    # Mapeia os objetos da interface com os objetos do banco de dados
    fields = User.attribute_names - ['id', 'created_at', 'updated_at']
    @interface_fields = fields.inject({}) do |hash, field|
      hash.merge field => @ui.send(field)
    end
  end
		
	def show_data_for_user(user)
		@interface_fields.each_pair do |field_name, ui_object|
      ui_object.text = user[field_name]
    end
	end
  
  def save_user(user)
    @interface_fields.each_pair do |field_name, ui_object|
      user[field_name] = ui_object.text
    end
    user.save
  end
end

Essa abordagem tem MUITAS vantagens: a principal está acima, porém outras podem ser: em caso de erro, temos um mapeamento simples em que podemos deixar todos os campos que deram erro em vermelho, ou o fato de já termos um mapeamento dos campos permite “zerar” todos os campos para seus valores padrão, etc.

SMOKE bindings

O qt_bindings possui um problema muito sério: todos os bindings dele de C++ para Ruby são feitos de forma automática usando uma biblioteca C++ chamada SMOKE. O problema do smoke é que seus bindings são extremamente fracos: primeiro, ele tenta fazer mais ou menos o que o JRuby faz com Java: tenta converter camelCaseMethods para camel_case_methods, e setValue(String) para value = String. Isso é muito bom, o problema é que quando tentamos fazer alguns códigos – por exemplo, se tentarmos fazer um some_input.text = 10, ele vai dar um erro de UndefinedMethod. Claro que o método está definido – o problema é que a assinatura não bateu. Às vezes, isso é difícil de achar.

Outra coisa perigosa é que o SMOKE não sabe quando seus objetos estão em foco ou não. Em Java, com Swing por exemplo, você pode usar algo como new MainWindow().show() que tudo funciona tranquilamente. Em Ruby com qt_bindings, isso não acontece. Os códigos abaixo, ambos, dariam problemas:

# 1: Main.new não cria uma variável, e ela sai de foco:
Main.new.show
app.exec

# 2: Main.new é criado numa variável local dentro da tela de login, e sai de escopo
class Login < Qt::Dialog
  #... códigos ...
  def show_main
    main = Main.new
    main.show
  end
end
login = Login.new
login.show
app.exec

Provavelmente você teria uma mensagem de erro BEM bizarra do tipo: UndefinedMethodError: no method “strip!” for 10:Fixnum, numa linha que obviamente chamaria o método strip! numa variável de instância que só poderia ser uma String. Isso acontece, bizarramente, porque o SMOKE não detectou que tinha que manter aquela variávem de instância em foco, então o Ruby chamou o Garbage Collector, coletou a variável, e o interpretador pôs um número no lugar (ou qualquer outro objeto – nesse caso, obviamente, a parte 10:Fixnum iria mudar). Ou, às vezes, ele dará um SegFault falando que o interpretador Ruby quebrou…

O ponto é: usando qt_bindings, SEMPRE mantenha as telas que estão sendo exibidas em foco. SEMPRE. Nem que você tenha que criar variáveis de instância em classes que não precisariam mantê-las. O qt_bindings NÃO FARÁ ISSO PRA VOCÊ, então fique atento. Estou pegando muito nessa tecla porque é um dos problemas mais difíceis de resolver, e um dos que eu mais cometi nas últimas vezes que mexi com Qt4.

Tirando esses problemas, interfaces gráficas com Qt e Ruby são fascinantemente simples de se fazer, e muito fáceis de se programar. A única coisa que ainda fica em falta é uma boa forma de testá-las unitariamente, que infelizmente no ramo de interfaces gráficas, ainda é uma grande deficiência.

Advertisements
This entry was posted in Ruby and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s