前言
隨著RubyGnome2庫越來越完善,以及ruby1.9的性能提升,用Ruby編寫GUI程序漸漸從我的業(yè)余愛好轉(zhuǎn)為我工作的一個重要部分。
用Ruby寫程序確實很有樂趣,它可以讓你的想法快速地以一種優(yōu)雅的方式實現(xiàn)。本文介紹的一個gem就是一個例子,用很少的代碼,實現(xiàn)很有趣的功能,讓編寫Ruby GUI程序變得輕松愉快。
RubyGnome2介紹
雖然我以前也曾經(jīng)多次地介紹過RubyGnome2,但我還是想再一次地推薦RubyGnome2,它實在是使用Ruby編寫GUI程序的首選。
RubyGnome2是GTK+庫的一個ruby擴展。它對GTK+的對象模型仔細地用Ruby的方式進行封裝,保留了GTK+ API命名方式和含義,因此GTK+的文檔對于RubyGnome2也是適用的---盡管我認(rèn)為RubyGnome2的文檔已經(jīng)做得非常不錯了,需要回去借鑒GTK文檔的地方實在不多。
雖然GTK本身是由C編寫的,但它有一套完整的精心設(shè)計對象體系,使得它的GUI元件可以非常靈活的自由組合,以實現(xiàn)復(fù)雜的,功能強大的界面。
由于GTK非常重視它的對象體系的靈活性,因此剛開始使用GTK編程并不容易。很多時候表面上看起來很簡單的一個功能,在GTK里面卻要繞幾個彎才能實現(xiàn)。例如要設(shè)置一個label的字體,要通過Pango來實現(xiàn),Pango接管了GTK的所有字體渲染事務(wù)....這種“多繞幾個彎”的情況很多,它確實使得編寫GTK程序不那么直接了當(dāng)。但換來的是整個GTK系統(tǒng)變得非常靈活,以較少的代價實現(xiàn)強大的功能,在跨平臺,換膚,國際化上面都有很好的表現(xiàn)。
RubyGnome2繼承了GTK的所有特點,包括優(yōu)點和缺點。
GUI布局
GTK程序的布局很靈活,有許多種容器可選擇。在布局的時候,大多數(shù)都是推薦相對位置而不是絕對位置,這樣GUI程序可以更好的適應(yīng)不同分辨率的屏幕,也有利于特定風(fēng)格對UI的fine tune。
最常見的容器就是“盒子”,包括“水平盒子”和“垂直盒子”。將可視的UI元件放入“盒子”,不同的“盒子”互相組合疊放就可以構(gòu)建出目標(biāo)布局。
理論上用盒子就可以構(gòu)建任何相對位置布局,但是為了方面,GTK還提供了像table這樣的更高級的容器。
盒子模型對于許多剛開始GTK編程的人覺得很難適應(yīng),即使用了可視化布局工具例如Glade,初學(xué)者也往往被盒子模型困擾??梢暬季制髯钌瞄L的是固定位置布局。對于相對位置布局,很多時候用代碼構(gòu)建界面比用Glade反而來的快捷方便。
但是用代碼構(gòu)建界面有一個顯著的缺點,那就是“不直觀”,即很難從代碼中看出UI布局,這樣會給后期維護以及變更帶來麻煩。
有一些GUI庫如Shose,用builder風(fēng)格的代碼來描述UI,使得UI布局可以通過代碼形象地體現(xiàn)出來,如以下這個例子來自shooose.net:
Shoes.app {
stack(:margin => 4) {
button "Mice"
button "Eagles"
button "Quail"
}
}
這樣,我們就可以從代碼中一下子看出UI布局了。
builder風(fēng)格的代碼和HTML很類似,對于熟悉HTML的web界面設(shè)計者來說,可視化的編輯器并沒有多大的必要。
RubyGnome2沒有為我們提供builder方式的布局,因此UI代碼寫起來就像:
class MyWin Gtk::Window
def initialize
super
vbox = Gtk::VBox.new
btn_mice = Gtk::Button.new 'Mice'
vbox.pack_start btn_mice
btn_eagles = Gtk::Button.new 'Eagles'
vbox.pack_start btn_eagles
btn_quail = Gtk::Button.new 'Quail'
vbox.pack_start btn_quail
add vbox
end
end
從上面的代碼中很難一下子看出UI布局。
如果也為RubyGnome2構(gòu)建一個builder風(fēng)格的布局器,那么代碼就會變成:
class MyWin Gtk::Window
def initialize
super
add my_layout
end
def my_layout
vbox do
button 'Mice'
button 'Eagles'
button 'Quail'
end
end
end
嗯,這個代碼就和Shose差不多了,可以從代碼中一眼看出UI布局。
本文所介紹的GtkSimpleLayout其功能之一就是為RubyGnome2提供builder風(fēng)格的布局器。
GtkSimpleLayout布局器
這個簡單的布局器原先只有200行不到的代碼,我經(jīng)常是直接拷貝到項目中使用。后來逐漸添了些功能,覺得它變得更有用了,于是便發(fā)布到github生成gem,方便感興趣者使用。
Source: git://github.com/rickyzheng/GtkSimpleLayout.git
or:
gem source -a http://gems.github.com gem install rickyzheng-GtkSimpleLayout
以下是主要功能介紹以及簡單例子。
提供Builder風(fēng)格布局
正如上面的例子中所介紹的,GtkSimpleLayout為RubyGnome2帶來了builder風(fēng)格的布局功能,只需要為布局的類擴展GtkSimpleLayout::Base即可,一個完整的例子:
require 'gtk2'
require 'simple_layout'
class MyWin Gtk::Window
include SimpleLayout::Base
def initialize
super
add my_layout
signal_connect('destroy') do
Gtk.main_quit
end
end
def my_layout
hbox do
label 'Hello, '
button 'World !'
end
end
end
MyWin.new.show_all
Gtk.main
從上面的例子中可以看出,GtkSimpleLayout并沒有改變RubyGnome2程序的主框架,它只是一個擴充。
屬性設(shè)置
在放置UI元件的時候,往往需要設(shè)置初始屬性,或者要指定布局參數(shù)。GtkSimpleLayout用Hash來傳遞這些屬性與參數(shù),例如:
vbox do
button 'sensitive = false', :sensitive => false # 初始為disable狀態(tài)
button 'expand space', :layout => [true, true] # 指定這個button填充剩余空間
end
![](http://img.jbzj.com/file_images/article/201512/20151214173108985.png?20151114173115)
上面這個例子中,第一個button的初始狀態(tài)為disable。 ":sensitive => false"這個參數(shù)最終被轉(zhuǎn)換成屬性設(shè)置:Gtk::Button#sensitive=false,至于Gtk::Button有那些屬性可以設(shè)置,請參閱RubyGnome2 API文檔或GTK文檔。GtkSimpleLayout在這里只是作一個簡單參數(shù)的轉(zhuǎn)換而已。
第二個button的":layout => [true, true]"有點特殊。":layout" 參數(shù)是GtkSimpleLayout的保留參數(shù),它會被轉(zhuǎn)換成當(dāng)這個UI被放入容器時候的參數(shù)。這個例子中,容器是vbox(Gtk::VBox),默認(rèn)的加入方法是Gtk::VBox#pack_start,這個例子中的[true, true] 最終會被傳遞到pack_start,因此這個button在被加入vbox的時候調(diào)用的方法以及參數(shù)是:"Gtk::VBox#pack_start( button, true, true)"。
因此,要使用GtkSimpleLayout,就首先要熟悉RubyGnome2的各個元件,容器的用法,以及參數(shù)。當(dāng)你熟悉了RubyGnome2以后,用GtkSimpleLayout就會非常簡單。
批量屬性設(shè)置
在UI布局的時候,經(jīng)常碰到要對一組UI元件設(shè)置相同的屬性的情況,例如:
hbox do
button 'C', :layout => [false, false, 5]
button 'D', :layout => [false, false, 5]
button 'E', :layout => [false, false, 5]
end
![](http://img.jbzj.com/file_images/article/201512/20151214173136286.png?20151114173151)
這個時候,可以用"with_attr"來簡化:
hbox do
with_attr :layout => [false, false, 5] do
button 'C'
button 'D'
button 'E'
end
end
特殊容器
有些容器的放置子元件的時候有 特殊要求,例如Gtk::HPaned,左邊子窗口要用Gtk::HPaned#add1()來添加,右邊的用Gtk::HPaned#add2()。對于這種容器,GtkSimpleLayout要特別對待,就以hpaned為例:
hpaned do
area_first do
frame 'first area'
end
area_second do
frame 'second area'
end
end
![](http://img.jbzj.com/file_images/article/201512/20151214173231646.png?20151114173239)
需要特殊對待的容器有:
hpaned/vpaned : 用area_first和area_second來添加子窗口。
table : 用grid來填充格子。
nodebook : 用page來添加子頁。
標(biāo)識UI元件
GtkSimpleLayout用":id => ??"這個參數(shù)為UI元件進行標(biāo)識,例如:
hbox do
button 'first', :id => :btn_first
button 'second', :id => :btn_second
end
之后,可以用component()函數(shù)取得這個UI元件:
my_first_button = component(:btn_first)
my_second_button = component(:btn_second)
...
my_first_button.signal_connect('clicked') do
puts "first button clicked"
end
my_second_button.signal_connect('clicked') do
puts "second button clicked"
end
如果嫌麻煩,GtkSimpleLayout還提供了expose_components()用于自動將所有已標(biāo)識的元件添加為實例讀屬性(getter):
expose_components() # 將自動添加btn_first和btn_second這兩個讀屬性(getter)。
...
btn_first.signal_connect('clicked') do
puts "first button clicked"
end
btn_second.signal_connect('clicked') do
puts "second button clicked"
end
自動事件響應(yīng)映射
如果你嫌顯式調(diào)用signal_connect來注冊事件麻煩,那么GtkSimpleLayout為你提供了自動事件響應(yīng)映射的功能:
require 'gtk2'
require 'simple_layout'
class MyWin Gtk::Window
include SimpleLayout::Base
def initialize
super
add my_layout
register_auto_events() # 注冊自動事件響應(yīng)映射
end
def my_layout
hbox do
button "First', :btn_first
button "Second", :btn_second
end
end
# 事件響應(yīng)函數(shù)
def btn_first_on_clicked(*_)
puts "First button clicked"
end
# 事件響應(yīng)函數(shù)
def btn_second_on_clicked(*_)
puts "Second button clicked"
end
# 退出事件響應(yīng)函數(shù)
def self_on_destroy(*_)
Gtk.main_quit
end
end
最后那個'self‘是指宿主容器。
UI分組
有時候你希望對UI元件進行分組,這樣就可以對同一組的UI元件進行控制,如使能或禁止整個組。GtkSimpleLayout允許你在布局的時候指定UI組。
GtkSimpleLayout的UI分組規(guī)則如下:
默認(rèn)情況下,已命名的容器(即傳入了:id參數(shù))自動對自己所屬的子元件建立一個組,組名就是容器明。
如果容器傳入:gid=>??參數(shù),則以此名稱為所屬子元件建立組。
允許多個容器的:gid名字相同,這種情況下所屬子元件將歸為同一個組。
可以用“group”來顯式對UI分組,group可以看作是一個虛擬的容器。
用component_children(group_name)來獲取UI組。
由于UI分組的例子比較長不在此列出,請參閱源碼中的examples/group.rb文件
UI與邏輯代碼分離
由于GtkSimpleLayout潛在地迫使使用者分離界面代碼和邏輯處理(或事件響應(yīng))代碼,使得整個程序的層次結(jié)構(gòu)更加清晰。對于界面元件比較多的程序,可以很方便的分區(qū)進行l(wèi)ayout,因為layout的結(jié)果還是容器,這個容器又可以放入其他容器組合成更復(fù)雜的界面。
由于GtkSimpleLayout并不改變RubyGnome2的程序結(jié)構(gòu),你可以選擇在你的程序中部分或全部使用GtkSimpleLayout。雖然本文所提供的例子都是靜態(tài)布局,但由于GtkSimpleLayout是存代碼構(gòu)建UI,因此你完全可以在布局的時候傳入變量,進行動態(tài)布局和動態(tài)生成UI,而仍然保持UI代碼的“可視化”。
有興趣者可以看看GtkSimpleLayout實現(xiàn)的代碼,不過300行而已,這就是Ruby的魅力。
最后,貼上一個計算器的界面部分的代碼例子,你能從代碼中看出UI布局么?
require 'gtk2'
require 'simple_layout'
class MyWin Gtk::Window
include SimpleLayout::Base
def initialize
super
add my_layout
signal_connect('destroy') do
Gtk.main_quit
end
end
def my_layout
vbox do
with_attr :border_width => 3 do
hbox do
entry :id => :ent_input, :layout => [true, true, 5]
end
hbox do
frame do
label 'M', :set_size_request => [20, 20]
end
hbutton_box do
button 'Backspace'
button 'CE'
button 'C'
end
end
hbox do
vbutton_box do
button 'MC'
button 'MR'
button 'MS'
button 'M+'
end
with_attr :layout => [true, true] do
number_and_operators_layout
end
end
end
end
end
def number_and_operators_layout
vbox do
[ ['7', '8', '9', '/', 'sqt'],
['4', '5', '6', '*', '%'],
['1', '2', '3', '-', '1/x'],
['0', '+/=', '.', '+', '=']].each do |cols|
hbox :layout => [true, true] do
cols.each do |txt|
button txt, :set_size_request => [20, 20], :layout => [true, true]
end
end
end
end
end
end
MyWin.new.show_all
Gtk.main
![](http://img.jbzj.com/file_images/article/201512/20151214173350854.png?20151114173358)
Enjoy it :-)