Виджеты и обработка событий

Графическая подсистема языка (известная под названием winter — WINdowing INTerface) основана на модели виджетов (widgets), интерактивных визуальных элементов интерфейса. Как и аплеты в Java, виджеты способны функционировать как независимые оконные приложения, а также могут быть встроенными в другие виджеты или электронные документы. Технически каждый виджет — потомок класса Widget, определенного в библиотеке winter.awl. Набор виртуальных методов, декларированных в Widget, предназначен для обработки событий, генерируемых оконной системой. Вот список этих методов:

on_open (ClientExtent)
on_close (ClientExtent)

Вызываются при открытии (on_open) и закрытии (on_close) окна, содержащего данный виджет. Аргумент ClientExtent хранит размеры клиентской области виджета в виде списка (width, height).

on_resize (ClientExtent)

Вызывается при изменении размеров окна, содержащего виджет, в ClientExtent передаются его новые размеры.

on_paint (AreaRect)

Вызывается, если необходимо полностью или частично перерисовать виджет. AreaRect задает относительные координаты перерисовываемого прямоугольника в виде списка ((left, top), (right, bottom)).

on_focus (Flag)

Вызывается, когда виджет получает (при Flag == 1) или теряет (при Flag == 0) фокус ввода. Лишь виджет, имеющий фокус ввода, может получать события клавиатуры (on_char, on_key).

on_char (Char)

Вызывается для виджета, имеющего фокус ввода, при нажатии клавиши с ASCII-кодом Char. (Внимание: Char — числовое значение кода, не строка!)

on_key (Event KeyCode)

Вызывается для виджета, имеющего фокус ввода, при нажатии (при Event > 0) или отжатии (при Event < 0) любой (не только алфавитно-цифровой) клавиши, код которой содержится в аргументе KeyCode (см. в winter.awl список определений, начинающихся с KB_). (При нажатии на алфавитно-цифровые клавиши виджет получает on_key вместе с on_char).

on_mouse_move (Buttons From To)

Вызывается при движении указателя мыши по клиентской области виджета из точки с координатами From в точку с координатами To. Это же событие возникает, когда мышь входит в область виджета (при From == undef) или выходит из нее (при To == undef). Аргумент Buttons содержит в старших 16 битах текущее состояние кнопок мыши (см. далее).

on_mouse_click (Event Buttons Location)

Вызывается при нажатии/отпускании кнопки мыши, находящейся в клиентской области виджета. Аргумент Event сообщает, что произошло (–1 — кнопка отжата, 1 — кнопка нажата, 2 — двойной щелчок); Location задает координаты указателя мыши (x, y); Buttons содержит код нажатой кнопки в младших 16 битах и состояние ее кнопок после события — в старших 16 битах. Коды кнопок также определены в winter.awl: MB_Left, MB_Middle, MB_Right, MB_Ext1, MB_Ext2 — левая, средняя, правая, первая и вторая дополнительные кнопки мыши соответственно.

on_mouse_wheel (State Rotation Location)

Вызывается при вращении колесика мыши, находящейся в клиентской области виджета. Аргумент Location задает координаты указателя мыши, State (верхние 16 битов) — состояние кнопок мыши, Rotation — насколько и куда повернуто колесико (< 0, если назад, > 0, если вперед).

on_timer (TimerID)

Вызывается активным таймером TimerID в регулярные интервалы времени (подробнее таймеры описаны в документации).

Кроме методов, каждый экземпляр Widget содержит компоненты Height и Width (текущая высота и ширина его клиентской области) и Title (заголовок по умолчанию для окна, где открывается виджет). Инициализация этих компонентов в конструкторе для потомков Widget позволяет задать значения по умолчанию для этих атрибутов окна.

Чтобы увидеть обработку событий в динамике, стоит взглянуть на пример WinTrace.awl, прилагаемый к статье (см. «Мир ПК-диск»).

Средства вывода графики

Реагируя на события, большинство виджетов должны выводить нечто на экран. Графический вывод осуществляется набором функторов (определенных в пространстве имен graphics), которые можно разделить на две основные подкатегории: графические примитивы и так называемые оболочки (wrappers).

Графические примитивы выполняют вывод простых графических объектов: например, plot(x, y) рисует точку с координатами (x, y), а rect((l, t), (r, b)) — прямоугольник, заданный координатами верхнего левого угла (l, t) и нижнего правого (r, b). Параметры, задающие контекст или атрибуты (например, цвет) графического вывода, не предусмотрены: дело в том, что в winter все перечисленное задается неявно, с помощью графических оболочек. Напомним, что оболочки — название функторов, выполняющих один из своих аргументов (так называемое ядро), с некими предваряющими и/или завершающими действиями (прологом/эпилогом). Для графических оболочек эффект состоит в том, что во время выполнения их ядра заданный атрибут графики неким образом изменяется, т.е. если в ядре выполняются примитивы графики, для них будет актуально данное значение атрибута. К примеру, для оболочки plot_color(FgColor, @Core) эффект заключается в том, что цвет рисования линий для всех графических примитивов, выводимых в Core, устанавливается в FgColor. Аналогично и оболочка fill_color(BgColor, @Core) устанавливает в BgColor цвет заполнения для примитивов графики, его имеющих. Например, можно нарисовать прямоугольник, определенный углами (100, 150) и (200, 250), с красным интерьером и зеленой рамкой, используя следующий код:

  graphics!!fill_color (Red(), plot_color(Green(), rect([100 150], [200 250])));

Функторы, определяющие оттенки стандартных цветов (Red, Green, Blue…), также декларированы в winter.awl. Для вложенных вызовов оболочек удобнее использовать альтернативный синтаксис: вызовы функторов типа f(A, g(B)) можно записать как f(A):: g(B). Приведенный выше пример допустимо записать и так:

  graphics!!fill_color (Red()):: plot_color(Green()):: rect([100 150], [200 250]);

Конечно, это простейший пример (в качестве ядра, вложенного в две оболочки, рисуется один-единственный прямоугольник), но на практике в любую оболочку можно вложить любую последовательность вычислений — для всех объектов графики, выводимых в процессе этих вычислений, будут активны заданные оболочками атрибуты. Исключением является ситуация, когда вложенная оболочка переопределяет эффект внешних, например,

  graphics!!plot_color (Red()):: { X; plot_color (Blue())::Y; Z };

Здесь для всех объектов графики, выводимых при вычислении X и Z, цвет рисования будет красным, а для Y — синим. Глубина вложенности оболочек не ограничена, но ни одна из них не имеет перманентного эффекта: он всегда временный, и при выходе из оболочки все атрибуты графики восстанавливаются. Таким образом, при обращении к любому функтору, работающему с графикой (даже не зная ничего о его устройстве), можно быть уверенным, что ни один из текущих атрибутов графики не будет им «испорчен». У «объектно-функциональной» графики есть и другие преимущества: код обычно становится компактнее, яснее и эффективнее, чем при использовании чисто объектного подхода к графическому выводу.

Наконец заметим, что определение новых графических оболочек так же просто, как и любых других пользовательских функторов. Например, если нужно часто сочетать атрибуты графики «зеленые линии на красном фоне», можно определить такой функтор:

  ! GreenOnRed(@Core) = graphics!! fill_color(Red()):: plot_color(Green()):: (^Core);

Теперь пример с прямоугольником можно записать так: GreenOnRed(graphics!!rect((100, 150), (200, 250))). Понятно, что и в данном случае в оболочку может быть вложен более сложный код.

Перечислим основные примитивы графического вывода, доступные в graphics. Запись wrapper():: func() означает, что дополнительные атрибуты для func задает внешняя оболочка wrapper:

  plot_color():: plot (x, y): точка с координатами (x, y);
  plot_color():: line ((x0, y0), (x1, y1)): отрезок прямой от (включая) точки (x0, y0) до (исключая) точки (x1, y1);
  plot_color():: fill_color():: rect ((left, top), (right, bottom)): прямоугольник с верхним/левым углом (left, top) и нижним/правым углом  (right, bottom);
  plot_color():: fill_color():: oval ((left, top), (right, bottom)): овал (эллипс), вписанный в прямоугольник, заданный верхним/левым (left, top) и нижним/правым (right, bottom) углами;
  plot_color():: fill_color():: round_rect ((left, top), (right, bottom), (x_rad, y_rad)): аналогично rect, но прямоугольник с закругленными углами, с радиусами закругления (x_rad, y_rad);
  plot_color():: arc ((left, top), (right, bottom), (x_from, y_from), (x_to, y_to));
  plot_color():: fill_color():: chord ((left, top), (right, bottom), (x_from, y_from), (x_to, y_to));
  plot_color():: fill_color():: wedge ((left, top), (right, bottom), (x_from, y_from), (x_to, y_to));

Все три функтора рисуют разные фрагменты эллипса: дугу (arc), заполненный сегмент (chord) и заполненный сектор (wedge). Эллипс задается аналогично oval: вписан в прямоугольник ((left, top), (right, bottom)). Область отсечения задается по часовой стрелке от радиуса-вектора (x_from, y_from) до радиуса-вектора (x_to, y_to) (заданных относительно центра эллипса);

  plot_color():: fill_color():: polygon ((x0, y0), (x1, y1), (x1, y2), … (xN, yN), ) рисует заполненный замкнутый многоугольник, определенный списком вершин (x0, y0) … (xN, yN). (Заметим, что в этом функторе список точек должен быть открытым.);
  fill_color():: fill ((left, top), (right, bottom)): заполняет прямоугольную область с верхним/левым углом (left, top) и нижним/правым углом (right, bottom).

Как графическая подсистема взаимодействует с виджетами? Обычно самой внешней из всех вложенных графических оболочек является обращение к with_widget, неявно создающее графический контекст, обеспечивающий вывод в клиентской области текущего виджета. Исключение из правила — обработка события on_paint, при котором графический контекст, обеспечивающий прорисовку области, создается автоматически, и весь код обработчика выполняется в нем неявно. При обработке других событий надо использовать with_widget (для рисования в виджете, которому адресовано событие) или W.with_widget (для рисования в виджете W). В последних версиях AWL есть поддержка внеэкранных растровых изображений: если создано изображение Img, оболочка with_image(Img, @Core) позволяет выполнить Core с Img вместо физического экрана в качестве контекста вывода.

Приведем пример виджета (к сожалению, очень тривиальный), рисующего прямоугольную «решетку» (с шагом HStep по горизонтали и VStep по вертикали), используя для вывода фона и линий цвета BgColor и FgColor. Взаимодействовать как-либо с пользователем MyTestWidget, к сожалению, не умеет.

  !! [Widget] MyTestWidget (BgColor FgColor HStep VStep) {

! #on_paint (PaintRect): [X Y] = graphics!! {

` (заполнение фона) `

fill_color (BgColor):: fill (PaintRect);

plot_color (FgColor):: {

` (вертикальные линии с шагом ‘HStep’) `

for_inc (X, Width % HStep + 1, line((X * HStep, 0), (X * HStep, Height)));

` (горизонтальные линии с шагом ‘VStep’) `

for_inc (Y, Height % VStep + 1, line((0, Y * VStep), (Width, Y * VStep)));

}

}

};

Для завершенной программы осталось запустить несколько экземпляров данного виджета в работу, что обычно делается обращением к WinSession — этот функтор получает в качестве параметров список виджетов с (необязательными) координатами тех окон, в которых они открываются.

  WinSession (

    (MyTestWidget (“Пример 1”, Cyan(), Red(), 20, 30), [0 0], [100 100]),
    (MyTestWidget (“Пример 2”, Green(), Yellow(), 10, 60), [100 0], [100 100]),

      );

 

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

Использовать этот простейший пример можно во многих случаях, в частности при написании игр (см. рисунок).

Вывод текста и создание документов

Вывод текста можно рассматривать как специальный случай графического вывода. Его специфика состоит и в том, что текст можно выводить как непосредственно в экранную область вывода, так и в специальный объект-контейнер (метадокумент), обеспечивающий форматирование выводимого текста. Вывод текстовых строк выполняется так: graphics!!text(String) обеспечивает вывод строки String, и при этом практически все атрибуты выводимого текста задаются неявно. Например, позиция для вывода текста на экран («виртуальная каретка») может быть явно установлена обращением к graphics!!text_org(x, y). Ее не обязательно устанавливать перед каждой операцией вывода, так как после каждого фрагмента текста каретка автоматически сдвигается так, что строки выводятся одна за другой с вертикальным выравниванием по базовой линии. Многие другие атрибуты выводимого текста задаются в уже знакомом нам стиле: функторами-оболочками. Вот их неполный список:

font_face(typeface, @Core): установить для вывода Core шрифт с гарнитурой typeface (задается, например, строкой “Times” или “Arial”).
font_size(size, @Core): установить для вывода Core шрифт с кеглем size (в пунктах).
font_weight(flag, @Core)
font_italic(flag, @Core)
font_underline(flag, @Core)
font_overstrike(flag, @Core)

Управление атрибутами текста: жирностью (font_weight), курсивом (font_italic), подчеркиванием (font_underline) и перечеркиванием (font_overstrike). Каждый из этих атрибутов включается/выключается при выводе Core в зависимости от истинности значения flag.

font_bgcolor(color, @Core)
font_fgcolor(color, @Core)

Управление цветом выводимого текста (font_bgcolor) и цветом фона (font_fgcolor) при выводе Core.

text_metrics (var, outflag, @Core): дает возможность (для текста, выводимого в Core) измерить общую ширину, максимальные подъем и спуск. Прилагаемый к статье пример (FontDemo.awl) наглядно демонстрирует вывод текста с различными атрибутами.

Все перечисленные средства можно применять при выводе текста не только на экран, но и в так называемые метадокументы. Для их создания удобно употреблять средства из стандартной библиотеки MetaDoc.awl, где также определены краткие синонимы для часто применяемых функторов: например, вместо graphics!!text(Str) допустимо использовать просто _(Str). Для конструирования документа очень удобен конструктор MetaDoc: его первым операндом является заголовок документа, далее идет собственно код документа в виде структуры вложенных функторов, определяющих выводимый текст и атрибуты его форматирования. К примеру,

  MyDoc = MetaDoc(“Тестовый пример”, FontFace (“Arial“):: FontSize (12):: B (I (_ “Простой пример вывода текста!”)));

Созданный документ подобен данному HTML-документу:

  </strong>Тестовый пример<strong>

 

  face=’Arial’ size=12> Простой пример вывода текста!

 

Правда, между ними есть сущест-венная разница: в AWL созданный метадокумент — это самостоятельный объект оконной системы. Его реализация и визуализация осуществляются независимо, а для отображения документа на экране подойдет библиотечный виджет MetaDocView:

  WinSession (

(MetaDocView ((), MyDoc), [0 0], [300 200]),

);

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

Основное преимущество документной технологии AWL заключается не в более компактном (и защищенном от ошибок) синтаксисе, а в динамической модели описания документов. Их содержимое в принципе не статично и определяется при выполнении программы. Например, если в переменную UserName занесено имя пользователя, его можно поприветствовать так:

  P(_“Добро пожаловать в систему, ”, B(_ (UserName)), _“!”);

Кроме того, при формировании содержимого метадокументов просто использовать условные и циклические конструкции (хороший пример последних — в MulTab.awl). Но важнее всего то, что инструментарий описания документов легко расширяем: в частности, для стилевой разметки документов удобно создавать новые функторы. Если все заголовки первого уровня в документе выводятся по центру шрифтом «Arial» размера 12, то после описания

  ! Heading1(Title) = ParaCenter (FontFace (“Arial”, FontSize (12, _(Title))));

вызов Heading1(“Глава 1”) создаст заголовок «Глава 1». На практике можно сделать больше, чем просто имитировать средства CSS: многие виды разметки документов, встроенные в HTML, в AWL легко реализуются как библиотечные функторы. В качестве примера дадим определения для нумерованных (

) и ненумерованных (
)
списков:

  ! NumList (List): [i] = for_inc (i, #List, P (_ “ “, B (_(i+1), _”: “), _ (List[i])));
  ! OptList (List): [v] = l_loop (v, List, P (_ ‘ ‘, graphics!!unichar (x25CB), _ ‘ ‘, _ (v)));

Функторы могут применяться и шире. Так, если нужно оформить в едином стиле набор страниц (со стандартными заголовками, декорированием и проч.), удобно определить для этого единый функтор-шаблон, в который «вкладывается» код самих страниц. Средствами AWL удается реализовать и весьма сложные механизмы: автоматические средства генерации оглавлений, списков перекрестных ссылок, глоссариев и т.п. Можно надеяться, что в процессе развития языка будут разработаны мощные инструментальные библиотеки для поддержки специфических документных средств (аналогичные тем, что реализованы в LaTeX и других подобных системах подготовки документов). В результате легко будет создавать документы со сложными таблицами, математическими формулами, блок-схемами, диаграммами и графиками, структурными химическими формулами и проч. — и все эти средства будут полностью реализованы на самом AWL.

Наконец, стоит отметить, что средствами AWL легко делается то, для чего используются XML/XSLT: автоматическое преобразование структурированных данных в форму документа. В рамках AWL будет осуществим эффективный динамический обмен данными между клиентом и сервером (модель, подобная той, что реализована в технологии AJAX). Другими словами, приложение-клиент будет гибко формировать запросы и получать в ответ от сервера минимально необходимый объем данных. Причем всю работу по преобразованию данных в пользовательский формат клиент сможет выполнять сам.

То, что AWL позволяет принципиально упростить многие задачи веб-программирования (традиционно решаемые с помощью таких разных технологий, как HTML, CSS, JavaScript, SVG, XML/XSLT и т.п.) дает основание надеяться, что в области технологий Всемирной паутины этот язык найдет широкое применение.