Книга: Программирование на языке Ruby
12.3.5. Прочие виджеты
Разделы на этой странице:
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, "&QuittCtl-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
полоса меню размещается в верхней части родительского окна и простирается от левой до правой границы. Текст команды меню "&QuittCtl-Q"
подразумевает, что комбинация клавиш Alt+Q играет роль «горячей клавиши», a Ctrl+Q — клавиши быстрого выбора пункта меню. Последовательное нажатие Alt+F и Alt+Q эквивалентно щелчку по меню File с последующим выбором пункта Quit. Нажатие Ctrl+Q заменяет всю последовательность.
В классе FXTopWindow
есть метод для свертывания главного окна. Следующие три строчки добавляют в меню File команду, которая свернет окно:
FXMenuCommand.new(filemenu, "&IcontCtl-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