Monday, May 30, 2011

Using QtDesigner for Ruby Programming

QtDesigner is a GUI designer for Qt.
I'd like to try using it for ruby apps.

1. Preparation
a. Qt Creator - All-in-one Qt IDE, which includes Designer. Installed through Ubuntu Software Center
b. qtbindings - Ruby bindings for Qt lib. See this for installation
c. rbui4 - A tool to convert Designer outputs (ui) to ruby files, installed with qtbindings gem

2. Create a Project Folder
~/work/ruby/gt/ui_test01     # project folder
~/work/ruby/qt/ui_test01/ui  # subfolder to place QtDesigner outputs (ui files)

3. Create Forms using Designer
I followed an example shown in the second chapter of "Foundations of Qt Development" (by Johan Thelin from Apress). And I created two forms that look like:













This is a simple Phone Book application. The idea is that when you click the "Add New" or "Edit" button in the List Dialog (shown on the left), it opens the Edit Dialog (shown on the right) to add or edit item. The "Delete" button will delete a selected item from the list, and the "Clear" button will delete selected items in the list.

Note that there is a red line that links the "Clear" button and the List box. The line signifies Qt's event handling mechanism: Signal and Slot. In the Designer, you draw the line by dragging from one object (Clear button) to another (Listbox) in Edit Connections mode, selecting clicked() as it's signal and the clearSelection() as its signal-receiving slot. When you make an event connection like this within the Designer, you will not have to write codes to make the connection between signal and slot as you will see below. (I did not create any link for the other 3 buttons. I will have to write codes manually for these connections.)

The Edit Dialog form is created using an existing template which already contains a pair of the CANCEL/OK buttons. And these buttons already have connections so that I will not have to write codes for their connections.

One important thing to remember when using the Designer is to add a grid lauout to the dialog itself (not any widget group within); othewise, widgets in the dialog will not line up nicely.




Save these files (listdialog.ui and editdialog.ui) in /ui folder.
$ cd ~/work/ruby/qt/ui_test01/ui
$ ls
editdialog.ui  listdialog.ui

4. Convert to Ruby files using rbuic4
$ rbuic4 listdialog.ui -x -o listdialog_ui.rb 
$ rbuic4 editdialog.ui -x -o editdialog_ui.rb 
-x = generate extra code to test the class
-o = output file

5. A Quick Test for Generated ruby files
$ ruby listdialog_ui.rb
The result is:


















$ ruby editdialog_ui.rb
The result is:














5. Contents of ui files
QtDesigner's forms are just xml files. Here is the content of listdialog.ui:

 ListDialog
 
  
   
    0
    0
    539
    481
   
  
  
   Phone Book
  
  
   
    
     
      
       
        Add new
       
      
     
     
      
       
        Edit
       
      
     
     
      
       
        Delete
       
      
     
     
      
       
        Qt::Vertical
       
       
        
         20
         40
        
       
      
     
     
      
       
        Clear
       
      
     
    
   
   
    
   
  
 
 
 
  
   clearButton
   clicked()
   list
   clearSelection()
   
    
     495
     455
    
    
     337
     391
    
   
  
 



And here is the contents of editdialog.ui file:

 EditDialog
 
  
   
    0
    0
    421
    143
   
  
  
   Editor
  
  
   
    
     
      
       
        Name
       
      
     
     
      
     
     
      
       
        Number
       
      
     
     
      
     
    
   
   
    
     
      Qt::Vertical
     
     
      
       20
       15
      
     
    
   
   
    
     
      
       
        Qt::Horizontal
       
       
        
         108
         20
        
       
      
     
     
      
       
        Qt::Horizontal
       
       
        QDialogButtonBox::Cancel|QDialogButtonBox::Ok
       
      
     
    
   
  
 
 
 
  
   buttonBox
   accepted()
   EditDialog
   accept()
   
    
     410
     132
    
    
     157
     274
    
   
  
  
   buttonBox
   rejected()
   EditDialog
   reject()
   
    
     410
     132
    
    
     286
     274
    
   
  
 



6. Contents of generated rb files
The tool rbuic4 (Ruby UI Compiler) generates ruby files (rb) from QtDesigner's output files (ui). So listdialog_ui.rb looks like this:
=begin
** Form generated from reading ui file 'listdialog.ui'
**
** Created: 日 6月 19 11:49:30 2011
**      by: Qt User Interface Compiler version 4.7.0
**
** WARNING! All changes made in this file will be lost when recompiling ui file!
=end

require 'Qt4'

class Ui_ListDialog
    attr_reader :gridLayout
    attr_reader :verticalLayout
    attr_reader :addButton
    attr_reader :editButton
    attr_reader :deleteButton
    attr_reader :verticalSpacer
    attr_reader :clearButton
    attr_reader :list

    def setupUi(listDialog)
    if listDialog.objectName.nil?
        listDialog.objectName = "listDialog"
    end
    listDialog.resize(539, 481)
    @gridLayout = Qt::GridLayout.new(listDialog)
    @gridLayout.objectName = "gridLayout"
    @verticalLayout = Qt::VBoxLayout.new()
    @verticalLayout.objectName = "verticalLayout"
    @addButton = Qt::PushButton.new(listDialog)
    @addButton.objectName = "addButton"

    @verticalLayout.addWidget(@addButton)

    @editButton = Qt::PushButton.new(listDialog)
    @editButton.objectName = "editButton"

    @verticalLayout.addWidget(@editButton)

    @deleteButton = Qt::PushButton.new(listDialog)
    @deleteButton.objectName = "deleteButton"

    @verticalLayout.addWidget(@deleteButton)

    @verticalSpacer = Qt::SpacerItem.new(20, 40, Qt::SizePolicy::Minimum, Qt::SizePolicy::Expanding)

    @verticalLayout.addItem(@verticalSpacer)

    @clearButton = Qt::PushButton.new(listDialog)
    @clearButton.objectName = "clearButton"

    @verticalLayout.addWidget(@clearButton)


    @gridLayout.addLayout(@verticalLayout, 0, 1, 1, 1)

    @list = Qt::ListWidget.new(listDialog)
    @list.objectName = "list"

    @gridLayout.addWidget(@list, 0, 0, 1, 1)


    retranslateUi(listDialog)
    Qt::Object.connect(@clearButton, SIGNAL('clicked()'), @list, SLOT('clearSelection()'))

    Qt::MetaObject.connectSlotsByName(listDialog)
    end # setupUi

    def setup_ui(listDialog)
        setupUi(listDialog)
    end

    def retranslateUi(listDialog)
    listDialog.windowTitle = Qt::Application.translate("ListDialog", "Phone Book", nil, Qt::Application::UnicodeUTF8)
    @addButton.text = Qt::Application.translate("ListDialog", "Add new", nil, Qt::Application::UnicodeUTF8)
    @editButton.text = Qt::Application.translate("ListDialog", "Edit", nil, Qt::Application::UnicodeUTF8)
    @deleteButton.text = Qt::Application.translate("ListDialog", "Delete", nil, Qt::Application::UnicodeUTF8)
    @clearButton.text = Qt::Application.translate("ListDialog", "Clear", nil, Qt::Application::UnicodeUTF8)
    end # retranslateUi

    def retranslate_ui(listDialog)
        retranslateUi(listDialog)
    end

end

module Ui
    class ListDialog < Ui_ListDialog
    end
end  # module Ui

if $0 == __FILE__
    a = Qt::Application.new(ARGV)
    u = Ui_ListDialog.new
    w = Qt::Dialog.new
    u.setupUi(w)
    w.show
    a.exec
end

And editdialog_ui.rb looks like this:
=begin
** Form generated from reading ui file 'editdialog.ui'
**
** Created: 金 6月 3 20:45:49 2011
**      by: Qt User Interface Compiler version 4.7.0
**
** WARNING! All changes made in this file will be lost when recompiling ui file!
=end

require 'Qt4'

class Ui_EditDialog
    attr_reader :gridLayout_2
    attr_reader :gridLayout
    attr_reader :nameLabel
    attr_reader :nameEdit
    attr_reader :numberLabel
    attr_reader :numberEdit
    attr_reader :verticalSpacer
    attr_reader :horizontalLayout_3
    attr_reader :horizontalSpacer
    attr_reader :buttonBox

    def setupUi(editDialog)"
    if editDialog.objectName.nil?
        editDialog.objectName = "editDialog"
    end
    editDialog.resize(421, 143)
    @gridLayout_2 = Qt::GridLayout.new(editDialog)
    @gridLayout_2.objectName = "gridLayout_2"
    @gridLayout = Qt::GridLayout.new()
    @gridLayout.objectName = "gridLayout"
    @nameLabel = Qt::Label.new(editDialog)
    @nameLabel.objectName = "nameLabel"

    @gridLayout.addWidget(@nameLabel, 0, 0, 1, 1)

    @nameEdit = Qt::LineEdit.new(editDialog)
    @nameEdit.objectName = "nameEdit"

    @gridLayout.addWidget(@nameEdit, 0, 1, 1, 1)

    @numberLabel = Qt::Label.new(editDialog)
    @numberLabel.objectName = "numberLabel"

    @gridLayout.addWidget(@numberLabel, 1, 0, 1, 1)

    @numberEdit = Qt::LineEdit.new(editDialog)
    @numberEdit.objectName = "numberEdit"

    @gridLayout.addWidget(@numberEdit, 1, 1, 1, 1)


    @gridLayout_2.addLayout(@gridLayout, 0, 0, 1, 1)

    @verticalSpacer = Qt::SpacerItem.new(20, 15, Qt::SizePolicy::Minimum, Qt::SizePolicy::Expanding)

    @gridLayout_2.addItem(@verticalSpacer, 1, 0, 1, 1)

    @horizontalLayout_3 = Qt::HBoxLayout.new()
    @horizontalLayout_3.objectName = "horizontalLayout_3"
    @horizontalSpacer = Qt::SpacerItem.new(108, 20, Qt::SizePolicy::Expanding, Qt::SizePolicy::Minimum)

    @horizontalLayout_3.addItem(@horizontalSpacer)

    @buttonBox = Qt::DialogButtonBox.new(editDialog)
    @buttonBox.objectName = "buttonBox"
    @buttonBox.orientation = Qt::Horizontal
    @buttonBox.standardButtons = Qt::DialogButtonBox::Cancel|Qt::DialogButtonBox::Ok

    @horizontalLayout_3.addWidget(@buttonBox)


    @gridLayout_2.addLayout(@horizontalLayout_3, 2, 0, 1, 1)


    retranslateUi(editDialog)
    Qt::Object.connect(@buttonBox, SIGNAL('accepted()'), editDialog, SLOT('accept()'))
    Qt::Object.connect(@buttonBox, SIGNAL('rejected()'), editDialog, SLOT('reject()'))

    Qt::MetaObject.connectSlotsByName(editDialog)
    end # setupUi

    def setup_ui(editDialog)
        setupUi(editDialog)
    end

    def retranslateUi(editDialog)
    editDialog.windowTitle = Qt::Application.translate("EditDialog", "Editor", nil, Qt::Application::UnicodeUTF8)
    @nameLabel.text = Qt::Application.translate("EditDialog", "Name", nil, Qt::Application::UnicodeUTF8)
    @numberLabel.text = Qt::Application.translate("EditDialog", "Number", nil, Qt::Application::UnicodeUTF8)
    end # retranslateUi

    def retranslate_ui(editDialog)
        retranslateUi(editDialog)
    end

end

module Ui
    class EditDialog < Ui_EditDialog
    end
end  # module Ui

if $0 == __FILE__
    a = Qt::Application.new(ARGV)
    u = Ui_EditDialog.new
    w = Qt::Dialog.new
    u.setupUi(w)
    w.show
    a.exec
end
7. Some notes on generated ruby codes (1) You should not modify these files manually because they will be lost when re-generated. (2) The block "if $0 == __FILE__ ... end" was added by -x option.
$ rbuic4 listdialog.ui -x -o listdialog_ui.rb    # when -x option is used....
if $0 == __FILE__                                # this block is added due to -x option above
    a = Qt::Application.new(ARGV)
    u = Ui_ListDialog.new
    w = Qt::Dialog.new
    u.setupUi(w)
    w.show
    a.exec
end
This is convenient in that you can run it immediately (before writing any codes) to see how its form looks like. (3) Another convenience for this block is that it can be used as a template for your main ruby program as you will see below (main.rb). (4) The connection between signal and slot is made using string literals of method names (including parenthesis):
Qt::Object.connect(@clearButton, SIGNAL('clicked()'), @listView, SLOT('clearSelection()'))
Qt::Object.connect(@buttonBox, SIGNAL('accepted()'), editDialog, SLOT('accept()'))
Qt::Object.connect(@buttonBox, SIGNAL('rejected()'), editDialog, SLOT('reject()'))
(5) A dialog within Qt application is an instance of Qt::Dialog or Qt::Widget class. However, you see from the examples above that dialog classes in generated ruby ui files (Ui_ListDialog and Ui_EditDialog) do not inherit from Qt::Dialog or from Qt::Widget.
class Ui_ListDialog
...
end
class Ui_EditDialog
...
end
This means that somewhere in my application, I have to creates a dialog object that inherits from Qt::Dialog and that somehow I have to link it to the dialog class that defines its ui elements created by the Designer. That is done by the 3 lines in above example:
u = Ui_ListDialog.new  # insatance of ui class
w = Qt::Dialog.new     # instance of Qt::Dialog
u.setupUi(w)           # link the two to make the Qt dialog object to use UI elements
(6) A new module "Ui" is created and empty subclasses (ListDialog, EditDialog) inherited from the generated classes (Ui_ListDialog, ListDialog) is created in the new module Ui. The idea is that your own codes and changes are to be made in subclasses so that re-generating will not erase your changes. (7) The method setupUi() makes its form alive in your application by creating a bunch of instance variables for widgets and layouts in the form. A more Ruby-like method setup_ui() is created to call setupUi(). (8) The method retranslateUi() is defined for localization/internationalization of UI. 8. Creating main.rb My first version of main program started with a copy from a generated ui file (listdialog_ui.rb) and modified little. First I load those two ui files. I use subclass Ui::ListDialog instead of the parent Ui_ListDialog class. I can changes the subclass without worrying about being over-written by re-generation of ui codes. Here the goal is just to open the Edit Dialog. There is no functionality: clicking on a button does nothing.
# main.rb
# version 1
#       
require 'Qt4'
require './ui/listdialog_ui'   # load ui file generated by the Designer. I get an error without "./"  in Ruby 1.9.2
require './ui/editdialog_ui'   # load ui file generated by the Designer

if $0 == __FILE__
    a = Qt::Application.new(ARGV)
    w = Qt::Dialog.new
    u = Ui::ListDialog.new     # use subclass
    u.setup_ui(w)              # more rubyish, replacing setupUi()
    w.show
    a.exec
end

Run it.
$ ruby main.rb
The result.


















9. Updating main.rb
Now I would like to add a functionality: clicking "Add New" or "Edit" button opens an Edit Dialog. To do this, I create two Qt::Dialog classes (MainForm and EditForm) and link them to ui objects created with the Designer. Then I declare 3 slot methods that respond to cliked() signals. (I do not have to declare 'clear_selection()' slot method - already done within the Designer by drawing a line from Clear button to the List Widget.) Those slot methods create an instance of EditForm and calls exec method so that the dialog opens in modal mode. Note that I compare the return value of exec with fixed number 1. I am supposed to use predefined constant Qt::Accepted, but it crashes on me when I use it.

# main.rb
# version 2
#       

require 'Qt4'
require './ui/listdialog_ui'
require './ui/editdialog_ui'

class MainForm < Qt::Dialog

  slots 'add_item()', 'edit_item()', 'delete_item()'  # declaration
  # slots 'clear_selection'  -- in parent class: Ui_ListDialog  

  def initialize
    super
    @ui = Ui::ListDialog.new
    @ui.setup_ui(self)
    Qt::Object.connect(@ui.addButton, SIGNAL('clicked()'), self, SLOT('add_item()'))
    Qt::Object.connect(@ui.editButton, SIGNAL('clicked()'), self, SLOT('edit_item()'))
    Qt::Object.connect(@ui.deleteButton, SIGNAL('clicked()'), self, SLOT('delete_item()'))
    #-- in parent class: Ui_ListDialog
    #Qt::Object.connect(@ui.clearButton, SIGNAL('clicked()'), self, SLOT('clear_selection()')) 
 
    self.show
  end
 
  def add_item()
    d = EditForm.new(self)
    if(d.exec == 1) # I use "1" instead of Qt::Accepted constant becuase it crashes on me
    end
  end
 
  def edit_item()
    d = EditForm.new(self)
    if(d.exec == 1) # I use "1" instead of Qt::Accepted constant becuase it crashes on me
    end
  end
 
  def delete_item()
  end
 
  # -- in parent class: Ui_ListDialog
  # def clear_selection()
  # end

end

class EditForm < Qt::Dialog

  def initialize(parent=nil)
    super(parent)
    @ui = Ui::EditDialog.new
    @ui.setup_ui(self)
    self.show
  end

end

if $0 == __FILE__
  a = Qt::Application.new(ARGV)
  MainForm.new
  a.exec
end




10. Final main.rb
Finally I define all slot methods, giving all required functions to the program.

# main.rb
# version 3
#       
require 'Qt4'
require './ui/listdialog_ui'
require './ui/editdialog_ui'

class MainForm < Qt::Dialog

  slots 'add_item()', 'edit_item()', 'delete_item()'
 
  def initialize
    super
  
    @ui = Ui::ListDialog.new
    @ui.setup_ui(self)
 
    Qt::Object.connect(@ui.addButton, SIGNAL('clicked()'), self, SLOT('add_item()'))
    Qt::Object.connect(@ui.editButton, SIGNAL('clicked()'), self, SLOT('edit_item()'))
    Qt::Object.connect(@ui.deleteButton, SIGNAL('clicked()'), self, SLOT('delete_item()'))
 
    self.show
 
  end
 
  def add_item()
    d = EditForm.new(self)
    if(d.exec == 1) # OK clicked
      @ui.list.add_item(d.name + ": " + d.number)
    end
  end
 
  def edit_item()
    if(@ui.list.current_item) # if any item is selected
      temp = @ui.list.current_item.text
      a = temp.split(/: /)
      d = EditForm.new(self)
      d.name = a[0]
      d.number = a[1]
      if(d.exec == 1) # OK clicked
        @ui.list.current_item.text = d.name + ": " + d.number
      end
    end
  end
 
  def delete_item()
    @ui.list.current_item.dispose # delete selected object
  end
  
end

class EditForm < Qt::Dialog
 
  def initialize(parent=nil)
    super(parent)
    @ui = Ui::EditDialog.new
    @ui.setup_ui(self)
    self.show
  end

  def name
    @ui.nameEdit.text
  end
 
  def name=(s)
    @ui.nameEdit.set_text(s)
  end
 
  def number
    @ui.numberEdit.text
  end
 
  def number=(s)
    @ui.numberEdit.set_text(s)
  end
 
end

if $0 == __FILE__
  a = Qt::Application.new(ARGV)
  MainForm.new
  a.exec
end

4 comments:

  1. Great tutorial, thanks a lot!

    ReplyDelete
  2. This is the best tutorial I've found explaining Ruby, Qt and QtCreator... really thanks

    ReplyDelete
  3. Thank you so much kind sir! You saved my life :) Awesome tutorial

    ReplyDelete
  4. Thanks for the tutorial but I still have a couple of questions to ask. How can I safely read and save binary files? You see, I've been trying to allow my code to store info in such a way both Ruby and QtRuby can read it. I know Marshal.dump and load methods don't work, their Marshal versions conflict some how. What should I do then?

    ReplyDelete