• 12.1. Ruby/Tk
  • 12.1.1. Обзор
  • 12.1.2. Простое оконное приложение
  • 12.1.3. Кнопки
  • 12.1.4. Текстовые поля
  • 12.1.5. Прочие виджеты
  • 12.1.6. Дополнительные замечания
  • 12.2. Ruby/GTK2
  • 12.2.1. Обзор
  • 12.2.2. Простое оконное приложение
  • 12.2.3. Кнопки
  • 12.2.4. Текстовые поля
  • 12.2.5. Прочие виджеты
  • 12.2.6. Дополнительные замечания
  • 12.3. FXRuby (FOX)
  • 12.3.1. Обзор
  • 12.3.2. Простое оконное приложение
  • 12.3.3. Кнопки
  • 12.3.4. Текстовые поля
  • 12.3.5. Прочие виджеты
  • 12.3.6. Дополнительные замечания
  • 12.4. QtRuby
  • 12.4.1. Обзор
  • 12.4.2. Простое оконное приложение
  • 12.4.3. Кнопки
  • 12.4.4. Текстовые поля
  • 12.4.5. Прочие виджеты
  • 12.4.6. Дополнительные замечания
  • 12.5. Другие библиотеки для создания графических интерфейсов
  • 12.5.1. Ruby и X
  • 12.5.2. Ruby и wxWidgets
  • 12.5.3. Apollo (Ruby и Delphi)
  • 12.5.4. Ruby и Windows API
  • 12.6. Заключение
  • Глава 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. Флажки в Tk

    require '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. Переключатели в Tk

    require '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. Обновление состояния интерфейса в FOX

    require '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. Переключатели в FOX

    require '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. QtRuby

    Qt — это библиотека и комплект средств разработки, созданные и распространяемые компанией 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. Простой редактор в Qt

    require '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. Прочие виджеты в Qt

    require '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. Нестандартный виджет TimerClock

    require '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 | Добавить материал | Нашёл ошибку | Наверх