|
||||
|
Глава 12. Графические интерфейсы для Ruby
Нет смысла отрицать, что мы живем в век графических интерфейсов (ГИ). В обозримом будущем тот или иной вид графического интерфейса станет основным способом взаимодействия с компьютерами. Я не думаю, что командная строка не переживет следующее десятилетие — у нее есть свое место в мире. Но даже закоренелые хакеры прежних лет (предпочитающие команду cp -Rперетаскиванию файлов мышкой) все-таки не прочь воспользоваться ГИ, когда это оправданно. Однако у графического программирования есть свои сложности. Главная проблема, конечно, состоит в том, чтобы определить, как должен выглядеть удобный интерфейс к программе; при проектировании интерфейсов картинка не всегда заменяет тысячу слов. В этой книге мы не можем уделить внимание данному аспекту, все-таки наша тема — не эргономика, не эстетика и не психология. Вторая очевидная проблема в том, что графическое программирование сложнее. Надо учитывать форму, размер, положение и поведение всех находящихся на экране элементов управления, которыми можно манипулировать с помощью мыши и клавиатуры. Третья трудность заключается в следующем: на различных платформах представления о том, что такое оконная система и как она должна быть реализована, существенно разнятся. Чтобы в полной мере оценить это расхождение, нужно иметь опыт работы в разных системах. Многие программисты пытались создать кросс-платформенные инструменты и обнаружили, что именно с нестыковкой графических интерфейсов справиться труднее всего. Эта глава не поможет вам в разрешении вышеназванных проблем. Максимум, что я могу сделать, — предложить очень краткое введение в несколько популярных графических систем (в той мере, в какой они относятся к Ruby), а также несколько советов и наблюдений. Большая часть главы посвящена библиотекам Tk, GTK+, FOX и Qt. Велики шансы на то, что у вас возникнет вопрос: «А почему здесь нет (подставьте имя своей любимой библиотеки)?» Причин несколько. Прежде всего это ограниченность места: все же книга посвящена не только графическим интерфейсам. Другая причина заключается в том, что для вашей системы могут быть еще не написаны привязки к Ruby (и в таком случае мы призываем вас этим заняться). И наконец, не все графические интерфейсы «равны». Мы попытались рассказать о самых важных и зрелых, а остальные только упомянули. 12.1. Ruby/TkСвоими корнями Tk уходит в далекий 1988 год, если считать и предварительные версии. Долгое время эта система считалась спутником языка Tcl, но вот уже несколько лет как она используется и с другими языками, в том числе Perl и Ruby. Если бы у Ruby был «родной» графический интерфейс, наверное, им стал бы Tk. В настоящее время он все еще широко распространен, а в некоторые дистрибутивы Ruby входит в более или менее готовом виде. Я упомянул о Perl не зря. Привязки Tk к Ruby и Perl похожи настолько, что любая информация о Perl/Tk применима и к Ruby/Tk. В этой связи стоит упомянуть книгу Нэнси Уолш (Nancy Walsh) «Learning Perl/Tk». 12.1.1. ОбзорВ 2001 году Tk был, наверное, самым популярным графическим интерфейсом для Ruby. Он был первым и долгое время входил в состав стандартного дистрибутива Ruby. Сейчас он, пожалуй, не так распространен, но все еще широко применяется. Кто-то скажет, что Tk уже устарел. Те, кому нравятся объектно-ориентированные интерфейсы, будут им немного разочарованы. Но есть и достоинства: широкая известность, переносимость и стабильность. Любое приложение Ruby/Tk должно загрузить расширение tk, выполнив директиву load tk. Далее интерфейс приложения строится поэтапно, начиная с того или иного контейнера, в который помещаются отдельные элементы управления. В конце выполняется вызов метода Tk.mainloop, в котором обрабатываются события: перемещения мыши, нажатия кнопок и т. д. require "tk" # Подготовка интерфейса приложения... Tk.mainloop Как и в большинстве оконных систем, графические элементы управления в Tk называются виджетами. Виджеты группируются с помощью контейнеров. Контейнер верхнего уровня называется корневым. Явно задавать корневой контейнер не всегда обязательно, но лучше это сделать. Классы виджетов именуются так, как принято в мире Tk (в начале идет префикс Tk). Например, виджету Frameсоответствует класс TkFrame. Понятно, что виджеты создаются методом new. Первый параметр определяет контейнер, в который помещается виджет; если он опущен, подразумевается корневой контейнер. Дополнительные параметры виджета можно задавать двумя способами. Первый (в духе Perl) — передать хэш, содержащий названия и значения атрибутов. (Напомним, что в Ruby при передаче хэша последним или единственным параметром фигурные скобки можно опускать). my_widget = TkSomewidget.new( "borderwidth" => 2, "height" => 40 , "justify" => "center" ) Другой способ — передать конструктору блок, который будет вычислен методом instance_eval. Внутри блока можно вызывать методы для установки атрибутов виджета (такие методы называются так же, как сами атрибуты). Имейте в виду, что блок вычисляется в контексте вызываемого объекта, а не вызывающей программы. Это означает, например, что к переменным экземпляра вызывающего объекта в блоке обращаться нельзя. my_widget = TkSomewidget.new do borderwidth 2 height 40 justify "center" end Tk предоставляет три геометрических менеджера для управления относительным размером и расположением виджетов на экране. Наиболее распространенный из них — pack, остальные два — gridи place. Менеджер gridобладает богатыми возможностями, но не свободен от ошибок; place— самый простой из трех, он требует, чтобы были заданы абсолютные координаты всех расположенных внутри него виджетов. В примерах ниже мы будем пользоваться только менеджером pack. 12.1.2. Простое оконное приложениеПродемонстрируем очень простое приложение — окно, в котором выводится текущая дата. Начнем с явного создания корневого контейнера root и поместим в него виджет Label. require "tk" root = TkRoot.new() { title "Today's Date" } str = Time.now.strftime("Today is \n%B %d, %Y") lab = TkLabel.new(root) do text str pack("padx" => 15, "pady" => 10, "side" => "top") end Tk.mainloop Здесь мы создали корневой контейнер, сформировали строку даты и создали метку. В качестве текста, изображаемого на метке, мы задали строку str, а чтобы все выглядело аккуратно, вызвали метод pack, которому сказали, что отступ по горизонтали должен составлять 15 пикселей, а по вертикали — 10 пикселей. Текст мы попросили отцентрировать в границах метки. На рис. 12.1 показано, как выглядит окно приложения. Рис. 12.1. Простое приложение Tk Как было сказано выше, создать метку можно было бы и так: lab = TkLabel.new(root) do text str pack("padx" => 15, "pady" => 10, "side" => "top") end Экранные единицы измерения (в примере выше мы их использовали для указания padxи pady) — по умолчанию пиксели. Можно применять и другие единицы, если добавить к числу суффикс. Тогда значение становится строковым, но поскольку Ruby/Tk на это наплевать, то и мы не станем беспокоиться. Допустимы следующие единицы измерения: сантиметры ( с), миллиметры ( m), дюймы ( i) и пункты ( р). Все показанные ниже способы указания padxправильны: pack("padx" => "80m") pack("padx" => "8с") pack("padx" => "3i") pack("padx" => "12p") Атрибут sideв данном случае не дает ничего, поскольку мы установили его значение по умолчанию. Если вы измените размер окна, то текст останется «приклеенным» к верхней части той области, которой принадлежит. Другие возможные значения side: right, leftи bottom. У метода packесть и другие атрибуты, управляющие размещением виджетов на экране. Мы рассмотрим не все. Атрибут fillуказывает, должен ли виджет заполнять весь выделенный для него прямоугольник (по горизонтали и/или по вертикали). Допустимые значения: x, у, bothи none(по умолчанию none). Атрибут anchor«скрепляет» виджет с теми или иными сторонами его прямоугольника; при этом применяется нотация «сторон света». По умолчанию подразумевается значение center, другие допустимые значения: n, s, e, w, ne, nw, se, sw. Атрибут inупаковывает виджет относительно контейнера, отличного от его родительского. По умолчанию, конечно, принимается родительский контейнер. Атрибуты beforeи afterпозволяют произвольно задавать порядок упаковки виджетов. Это полезно, поскольку виджеты могут создаваться не в том порядке, в котором расположены на экране. В общем, Tk обеспечивает достаточную гибкость при размещении виджетов в окне. Читайте документацию и экспериментируйте. 12.1.3. КнопкиВ любом графическом интерфейсе кнопка — один из наиболее употребительных виджетов. Как и следовало ожидать, в Ruby/Tk кнопка представляется классом TkButton. В нетривиальных приложениях обычно создаются фреймы, содержащие разные виджеты. Кнопка может располагаться внутри такого фрейма. Обычно для кнопки задаются по меньшей мере три атрибута: • текст кнопки; • ассоциированная с кнопкой команда (исполняемая в результате нажатия); • способ упаковки кнопки в объемлющем контейнере. Вот простенький пример: btn_OK = TkButton.new do text "OK" command (proc ( puts "Пользователь говорит OK." }) pack("side" => "left") end Здесь мы создаем новую кнопку и присваиваем объект переменной btn_OK. Конструктору передается блок, хотя при желании можно было бы воспользоваться и хэшем. В данном случае мы записали блок на нескольких строчках (нам так больше нравится), хотя на практике в одну строку можно «напихать» сколько угодно кода. Напомним, кстати, что блок вычисляется методом instance_eval, то есть в контексте объекта (в данном случае — вновь созданного объекта TkButton). Текст, заданный в качестве значения атрибута text, рисуется на кнопке. Он может состоять из нескольких слов и даже строк. Как работает метод pack, мы уже видели, ничего нового здесь нет. Стоит лишь отметить, что без вызова packвиджет не будет виден. Интересная часть — метод command, который принимает объект Procи ассоциирует его с кнопкой. Часто для этой цели — и в данном случае тоже — применяется метод lambdaprocиз модуля Kernel, который преобразует блок в объект Proc. Выполняемое действие не очень осмысленно. Когда пользователь нажимает кнопку, вызывается неграфический метод puts, выводящий строку в окно команд, из которого была запущена программа, или, быть может, в окно дополнительной консоли. Следующий пример более содержателен. В листинге 12.1 приведено приложение, имитирующее термостат. В нем отображается то увеличивающаяся, то уменьшающаяся температура (создавая иллюзию, будто мы включаем обогрев или охлаждение). Код сопровождается комментариями. Листинг 12.1. Имитация термостатаrequire 'tk' # Типичные параметры упаковки... Тор = { 'side' => 'top', 'padx'=>5, 'pady'=>5 } Left = { 'side' => 'left', 'padx'=>5, 'pady'=>5 } Bottom = { 'side' => 'bottom', 'padx'=>5, 'pady'=>5 } temp =74 # Начальная температура... root = TkRoot.new { title "Thermostat" } top = TkFrame.new(root) { background "#606060" } bottom = TkFrame.new(root) tlab = TkLabel.new(top) do text temp.to_s font "{Arial} 54 {bold}" foreground "green" background "#606060" pack Left end TkLabel.new(top) do # Символ градуса text "о" font "{Arial} 14 {bold}" foreground "green" background "#606060" # Включить в хэш прикрепление к северу (символ градуса отображается # в виде верхнего индекса). pack Left.update({ 'anchor' => 'n' }) end TkButton.new(bottom) do text " Up " command proc { tlab.configure("text"=>(temp+=1).to_s) } pack Left end TkButton.new(bottom) do text "Down" command proc { tlab.configure("text"=>(temp-=1).to_s) } pack Left end top.pack Top bottom.pack Bottom Tk.mainloop Здесь мы создали два фрейма. Верхний служит только для отображения температуры. Она измеряется по шкале Фаренгейта и для улучшения дизайна выводится крупным шрифтом (а символ градуса отображается маленькой буквой «о», расположенной справа сверху). Нижний фрейм содержит кнопки «вверх» и «вниз». Обратите внимание на не встречавшиеся еще атрибуты объекта TkLabel. Метод fontзадает гарнитуру и размер шрифта, которым выводится текст метки. Строковое значение платформенно-зависимо; то, что приведено в примере, предназначено для ОС Windows. В системах UNIX обычно указывается длинное и малопонятное имя шрифта, принятое в X Window, например: -Adobe-Helvetica- Bold-R-Normal*-120-*-*-*-*-*-*. Метод foregroundзадает цвет текста. Здесь мы передаем строку "green"(которая в Tk имеет предопределенный смысл). Если вы хотите знать, предопределен ли тот иной цвет в Tk, то самое простое — попробовать. Аналогично метод backgroundзадает цвет фона, на котором выводится текст. В данном случае мы передаем строку в другом формате, а именно указываем красную, зеленую и синюю компоненты в шестнадцатеричном виде, как принято в языке HTML и других случаях. (Строка "#606060"соответствует приятному серому цвету.) Мы не включили никакой кнопки «выхода» (чтобы не загромождать окно). Как обычно, для того чтобы закрыть приложение, достаточно щелкнуть по иконке Close в правом верхнем углу рамки окна. Отметим использование метода configureв описании команд для кнопок; он изменяет текст метки по мере того, как текущая температура уменьшается или увеличивается. Мы уже говорили, что таким образом почти все атрибуты можно изменять во время выполнения, причем изменение отображается на экране незамедлительно. Упомянем еще две операции над текстовыми кнопками. Метод justifyпринимает один параметр ( "left", "right"или "center"), определяющий выравнивание текста внутри кнопки (по умолчанию подразумевается "center"). Мы говорили, что можно отображать многострочный текст; метод wraplengthзадает номер колонки, в которой происходит перенос слова. Стиль кнопки можно изменить методом relief, придав ей трехмерный вид. В качестве параметра этому методу можно передать одну из строк: "flat", "groove", "raised", "ridge"(по умолчанию), "sunken"или "solid". Методы widthи heightявно задают размеры кнопки. Имеется также метод borderwidthи аналогичные. О других атрибутах (которых немало) вы можете прочесть в руководстве. Рассмотрим еще один пример использования кнопки. На этой кнопке будет изображение, а не просто текст. Я создал GIF-файлы с изображениями стрелок, указывающих вверх и вниз ( up.gifи down.gif). Для получения ссылок на них можно воспользоваться классом TkPhotoimage, а затем передать эти ссылки в качестве параметров при создании кнопок. up_img = TkPhotoimage.new("file"=>"up.gif") down_img = TkPhotoimage.new("file"=>"down.gif") TkButton.new(bottom) do image up_img command proc { tlab.configure("text"=>(temp+=1).to_s) } pack Left end TkButton.new(bottom) do image down_img command proc { tlab.configure("text"=>(temp-=1).to_s) } pack Left end Здесь просто заменены некоторые строки в первом примере. Если не считать внешнего вида кнопок, то поведение не изменилось. На рис. 12.2 показано окно приложения. Рис. 12.2. Имитация термостата (с графическими кнопками) 12.1.4. Текстовые поляЧтобы отобразить поле для ввода текста и манипулировать им, применяется виджет TkEntry. Как и следовало ожидать, для указания размера, цвета и поведения предусмотрены многочисленные атрибуты. Мы приведем довольно объемный пример, иллюстрирующий применение некоторых из них. Поле ввода полезно лишь, если существует способ получить введенное в него значение. Обычно поле связывается с переменной (если быть точным, с объектом TkVariable), хотя можно воспользоваться и методом get. Предположим, что мы разрабатываем telnet-клиент, который принимает четыре параметра: адрес хоста, номер порта (по умолчанию 23), имя пользователя и его пароль. Для красоты добавим еще две кнопки для операций «войти» и «отменить». В представленном фрагменте используются фреймы, чтобы форма выглядела аккуратнее. Правда, написанный код не переносим, и настоящий знаток Tk с презрением отверг бы его. Но просто для сведения мы все-таки документировали этот «небрежный» подход к организации информации на экране. Вид окна показан на рис. 12.3, а код — в листинге 12.2. Рис. 12.3. Имитация telnet-клиента Листинг 12.2. Имитация telnet-клиентаrequire "tk" def packing(padx, pady, side=:left, anchor=:n) { "padx" => padx, "pady" => pady, "side" => side.to_s, "anchor" => anchor.to_s } end root = TkRoot.new() { title "Telnet session" } top = TkFrame.new(root) fr1 = TkFrame.new(top) fr1a = TkFrame.new(fr1) fr1b = TkFrame.new(fr1) fr2 = TkFrame.new(top) fr3 = TkFrame.new(top) fr4 = TkFrame.new(top) LabelPack = packing(5, 5, :top, :w) EntryPack = packing(5, 2, :top) ButtonPack = packing(15, 5, :left, :center) FramePack = packing(2, 2, :top) FramelPack = packing(2, 2, :left) var_host = TkVariable.new var_port = TkVariable.new var_user = TkVariable.new var_pass = TkVariable.new lab_host = TkLabel.new(fr1a) do text "Host name" pack LabelPack end ent_host = TkEntry.new(fr1a) do textvariable var_host font "{Arial} 10" pack EntryPack end lab_port = TkLabel.new(fr1b) do text "Port" pack LabelPack end ent_port = TkEntry.new(fr1b) do width 4 textvariable var_port font "{Arial} 10" pack EntryPack end lab_user = TkLabel.new(fr2) do text "User name" pack LabelPack end ent_user = TkEntry.new(fr2) do width 21 font "{Arial} 12" textvariable var_user pack EntryPack end lab_pass = TkLabel.new(fr3) do text "Password" pack LabelPack end ent_pass = TkEntry.new(fr3) do width 21 show "*" textvariable var_pass font "{Arial} 12" pack EntryPack end btn_signon = TkButton.new(fr4) do text "Sign on" command proc {} # Ничего не делает! pack ButtonPack end btn_cancel = TkButton.new(fr4) do text "Cancel" command proc { exit } # Просто выход. pack ButtonPack end top.pack FramePack fr1.pack FramePack fr2.pack FramePack fr3.pack FramePack fr4.pack FramePack fr1a.pack Frame1Pack fr1b.pack Frame1Pack var_host.value = "addison-wesley.com" var_user.value = "debra" var_port.value =23 ent_pass.focus foo = ent_user.font Tk.mainloop Прежде всего разберемся с размещением виджетов. Сначала мы создали несколько фреймов, расположенных друг под другом. В самом верхнем фрейме есть два фрейма поменьше, расположенных по горизонтали. В листинге 12.2 встречается также метод packing, единственная цель которого — сделать код чуточку чище. Он возвращает хэш, содержащий значения атрибутов padx, pady, sideи anchor. Объекты TkVariableпредназначены для ассоциирования полей ввода с переменными. В классе TkVariableопределен метод доступа value, который позволяет получать и устанавливать значение, хранящееся в объекте. При создании объекта TkEntry, например ent_host, задаем атрибут textvariable, который связывает его с соответствующим объектом TkVariable. Иногда мы явно указываем ширину поля методом width; если это не сделано, то будет автоматически выбрана разумная ширина, обычно определяемая значением, которое в данный момент хранится в поле. Часто ширину подбирают методом проб и ошибок. Шрифты задаются для полей ввода так же, как для меток. Аналогично обстоит дело и с цветами, которые в этом примере не задаются. Если шрифт пропорциональный, то два поля одинаковой ширины на экране могут оказаться различными. Как обычно, необходимо вызвать метод pack. Мы немного упростили вызовы за счет использования констант. Для поля, содержащего пароль, вызывается метод show, поскольку вводимое в него значение не должен видеть человек, заглядывающий через плечо. Вместо набираемых пользователем символов в таком поле будет отображаться символ, переданный методу showв качестве параметра (в данном случае звездочка). Я уже сказал, что кнопки тут нужны только для красоты. Кнопка Sign on вообще ничего не делает, a Cancel завершает программу. У полей ввода есть и другие атрибуты. Можно изменять значение из программы, не заставляя пользователя вводить его; можно задать шрифт и цвета; можно изменить характеристики курсора, указывающего место вставки, и перемещать его в нужное место. И многое, многое другое. Полное описание вы найдете в руководстве. Раз уж мы заговорили о вводе текста, будет уместно упомянуть виджет Text. По сравнению с полем ввода это примерно то же, что двухместный планер по сравнению космическим челноком. Виджет Textспроектирован специально для ввода больших фрагментов текста, насчитывающих много строк, и по существу является основой для создания полноценного редактора. Он довольно сложен, поэтому мы не будем его здесь рассматривать. 12.1.5. Прочие виджетыTk содержит еще много виджетов. Упомянем некоторые из них. Флажок обычно используется для представления полей, которые могут принимать одно из двух значений: да/нет или вкл/выкл. В Tk он называется «check button», а соответствующий ему класс — TkCheckButton. Пример в листинге 12.3 — это лишь скелет, в нем даже ни одной кнопки нет. Выводятся три флажка, соответствующие курсам, на которые можно записаться (информатика, музыка и литература). На консоль подается сообщение при каждом изменении состояния флажка. Листинг 12.3. Флажки в Tkrequire 'tk' root = TkRoot.new { title "Checkbutton demo" } top = TkFrame.new(root) PackOpts = { "side" => "top", "anchor" => "w" } cb1var = TkVariable.new cb2var = TkVariable.new cb3var = TkVariable.new cb1 = TkCheckButton.new(top) do variable cblvar text "Информатика" command { puts "Button 1 = #{cb1var.value}" } pack PackOpts end cb2 = TkCheckButton.new(top) do variable cb2var text "Музыка" command { puts "Button 2 = #{cb2var.value}" } pack PackOpts end cb3 = TkCheckButton.new(top) do variable cb3var text "Литература" command { puts "Button 3 = #{cb3var.value}" } pack PackOpts end top.pack PackOpts Tk.mainloop Отметим, что переменная, ассоциированная с флажком, принимает значение 1, когда флажок отмечен, и 0 — когда он сброшен. Эти значения можно изменить с помощью методов onvalueи offvalue. Кроме того, еще до создания флажка можно установить значение переменной и тем самым задать начальное состояние флажка. Если по какой-то причине мы хотим закрасить флажок серым, то можем с помощью метода stateустановить состояние disabled. Остальные состояния — active(отмечен) и normal(сброшен), причем последнее принято по умолчанию. Изменим пример в листинге 12.3. Пусть речь идет не о дополнительных, а о профилирующих университетских курсах. Если не считать сдвоенных курсов, то в каждый момент времени можно выбрать не более одного предмета. В таком случае нам понадобятся переключатели (их реализует класс TkRadioButton). Пример в листинге 12.4 мало чем отличается от листинга 12.3. Конечно, имя класса теперь другое. Еще одно важное отличие состоит в том, что всем переключателям соответствует одна и та же переменная. Именно поэтому Tk знает, что переключатели принадлежат одной группе. В форме может быть и несколько групп переключателей, но для каждой группы выделяется по одной переменной. Листинг 12.4. Переключатели в Tkrequire 'tk' root = TkRoot.new() { title "Radiobutton demo" } top = TkFrame.new(root) PackOpts = { "side" => "top", "anchor" => "w" } major = TkVariable.new b1 = TkRadioButton.new(top) do variable major text "Информатика" value 1 command { puts "Major = #{major.value}" } pack PackOpts end b2 = TkRadioButton.new(top) do variable major text "Музыка" value 2 command { puts "Major = #{major.value}" } pack PackOpts end b3 = TkRadioButton.new(top) do variable major text "Литература" value 3 command { puts "Major = #{major.value}" } pack PackOpts end top.pack PackOpts Tk.mainloop Здесь метод valueассоциирует с каждым переключателем конкретное значение. Значения могут быть произвольны (например, строки). Мы не использовали строки, так как хотели подчеркнуть, что не существует прямой связи между текстом виджета и возвращаемым им значением. Для настройки внешнего вида и поведения флажков и переключателей есть множество атрибутов. К примеру, метод imageпозволяет выводить не текстовую строку, а изображение. Применимы также обычные атрибуты форматирования и отображения виджетов; подробности вы найдете в руководстве. Если бы данная книга (или хотя бы эта глава) была целиком посвящена Tk, мы бы рассказали много чего еще. Но невозможно уделить внимание всем деталям — упомянем их лишь для того, чтобы вы знали, что имеется в вашем распоряжении. Виджет ListBox ( TkListBox) позволяет вывести список, из которого пользователь выбирает элементы. Режим выбора (метод selectmode) принимает следующие значения: single, extended, browse. Первые два режима определяют, можно ли выбрать только один или сразу несколько элементов. Режим browseаналогичен режиму singleс тем отличием, что выбранный элемент можно перемещать в списке мышью. Список можно прокручивать, так что число элементов в нем не ограничено. Tk располагает развитыми средствами для работы с меню: выпадающие меню, уединенные (tear-off) меню, каскадные подменю, клавиши быстрого выбора, переключатели в меню и многое другое. Ознакомьтесь с классами TkMenu, TkMenuBarи TkMenuButton. Пожалуй, самый «творческий» виджет — это TkCanvas, который позволяет программисту манипулировать изображением на уровне пикселей. У него есть методы для рисования линий и фигур, манипулирования цветами и загрузки изображений в различных форматах. Если вашему приложению необходима развитая графика или рисование под контролем пользователя, то этот виджет вас заинтересует. Полоса прокрутки позволяет реализовать нестандартную прокрутку по горизонтали и по вертикали (например, синхронную прокрутку двух окон). Виджет Scaleпредставляет собой бегунок для представления числовых значений; его можно ориентировать по горизонтали или по вертикали и использовать как для ввода, так и для вывода. О других виджетах вы можете прочесть в документации. 12.1.6. Дополнительные замечанияПерспективы Tk туманны (то же можно сказать и о большинстве программных систем), но в ближайшем будущем он никуда не денется. Текущая версия Ruby/Tk основана на Tk 8.4, но, вероятно, со временем будет обновлена. Нужно еще сказать несколько слов об операционных системах. Теоретически Tk — платформенно-независимая система, и практика не далека от теории. Однако есть сообщения о том, что версия для Windows не так стабильна, как для UNIX. На всякий случай отмечу, что все приведенные в этой главе примеры были протестированы в Windows и работают как задумано. 12.2. Ruby/GTK2Библиотека GTK+ представляет собой побочный продукт развития графического редактора GIMP (the GNU Image Manipulation Program); аббревиатура расшифровывается как GIMP Toolkit. Как UNIX и BSD, GTK+ разработан в Калифорнийском университете в Беркли. Если вы знакомы с системой X/Motif, скажем, что GTK+ внешне похожа на нее, но не так громоздка. Библиотека GTK+ зародилась в мире UNIX и лежит в основе графического менеджера GNOME (набирающего популярность у пользователей Linux), но при этом является более или менее кросс-платформенной. Начиная с версии GTK+ 2.0, поддерживаются не только различные варианты UNIX, но и семейство операционных систем MS Windows, а также Mac OS X с X Window System. Идет перенос на «родную» платформу Mac OS X, хотя пока эта версия еще не стабильна. Расширение Ruby/GTK2 основано на GTK+ 2.0. Не путайте с Ruby/GTK (основанном на GTK+ 1.2), это расширение не совместимо и вообще считается устаревшим. В этом разделе мы будем говорить только о Ruby/GTK2. 12.2.1. ОбзорRuby/GTK2 — это библиотека, позволяющая приложениям, написанным на языке Ruby, обращаться к средствам библиотеки GTK+ 2.x. GTK+ распространяется в исходных текстах на условиях лицензии GNU LGPL, поэтому может бесплатно использоваться в коммерческих приложениях. Как и в большинстве библиотек для построения графических интерфейсов, в GTK+ есть концепции фреймов, окон, диалогов и менеджеров размещения. Она располагает богатым набором виджетов, включающим большинство стандартных, например метки, кнопки и текстовые поля, а также ряд более сложных: деревья и многоколонные списки. Хотя GTK+ написана на С, спроектирована она в объектно-ориентированной манере. В связи с этим Ruby/GTK2 предоставляет объектно-ориентированный API, не слишком отдаляясь от исходной реализации на С. Кроме того, Ruby/GTK2 написана вручную, а не с помощью таких генераторов кода, как SWIG. Поэтому API выдержан в духе Ruby, с использованием блоков, необязательных аргументов и т.д. Справочное руководство можно найти на сайте http://ruby-gnome2.sourceforge.ip/. Библиотека GTK+ создана на базе других библиотек: GLib, Pango, ATK, Cairo и GDK. Она поддерживает неграфические функции (GLib), отображение многоязычных текстов в кодировке UTF-8 (Pango), средства облегчения работы (Atk), графический рендеринг (Cairo), низкоуровневые графические объекты (Gdk), а также множество виджетов и высокоуровневых графических объектов (Gtk). Во время работы над книгой текущей была версия Ruby/GTK2 0.14.1, совместимая с текущими стабильными версиями Ruby и GTK+ (2.0). Помимо Linux, она поддерживает семейство ОС Windows и Mac OS X (с X Window System). Идет работа по переносу на «родную» платформу Mac OS X, хотя пока эта версия еще не стабильна. GTK+ — объектно-ориентированная библиотека, поддерживающая логически стройную иерархию виджетов. Классы Gtk::Binи Gtk::Containerвесьма развиты, а комбинация менеджеров размещения Gtk::Вохи Gtk::Tableобеспечивает простые, но в то же время гибкие средства управления геометрией. В Ruby/GTK2 есть удобный механизм установки обработчиков сигналов. Среди виджетов GTK+ вы найдете меню, панели инструментов, всплывающие подсказки, деревья, индикаторы хода процесса, бегунки и календари. К слабым местам текущей версии GTK+ можно отнести недостаточно богатый набор стандартных диалогов, с которыми к тому же трудно работать в модальном режиме. Есть недостатки и у стандартного многострочного текстового редактора. Все строки, передаваемые методам Ruby/GTK2, должны быть представлены в кодировке UTF-8. Нельзя употреблять не ASCII-символы из некоторых одно- или многобайтовых кодовых страниц Windows. Поэтому не забывайте при редактировании Ruby-сценариев переключать редактор в режим UTF-8 и помещайте предложение $KCODE="U"в начале сценария. 12.2.2. Простое оконное приложениеЛюбая программа, в которой используется Ruby/GTK2, должна содержать директиву require "gtk2". Функциональность Ruby/GTK2 предоставляется посредством модулей Gtkи Gdk, поэтому имена классов GTK+ обычно начинаются с префикса Gtk::или Gdk::. Как правило, для инициализации Ruby/GTK2 мы вызываем метод Gtk.init, а затем создаем окно верхнего уровня и обработчик сигнала destroy(который поступает, когда пользователь закрывает окно). Метод show_allделает окно видимым, а обращение к Gtk .mainзапускает цикл обработки событий. Мы еще вернемся к этой теме, но сначала рассмотрим пример. Следующий код, как и рассмотренная выше программа для Tk, отображает текущую дату: $KCODE = "U" require "gtk2" Gtk.init window = Gtk::Window.new("Today's Date") window.signal_connect("destroy") { Gtk.main_quit } str = Time.now.strftime("Today is \n%B %d, %Y") window.add(Gtk::Label.new(str)) window.set_default_size(200, 100) window.show_all Gtk.main О переменной $KCODEречь шла в главе 4. Метод Gtk.initинициализирует Ruby/GTK2. Главное окно (типа Gtk::window) создается как окно «верхнего уровня», а указанный текст отображается в полосе заголовка. Далее создается обработчик сигнала destroy, который посылается при закрытии главного окна. Этот обработчик (в данном случае один блок) просто завершает главный цикл обработки событий. В документации по Ruby/GTK2 перечислены все сигналы, которые могут поступать каждому виджету (не забудьте и о суперклассах). Обычно они генерируются в результате манипуляций с мышью и клавиатурой, срабатывания таймеров, изменений состояния окна и т.д. В следующей строке мы добавляем метку прямо в главное окно. Размер метки вычисляется автоматически на основе длины текста. По умолчанию размеры родительских виджетов в GTK+ устанавливаются автоматически, исходя из размеров потомков. В данном случае длина строки, отображаемой выбранным по умолчанию шрифтом, определяет размер метки, а размер главного окна выбирается так, чтобы в нем поместилась метка. Такое окно получилось бы слишком маленьким, поэтому с помощью метода set_default_sizeмы говорим, что начальный размер главного окна должен составлять 200×100 пикселей. Затем мы вызываем метод show_all, чтобы сделать главное окно и всех его потомков видимыми. По умолчанию главное окно скрыто, поэтому в большинстве приложений такой вызов необходим. Метод Gtk.mainзапускает цикл обработки событий в GTK+. Он не возвращает управления, пока приложение не завершится. В данном случае обработчик события destroyприводит к выходу из Gtk.main, после чего завершается и все приложение. 12.2.3. КнопкиДля создания кнопки в Ruby/GTK2 предназначен класс Gtk::Button. В простейшем случае мы задаем обработчик события clicked, которое возникает, когда пользователь щелкает по кнопке. Программа в листинге 12.5 позволяет ввести одну строку в текстовое поле и после нажатия кнопки All Caps! преобразует ее в верхний регистр. На рис. 12.4 показано текстовое поле до нажатия кнопки. Листинг 12.5. Кнопки в GTK$KCODE = "U" require "gtk2" class SampleWindow < Gtk::Window def initialize super("Ruby/GTK2 Sample") signal_connect("destroy") { Gtk.main_quit } entry = Gtk::Entry.new button = Gtk::Button.new("All Caps!") button.signal_connect("clicked") { entry.text = entry.text.upcase } box = Gtk::HBox.new box.add(Gtk::Label.new("Text:")) box.add(entry) box.add(button) add(box) show_all end end Gtk.init SampleWindow.new Gtk.main Рис. 12.4. Пример простой кнопки в GTK В листинге 12.5 определен класс SampleWindow; при таком подходе класс может управлять собственным отображением и поведением (не заставляя вызывающую программу конфигурировать окно). Класс главного окна наследует Gtk::window. Как и в примере «Текущая дата», обработчик сигнала destroy завершает цикл обработки событий после закрытия главного окна. Этот класс создает однострочное поле ввода (класс Gtk::Entry) и кнопку Gtk::Buttonс текстом All Caps!. С кнопкой связан обработчик события clicked, которое генерируется, когда пользователь нажимает и отпускает кнопку мыши, в то время как ее указатель находится над кнопкой. Класс Gtk::Window— производный от Gtk::Bin, поэтому может содержать только один дочерний виджет. Чтобы добавить в окно два виджета, мы сначала помещаем их в контейнер HBox, который, в свою очередь, делаем потомком главного окна. Виджеты, добавляемые в контейнер Gtk::НВох, по умолчанию размещаются начиная с его правой границы. Есть также контейнер Gtk::VBox, который упаковывает своих потомков по вертикали. Как и раньше, чтобы главное окно (и все его потомки) стало видимым, необходимо вызвать метод show_all. Обработчик события clickedвызывается при нажатии кнопки. Он получает текст, находящийся в поле ввода, преобразует его в верхний регистр и записывает обратно в поле ввода. Собственно код приложения находится после определения класса SampleWindow. В нем всего лишь создается главное окно и запускается цикл обработки событий. 12.2.4. Текстовые поляВ библиотеке GTK+ есть класс Gtk::Entryдля ввода одной строки текста — мы видели его в предыдущем примере. Существует также класс Gtk::Textview, представляющий собой мощный многострочный редактор; его мы и опишем. Программа в листинге 12.6 создает многострочное текстовое поле и помещает в него текст. По мере изменения содержимого поля текущая длина текста отображается с помощью метки, расположенной в нижней части окна (рис. 12.5). Листинг 12.6. Текстовый редактор в GTK$KCODE = "U" require "gtk2" class TextWindow < Gtk::Window def initialize super("Ruby/GTK2 Text Sample") signal_connect("destroy") { Gtk.main_quit } set_default_size(200, 100) @text = Gtk::TextView.new @text.wrap_mode = Gtk::TextTag::WRAP_WORD @buffer = @text.buffer @buffer.signal_connect("changed") { @status.text = "Length: :" + @buffer.char_count.to_s } @buffer.create_tag('notice', 'font' => "Times Bold Italic 18", 'foreground' => "red") @status = Gtk::Label.new scroller = Gtk::ScrolledWindow.new scroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER) scroller.add(@text) box = Gtk::VBox.new box.add(scroller) box.add(@status) add(box) iter = @buffer.start_iter @buffer.insert(iter, "This is an editor") iter.offset = 5 @buffer.insert(iter, "really ", "notice") show_all end end Gtk.init TextWindow.new Gtk.main Рис. 12.5. Небольшой текстовый редактор в GTK Структура программы такая же, как в примере с кнопкой: инициализировать Ruby/GTK2, определить класс главного окна, задать обработчик события, корректно завершающий приложение, и установить начальный размер окна. После initializeвызывается метод show_all, который делает окно видимым. В последних двух строчках создается окно и запускается цикл обработки событий. Мы создали виджет редактора с именем @text. Включен режим переноса строк, по умолчанию строки разрываются без учета границы слов. Переменная @buffer— это текстовый буфер для виджета @text. Мы установили обработчик события changed; он будет вызываться при вставке, удалении и изменении текста. Обработчик пользуется методом char_count, чтобы узнать текущую длину текста в редакторе и преобразовать ее в строку сообщения. Предложение @status.text=textотображает это сообщение в окне. Далее мы конфигурируем виджет @textтак, чтобы он показывал текст другим стилем. Для этого с помощью метода create_tagсоздается тег «notice», с которым связан шрифт «Times Bold Italic 18» и красный цвет. Класс Gtk::TextTagпозволяет задавать и другие свойства тегов. В данном случае мы хотим воспользоваться шрифтом из семейства Times; на платформе Windows мы, скорее всего, получим какой-то вариант шрифта Times Roman. В ОС Linux/UNIX параметром должна быть стандартная для X Window System строка указания шрифта. Система вернет шрифт, наиболее близкий к заданному. Метка @statusпервоначально пуста. Ее текст будет изменен позже. GTK+ предлагает два способа добавить полосы прокрутки. Можно напрямую создать объект Gtk::ScrollBarи с помощью сигналов синхронизировать его с ассоциированным виджетом. Но в большинстве случаев проще воспользоваться виджетом Gtk::ScrolledWindow. Виджет Gtk::ScrolledWindowнаследует Gtk::Bin, поэтому может содержать только один дочерний виджет. Но этот виджет может принадлежать классу Gtk::Вохили любому другому контейнеру, допускающему несколько потомков. Ряд виджетов GTK+, в том числе и Gtk::TextView, автоматически взаимодействуют с Gtk::ScrolledWindow, не требуя почти никакого дополнительного кода. В данном примере мы создали виджет Gtk::ScrolledWindowс именем scrollerи сконфигурировали его методом set_policy. Мы решили не отображать горизонтальную полосу прокрутки вовсе, а вертикальную — только тогда, когда в редакторе больше строк, чем видно в окне. Сам текстовый редактор сделан непосредственным потомком scroller. Теперь надо настроить контейнер Gtk::Vbox, который расположит наши виджеты по вертикали. Сначала добавляется прокручиваемое окно, содержащее поле ввода, поэтому оно окажется самым верхним. Метка @statusрасполагается под ним. Напоследок сам контейнер добавляется в главное окно. В следующих четырех строчках в поле ввода добавляется текст. В первой строчке мы получаем объект Gtk::TextIter, соответствующий началу текста (offset = 0), и вставляем в это место строку. Поскольку до этого момента никакого текста в поле еще не было, только сюда и можно его вставить. Затем вставляется другой кусок текста со смещением 5. В результате редактор будет содержать строку This really is an editor. Поскольку мы предварительно установили обработчик события changed, он будет вызываться после каждого обращения к insert. Следовательно, статус будет отображаться правильно, несмотря на то что пользователь еще не вносил никаких изменений в текст. 12.2.5. Прочие виджетыДаже для организации сравнительно простого графического интерфейса текстовых полей и кнопок может оказаться недостаточно. Нужны переключатели, флажки и другие виджеты. В следующем примере демонстрируются некоторые из них. В листинге 12.7 предполагается, что пользователь хочет заказать билет на самолет. Для выбора города назначения используются классы Gtk::TreeView, Gtk::ListStoreи Gtk::TreeViewColumn(многоколонный список). Флажок (класс Gtk::CheckButton) определяет, нужен ли обратный билет, а переключатель (класс Gtk::RadioButton) позволяет указать класс салона. Завершает интерфейс кнопка Purchase(Заказать) — рис. 12.6. Листинг 12.7. Заказ билета на самолет $KCODE = "U" require "gtk2" class TicketWindow < Gtk::Window def initialize super("Purchase Ticket") signal_connect("destroy") { Gtk.main_quit } dest_model = Gtk::ListStore.new(String, String) dest_view = Gtk::TreeView.new(dest_model) dest_column = Gtk::TreeViewColumn.new("Destination", Gtk::CellRendererText.new, :text => 0) dest_view.append_column(dest_column) country_column = Gtk::TreeViewColumn.new("Country", Gtk::CellRendererText.new, :text => 1) dest_view.append_cоlumn(country_cоlumn) dest_view.selection.set_mode(Gtk::SELECTION_SINGLE) [["Cairo", "Egypt"], ["New York", "USA"], ["Tokyo", "Japan"]].each do |destination, country| iter = dest_model.append iter[0] = destination iter[1] = country end dest_view.selection.signal_connect("changed") do @city = dest_view.selection.selected[0] end @round_trip = Gtk::CheckButton.new("Round Trip") purchase = Gtk::Button.new("Purchase") purchase.signal_connect("clicked") { cmd_purchase } @result = Gtk::Label.new @coach = Gtk::RadioButton.new("Coach class") @business = Gtk::RadioButton.new(@coach, "Business class") @first = Gtk::RadioButton.new(@coach, "First class") flight_box = Gtk::VBox.new flight_box.add(dest_view).add(@round_trip) seat_box = Gtk::VBox.new seat_box.add(@coach).add(@business).add(@first) top_box = Gtk::HBox.new top_box.add(flight_box).add(seat_box) main_box = Gtk::VBox.new main_box.add(top_box).add(purchase).add(@result) add(main_box) show_all end def cmd_purchase text = @city if @first.active? text += ": first class" elsif @business.active? text += ": business class" elsif @coach.active? text += ": coach" end text += ", round trip " if @round_trip.active? @result.text = text end end Gtk.init TicketWindow.new Gtk.main Рис. 12.6. Различные виджеты GTK В этом приложении, как и в предыдущих примерах, создается главное окно с обработчиком события. Затем формируется список с двумя колонками, дизайн которого следует паттерну Модель-Вид-Контроллер (Model-View-Controller — MVC); класс Gtk::ListStore(модель) имеет две колонки типа String. Далее создается виджет Gtk::TReeView.Класс Gtk::treeViewColumnконфигурирует эту колонку. Первая колонка называется «Destination», а для отображения клеток применяется класс рисовальщика Gtk::CellRendererText. Первая колонка модели (с номером 0) — Gtk::ListStore— служит значением текстового свойства. Итак, рисовальщики клеток наполняют древесную модель данными. В GTK+ 2.x есть несколько готовых рисовальщиков клеток, в том числе Gtk::CellRendererText, Gtk::CellRendererPixbufи Gtk::CellRendererToggle. Далее в список добавляются три строки данных и устанавливается обработчик события "changed", который будет вызываться, когда пользователь выберет другую строку. Этот обработчик изменит значение переменной @city, записав в нее текст из первой колонки только что выбранной строки. Затем создается простой флажок ( Gtk::CheckButton) и кнопка ( Gtk::Button). Обработчик события нажатия кнопки вызовет метод cmd_purchase. Метка @resultпервоначально пуста, но позже в нее будет записана строка, определяющая вид заказанного билета. Три переключателя создаются как члены одной группы, то есть в любой момент может быть выбран лишь один из них. Когда пользователь щелкает по любому переключателю, равнее выбранный сбрасывается. Первым параметром конструктору переключателя передается первый переключатель из той же группы. Поэтому у конструктора первого переключателя в группе этого параметра нет, а остальным передается ссылка на первый переключатель. Виджеты нужно организовать на экране так, чтобы пользователю было удобно. Мы воспользовались комбинацией контейнеров Gtk::НВохи Gtk::VBox. Список расположен над флажком. Все три переключателя расположены вертикально справа от списка. А кнопка помещена под всеми остальными виджетами. Метод cmd_purchaseочень прост: он строит строку, отражающую состояние всех виджетов в момент нажатия кнопки. У переключателей и флажков есть метод active?, который возвращает true, если виджет отмечен. Построенная строка записывается в метку @resultи потому появляется на экране. Во многих приложениях интерфейс содержит меню. В следующем примере показано, как можно организовать меню в Ruby/GTK2. Заодно демонстрируется применение всплывающих подсказок — мелкая деталь, способная украсить любую программу. В листинге 12.8 создается главное окно с меню, содержащим пункт Fileи еще два фиктивных пункта. В меню Fileесть команда Exit, которая завершает приложение. Оба пункта Fileи Exitснабжены всплывающими подсказками. Листинг 12.8. Пример меню в GTK $KCODE = "U" require "gtk2" class MenuWindow < Gtk::Window def initialize super("Ruby/GTK2 Menu Sample") signal_connect("destroy") { Gtk.main_quit } file_exit_item = Gtk::MenuItem.new("_Exit") file_exit_item.signal_connect("activate") { Gtk.main_quit } file_menu = Gtk::Menu.new file_menu.add(file_exit_item) file_menu_item = Gtk::MenuItem.new("_File") file_menu_item.submenu = file_menu menubar = Gtk::MenuBar.new menubar.append(file_menu_item) menubar.append(Gtk::MenuItem.new("_Nothing")) menubar.append(Gtk::MenuItem.new("_Useless")) tooltips = Gtk::Tooltips.new tooltips.set_tip(file_exit_item, "Exit the app", "") box = Gtk::VBox.new box.pack_start(menubar, false, false, 0) box.add(Gtk::Label.new("Try the menu and tooltips!")) add(box) set_default_size(300, 100) show_all end end Gtk.init MenuWindow.new Gtk.main И здесь базовая структура программы такая же, как в предыдущих примерах. В данном случае мы создаем пункт меню Gtk::MenuItemс именем Exitи задаем для него обработчик события, который завершает программу. Событие называется activateи генерируется, когда пользователь выбирает пункт меню. Далее создается меню Fileи в него добавляется пункт Exit. Это все, что требуется для создания выпадающего меню. В конце создается пункт меню File; именно он и появится в полосе меню. Чтобы присоединить пункт Fileк меню File, мы вызываем метод submenu=. Затем создаетсяполоса меню Gtk::MenuBar, в которую добавляются три меню: File, Nothingи Useless. Что-то делает лишь первое меню, остальные приведены только для демонстрации. Всплывающими подсказками управляет единственный объект Gtk::Tooltips. Чтобы создать подсказку для любого виджета, например для пункта меню, нужно вызвать метод set_tip, которому передаются сам виджет, текст подсказки и строка, содержащая дополнительный скрытый текст. Скрытый текст не показывается в составе подсказки, но может, например, использоваться для организации оперативной справки. Чтобы разместить полосу меню в верхней части главного окна, мы взяли Gtk::VBoxв качестве самого внешнего контейнера. В данном случае мы добавляем в него полосу меню не методом add, а методом pack_start, чтобы точнее контролировать внешний вид и положение виджета. Первым параметром методу pack_startпередается размещаемый виджет. Второй параметр — булевский признак, показывающий, должен ли виджет занимать все доступное пространство. Отметим, что виджет при этом не растет, а обычно просто размещается в центре контейнера. Мы хотим, чтобы полоса меню располагалась сверху, поэтому передаем false. Третий параметр — тоже булевская величина, говорящая о том, должны ли размеры виджет быть изменены так, чтобы он занял все отведенное пространство. Нам нужна лишь узкая полоса меню, поэтому мы и тут передаем false. Последний параметр метода pack_startзадает отступы, то есть пустое место вокруг виджета. Нам это ни к чему, поэтому мы передаем нуль. Большую часть главного окна занимает метка. Напоследок мы принудительно устанавливаем размер окна 300×100 пикселей. 12.2.6. Дополнительные замечанияRuby/GTK2 — это часть проекта Ruby-GNOME2. GNOME — пакет более высокого уровня, основанный на библиотеке GTK+, a Ruby-GNOME2 — набор привязок для библиотек, входящих в состав GNOME. Ruby-GNOME2 включает следующие библиотеки: • Базовые библиотеки. Они включены в пакеты ruby-gtk2. Иногда термином «Ruby/GTK2» обозначают всю совокупность этих библиотек. Они работают на платформах UNIX, MS Windows, Mac OS X (с X11) и Cygwin (с X11). Все они необходимы для других библиотек, входящих в состав Ruby-GNOME2. • Ruby/GLib2. GLib — низкоуровневая инфраструктурная библиотека. Она предоставляет структуры данных на языке С, слой, обеспечивающий переносимость, поддержку Unicode и интерфейсы для поддержки цикла обработки событий, потоков, динамической загрузки и системы объектов. Ruby/GLib2 — обертка библиотеки GLib. Поскольку в Ruby уже есть хорошие классы для работы со строками и списками, некоторые функции GLib не реализованы. С другой стороны, Ruby/GLib2 содержит ряд важных функций для преобразования между объектами на С и на Ruby. Эта библиотека необходима для всех остальных библиотек, входящих в состав Ruby/GTK2. • Ruby/ATK. Эта библиотека предоставляет набор интерфейсов для облегчения работы. Приложение или набор средств разработки, поддерживающие интерфейсы ATK, могут применяться с такими инструментами, как считыватели с экрана, лупы и альтернативные устройства ввода. • Ruby/Pango. Библиотека для отображения текста с упором на интернационализацию с использованием кодировки UTF-8. Образует основу для работы с текстами и шрифтами в GTK+ (2.0). • Ruby/GdkPixbuf2. Библиотека для загрузки и манипулирования изображениями. Поддерживает многочисленные графические форматы, включая JPEG, PNG, GIF и другие. • Ruby/GDK2. Промежуточный слой, изолирующий GTK+ от деталей оконной системы. • Ruby/GTK2. Основные виджеты для построения графических интерфейсов. • Дополнительные библиотеки включены в пакеты ruby-gnome2наряду с базовыми. Все они работают в UNIX, а некоторые (Ruby/GtkGLExt, Ruby/Libglade2) также в MS Windows и Mac OS X. Некоторые библиотеки теоретически должны работать в Mac OS X (с X11) и Cygwin (с X11), но недостаточно хорошо протестированы. • Ruby/GNOME2. Содержит дополнительные виджеты для проекта GNOME. • Ruby/GnomeCanvas2. Виджет для интерактивного создания структурной графики. • Ruby/GConf2. Прозрачная для процесса конфигурационная база данных (аналог реестра в Windows). • Ruby/GnomeVFS. Позволяет приложениям одинаково обращаться к локальным и удаленным файлам. • Ruby/Gstreamer. Мультимедийный каркас для обработки аудио и видеоинформации. • Ruby/GtkHtml2. Виджет для представления HTML-документов. • Ruby/GtkGLExt. Предлагает трехмерный рендеринг с использованием технологии OpenGL. • Ruby/GtkSourceView. Виджет Textс поддержкой синтаксической подсветки и других возможностей, ожидаемых от редактора исходных текстов. • Ruby/GtkMozEmbed. Виджет, включающий механизм рендеринга Mozilla Gecko. • Ruby/Libart2. Поддержка базовых средств рисования. • Ruby/Libgda. Интерфейс к архитектуре GDA (GNU Data Access), обеспечивающий доступ к источникам данных, например СУБД и LDAP. • Ruby/Libglade2. Позволяет приложению загружать описание пользовательского интерфейса из XML-файлов во время выполнения. XML-файлы создаются мощным редактором интерфейсов GLADE, который упрощает издание интернационализированных графических интерфейсов пользователя. • Ruby/PanelApplet. Библиотека для создания аплетов, размещаемых на панели GNOME. • Ruby/GnomePrint и Ruby/GnomePrintUI. Виджеты для печати. • Ruby/RSVG. Поддержка векторной графики в формате SVG. • Внешние библиотеки загружаются библиотеками, входящими в состав Ruby-GNOME2. • Ruby/Cairo. Библиотека двумерной графики с поддержкой разнообразных устройств вывода. В текущей версии поддерживаются X Window System, Win32 и буферы изображения. На стадии эксперимента находятся поддержка OpenGL (с помощью библиотеки glitz), Quartz, XCB, PostScript и PDF. Эта библиотека загружается базовыми библиотеками. Для Ruby/Cairo требуется также Ruby/GLib2. Официальный сайт проекта — http://cairographics.org/. • Ruby/OpenGL. Интерфейс к библиотеке трехмерной графики OpenGL. Требуется библиотеке Ruby/GtkGLExt2. Работает на многих платформах. Официальный сайт проекта — http://www2.giganet.net/~yoshi/. • Ruby-GetText-Package. Предоставляет средства для управления справочниками переведенных сообщений для локализации (см. главу 4). С помощью этого пакета локализована библиотека Ruby/Libglade2, то же самое можно сделать и для других библиотек. Официальный сайт проекта — http://gettext.rubyforge.org/. Официальная домашняя страница проекта Ruby-GNOME2 — http://ruby-gnome2.sourceforge.jp/. Там вы найдете выпущенные версии всех библиотек, руководство по установке, справочные руководства по API, учебные пособия и примеры программ. Официальный сайт проекта GNOME — http://www.gnome.org/, а проекта GTK+ — http://www.gtk.org/. 12.3. FXRuby (FOX)FOX — относительно новая технология, упор в ней сделан на быстродействие и межплатформенную совместимость. В значительной степени совместимость обусловлена самодостаточностью — в отличие от многих систем, это не обертка платформенного API. Сама система написана на языке C++, хотя привязки можно создать практически для любого языка (для Ruby они уже имеются). Поскольку система изначально объектно-ориентированная, она хорошо сопрягается с Ruby и довольно естественно расширяется. Технология FOX не так широко распространена, как Tk или GTK+, но популярна в среде программистов на Ruby. Отчасти это обусловлено наличием великолепной привязки FXRuby (см. сайт http://fxruby.org). FXRuby — плод трудов Лайла Джонсона (Lyle Johnson), который немало сделал для поддержки и документирования библиотеки. Он же в течение многих лет предоставляет техническую поддержку и оказал неоценимую помощь при написании этого раздела. 12.3.1. ОбзорFXRuby — это привязка к Ruby библиотеки FOX, написанной на C++. В нее входит много классов для разработки полноценных графических приложений. Хотя аббревиатура FOX означает Free Objects for X (Бесплатные объекты для X), она была успешно перенесена и на другие платформы, включая MS Windows. Лайл Джонсон написал привязку FOX к Ruby, а также перенес саму библиотеку на платформу Windows. Исходную версия библиотеки FOX разработал Джероен ван дер Зийп (Jeroen van der Zijp) при поддержке компании CFD Research Corporation. Виджеты FOX обладают современным внешним обликом. По полноте они могут соперничать с платформенными интерфейсами, в том числе и с MS Windows, при этом располагая возможностями, сильно превосходящими многие другие библиотеки виджетов. Библиотеку классов FOX легко освоит программист, знакомый с другими средствами разработки графических интерфейсов. API не содержит зависимостей от платформы. Поскольку FOX написана на C++, некоторые аспекты API FxRuby сохраняют влияние статической природы и соглашений, принятых в C++ (например, перечисления и поразрядные операции). Центральным механизмом, упрощающим работу с FOX, является парадигма сообщение/получатель. Любой объект в FOX — это экземпляр класса FXObjectили одного из его подклассов. Определяемые пользователем объекты также должны наследовать одному из этих классов. Любой экземпляр FXObjectможет посылать и получать сообщения. Сообщение связывается к конкретным получателем во время выполнения в момент отправки. Внутри FOX сообщение представляется типом, идентификатором и данными. Классы FOX пользуются общим набором определений сообщений, что позволяет виджетам взаимодействовать. Обработчик сообщения должен вернуть 1, если сообщение обработано, и 0 в противном случае. FOX не перенаправляет необработанные сообщения другим виджетам неявно. Возвращаемое значение используется для того, чтобы понять, нужно ли обновлять интерфейс. Приложение FXRuby могло бы воспользоваться возвращаемым значением, чтобы самостоятельно перенаправить необработанные сообщения и тем самым реализовать паттерн Chain of Responsibility (цепочка обязанностей), описанный в книге E. Gamma, R. Helm, R. Johnson, J. Vlissides «Design Patterns»[14]. Еще один механизм FOX — парадигма автоматического обновления. Неявный цикл обработки событий в FOX включает фазу обновления, в которой объекты FOX могут обработать сообщения об обновлении. Обычно обработчик такого сообщения изменяет внешний вид того или иного виджета, основываясь на текущем состоянии данных приложения. Например, программа, показанная в листинге 12.9 (см. раздел 12.3.3), имеет кнопку, которая обновляет собственное состояние «активна/не активна» в зависимости от значения некоторой переменной. 12.3.2. Простое оконное приложениеВот пример минимального приложения FXRuby, которое делает то же самое, что рассмотренные выше приложения Tk и GTK+: require 'fox16' # Используются привязки к FOX 1.6. include Fox application = FXApp.new main = FXMainWindow.new(application, "Today's Date") str = Time.now.strftime("&Today is %B %d, %Y") button = FXButton.new(main, str) button.connect(SEL_COMMAND) { application.exit } application.create main.show(PLACEMENT_SCREEN) application.run Этого примера достаточно для демонстрации двух важнейших классов FXRuby: FXAppи FXMainWindow. Приложение должно в самом начале создать и инициализировать объект FXApp. FXMainWindow— подкласс FXTopWindow; каждый виджет в FOX — некая разновидность «окна». Класс FXTopWindowпредставляет окно верхнего уровня, которое появляется непосредственно на экране. Более сложное приложение FXRuby обычно создает подкласс FXMainWindowи размещает в нем виджеты на этапе инициализации. Конструктору FXMainWindowнеобходимо передать первым параметром объект FXApp. Второй параметр — заголовок окна. По умолчанию экземпляр FXMainWindowразмещается в центре экрана и снабжается всеми стандартными элементами, присущими FXTopWindow. Таким образом, для окна отображается полоса заголовка с кнопками свертывания, развертывания и закрытия. Его размеры можно изменять. Атрибут decorationsглавного окна позволяет явно указать необходимые элементы оформления. Например, можно запретить изменение размеров: main = FXMainWindow.new(application, "Today's Date") main.decorations = DECOR_TITLE | DECOR_CLOSE Значение decorationsобразуется комбинированием битовых флагов, как это принято в C++. В примере выше окно имеет только заголовок и кнопку закрытия. В этом простом примере главное окно содержит всего один виджет — экземпляр класса FXButton, в котором отображается текущая дата. str = Time.now.strftime("&Today is %B %d, %Y") button = FXButton.new(main, str) Первый аргумент конструктора FXButton— родительское окно, содержащее данный виджет. В нашем примере это главное окно. Второй аргумент — текст, рисуемый на кнопке. В следующей строчке показано, как с помощью метода connectассоциировать с кнопкой блок: button.connect(SEL_COMMAND) { application.exit } Здесь говорится, что когда кнопка отправляет командное сообщение (то есть сообщение типа SEL_COMMAND), следует вызвать метод exit. В оставшихся строчках мы наблюдаем «ритуал обручения» объектов FXAppи FXMainWindow: application.create main.show(PLACEMENT_SCREEN) application.run Любое приложение FXRuby должно включать подобные строки, чтобы создать экземпляр приложения, показать окно FXMainWindowи запустить цикл обработки событий. Аргумент PLACEMENT_SCREENметода showопределяет, в каком месте экрана должно появиться окно. Из других возможных значений упомянем PLACEMENT_CURSOR(поместить окно там, где находится курсор), PLACEMENT_OWNER(в центре окна-владельца) и PLACEMENT_MAXIMIZED(раскрыть окно на весь экран). 12.3.3. КнопкиВы уже видели, как организуется работа с кнопками в FXRuby. Заглянем немного глубже. На кнопке может размещаться не только короткая строка. Допустимы и несколько строк, разделенных символом новой строки: text = "&Hello, World!\n" + "Do you see multiple lines of text?" FXButton.new(self, text) Обратите внимание на амперсанд перед буквой H в строке "Hello, World!". Он задает «горячую клавишу», нажатие которой эквивалентно щелчку по кнопке. На кнопке может быть также нарисовано изображение, заданное в разных форматах. Например: text = "&Неllо, World!\n" + "Do you see the icon?\n" + "Do you see multiple lines of text?" icon = File.open("some_icon.gif", "rb") do |file| FXGIFIcon.new(app, file.read) end FXButton.new(self, text, icon) В листинге 12.9 иллюстрируется механизм обновления состояния интерфейса, реализованный в FOX: Листинг 12.9. Обновление состояния интерфейса в FOXrequire 'fox16' include Fox class TwoButtonUpdateWindow < FXMainWindow def initialize(app) # Сначала инициализируем базовый класс. super(app, "Update Example", nil, nil, DECOR_TITLE | DECOR_CLOSE) # Первая кнопка: @button_one = FXButton.new(self, "Enable Button 2") @button_one_enabled = true # Вторая кнопка: @button_two = FXButton.new(self, "Enable Button 1") @button_two.disable @button_two_enabled = false # Устанавливаем обработчики сообщений. @button_one.connect(SEL_COMMAND, method(:onCommand)) @button_two.connect(SEL_COMMAND, method(:onCommand)) @button_one.connect(SEL_UPDATE, method(:onUpdate)) @button_two.connect(SEL_UPDATE, method(:onUpdate)) end def onCommand(sender, sel, ptr) # Обновить состояние приложения. @button_one_enabled = !@button_one_enabled @button_two_enabled = !@button_two_enabled end def onUpdate(sender, sel, ptr) # Обновить кнопки в зависимости от состояния приложения. @button_one_enabled ? @button_one.enable : @button_one.disable @button_two_enabled ? @button_two.enable : @button_two.disable end end application = FXApp.new main = TwoButtonUpdateWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run Здесь в главное окно добавлено две кнопки. Мы снова воспользовались методом connect, чтобы связать сообщение SEL_COMMANDот кнопок с кодом, но на этот раз код представляет собой метод, а не блок: @button_one.connect(SEL_COMMAND, method(:onCommand)) В этом примере мы встречаем еще один тип сообщения — SEL_UPDATE. Такое сообщение позволяет сделать виджеты независимыми друг от друга и от кода приложения. Как видим, ни одна кнопка не подозревает о существовании другой. Первая кнопка обновляет состояние второй, посылая сообщение обработчикам, которые отвечают за изменение состояния. 12.3.4. Текстовые поляFOX располагает полезными средствами для ввода текста. В следующем примере демонстрируется применение класса FXTextFieldдля редактирования одной строки. Параметры определяют формат текста. Значение TEXTFIELD_PASSWDскрывает текст, являющийся паролем, TEXTFIELD_REALпозволяет вводить только действительные числа в научной нотации, a TEXTFIELD_INTEGER— только целые числа. simple = FXTextField.new(main, 20, nil, 0, JUSTIFY_RIGHT|FRAME_SUNKEN| FRAME_THICK|LAYOUT_SIDE_TOP) simple.text = "Simple Text Field" passwd = FXTextField.new(main, 20, nil, 0, JUSTIFY_RIGHT|TEXTFIELD_PASSWD| FRAME_SUNKEN|FRAME_THICK| LAYOUT_SIDE_TOP) passwd.text = "Password" real = FXTextField.new(main, 20, nil, 0, TEXTFIELD_REAL|FRAME_SUNKEN| FRAME_THICK|LAYOUT_SIDE_TOP| LAYOUT_FIX_HEIGHT, 0, 0, 0, 30) real.text = "1.0E+3" int = FXTextField.new(main, 20, nil, 0, TEXTFIELD_INTEGER| FRAME_SUNKEN|FRAME_THICK| LAYOUT_SIDE_TOP|LAYOUT_FIX_HEIGHT, 0, 0, 0, 30) int.text = "1000" Ниже показан простой способ ввода текста с помощью диалогового окна. В зависимости от выбранного метода можно будет вводить любой текст, только действительные или только целые числа. puts FXInputDialog.getString("initial text", self, "Диалог для ввода текст", "Введите текст:", nil) puts FXInputDialog.getInteger(1200, self, "Диалог для ввода целого числа", "Введите целое число:", nil) puts FXInputDialog.getReal(1.03е7, self, "Диалог для ввода числа в научной нотации", "Введите действительное число:", nil) Для экономии места мы не станем приводить полный текст приложения. Но, конечно, перед выводом диалогового окна необходимо выполнить обычную инициализацию. 12.3.5. Прочие виджетыВ следующем примере демонстрируется использование меню и полос меню в приложениях. Отметим, что объекты FXMenuCommandследуют общей для FOX парадигме сообщение/получатель, с которой мы уже сталкивались при работе с кнопками: require 'fox16' include Fox application = FXApp.new main = FXMainWindow.new(application, "Simple Menu") menubar = FXMenuBar.new(main, LAYOUT_SIDE_TOP | LAYOUT_FILL_X) filemenu = FXMenuPane.new(main) quit_cmd = FXMenuCommand.new(filemenu, "&Quit\tCtl-Q") quit_cmd.connect(SEL_COMMAND) { application.exit } FXMenuTitie.new(menubar, "&File", nil, filemenu) application.create main.show(PLACEMENT_SCREEN) application.run Здесь и FXMenuBar, и FXMenuPaneдобавляются непосредственно в главное окно FXMainWindow. Благодаря параметрам LAYOUT_SIDE_TOPи LAYOUT_FILL_Xполоса меню размещается в верхней части родительского окна и простирается от левой до правой границы. Текст команды меню "&Quit\tCtl-Q"подразумевает, что комбинация клавиш Alt+Q играет роль «горячей клавиши», a Ctrl+Q — клавиши быстрого выбора пункта меню. Последовательное нажатие Alt+F и Alt+Q эквивалентно щелчку по меню File с последующим выбором пункта Quit. Нажатие Ctrl+Q заменяет всю последовательность. В классе FXTopWindowесть метод для свертывания главного окна. Следующие три строчки добавляют в меню File команду, которая свернет окно: FXMenuCommand.new(filemenu, "&Icon\tCtl-I") do |cmd| cmd.connect(SEL_COMMAND) { main.minimize } end На этом примере мы видим еще один прием, полезный при конструировании команды меню. Если вам не нужна ссылка на виджет, представляющий команду меню, то можно просто присоединить блок к вызову FXMenuCommand.newи выполнить всю инициализацию виджета внутри блока. Разумеется, этот прием применим к любому встроенному в FOX классу. В листинге 12.10 демонстрируются переключатели. Листинг 12.10. Переключатели в FOXrequire 'fox16' include Fox class RadioButtonHandlerWindow < FXMainWindow def initialize(app) # Invoke base class initialize first super(app, "Radio Button Handler", nil, nil, DECOR_TITLE | DECOR_CLOSE) choices = [ "Good", "Better", "Best" ] group = FXGroupBox.new(self, "Radio Test Group", LAYOUT_SIDE_TOP | FRAME_GROOVE | LAYOUT_FILL_X) choices.each do |choice| FXRadioButton.new(group, choice, nil, 0, ICON_BEFORE_TEXT | LAYOUT_SIDE_TOP) end end end application = FXApp.new main = RadioButtonHandlerWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run Группы переключателей — стандартное средство в графических приложениях, предназначенное для выбора одного из взаимно исключающих вариантов. В данном примере варианты представлены массивом из трех строк: choices = [ "Good", "Better", "Best" ] В главное окно добавляется объект FXGroupBox, который визуально указывает, что три переключателя взаимосвязаны, а затем в этот контейнер добавляются сами переключатели (по одному на каждый вариант). Но сам контейнер FXGroupBoxничего не делает для того, чтобы обеспечить взаимное исключение. Если запустить пример в таком виде, то вы сможете выбрать более одного переключателя. Есть несколько способов обеспечить ожидаемое поведение переключателей, но в приложениях FOX чаще всего для этой цели используют получатель данных — класс FXDataTarget. Это специальный объект, играющий роль хранителя какого-то значения. Как и любой другой объект в FOX, FXDataTargetможет посылать и получать сообщения. Программа в листинге 12.11 — модифицированный вариант предыдущей, в ней демонстрируется применение получателей данных. Листинг 12.11. Переключатели в FOX и получатели данныхrequire 'fox16' include Fox class RadioButtonHandlerWindow < FXMainWindow def initialize(app) # Сначала вызвать инициализатор базового класса. super(app, "Radio Button Handler", nil, nil, DECOR_TITLE | DECOR_CLOSE) choices = [ "Good", "Better", "Best" ] default_choice = 0 @choice = FXDataTarget.new{default_choice) group = FXGroupBox.new(self, "Radio Test Group", LAYOUT_SIDE_TOP | FRAME_GROOVE | LAYOUT_FILL_X) choices.each_with_index do |choice, index| FXRadioButton.new(group, choice, @choice, FXDataTarget::ID_OPTION+index, ICON_BEFORE_TEXT | LAYOUT_SIDE_TOP) end end end application = FXApp.new main = RadioButtonHandlerWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run В этом примере @choice— экземпляр FXDataTarget, значением которого является целочисленный индекс выбранного в данный момент положения переключателя. Получатель данных инициализирован нулем, что соответствует элементу «Good» массива choices. При конструировании каждого переключателя задается получатель данных, а идентификатор сообщения от переключателя делается равным FXDataTarget::ID_OPTIONплюс желаемое значение. Если теперь запустить пример, то вы увидите, что переключатель стал вести себя как положено. Для добавления в окно списка FXListи его инициализации тоже достаточно нескольких строк. Значение LIST_BROWSESELECTпозволяет выбирать из списка ровно один элемент. В начальный момент выбран самый первый из них. Значение LIST_SINGLESELECTдопускает выбор не более одного элемента; в этом случае в начальный момент ни один элемент не выбран: @list = FXList.new(self, nil, 0, LIST_BROWSESELECT | LAYOUT_FILL_X) @names = ["Chuck", "Sally", "Franklin", "Schroeder", "Woodstock", "Matz", "Lucy"] @names.each { |name| @list.appendItem(name) } Отметим, что вместо метода appendItemможно использовать оператор вставки в массив, то есть последнюю строку можно было бы записать и так: @names.each { |name| @list << name } Весь пример целиком приведен в листинге 12.12. Сообщение обрабатывается в главном окне, в результате выводится выбранный элемент. Если был задан режим LIST_SINGLE_SELECT, то важно отличать щелчок, при котором элемент был выбран, от щелчка, который отменил выбор. Листинг 12.12. Виджет FXList require 'fox16' include Fox class ListHandlerWindow < FXMainWindow def initialize(app) # Сначала вызвать инициализатор базового класса. super(app, "List Handler", nil, nil, DECOR_TITLE | DECOR_CLOSE) @list = FXList.new(self, nil, 0, LIST_BROWSESELECT | LAYOUT_FILL_X) @list.connect(SEL_COMMAND) do |sender, sel, pos| puts pos.to_s + " => " + @names[pos] end @names = ["Chuck", "Sally", "Franklin", "Schroeder", "Woodstock", "Matz", "Lucy"] @names.each { |name| @list << name } end end application = FXApp.new main = ListHandlerWindow.new(application) application.create main.show(PLACEMENT_SCREEN) application.run Если вместо LIST_BROWSESELECTпоставить LIST_EXTENDEDSELECT, то в списке можно будет выбирать несколько элементов: @list = FXList.new(self, nil, 0, LIST_EXTENDEDSELECT | LAYOUT_FILL_X) Обработчик сообщений можно изменить так, чтобы он отображал все выбранные элементы. Чтобы понять, какие элементы списка выбраны, придется перебрать все: @list.connect(SEL_COMMAND) do |sender, sel, pos| puts "Был щелчок по " + pos.to_s +"=>" + @names[pos] puts "Выбраны следующие элементы:" @list.each do |item| if item.selected? puts " " + item.text end end end Атрибут numVisibleобъекта FXListпозволяет указать, сколько элементов списка видно одновременно. Существует также виджет FXListBox, который отображает только выбранное значение. Его интерфейс похож на интерфейс FXListс несколькими отличиями. Аргументы конструктора точно такие же, как видно из следующего примера. Отметим, что FXListBoxпозволяет выбирать только один элемент, поэтому значение LIST_EXTENDEDSELECTигнорируется: @list_box = FXListBox.new(self,nil,0,LIST_BROWSESELECT | LAYOUT_FILL_X) @names = ["Chuck", "Sally", "Franklin", "Schroeder", "Woodstock", "Matz", "Lucy"] @names.each { |name| @list_box << name } Диалоговое окно можно определить один раз как подкласс класса FXDialogBox, а затем использовать для создания модальных или немодальных диалогов. Однако способы взаимодействия модальных и немодальных диалогов со своим владельцем различны. Под модальным мы понимаем окно или диалог, который препятствует доступу к другим частям приложения, пока не будет закрыт. Немодальный диалог позволяет передавать фокус другим окнам приложения. В следующем примере определяется класс модального и немодального диалога. Для модального класса используются предопределенные сообщения ID_CANCELи ID_ACCEPT. Немодальный класс пользуется только предопределенным сообщением ID_HIDE. Для отображения немодального диалога применяется уже знакомый метод FXTopwindow.show. Модальный диалог имеет собственный цикл обработки событий, отличный от цикла всего приложения. Для его отображения служит метод FXDialogBox.execute. Как видно из полного листинга программы, значение, возвращаемое методом execute, зависит от того, какое значение было передано методу приложения stopModalдля завершения цикла обработки событий модального диалога. В этом примере значение 1 говорит о том, что пользователь нажал кнопку Accept. modal_btn.connect do dialog = ModalDialogBox.new(self) if dialog.execute(PLACEMENT_OWNER) == 1 puts dialog.text end end Немодальный диалог работает параллельно с другими окнами приложения. Приложение должно запрашивать интересующие его данные у диалога по мере необходимости. Один из способов известить о появлении новых данных - включить в диалог кнопку Apply (Применить), которая будет посылать зависящее от приложения сообщение главному окну. В примере ниже используется также таймер — еще одна интересная особенность FxRuby. Когда таймер срабатывает, главному окну посылается сообщение. Обработчик этого сообщения (показан ниже) запрашивает у диалога новое значение и взводит таймер еще на одну секунду: def onTimer(sender, sel, ptr) text = @non_modal_dialog.text unless text == @previous @previous = text puts @previous end getApp().addTimeout(1000, method(:onTimer)) end В листинге 12.13 приведен полный текст примера использования модальных и немодальных диалогов. Листинг 12.13. Модальные и немодальные диалогиrequire 'fox16' include Fox class NonModalDialogBox < FXDialogBox def initialize(owner) # Сначала вызвать инициализатор базового класса. super(owner, "Test of Dialog Box", DECOR_TITLE|DECOR_BORDER) text_options = JUSTIFY_RIGHT | FRAME_SUNKEN | FRAME_THICK | LAYOUT_SIDE_TOP @text_field = FXTextField.new(self, 20, nil, 0, text_options) @text_field.text = "" layout_options = LAYOUT_SIDE_TOP | FRAME_NONE | LAYOUT_FILL_X | LAYOUT_FILL_Y | РАСK_UNIFORM_WIDTH layout = FXHorizontalFrame.new(self, layout_options) options = FRAME_RAISED | FRAME_THICK | LAYOUT_RIGHT | LAYOUT_CENTER_Y hide_btn = FXButton.new(layout, "&Hide", nil, nil, 0, options) hide_btn.connect(SEL_COMMAND) { hide } end def text @text_field.text end end class ModalDialogBox < FXDialogBox def initialize(owner) # Сначала вызвать инициализатор базового класса. super(owner, "Test of Dialog Box", DECOR_TITLE|DECOR_BORDER) text_options = JUSTIFY_RIGHT | FRAME_SUNKEN | FRAME_THICK | LAYOUT_SIDE_TOP @text_field = FXTextField.new(self, 20, nil, 0, text_options) @text_field.text = "" layout.options = LAYOUT_SIDE_TOP | FRAME_NONE | LAYOUT_FILL_X | LAYOUT_FILL_Y | PACK_UNIFORM_WIDTH layout = FXHorizontalFrame.new(self, layout_options) options = FRAME_RAISED | FRAME_THICK | LAYOUT_RIGHT | LAYOUT_CENTER_Y cancel_btn = FXButton.new(layout, "&Cancel", nil, self, 0, options) cancel_btn.connect(SEL_COMMAND) do app.stopModal(self, 0) hide end accept_btn = FXButton.new(layout, "&Accept", nil, self, 0, options) accept_btn.connect(SEL_COMMAND) do app.stopModal(self, 1) hide end end def text @text_field.text end end class DialogTestWindow < FXMainWindow def initialize(app) # Сначала инициализировать базовый класс. super(app, "Dialog Test", nil, nil, DECOR_ALL, 0, 0, 400, 200) layout_options = LAYOUT_SIDE_TOP | FRAME_NONE | LAYOUT_FILL_X | LAYOUT_FILL_Y | PACK_UNIFORM_WIDTH layout = FXHorizontalFrame.new(self, layout_options) button_options = FRAME_RAISED | FRAME_THICK | LAYOUT_CENTER_X | LAYOUT_CENTER_Y nonmodal_btn = FXButton.new(layout, "&Non-Modal Dialog...", nil, nil, 0, button_options) nonmodal_btn.connect(SEL_COMMAND) do @non_modal_dialоg.show(PLACEMENT_OWNER) end modal_btn = FXButton.new(layout, "&Modal Dialog...", nil, nil, 0, button_options) modal_btn.connect(SEL_COMMAND) do dialog = ModalDialogBox.new(self) if dialog.execute(PLACEMENT_OWNER) == 1 puts dialog.text end end getApp.addTimeout(1000, method(:onTimer)) @non_modal_dialog = NonModalDialogBox.new(self) end def onTimer(sender, sel, ptr) text = @non_modal_dialog.text unless text == @previous @previous = text puts @previous end getApp.addTimeout(1000, method(:onTimer)) end def create super show(PLACEMENT_SСREEN) end end application = FXApp.new DialogTestWindow.new(application) application.create application.run Перед началом длинного вычисления в FXRuby следует заменить текущий курсор курсором ожидания, а по завершении восстановить исходный. В классе FXAppесть два удобных метода, позволяющих изменить курсор без явного запоминания предыдущего: beginWaitCursorи endWaitCursor. Если метод beginWaitCursorвызывается в блоке, то по выходе из блока будет автоматически вызван метод endWaitCursor: getApp.beginWaitCursor do # Выполнить длительную операцию... end 12.3.6. Дополнительные замечанияБиблиотека FOX располагает еще многими виджетами и возможностями, например: деревья, стыкуемые панели инструментов, всплывающие подсказки, строки состояния и страницы с вкладками. К числу более сложных средств следует отнести перетаскивание между приложениями и получатели данных, упрощающие связывание данных с виджетами. В библиотеке имеются также неграфические средства для поддержки кросс-платформенного программирования, в том числе класс FXRegistry. Для связи между приложением и его окружением можно использовать сигналы, а также различные каналы ввода и вывода, которые транслируются в сообщения, посылаемые объектам FOX. Имеются виджеты, поддерживающие наиболее распространенные графические форматы, а также API для работы с библиотекой OpenGL. Это не просто дань вежливости трехмерной графике: на базе библиотеки FOX C++ было реализовано немало инженерных приложений. Учитывая все вышесказанное, библиотеку FXRuby можно считать мощным и гибким инструментом. В последние несколько лет она приобрела популярность в сообществе пользователей Ruby; ожидается, что число поклонников будет расти и дальше. Возможности библиотеки быстро изменяются и расширяются, самую актуальную информацию о привязках к Ruby можно найти на сайте http://fxruby.org. 12.4. QtRubyQt — это библиотека и комплект средств разработки, созданные и распространяемые компанией Trolltech. Основной упор в Qt сделан на кросс-платформенности, единый программный интерфейс предоставляется для операционных систем Windows, Mac, и UNIX. Разработчику нужно написать код только один раз, он будет оттранслирован на всех трех платформах без модификации. Qt распространяется на условиях одной из двух лицензий: GPL или коммерческая лицензия для разработки продуктов без раскрытия исходных текстов. Такой же политики двойного лицензирования придерживаются и другие компании, например MySQL. Она позволяет использовать библиотеку в проектах с открытыми исходными текстами, в которых предлагаемые средства находят полезное применение. Но при этом Trolltech может получать доход от продажи коммерческих лицензий клиентам, которых не устраивают ограничения GPL. 12.4.1. ОбзорПривязки QtRuby — результат работы многих людей, прежде всего Ричарда Дейла (Richard Dale). Эшли Уинтерс (Ashley Winters), Жермен Гаран (Germain Garand) и Давид Форе (David Faure) написали большую часть инструмента генерации кода привязки (он называется SMOKE). Другие отправляли отчеты о найденных ошибках и вносили исправления. Расширение QtRuby содержит не только обширный набор относящихся к графическим интерфейсам классов, но и целый комплект дополнительных средств, часто необходимых программистам (например, библиотеки для работы с XML и SQL). В последние несколько лет привязки QtRuby основывались на версии Qt 3.x. В конце 2005 года вышла версия 4. Сейчас есть варианты QtRuby и для Qt3, и для Qt4, но это разные пакеты. Поскольку Qt3 никогда не поставлялась в исходных текстах для Windows, то в этой книге мы рассматриваем только привязки к Qt4. Однако приведенные в этом разделе примеры будут работать и для Qt3. Весь код был проверен на платформах Windows, Linux и Mac с версией QtRuby для Qt4. Ключевой аспект Qt, а значит и QtRuby, — концепция сигналов и слотов. Сигналы представляют собой асинхронные события, возникающие, когда в приложении происходит какое-то событие (например, щелчок кнопкой мыши или ввод текста в поле). Слот — это просто метод, вызываемый в ответ на возникновение сигнала. Для связывания сигналов со слотами мы будем использовать метод connect. Чтобы иметь возможность пользоваться сигналами и слотами, а также многими другими возможностями QtRuby, все наши классы будут наследовать классу Qt::Object. Более того, классы, используемые в графических интерфейсах, будут наследовать классу Qt::Widget, который, в свою очередь, является производным от Qt::Object. 12.4.2. Простое оконное приложениеПриложение QtRuby должно в самом начале загрузить библиотеку Qt. QtRuby раскрывает свою функциональность посредством модуля Qt(следовательно, имена всех классов начинаются с префикса Qt::). Имена всех классов в исходной библиотеке Qt начинаются с буквы Q, но при переходе к QtRuby эта буква опускается. Так, например, класс, основанный на QWidget, в QtRuby будет называться Qt::Widget. require 'Qt' app = Qt::Application.new(ARGV) str = Time.now.strftime("Today is %B %d, %Y") label = Qt::Label.new(str) label.show app.exec Рассмотрим этот код подробнее. Вызов Qt::Application.newзапускает приложение Qt; он инициализирует оконную систему и выполняет подготовительные действия для создания виджетов. Затем создается объект Qt::Label— простейший способ показать текст пользователю. В данном случае текст инициализируется в предыдущей строчке. Следующая строчка говорит метке, что она должна отобразить себя на экране. На последнем шаге вызов арр.ехес запускает цикл обработки событий. Он не возвращает управления, пока приложение не завершится. Обычно это происходит, когда пользователь нажимает кнопку закрытия окна. 12.4.3. КнопкиСоздание кнопки в QtRuby сводится к созданию экземпляра класса Qt::PushButton(см. листинг 12.14 и рис. 12.7). Обычно при нажатии кнопки нужно выполнить некоторое действие. Для этого применяется механизм событий и слотов QtRuby. Листинг 12.14. Кнопки в QtRuby require 'Qt' class MyWidget < Qt::Widget slots 'buttonClickedSlot()' def initialize(parent = nil) super(parent) setWindowTitle("QtRuby example"); @lineedit = Qt::LineEdit.new(self) @button = Qt::PushButton.new("All Caps!",self) connect(@button, SIGNAL('clicked()'), self, SLOT('buttonClickedSlot()')) box = Qt::HBoxLayout.new box.addWidget(Qt::Label.new("Text:")) box.addWidget(@lineedit) box.addWidget(@button) setLayout(box) end def buttonClickedSlot @lineedit.setText(@lineedit.text.upcase) end end app = Qt::Application.new(ARGV) widget = MyWidget.new widget.show app.exec Рис.12.7. Кнопки в Qt В этом примере мы создали собственный класс виджета с именем MyWidget, он наследует классу Qt::Widget, являющемуся предком любого нестандартного виджета. Перед инициализацией мы подготовили список слотов, которые будут определены в нашем классе. Слоты — это обычные методы класса, но необходимо указать их имена, чтобы во время выполнения QtRuby знала, что мы собираемся использовать их именно в качестве слотов. Метод класса slotsпринимает список строк: slots = 'slot1()', 'slot2()' Инициализатор класса принимает аргумент parent, он есть почти у всех виджетов в Qt и определяет, какой виджет будет владельцем вновь создаваемого. Значение nilозначает, что это «виджет верхнего уровня», у которого нет владельца. Концепция «владения», наверное, имеет более понятный смысл в C++; родители владеют своими детьми, то есть при уничтожении или удалении родителя удаляются и все его потомки. Наш класс создает объект Qt::LineEditдля ввода текста и кнопку Qt::PushButtonс надписью All Caps!. В качестве родителя каждому виджету передается self. Это означает, что создаваемый экземпляр MyWidget«усыновляет» эти виджеты. Далее мы обращаемся к ключевой части библиотеки Qt — механизму соединения сигналов со слотами. В классе Qt::Pushbuttonопределен сигнал clicked, который испускается при нажатии кнопки. Этот сигнал можно соединить со слотом, в данном случае с методом buttonClickedSlot. Имя слота может быть любым, суффикс Slotмы употребили просто для наглядности. В самом конце мы создаем экземпляр класса Qt::HBoxLayout. При добавлении виджетов в этот контейнер он автоматически изменяет их размеры, так что нам больше не о чем беспокоиться. 12.4.4. Текстовые поляКак видно из листинга 12.14, в QtRuby есть класс Qt::LineEditдля ввода одной строки текста. Для ввода нескольких строк предназначен класс Qt::TextEdit. В листинге 12.15 демонстрируется многострочное текстовое поле. Под ним расположена метка, в которой отображается текущая длина текста (рис. 12.8). Листинг 12.15. Простой редактор в Qtrequire 'Qt' class MyTextWindow < Qt::Widget slots 'theTextChanged()' def initialize(parent = nil) super(parent) @textedit = Qt::TextEdit.new(self) @textedit.setWordWrapMode(Qt::TextOption::WordWrap) @textedit.setFont( Qt::Font.new("Times", 24) ) @status = Qt::Label.new(self) box = Qt::VBoxLayout.new box.addWidget(@textedit) box.addWidget(@status) setLayout(box) @textedit.insertPlainText("This really is an editor") connect(@textedit, SIGNAL('textChanged()'), self, SLOT('theTextChanged()')) end def theTextChanged text = "Length: " + @textedit.toPlainText.length.to_s @status.setText(text) end end app = Qt:Application.new(ARGV) widget = MyTextWindow.new widget.setWindowTitle("QtRuby Text Editor") widget.show app.exec Рис. 12.8. Простой редактор в Qt Виджет конструируется примерно так же, как в предыдущем примере. Но теперь мы создаем объект Qt::TextEdit, а также метку Qt::Labelдля показа текущего состояния. Стоит отметить, что для объекта @texteditмы указали шрифт Times высотой 24 пункта. У каждого класса, наследующего Qt::Widget(в том числе и у Qt::TextEdit) есть свойство font, которое можно опросить или установить. Затем мы создаем менеджер вертикального размещения ( Qt::QBoxLayout), который будет контейнером для всех своих потомков, добавляем в него виджет @texteditи связываем сигнал textChangedс определенным нами слотом theTextChanged. В методе theTextChangedмы запрашиваем у редактора текст и получаем его длину, а затем записываем возвращенное значение в метку @status. Отметим, что весь механизм сигналов и слотов работает асинхронно. После того как приложение входит в цикл обработки событий ( арр.ехес), оно уже не получает управления явно. Вот почему сигналы и слоты так важны. Мы определяем события, которые нас интересуют (сигналы), и действия, которые нужно выполнить при возникновении таких событий (слоты). 12.4.5. Прочие виджетыВ библиотеке Qt есть еще много встроенных виджетов, например переключатели, флажки и т.п. В листинге 12.16 продемонстрированы некоторые из них, а на рис. 12.9 показано, как выглядит окно приложения. Листинг 12.16. Прочие виджеты в Qtrequire 'Qt' class MyWindow < Qt::Widget slots 'somethingClicked(QAbstractButton *)' def initialize(parent = nil) super(parent) groupbox = Qt::GroupBox.new("Some Radio Button",self) radio1 = Qt::RadioButton.new("Radio Button 1", groupbox) radio2 = Qt::RadioButton.new("Radio Button 2", groupbox) check1 = Qt::CheckBox.new("Check Box 1", groupbox) vbox = Qt::QBoxLayout.new vbox.addWidget(radio1) vbox.addWidget(radio2) vbox.addWidget(check1) groupbox.setLayout(vbox) bg = Qt::ButtonGroup.new(self) bg.addButton(radio1) bg.addButton(radio2) bg.addButton(check1) connect(bg, SIGNAL('buttonClicked(QAbscractButton *)'), self, SLOT('somethingClicked(QAbstractButton *)') ) @label = Qt::Label.new(self) vbox = Qt::VBoxLayout.new vbox.addWidget(groupbox) vbox.addWidget(@label) setLayout(vbox) end def somethingClicked(who) @label.setText("You clicked on a " + who.className) end end app = Qt::Application.new(ARGV) widget = MyWindow.new widget.show app.exec Рис. 12.9. Простое приложение Tk В этом классе мы сначала создаем объект Qt::GroupBox— контейнер с рамкой и необязательным заголовком, в который можно помещать другие виджеты. Далее создаются два переключателя Qt::RadioButtonsи флажок Qt::CheckBox, а в качестве их родителя указывается ранее созданный контейнер. Затем создается менеджер размещения Qt::VBoxLayout, в который помещаются переключатели и флажок, после чего этот менеджер связывается с групповым контейнером и начинает управлять его размещением на экране. Следующий важный шаг — создание объекта Qt::ButtonGroup, в который помещаются флажок и переключатели. Qt::ButtonGroupпредназначен для логической группировки кнопок, флажков и переключателей. На их визуальное расположение он никак не влияет, зато обеспечивает, к примеру, взаимное исключение (гарантирует, что только один из группы виджетов может быть отмечен). В данном случае этот объект будет источником сигнала buttonClicked, который испускается при нажатии любой кнопки в группе. Этот сигнал отличается от виденных ранее тем, что ему сопутствует аргумент, а именно объект, по которому щелкнули мышкой. Обратите внимание на то, как синтаксис — QAbstractButton*— напоминает о C++-ных корнях Qt. В некоторых случаях употребления принятой в C++ нотации для обозначения типов параметров не избежать (хотя в будущих версиях это, возможно, и исправят). В результате такого вызова метода connectпри щелчке по любому виджету, принадлежащему группе, этот виджет будет передан слоту somethingClicked. Наконец, мы создаем метку Qt::Label, контейнер Qt::QBoxLayoutи увязываем все вместе. Внутри слота somethingClickedмы модифицируем текст метки при щелчке по любому переключателю или флажку. В данном случае выводится имя класса объекта, который испустил сигнал, приведший к вызову слота. Если встроенных виджетов недостаточно, то Qt предоставляет мощную систему рисования для создания собственных. В листинге 12.17 приведен небольшой пример, иллюстрирующий малую часть возможностей. Листинг 12.17. Нестандартный виджет TimerClockrequire 'Qt' class TimerClock < Qt::Widget def initialize(parent = nil) super(parent) @timer = Qt::Timer.new(self) connect(@timer, SIGNAL('timeout()'), self, SLOT('update()')) @timer.start(25) setWindowTitle('Stop Watch') resize(200, 200) end def paintEvent(e) fastHand = Qt::Polygon.new([Qt::Point.new(7, 8), Qt::Point.new(-7, 8), Qt::Point.new(0, -80)]) secondHand = Qt::Polygon.new([Qt::Point.new(7, 8), Qt::Point.new(-7, 8), Qt::Point.new(0, -65)]) secondColor = Qt::Color.new(100, 0, 100) fastColor = Qt::Color.new(0, 150, 150, 150) side = [width, height].min time = Qt::Time.currentTime painter = Qt::Painter.new(self) painter.renderHint = Qt::Painter::Antialiasing painter.translate(width() / 2, height() / 2) painter.scale(side / 200.0, side / 200.0) painter.pen = Qt::NoPen painter.brush = Qt::Brush.new(secondColor) painter.save painter.rotate(6.0 * time.second) painter.drawConvexPolygon(secondHand) painter.restore painter.pen = secondColor (0...12).each do |i| painter.drawLine(88, 0, 96, 0) painter.rotate(30.0) end painter.pen = Qt::NoPen painter.brush = Qt::Brush.new(fastColor) painter.save painter.rotate(36.0 * (time.msec / 100.0)) painter.drawConvexPolygon(fastHand) painter.restore painter.pen = fastColor (0...60).each do |j| if (j % 5) != 0 painter.drawLine(92, 0, 96, 0) end painter.rotate(6.0) end painter.end end end app = Qt::Application.new(ARGV) wid = TimerClock.new wid.show app.exec Созданный в этом примере виджет называется TimerClock. В инициализаторе мы создаем объект Qt::Timer, который конфигурируется для периодического испускания сигнала. Его сигнал timeoutмы соединяем со слотом updateнашего виджета. Это встроенный слот, он заставляет виджет перерисовать себя. Таймер запускается методом start. Переданный ему аргумент говорит, что таймер должен срабатывать (и испускать сигнал timeout) каждые 25 миллисекунд. Следовательно, слот updateбудет вызываться каждые 25 миллисекунд. Далее определяется метод paintEvent. Мы переопределяем одноименный метод класса Qt::Widget. Когда виджет собирается перерисовать себя (то есть при срабатывании таймера), он вызывает этот метод. Переопределяя его, мы решаем, как виджет должен отображаться на экране. Код этого метода вызывает различные графические примитивы рисования. Начиная с этого места идет сплошная геометрия. Мы создаем несколько многоугольников Qt::Polygon, представляющих стрелки часов. Ориентация многоугольников не имеет значения, потому что манипулировать ими мы будем позже. Задаются значения нескольких свойств. Устанавливаются цвета Qt::Colorобеих стрелок. Аргументами инициализатора Qt::Colorявляются значения в формате RGB с необязательной альфа-прозрачностью. Часы должны быть квадратными, поэтому в переменную side(длина стороны) записывается минимум из ширины и высота виджета. Кроме того, мы запоминаем текущее время, обращаясь к методу Qt::Time.currentTime. Далее создается объект Qt::Painter, и с его помощью мы начинаем рисовать. Задается режим сглаживания (antialiasing), чтобы на стрелках часов не было «лесенки». Начало координат помещается в центр области рисования ( painter.translate (width/2, height/2)). Для объекта Painter устанавливается масштаб в предположении, что сторона квадрата составляет 200 единиц. Если размер окна изменится, то масштабирование будет произведено автоматически. Затем выполняется последовательность операций рисования. Различные геометрические преобразования (например, поворот), сопровождаются парой вызовов painter.saveи painter.restore. Метод save сохраняет текущие свойства объекта Painterв стеке, чтобы их можно было позднее восстановить. Программа рисует обе стрелки, предварительно повернув их на нужный угол в соответствии с текущим временем. Кроме того, мы наносим риски вдоль границы циферблата. И напоследок мы сообщаем объекту Painter, что рисование закончилось (вызывая метод painter.end). Довершают картину четыре строчки, в которых создаются объект приложения Qt::Applicationи наш виджет, а затем запускается цикл обработки событий. На рис. 12.10 показан конечный результат. Рис. 12.10. Виджет TimerClock 12.4.6. Дополнительные замечанияПоскольку библиотека Qt написана на C++, неизбежны некоторые идиомы, отражающие ограничения этого языка. Иногда перевод на Ruby не выглядит на 100% естественным, поскольку в Ruby аналогичные вещи делаются несколько иначе. Поэтому в ряде случаев вводится избыточность, позволяющая выражать свои намерения «по-рубистски». Например, имена методов, в которых сохранена «верблюжьяНотация», свойственная C++, можно записывать и с подчерками (_). Так, следующие два вызова эквивалентны: Qt::Widget::minimumSizeHint Qt::Widget::minimum_size_hint Все методы установки свойств в Qt начинаются со слова set, например, Qt::Widget::setMinimumSize. В Ruby можно это слово опускать и пользоваться присваиванием, например: widget.setMinimumSize(50) widget.minimumSize = 50 # To же самое. widget.minimum_size = 50 # To же самое. Аналогично в Qt имена методов, возвращающих булевское значение, часто начинаются с isили has, например, Qt::Widget::isVisible. QtRuby позволяет именовать их в духе Ruby: а.isVisible a.visible? # То же самое. 12.5. Другие библиотеки для создания графических интерфейсовМы предупреждали, что вы можете и не найти своего любимого графического интерфейса. Но напоследок кратко упомянем имеющиеся альтернативы. Некоторые из упоминаемых продуктов еще не вполне зрелые, могут быть неполными и содержать ошибки. Но мы полагаем, что этот список будет расти, а поддерживаемые привязки со временем обретут стабильность. 12.5.1. Ruby и XСистему X Window System в разговорной речи называют (не совсем корректно) просто X Windows. Вероятно, она является прародителем если не всех, то абсолютного большинства графических интерфейсов пользователя. Пользователи всех вариантов UNIX давно уже знакомы с X (как пользователи, а то и как разработчики). Часто поверх X запускается оконный менеджер Motif. К достоинствам X следует отнести широкую известность, переносимость и богатый набор возможностей. К недостаткам — сложность работы. Неудивительно, что существуют библиотеки для работы с X из Ruby. Из-за их сложности мы не приводим документацию, а отсылаем вас к архиву приложений Ruby RAA, где вы найдете библиотеку Xlib, написанную Кадзухиро Иосида (Kazuhiro Yoshida, известный также как moriq), и Ruby/X11 Мэтью Бушара (Mathieu Bouchard, он же matju). Обе годятся для создания X-клиентов. 12.5.2. Ruby и wxWidgetsСистема wxWidgets (прежнее название wxWindows) функционально богата и стабильна. Они широко применяется в мире Python и по существу является «родным» графическим интерфейсом для этого языка. Философия библиотеки - пользоваться платформенными виджетами, когда это возможно. Версия для UNIX более зрелая, чем для Windows, но это положение, конечно, меняется. В данный момент существует достаточно зрелая библиотека wxRuby. Если вам нравится именно эта система, то можете найти ее вместе с документацией на сайте http://wxruby.rubyforge.org/. 12.5.3. Apollo (Ruby и Delphi)Настоящий хакер знает, что для серьезного программирования чистый Pascal бесполезен. Но на протяжении многих лет предпринималось немало попыток сделать этот язык пригодным для практического применения. Одна из самых успешных — Object Pascal компании Borland, ставший основой среды быстрой разработки Delphi. Своей популярностью Delphi обязана не расширениям языка Pascal, хотя это тоже играет свою роль, но самой среде и богатству графического интерфейса. Delphi предлагает множество виджетов для создания стабильных, привлекательных графических приложений на платформе MS Windows. Библиотека Apollo — попытка «поженить» Ruby и Delphi. Это детище Кадзухиро Иосида, хотя свой вклад внесли и многие другие. Основное достоинство Apollo — гигантский набор стабильных, удобных виджетов, а основной недостаток заключается в том, что на сегодняшний день она требует слегка «подправленной» версии Ruby. Она должна работать и с «классическим» продуктом Borland Kylix, который, по существу, является версией Delphi для Linux. Дополнительную информацию ищите в архиве RAA. 12.5.4. Ruby и Windows APIВ главе 8 мы рассматривали вариант «графического интерфейса для бедных», когда для доступа к возможностям браузера Internet Explorer и другим подобным вещам используется библиотека WIN32OLE. С деталями можно ознакомиться по приведенным там примерам. Если вам нужно что-то сделать быстро, не заботясь об элегантности, то такое решение может оказаться приемлемым. Если у вас есть склонность к мазохизму, то можете работать с Windows API напрямую. В этом вам поможет библиотека WIN32API(она обсуждается также в главе 14). Лично я не рекомендую такой подход, но о его существовании вы должны знать. 12.6. ЗаключениеВ этой главе был представлен обзор различных средств разработки графических интерфейсов пользователя для Ruby. Мы познакомились с общими концепциями: циклами обработки событий, сообщениями или сигналами и т.п. Была продемонстрирована работа с различными виджетами: кнопками, флажками, переключателями, текстовыми полями и т.д. Мы рассмотрели реализацию общих концепций на примере библиотек Tk, GTK, FOX и Qt. Выяснили, что в каждом случае применяется своя терминология и слегка отличающиеся варианты основной парадигмы. Отметили также специфические средства и достоинства, присущие каждой библиотеке. А теперь перейдем к совсем другой теме. В главе 13 будет рассмотрена работа с потоками в Ruby. Примечания:1 Огромное спасибо (яп.) 14 Русский перевод: Э. Гамма, Р. Хелм. Р. Джонсон, Дж. Влиссидес. Приемы объектно-ориентированного проектирования. Паттерны проектирования. — М.: ДМК, Питер, 2001. |
|
||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||
|