Создание GUI приложений с помощью wxErlang

29 октября 2012

Помните, как недавно мы с вами собирали deb-пакет Erlang с поддержкой wxWidgets? Как вы могли догадаться, все это было неспроста. И действительно, то была лишь подготовка к данной заметке, посвященной созданию GUI приложений на языке Erlang.

Итак, чтобы получить Erlang с поддержкой wxWidgets под Linux, вам может потребоваться собрать Erlang из исходников. Под FreeBSD достаточно установить Erlang из портов, не забыв поставить соответствующую галочку. Под Windows нужно установить бинарный пакет, доступный на erlang.org. В комментариях к предыдущей заметке некто под ником reception108 посоветовал сайт erlang-solutions.com. Там представлены готовые и, видимо, полные сборки Erlang для различных систем/дистрибутивов, в том числе и для MacOS.

При использовании wxWidgets я предпочитаю генерировать XRC-файл в wxGlade, а не программировать GUI вручную. Такой подход не только удобнее, но и автоматически приводит к разделению представления и логики приложения.

Установка wxGlade в Debian/Ubuntu:

sudo apt-get install python-wxglade

Установка во FreeBSD:

portmaster -d devel/wxGlade

Поковырявшись немного в wxGlade я состряпал совершенно бесполезный, но вполне пригодный для обучающих целей интерфейс.

Так он выглядит в Xubuntu:

GUI приложение на Erlang - вид под Xfce

А так — под Windows:

Пример использования wxErlang - вид под Windows

Отлично, интерфейс у нас есть. Теперь нужно заставить его делать что-то более-менее осмысленное:

#!/usr/bin/env escript
%%! -smp enable
-mode(compile).

-include_lib("wx/include/wx.hrl").
-define(MENU_EXIT, 101).
-define(MENU_TUTORIAL, 102).
-define(MENU_ABOUT, 103).

main(_Args) ->
  Wx = wx:new(),
  Xrc = wxXmlResource:get(),
  wxXmlResource:initAllHandlers(Xrc),
  true = wxXmlResource:load(Xrc, "gui.xrc"),
  Frame = wxFrame:new(),
  wxXmlResource:loadFrame(Xrc, Frame, Wx, "MainFrame"),
  wxFrame:setMinSize(Frame, { 400, 200 }),
  wxFrame:show(Frame),
  initCallbacks(Frame),
  eventLoop(Frame),
  wx:destroy().

initCallbacks(Frame) ->
  wxFrame:connect(Frame, close_window),

  InfileBrowseButton = wxXmlResource:xrcctrl(Frame,
    "InfileBrowseButton", wxButton),
  InfileTextCtrl = wxXmlResource:xrcctrl(Frame,
    "InfileTextCtrl", wxTextCtrl),

  wxButton:connect(InfileBrowseButton, command_button_clicked,
    [{ callback, fun onInfileBrowseButtonClicked/2 },
     { userData, { Frame, InfileTextCtrl } }]),

  wxFrame:connect(Frame, command_menu_selected,
    [{ callback, fun onMenuSelected/2 },
     { userData, Frame }]),
  ok.

onMenuSelected(Event,_) ->
  case Event of
    #wx{ id = ?MENU_EXIT, userData = Frame } ->
      wxFrame:close(Frame);
    #wx{ id = ?MENU_TUTORIAL } ->
      wx_misc:launchDefaultBrowser("http://eax.me/wxerlang/");
    #wx{ id = ?MENU_ABOUT, userData = Frame } ->
      Dialog = wxMessageDialog:new(Frame,
        "wxErlang example\n"
        "(c) Alex Alexeev 2012\n"
        "http://eax.me",
        [
          { caption, "wxErlang example" },
          { style, ?wxOK bor ?wxICON_INFORMATION }
        ]),
      wxDialog:showModal(Dialog),
      wxDialog:destroy(Dialog);
    _ ->
      io:format("onMenuSelected, Event = ~p~n", [Event])
  end.

onInfileBrowseButtonClicked(
    #wx{ userData = { Frame, InfileTextCtrl} },_) ->
  Dialog = wxFileDialog:new(Frame),
  wxDialog:showModal(Dialog),
  FilePath = wxFileDialog:getPath(Dialog),
  wxFileDialog:destroy(Dialog),
  wxTextCtrl:setValue(InfileTextCtrl, FilePath).

eventLoop(Frame) ->
  receive
    #wx{ event = #wxClose{} } ->
      ok;
    Event ->
      io:format("eventLoop: ~p~n", [Event]),
      eventLoop(Frame)
  end.

Основные моменты тут следующие.

-include_lib("wx/include/wx.hrl").

В файле wx.hrl содержатся определения некоторых записей (records) и макросов. Без него нам пришлось бы писать:

  receive
    {_,_,_,_,{wxClose,close_window}} ->
      ok;

… вместо:

  receive
    #wx{ event = #wxClose{} } ->
      ok;

… и испытывать прочие неудобства. Кстати, рассмотрим функцию eventLoop:

eventLoop(Frame) ->
  receive
    #wx{ event = #wxClose{} } ->
      ok;
    Event ->
      io:format("eventLoop: ~p~n", [Event]),
      eventLoop(Frame)
  end.

По большому счету она просто выводит в STDOUT все сообщения, за исключением тех, для которых мы написали колбэки (см ниже). При получении сообщения о закрытии главного окна (первый клоз) мы выходим из eventLoop и возвращаемся в функцию main, где происходит завершение приложения:

  eventLoop(Frame),
  wx:destroy().

Чуть выше по коду вы могли заметить:

  wxFrame:setMinSize(Frame, { 400, 200 }),

Тут мы задаем минимальный размер окна — мы же не хотим, чтобы пользователь мог свернуть его в одну точку? Почему-то в wxGlade не предусмотрена возможность указывать соответствующие параметры, так что приходится делать это в коде. Что интересно, в оконном менеджере i3 размер окна все равно можно изменять, как вздумается. В Xfce и под Windows все ОК.

  wxFrame:connect(Frame, command_menu_selected,
    [{ callback, fun onMenuSelected/2 },
     { userData, Frame }]),

При клике по любому пункту меню будет вызвана функция onMenuSelected:

onMenuSelected(Event,_) ->
  case Event of
    #wx{ id = ?MENU_EXIT, userData = Frame } ->
      wxFrame:close(Frame);

Если пользователь нажал «File → Exit», закрываем окно приложения. При этом мы автоматически выйдем из eventLoop, в результате чего приложение завершит свою работу. Обратите внимание на то, как происходит передача данных через userData.

Макросы MENU_EXIT, MENU_TUTORIAL и MENU_ABOUT — это числовые идентификаторы пунктов меню. Почему-то идентификаторы, указанные в wxGlade, игнорируются. Мне пришлось определить их экспериментальным путем.

    #wx{ id = ?MENU_TUTORIAL } ->
      wx_misc:launchDefaultBrowser("http://eax.me/wxerlang/");

При нажатии «Help → Tutorial» показываем пользователю страничку, которую вы читаете в данный момент. Во FreeBSD браузер по умолчанию определяется перебором трех вариантов — firefox, mozilla и netscape. Чтобы все заработало, мне пришлось создать симлинк:

ln -s /usr/local/bin/chrome /usr/local/bin/firefox

А может, это все из-за того, что я пользуюсь i3?

    #wx{ id = ?MENU_ABOUT, userData = Frame } ->
      Dialog = wxMessageDialog:new(Frame,
        "wxErlang example\n"
        "(c) Alex Alexeev 2012\n"
        "http://eax.me",
        [
          { caption, "wxErlang example" },
          { style, ?wxOK bor ?wxICON_INFORMATION }
        ]),
      wxDialog:showModal(Dialog),
      wxDialog:destroy(Dialog);

При нажатии «Help → About» показываем пользователю окошко с информацией о программе.

  InfileBrowseButton = wxXmlResource:xrcctrl(Frame,
    "InfileBrowseButton", wxButton),
  InfileTextCtrl = wxXmlResource:xrcctrl(Frame,
    "InfileTextCtrl", wxTextCtrl),

  wxButton:connect(InfileBrowseButton, command_button_clicked,
    [{ callback, fun onInfileBrowseButtonClicked/2 },
     { userData, { Frame, InfileTextCtrl } }]),

Находим кнопку «Browse» и поле ввода слева от нее. Говорим при нажатии на кнопку вызывать функцию onInfileBrowseButtonClicked. Имена всех элементов управления можно указать в wxGlade (однако найти конкретный пункт меню и повесить колбэк на клик по нему мне не удалось).

onInfileBrowseButtonClicked(
    #wx{ userData = { Frame, InfileTextCtrl} },_) ->
  Dialog = wxFileDialog:new(Frame),
  wxDialog:showModal(Dialog),
  FilePath = wxFileDialog:getPath(Dialog),
  wxFileDialog:destroy(Dialog),
  wxTextCtrl:setValue(InfileTextCtrl, FilePath).

При нажатии на кнопку «Browse» просим пользователя выбрать файл, после чего в поле ввода отображаем полный путь к этому файлу.

В общем-то, это все. Как видите, ничего сложного в создании GUI приложений на Erlang нет. Все исходники к этой заметке вы можете найти в этом архиве.

Дополнение: См также заметку про парсинг выдачи Google на Erlang.

Метки: , , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.