Статьи

Введение в FXRuby

FXRuby – это мощная библиотека для разработки кроссплатформенных графических пользовательских интерфейсов (GUI). Он основан на наборе инструментов FOX (высоко оптимизированная библиотека с открытым исходным кодом, написанная на C ++) и предлагает разработчикам Ruby возможность кодировать приложения на языке, который они любят, и в то же время пользоваться преимуществами базовой производительности и функциональности FOX.

В этой статье я собираюсь показать вам, как начать работать с FXRuby, познакомить вас с некоторыми из наиболее часто используемых виджетов и продемонстрировать, как создать простое приложение с реальной ценностью.

Код из этого руководства доступен в репозитории GitHub .

Установка

Предполагается, что на вашем компьютере установлен Ruby 1.9.

Windows 7:

  • gem install fxruby

Ubuntu 12.04:

  • sudo apt-get install ruby1.9.1-dev g++ libxrandr-dev libfox-1.6-dev
  • sudo gem install fxruby

Mac OS X:

  • sudo gem install fxruby

Если у вас возникнут какие-либо проблемы, более подробные инструкции можно найти здесь: https://github.com/lylejohnson/fxruby/wiki

Привет, мир!

Итак, начнем с обычного приложения «Здравствуй, мир!».

Для этого создайте новый файл на своем компьютере, назовите его hello_world.rb и введите следующий код:

 require 'fox16' include Fox app = FXApp.new main = FXMainWindow.new(app, "Hello, World!" , :width => 200, :height => 50) app.create main.show(PLACEMENT_SCREEN) app.run 

Запустите этот файл с помощью команды ruby hello_world.rb и вы должны увидеть что-то вроде этого:

Давайте посмотрим, что делает приведенный выше код:

  • Начнем с требования библиотеки fox16.
  • Все классы FXRuby определены в модуле Fox, поэтому включение Fox в глобальное пространство имен нашей программы избавляет от необходимости ставить перед этими классами префикс Fox:: .
  • Затем мы создаем экземпляр класса FXApp (где App обозначает Application Object). Экземпляр FXApp является центральным в программе FXRuby и имеет много важных задач, таких как управление очередью событий и обработка сигналов.
  • Затем мы создаем экземпляр FXMainWindow , передавая ему ранее FXApp объект FXApp в качестве первого аргумента. Это связывает окно, которое мы создаем, с нашим приложением. Мы также передаем ему еще три аргумента: заголовок окна, ширину окна и высоту окна.
  • Вызов FXApp#create обеспечивает FXApp#create окна приложения.
  • Однако Windows по умолчанию невидимы в FXRuby, поэтому нам нужно вызвать FXMainWindow#show для его отображения. Аргумент PLACEMENT_SCREEN обеспечивает его центрирование на экране.
  • Наконец, мы запускаем основной цикл программы, вызывая FXApp#run . Этот метод не вернется, пока программа не выйдет.

Время для небольшой реорганизации

Хотя приведенный выше код работает просто отлично, он не очень похож на Ruby, и когда вы начинаете добавлять виджеты в ваше приложение, все быстро становится загроможденным. Поэтому в FXRuby распространенной идиомой является создание окна вашего приложения в качестве подкласса FXMainWindow , например:

 require 'fox16' include Fox class HelloWorld < FXMainWindow def initialize(app) super(app, "Hello, World!" , :width => 200, :height => 50) end def create super show(PLACEMENT_SCREEN) end end app = FXApp.new HelloWorld.new(app) app.create app.run 

Вы заметили, что мы определили метод create в нашем классе HelloWorld . Нам это необходимо, так как при вызове app.create наш экземпляр FXApp , в свою очередь, будет вызывать метод create всех окон, с которыми он связан. Мы также можем использовать этот метод, чтобы наш новый вызов объекта HelloWorld show сам после того, как он был создан.

Другой популярный способ FXRuby – перенести конструкцию FXApp и HelloWorld в стартовый блок следующим образом:

 if __FILE__ == $0 FXApp.new do |app| HelloWorld.new(app) app.create app.run end end 

Здесь __FILE__ – имя текущего файла, а $0 – имя файла, с которого началось выполнение. Сравнивая их, мы можем убедиться, что наш файл – это основной исполняемый файл, а не тот, который был запрошен или загружен другим файлом. Это определенно излишне для такого маленького приложения, но служит для демонстрации типичного стиля программы FXRuby.

Нечто более захватывающее

Приложения «Hello, World!» Великолепны, но давайте перейдем к чему-то более практичному. В следующем разделе я покажу вам, как создать простой генератор паролей, который одним нажатием кнопки выведет случайный пароль произвольной длины.

Расположение

Прежде чем приступить к написанию кода, давайте немного подумаем, какие элементы должны присутствовать в нашем графическом интерфейсе. Нам понадобится текстовое поле, в которое пользователь может ввести длину желаемого пароля. Нам также понадобится флажок, чтобы пользователь мог включить в пароль специальные символы. Сам пароль должен отображаться в некоторой текстовой области, и, наконец, нам понадобятся две кнопки: одна для генерации пароля и одна для его копирования в буфер обмена.

Вот простой макет того, как должен выглядеть наш графический интерфейс:

В FXRuby мы используем менеджеры по расположению, чтобы управлять положением и размером виджетов. В этом случае я собираюсь использовать объекты класса FXHorizontalFrame (который FXHorizontalFrame своих дочерних FXVerticalFrame горизонтали) и FXVerticalFrame (который FXVerticalFrame своих дочерних FXVerticalFrame вертикали). Первым аргументом для каждого из этих менеджеров компоновки является его родительское окно. Я также LAYOUT_FILL подсказку макета в вертикальную рамку ( LAYOUT_FILL ), которая говорит, что он занимает столько места, сколько ему доступно, как по горизонтали, так и по вертикали.

Для создания самих виджетов я буду использовать экземпляры следующих классов FXRuby:

  • FXLabel – создать ярлык, на котором мы можем отобразить некоторый текст
  • FXTextField – для создания текстового поля, в которое пользователь может ввести одну строку ввода
  • FXCheckButton – чтобы создать кнопку проверки, чтобы позволить пользователю выбрать или отменить выбор опции
  • FXText – создать текстовую область для отображения вывода
  • FXButton – создать нажимную кнопку для выполнения команды
 require 'fox16' include Fox class PasswordGenerator < FXMainWindow def initialize(app) super(app, "Password generator", :width => 400, :height => 200) hFrame1 = FXHorizontalFrame.new(self) chrLabel = FXLabel.new(hFrame1, "Number of characters in password:") chrTextField = FXTextField.new(hFrame1, 4) hFrame2 = FXHorizontalFrame.new(self) specialChrsCheck = FXCheckButton.new(hFrame2, "Include special characters in password") vFrame1 = FXVerticalFrame.new(self, :opts => LAYOUT_FILL) textArea = FXText.new(vFrame1, :opts => LAYOUT_FILL | TEXT_READONLY | TEXT_WORDWRAP) hFrame3 = FXHorizontalFrame.new(vFrame1) generateButton = FXButton.new(hFrame3, "Generate") copyButton = FXButton.new(hFrame3, "Copy to clipboard") end def create super show(PLACEMENT_SCREEN) end end if __FILE__ == $0 FXApp.new do |app| PasswordGenerator.new(app) app.create app.run end end 

Если вы запустите этот код на своем компьютере, вы увидите скелет нашего приложения, выглядящий примерно так:

Генерация случайной строки на основе пользовательского ввода

Чтобы создать наш пароль, мы можем использовать метод Ruby Integer#chr , который возвращает строку, содержащую символ, представленный значением получателя. Если пользователь хочет включить в свой пароль специальные символы, то мы можем использовать любой из 93 символов из 33-126 на диаграмме ASCII. В противном случае мы придерживаемся значений 48-57, 65-90 и 97-122, которые представляют цифры 1-9, прописные AZ и строчные az соответственно.

 def generatePassword(pwLength, charArray) len = charArray.length (1..pwLength).map do charArray[rand(len)] end.join end numbers = (1..9).to_a alphabetLowerCase = ("a".."z").to_a alphabetUpperCase = ("A".."Z").to_a allPossibleChars = (33..126).map{|a| a.chr} 
 p generatePassword(25, numbers + alphabetLowerCase + alphabetUpperCase) => "fO0470a7tfdAM80u8jZ2aA0SG" p generatePassword(25, allPossibleChars) => "o>0]bl{6._l;s%MFCYz1Gl;hV" 

Конечно, чтобы запомнить такой длинный и случайный пароль, вам нужно будет использовать менеджер паролей, такой как KeePass , но все равно все так делают, верно? 🙂

Соединяя два

Итак, у нас есть скелет нашего графического интерфейса, и наш метод generatePassword делает то, что должен. Пришло время соединить два.

В FXRuby мы используем метод connect чтобы связать действия пользователя, такие как щелчки мыши, с блоками кода. В случае FXButton он отправляет сообщение SEL_COMMAND своей цели при нажатии. Синтаксис выглядит следующим образом:

 FXButton.connect(SEL_COMMAND)do # This code fires when the button is clicked p "Yay! I was clicked!" end 

Давайте применим это к нашему коду.

Вы заметите, что в блоке generateButton.connect я сделал следующее:

  • Я добавил строку, чтобы очистить виджет FXText в котором мы хотим отобразить наш вывод. Если мы этого не сделаем, то каждый раз, когда мы генерируем новый пароль, он добавляется к старому.
  • Затем я добавляю результат вызова generatePassword к FXText виджету FXText .
  • Я вызываю generatePassword с аргументом chrTextField.text.to_i . Это целочисленное значение того, что пользователь ввел в текстовое поле.
 require 'fox16' include Fox NUMBERS = (1..9).to_a ALPHABET_LOWER = ("a".."z").to_a ALPHABET_UPPER = ("A".."Z").to_a ALL_POSSIBLE_CHARS = (33..126).map{|a| a.chr} class PasswordGenerator < FXMainWindow def initialize(app) super(app, "Password generator", :width => 400, :height => 200) hFrame1 = FXHorizontalFrame.new(self) chrLabel = FXLabel.new(hFrame1, "Number of characters in password:") chrTextField = FXTextField.new(hFrame1, 4) hFrame2 = FXHorizontalFrame.new(self) specialChrsCheck = FXCheckButton.new(hFrame2, "Include special characters in password") vFrame1 = FXVerticalFrame.new(self, :opts => LAYOUT_FILL) textArea = FXText.new(vFrame1, :opts => LAYOUT_FILL | TEXT_READONLY | TEXT_WORDWRAP) hFrame3 = FXHorizontalFrame.new(vFrame1) generateButton = FXButton.new(hFrame3, "Generate") copyButton = FXButton.new(hFrame3, "Copy to clipboard") generateButton.connect(SEL_COMMAND) do textArea.removeText(0, textArea.length) textArea.appendText(generatePassword(chrTextField.text.to_i, ALL_POSSIBLE_CHARS)) end end def generatePassword(pwLength, charArray) len = charArray.length (1..pwLength).map do charArray[rand(len)] end.join end def create super show(PLACEMENT_SCREEN) end end if __FILE__ == $0 FXApp.new do |app| PasswordGenerator.new(app) app.create app.run end end 

Специальные символы

Теперь давайте дадим пользователю возможность выбрать, хотят ли они, чтобы специальные символы были включены в пароль, или нет. Самый простой способ сделать это – объявить переменную экземпляра @includeSpecialCharacters , которую мы можем инициализировать как false . Затем мы можем подключить наш флажок к блоку кода, который будет обновлять значение этой переменной экземпляра (путем ее сохранения в значении true ) всякий раз, когда флажок выбран или отменен.

 @includeSpecialCharacters = false specialChrsCheck = FXCheckButton.new(hFrame2, "Include special characters in password") specialChrsCheck.connect(SEL_COMMAND) { @includeSpecialCharacters ^= true } 

Я также включил второй метод с именем chooseCharset , который @includeSpecialCharacters в качестве аргумента и возвращает массив, содержащий набор символов, из которых должен быть создан пароль.

 def chooseCharset(includeSpecialCharacters) if includeSpecialCharacters @charSets.first else @charSets.last end end 

Сами наборы символов передаются объекту PasswordGenerator после инициализации и доступны в переменной экземпляра @charSets .

 charSets = [ALL_POSSIBLE_CHARS, NUMBERS + ALPHABET_LOWER + ALPHABET_UPPER] PasswordGenerator.new(app, charSets) 

Наш метод generatePassword , в свою очередь, возьмет массив, возвращаемый chooseCharset в качестве аргумента и сгенерирует пароль соответственно.

Последние штрихи

Было бы хорошо, если бы пользователь мог скопировать сгенерированный пароль в буфер обмена нажатием кнопки. К счастью, виджет FXText обеспечивает FXText поддержку буфера обмена (т.е. вы можете скопировать его текст в буфер обмена, используя Ctrl + C), и нам не требуется много дополнительного кода для программного взаимодействия с буфером обмена.

Первое, что нужно сделать, это вызвать FXWindow#acquireClipboard когда нажата кнопка «Копировать в буфер обмена»:

 copyButton.connect(SEL_COMMAND) do acquireClipboard([FXWindow.stringType]) end 

Мы передаем методу массив, содержащий FXWindow.stringType (один из предварительно зарегистрированных типов перетаскивания FOX), чтобы указать, что у нас есть некоторые строковые данные для размещения в буфере обмена. В случае успеха метод acquireClipboard вернет true .

Теперь, когда другое окно запрашивает содержимое буфера обмена, FOX отправляет сообщение SEL_CLIPBOARD_REQUEST текущему владельцу буфера обмена. Как мы назвали acquireClipboard в главном окне, главное окно теперь является владельцем буфера обмена и должно ответить на этот тип сообщения:

 self.connect(SEL_CLIPBOARD_REQUEST) do setDNDData(FROM_CLIPBOARD, FXWindow.stringType, Fox.fxencodeStringData(textArea.text)) end 

Метод setDNDData принимает три аргумента. Первая сообщает FOX, какой тип передачи данных мы пытаемся выполнить, вторая – это тип данных, а последняя – сами данные.

эстетика

После этого я собираюсь внести два небольших изменения в макет графического интерфейса. Во-первых, я собираюсь поместить FXTextField и FXCheckButton в групповое поле, а во-вторых, я собираюсь придать двум кнопкам одинаковую ширину.

Групповое поле будет объектом класса FXGroupBox . Это менеджер раскладки, и, как и в случае с другими менеджерами раскладки, его первый аргумент определяет его родительское окно. Он также принимает различные подсказки макета в качестве дополнительных аргументов. Здесь я использовал FRAME_RIDGE и LAYOUT_FILL_X которые дают ему ребристый фрейм и говорят ему занимать столько места, сколько ему доступно по горизонтали. Чтобы добавить некоторые внешние отступы в групповой ящик, я ввел объект класса FXPacker который будет инкапсулировать все другие менеджеры компоновки.

 packer = FXPacker.new(self, :opts => LAYOUT_FILL) groupBox = FXGroupBox.new(packer, nil, :opts => FRAME_RIDGE | LAYOUT_FILL_X) 

Придать нашим двум кнопкам одинаковую ширину немного проще. Мы просто включаем подсказку макета PACK_UNIFORM_WIDTH при создании FXHorizontalFrame который является их прямым родителем.

 hFrame3 = FXHorizontalFrame.new(vFrame1, :opts => PACK_UNIFORM_WIDTH) 

Окончательное исправление ошибки

В Ruby 1.87 ввод отрицательного числа в chrTextField заставил интерпретатор войти в бесконечный цикл. Чтобы избежать этой проблемы, мы можем передать [0, chrTextField.text.to_i].max в качестве первого аргумента для generatePassword который затем примет значение 0 или любое [0, chrTextField.text.to_i].max значение, введенное пользователем, в зависимости от того, что выше.

Вот окончательный код

 require 'fox16' include Fox NUMBERS = (1..9).to_a ALPHABET_LOWER = ("a".."z").to_a ALPHABET_UPPER = ("A".."Z").to_a ALL_POSSIBLE_CHARS = (33..126).map{|a| a.chr} class PasswordGenerator < FXMainWindow def initialize(app, charSets) super(app, "Password generator", :width => 400, :height => 200) @charSets = charSets packer = FXPacker.new(self, :opts => LAYOUT_FILL) groupBox = FXGroupBox.new(packer, nil, :opts => FRAME_RIDGE | LAYOUT_FILL_X) hFrame1 = FXHorizontalFrame.new(groupBox) chrLabel = FXLabel.new(hFrame1, "Number of characters in password:") chrTextField = FXTextField.new(hFrame1, 4) hFrame2 = FXHorizontalFrame.new(groupBox) @includeSpecialCharacters = false specialChrsCheck = FXCheckButton.new(hFrame2, "Include special characters in password") specialChrsCheck.connect(SEL_COMMAND){ @includeSpecialCharacters ^= true } vFrame1 = FXVerticalFrame.new(packer, :opts => LAYOUT_FILL) textArea = FXText.new(vFrame1, :opts => LAYOUT_FILL | TEXT_READONLY | TEXT_WORDWRAP) hFrame3 = FXHorizontalFrame.new(vFrame1, :opts => PACK_UNIFORM_WIDTH) generateButton = FXButton.new(hFrame3, "Generate") copyButton = FXButton.new(hFrame3, "Copy to clipboard") generateButton.connect(SEL_COMMAND) do textArea.removeText(0, textArea.length) pwLength = [0, chrTextField.text.to_i].max charSet = chooseCharset(@includeSpecialCharacters) textArea.appendText(generatePassword(pwLength, charSet)) end copyButton.connect(SEL_COMMAND) do acquireClipboard([FXWindow.stringType]) end self.connect(SEL_CLIPBOARD_REQUEST) do setDNDData(FROM_CLIPBOARD, FXWindow.stringType, Fox.fxencodeStringData(textArea.text)) end end def generatePassword(pwLength, charArray) len = charArray.length (1..pwLength).map do charArray[rand(len)] end.join end def chooseCharset(includeSpecialCharacters) if includeSpecialCharacters @charSets.first else @charSets.last end end def create super show(PLACEMENT_SCREEN) end end if __FILE__ == $0 FXApp.new do |app| charSets = [ALL_POSSIBLE_CHARS, NUMBERS + ALPHABET_LOWER + ALPHABET_UPPER] PasswordGenerator.new(app, charSets) app.create app.run end end 

Вы также можете получить этот код из нашего репозитория GitHub .

Вывод

Я надеюсь, что в этой статье я смог дать вам полный обзор того, как работает FXRuby, и продемонстрировать простоту, с которой вы можете создавать кроссплатформенный графический интерфейс пользователя на языке, который вы любите. Я хотел бы закончить, представив несколько ресурсов, которые мне очень помогли при использовании этой библиотеки: