REBOL

Создание бизнес-приложений с REBOL

By Nick Antonaccio
Updated: 12-16-2015
Перевод pochinok@bk.ru
Научитесь решать типичные проблемы управления бизнес-данными с помощью универсального инструмента разработки, достаточно простого для "непрограммистов".
** Рекомендуется сначала прочитать http://re-bol.com/rebol_quick_start.html и попробовать конструктор приложений на http://withoutwritingcode.com, чтобы быстро познакомиться с кодированием Rebol. Затем вернитесь к прочтению этого текста, чтобы получить более полное представление обо всех возможностях Rebol.
Кроме того, не забудьте посмотреть короткие примеры на http://re-bol.com/short_rebol_examples.r для быстрого и интересного обзора простого и продуктивного стиля кодирования Rebol.
См. http://re-bol.com/examples.txt для многих других примеров кода Rebol.
Посетите http://rebolforum.com, чтобы задать вопросы.
См. также 68 видеоуроков на YouTube о REBOL (10 часов видео).
Предыдущее введение к этому тексту было удалено. Он доступен здесь, а его более короткая версия - здесь.
Презентация слайд-шоу, охватывающая предыдущее введение, доступна здесь.
(от переводчика - ОТСУТСТВУЕТ глава 14.16 (WAP))

Contents:

1. Краткий курс. Введение в REBOL
1.1 Установка и запуск программ
1.2 Открытие REBOL непосредственно в консоль
1.3 Некоторые короткие примеры программ для "затравки"
1.4 Основы кодирования REBOL
1.5 Условные оценки
1.6 Ещё несколько полезных функций
2. Списки, таблицы и функция "Foreach" (по каждому)
2.1 Управление данными, подобными электронным таблицам
2.2 Некоторые простые алгоритмы списков (подсчёт, сумма, среднее, макс./мин.)
2.3 Сбор данных и функция "Copy"
2.4 Список функций сравнения
2.5 Создание списков на основе пользовательского ввода
2.6 Сохранение и чтение данных блока в/из файла
2.7 Три полезные программы хранения данных: инвентаризация, контакты, расписание
2.8 Работа с таблицами данных: столбцы и строки
2.9 Дополнительные функции и методы работы со списком/блоком/серией
2.10 Сортировка списков и таблиц данных
2.11 Файлы CSV и функция "синтаксического анализа" parse
2.12 Две программы отчётов Paypal, Анализ
2.13 Некоторые перспективы изучения этих тем
3. Использование окон и виджетов с графическим интерфейсом для ввода и отображения данных.
3.1 Основные принципы компоновки и виджеты
3.2 Вдыхание жизни в программы с графическим интерфейсом пользователя - выполнение действий
3.3 Справочник по языку графического интерфейса
3.4 Показательное сравнение
4. Быстрый обзор и разъяснения
5. НЕКОТОРЫЕ ПРИМЕРЫ ПОЛНЫХ ПРИЛОЖЕНИЙ GUI
5.1 Универсальная сохронялка текстовых полей
5.2 Калькулятор
5.3 Редактор файлов
5.4 Редактор веб-страниц
5.5 Создатель инвентарного списка
5.6 Сортировщик и просмотрщик инвентаря
5.7 Просмотр контактов
5.8 Минимальная розничная касса и система отчётов о продажах
5.9 Электронное письмо
5.10 Планировщик
5.11 База данных деталей
5.12 Часы и отчёт о заработной плате
5.13 Блоггер
5.14 Групповой чат FTP
5.15 Напоминание для группе
5.16 Универсальный генератор отчётов для Paypal и любых других данных таблицы CSV
5.17 Просмотр и использование кода, который вы видели, для моделирования новых приложений
6. Пользовательские функции и модули импортированного кода
6.1 "Do", "Does" и "Func"
6.2 Возвращаемые значения
6.3 Область видимости
6.4 Документация по функциям
6.5 Выполнение импортированного кода
6.6 Разделение формы и функций в графическом интерфейсе пользователя - приложение Check Writer
6.7 Полнофункциональное приложение для обмена групповыми заметками
7. Несколько полезных инструментов визуализации данных
7.1 Отображение и сортировка данных с использованием графических сеток в виде электронных таблиц
7.2 Создание графиков, графиков и диаграмм с помощью "Q-Plot"
7.3 Рисование диаграмм с использованием исходного кода графического интерфейса
7.4 Создание 3D-графиков с помощью r3D
7.5 Использование Google Chart API
7.6 Использование приложения для работы с электронными таблицами "Nano-Sheets"
8. Использование REBOL для создания презентаций
8.1 REBOL как презентационное программное обеспечение
8.2 Некоторые базовые идеи макета и простая структура кода для презентаций
8.3 Использование панелей вкладок и меню для представления информации
8.4 Show.r - полезная система построчной презентации
8.5 Создание "снимков экрана" графических интерфейсов
8.6 Встраивание двоичных ресурсов (изображений, звуков, файлов и т.д.) в код
8.7 Воспроизведение звуков
8.8 Запуск кода в отдельных процессах
8.9 Запуск приложений командной строки
8.10 Создание простых анимаций
8.11 Простая структура анимации для презентаций
8.12 Использование анимированных изображений в формате GIF
8.13 И это только начало
9. Makedoc и другие полезные инструменты для повышения производительности REBOL
9.1 Makedoc.r - Конструктор HTML-документов
9.2 Улучшенный текстовый редактор
9.3 Конструкторы графического интерфейса пользователя и средства обучения
10. Проблемы и примеры из реального мира: почему "Программирование"> Офисное программное обеспечение
10.1 Расширенная программа инвентаризации
10.2 Чековый принтер
10.3 Расширенные часы и автоматические отчёты о заработной плате
11. Больше основ языка REBOL
11.1 Комментарии
11.2 Уточнения функций
11.3 Пробелы и отступы
11.4 Многострочные строки, кавычки и конкатенация
11.5 Подробнее о переменных
11.6 Типы данных
11.7 Случайные значения
11.8 Подробнее о чтении, написании, загрузке и сохранении в и из различных источников
11.9 Понимание возвращаемого значения и порядка оценки
11.10 Подробнее об условных оценках
11.11 Подробнее о циклах
11.12 Подробнее о том, почему и чем полезны блоки
11.13 Строки в REBOL
12. Более важные темы
12.1 Встроенная справка и онлайн-ресурсы
12.2 Сохранение и запуск скриптов REBOL
12.3 "Компиляция" программ REBOL - распространение исполняемых .EXE файлов
12.4 Распространённые ошибки REBOL и как их исправить
13. Создание веб-приложений с использованием REBOL CGI
13.1 Ускоренный курс HTML
13.2 Стандартный CGI шаблон для запоминания
14. Примеры приложений CGI
14.1 Почтовая форма
14.2 Типичное приложение с раскрывающимся списком
14.3 Фотоальбом
14.4 Простая интерактивная консоль веб-сайта REBOL
14.5 Посещаемость
14.6 Доска объявлений
14.7 Пример GET и POST
14.8 Система групповых заметок
14.9 Универсальный обработчик форм
14.10 Загрузчик файлов
14.11 Скачивальщик файлов
14.12 Полноценное приложение для управления веб-сервером
14.13 Код CGI RebolForum.com
14.14 Менеджер по работе с клиентами Etsy
14.15 Замечание о работе с веб-серверами
15. Организация эффективных структур данных и алгоритмов
15.1 Пример простого цикла
15.2 Пример из реальной жизни: кассовая регистратор и система отчётов кассира
16. Дополнительные темы
16.1 Объекты
16.2 Порты - точный доступ к файлам, электронной почте, сети и т.д.
16.3 Консоль и приложения электронной почты CGI, использующие порты
16.4 Сетевые порты - передача данных и файлов с помощью HTTP
16.5 Передача двоичных файлов через сетевые сокеты TCP
16.6 Передача данных через сетевые порты UDP
16.7 Parse (ответ REBOL на регулярные выражения)
16.8 Использование синтаксического анализа для загрузки файлов электронных таблиц CSV и других структурированных данных
16.9 Использование режима сопоставления с образцом в Parse для поиска данных
16.10 Реагирование на особые события в графическом интерфейсе - "Feel"
16.11 2D рисование, графика и анимация
16.12 3D-графика с r3D
16.13 Несколько 3D-скриптов с использованием Raw REBOL Draw Dialect
16.14 Таблицы спрайтов
16.15 Многозадачность
16.16 Использование DLL и файлов общего кода в REBOL
16.17 Приложение с несколькими сетевыми камерами безопасности, использующее DLL веб-камеры Windows
16.18 REBOL как плагин для браузера
16.19 Использование баз данных
16.20 Меню
16.21 Создание многоколоночных текстовых списков графического интерфейса пользователя (таблиц данных) с нуля
16.22 RebGUI
16.23 Приложения RebGUI - электронная таблица, Rolodex, менеджер, редактор, POS-система
16.24 Создание файлов PDF с помощью pdf-maker.r
16.25 Штрих-коды
16.26 Создание файлов .swf с помощью REBOL/Flash
16.27 Печать с REBOL
16.28 Приложение для удалённой печати чеков
16.29 Создание приложений на платформах, не поддерживающих графические интерфейсы пользователя
16.30 Шифрование и безопасность
16.31 Rebcode
16.32 Полезные инструменты REBOL: XML, Zip, база данных, сеть, веб-сервер и др.
16.33 6 ароматов REBOL
16.34 Биндология, диалекты, метапрограммирование и другие продвинутые темы
17. REBOL для Android, R3 с открытым исходным кодом (сборки Saphirion) и RED
17.1 Открытый исходный код
17.2 Создание рабочей среды Android - необходимые инструменты
17.3 Основы графического интерфейса пользователя R3
17.4 Простые запрощики
17.5 Макет (layout)
17.6 Стили
17.7 Ещё несколько простых примеров
17.8 Дополнительные важные ресурсы
17.9 RED
18. Реализация приложений для управления многопользовательскими данными с помощью Rebol
18.1 Многопользовательские системы баз данных в Rebol
18.2 Типичный 101-й пример REBOL
18.3 Многопользовательские базы данных
18.4 Более длинный пример
18.5 Получение динамически назначаемых адресов серверов
18.6 Интерфейсы HTML-форм для клиентов
18.7 Простота
19. Создание мобильных и веб-приложений с помощью jsLinb и Sigma Visual Builder
19.1 Что такое библиотека jsLinb и Sigma Visual Builder?
19.2 Установка Sigma Builder на веб-сервер
19.3 Базовый код jsLinb и примеры Sigma IDE
19.4 Подключение приложений jsLinb к серверным приложениям Rebol CGI
19.5 Примеры приложений, созданных с использованием кода CGI jsLinb и Rebol
19.6 Сохранение и развёртывание ваших приложений jsLinb в Sigma IDE
19.7 Некоторые примеры Data Grid
19.8 Мощные виджеты макета
19.9 Возможности документации jsLinb и Sigma Builder
19.10 Использование jsLinb Databinder для сбора и установки данных формы
19.11 Пример приложения большего размера
20. Подключение к автономным серверным приложениям Rebol
21. CrossUI
21.1 Мощное дополнение к набору инструментов Rebol
22. НАСТОЯЩИЕ МИРОВЫЕ ПРИМЕРЫ - Обучение мыслить кодом
22.1 Случай: Расписание учителей
22.2 Случай: простая программа CGI для галереи изображений
22.3 Случай: Калькулятор дней между двумя датами
22.4 Случай: простой поиск
22.5 Случай: простое приложение-калькулятор
22.6 Случай: музыкальный генератор фона(исполнитель аккордового аккомпанемента)
22.7 Случай: FTP Tool
22.8 Случай: Тренировочная программа "Jeoparody"
22.9 Случай: Составление расписания учителей, часть вторая
22.10 Случай: CGI-программа онлайн-страницы участника
22.11 Случай: Календарь событий CGI
22.12 Случай: медиаплеер (Wave/Mp3 Jukebox)
22.13 Случай: Принтер для гитарных аккордов
22.14 Случай: система управления контентом веб-сайта (CMS), Sitebuilder.cgi
22.15 Случай: загрузка каталогов - приложение для работы с сервером
22.16 Случай: овощеводство
22.17 Дополнительный проект автоматизации учителей
23. Программирование игр для улучшения алгоритмического мышления и графических навыков.
23.1 Случай: Подробнее о творческой алгоритмической мысли: клон тетриса
23.2 Случай: более полные циклы программы: лыжи, змейка и захватчики
23.3 Случай: фреймворк игральных карт с графическим интерфейсом (создание клона свободной ячейки)
23.4 Случай: Создание REBOL "Demo"
24. Прочие скрипты
24.1 Создатель эскизов
24.2 Циклы и условия - простое приложение для хранения данных
24.3 Пример многоколоночной сетки данных Listview
24.4 Эффектор изображения
24.5 Пример маленького меню
24.6 Shoot-Em-Up Компьютерная Игра
24.7 Бинго Доска
24.8 Голосовые оповещения
24.9 Мелочи в конце
25. Дополнительные сведения о REBOL - важные ссылки на документацию
26. Помимо REBOL
27. Об авторе
27.1 Мой бизнес
27.2 Список клиентов и предыдущий опыт
27.3 Свяжитесь со мной

1. Краткий курс. Введение в REBOL

1.1 Установка и запуск программ

Для начала загрузите и установите REBOL/View с http://www.rebol.com/download-view.html (это займёт всего несколько секунд).

После установки запустите REBOL (Пуск -> Программы -> REBOL -> REBOL View), затем щёлкните значок "Console" (Консоль).

Введите в командной строке "editor none" - это запустит встроенный текстовый редактор REBOL.

На этом этапе вы готовы начать вводить текст в программах REBOL. Скопируйте/вставьте каждый пример из этого руководства в редактор REBOL, чтобы увидеть, что делает код. Попробуйте прямо сейчас. Вставьте следующий код в редактор REBOL, затем нажмите [F5] на клавиатуре, чтобы сохранить и запустить программу. Вы можете сохранить файл, используя имя файла по умолчанию "temp.txt", как предложено, или переименовать его, если хотите. Если вы видите запросчик безопасности REBOL, выберите "Allow all" (Разрешить все):

REBOL []
alert "Hello World!"

Если вы сохраните свою программу с расширением ".r" в имени файла (например, "myprogram.r"), вы также можете щёлкнуть значок файла сохранённой программы, и он будет работать так же, как любой обычный исполняемый файл (.exe). файл. Попробуйте сохранить указанную выше программу на рабочем столе под именем "hello.r", затем запустите её, щёлкнув мышью значок hello.r на рабочем столе.

1.2 Открытие REBOL непосредственно в консоль

Прежде чем вводить или вставлять какой-либо другой код, настройте следующую опцию в интерпретаторе REBOL: щёлкните меню "User" (Пользователь) в графической области просмотра, которая открывается по умолчанию с REBOL, и снимите флажок "Open Desktop On Startup" (Открывать рабочий стол при запуске). Это избавит вас от необходимости нажимать кнопку "Console" (Консоль) каждый раз, когда вы запускаете REBOL.

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

1.3 Некоторые короткие примеры программ для "затравки"

Вот несколько примеров программ REBOL, которые демонстрируют простой и лаконичный характер кода REBOL. Вставьте каждую программу в редактор REBOL и нажмите [F5], чтобы увидеть, как она запустится. Кратко прочтите каждую строку программ, чтобы ознакомиться с тем, как выглядит код REBOL. Вы очень скоро поймёте, как все работает.

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

REBOL [title: "Text Field Saver"]
view layout [
    f1: field
    f2: field
    f3: field
    btn "Save Fields" [
        write/append %fields.csv rejoin [
            mold f1/text ", " mold f2/text ", " mold f3/text newline
        ]
        alert "Added to File"
    ]
]

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

REBOL [title: "Text Editor"]
view layout [
    h1 "Text Editor:"
    f: field 600 "filename.txt"
    a: area 600x350 
    across 
    btn "Load" [
        f/text: request-file
        show f
        filename: to-file f/text
        a/text: read filename 
        show a
    ]
    btn "Save" [
        filename: to-file request-file/save/file f/text
        write filename a/text
        alert "Saved"
    ]
]

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

REBOL [title: "Web Page Editor"]
view layout [
    h1 "Web Page Editor:"
    f: field 600 "ftp://user:pass@site.com/public_html/page.html"
    a: area 600x350 
    across 
    btn "Load" [
        a/text: read to-url f/text 
        show a
    ]
    btn "Save" [
        write (to-url f/text) a/text
        alert "Saved"
    ]
]

Вот простое приложение-калькулятор:

REBOL [title: "Calculator"]
view layout [
    origin 0  space 0x0  across
    style btn btn 50x50 [append f/text face/text  show f]
    f: field 200x40 font-size 20 return
    btn "1"  btn "2"  btn "3"  btn " + "  return
    btn "4"  btn "5"  btn "6"  btn " - "  return
    btn "7"  btn "8"  btn "9"  btn " * "  return
    btn "0"  btn "."  btn " / "   btn "=" [
        attempt [f/text: form do f/text  show f]
    ]
]

Вот вариант примера Paypal из введения в это руководство. Он загружает файл учётной записи Paypal из Интернета и сообщает сумму всех валовых транзакций по счёту, отображает все покупки, сделанные от имени "Saoud Gorn", и вычисляет общую сумму всех транзакций с "Ourliptef.com", которые произошли с полуночи до полудня. Попробуйте запустить его на компьютере, подключённом к Интернету:

REBOL [title: "Paypal Reports"]
sum1: sum2: $0
foreach line at read/lines http://re-bol.com/Download.csv 2 [
    sum1: sum1 + to-money pick row: parse/all line "," 8
    if find row/4 "Saoud" [print rejoin [row/1 ", Saoud Gorn: " row/8]]
    if find row/4 "Ourliptef.com" [
        if (0:00am <= time: to-time row/2) and (time <= 12:00pm) [
            sum2: sum2 + to-money row/8
        ]
    ]
]
alert join "GROSS ACCOUNT TRANSACTIONS: " sum1
alert join "2012 Ourliptef.com Morning Total: " sum2

В этом примере приведённые выше отчёты дополняются графиками собранных данных (для этого примера также требуется подключение к Интернету):

REBOL [title: "Paypal Report Charts"]
transactions: copy []
saoud: copy []
dates: copy []
foreach line at read/lines http://re-bol.com/Download.csv 2 [
    row: parse/all line ","
    append transactions to-integer row/8
    if find row/4 "Saoud" [
        append saoud to-integer row/8
        append dates replace row/1 "/2012" ""
    ]
]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view center-face quick-plot [
    594x400
    bars [(data: copy transactions)]
    label "All Paypal Transactions"
]
view center-face quick-plot [
    495x530
    pen blue
    pie [(data: copy saoud)] labels [(data: copy dates)] explode [1 2 3]
    title "Saoud" style vh2     
]

Вот немного более зрелая версия первого примера в этом разделе. Эта программа создаёт инвентарный список, используя простую форму графического интерфейса (окно с некоторыми текстовыми полями и кнопками). Созданный файл можно использовать, например, для определения требований к повторному заказу, для расчёта подлежащих оплате запасов и налога с продаж или для отправки бухгалтеру для импорта и использования в электронной таблице и т.д.:

REBOL [title: "Inventory"]
view layout [
    text "SKU:"
    f1: field
    text "Cost:"
    f2: field "1.00"
    text "Quantity:"
    f3: field
    across
    btn "Save" [
        write/append %inventory.txt rejoin [
            mold f1/text " " mold f2/text " " mold f3/text newline
        ]
        alert "Saved"
    ]
    btn "View Data" [editor %inventory.txt]
]

Эта программа позволяет пользователям просматривать данные инвентаризации, созданные вышеуказанной программой, отсортированные по любому выбранному столбцу:

REBOL [title: "Sort Inventory"]
inventory: load %inventory.txt
blocked: copy []
foreach [sku cost qty] inventory [
    append/only blocked reduce [
        sku 
        to-money cost
        to-integer qty
    ]
]
field-name: request-list "Choose Field To Sort By:" [
    "sku" "cost" "qty"
]
field: select ["sku" 1 "cost" 2 "qty" 3] field-name
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
    sort/compare blocked func [a b] [(at a field) < (at b field)]
][
    sort/compare blocked func [a b] [(at a field) > (at b field)]
]
foreach item blocked [
    print rejoin [
        "SKU:  " item/1 "  COST: " item/2 "  QTY: " item/3 newline
    ]
]
halt

Вот небольшое приложение для базы данных контактов, которое отображает информацию о пользователе в виде таблицы:

REBOL [title: "Contacts"]
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]
gui: [
    backdrop white
    across
    style header text black 200
    header "Name:" header "Address:" header "Phone:" return
]
foreach [name address phone] users [
    append gui compose [
        field (name) field (address) field (phone) return
    ]
]
view layout gui

Вот простое почтовое приложение:

REBOL [title: "Email"]
view layout[
    h1 "Send:"
    btn "Server settings" [
        system/schemes/default/host: request-text/title "SMTP Server:"
        system/schemes/pop/host:     request-text/title "POP Server:"
        system/schemes/default/user: request-text/title "SMTP User Name:"
        system/schemes/default/pass: request-text/title "SMTP Password:"
        system/user/email: to-email request-text/title "Your Email Addr:"
    ]
    a: field "user@website.com"
    s: field "Subject" 
    b: area
    btn "Send"[
        send/subject to-email a/text b/text s/text
        alert "Sent"
    ]
    h1 "Read:" 
    f: field "pop://user:pass@site.com"
    btn "Read" [editor read to-url f/text]
]

Вот приложение для планирования, которое позволяет пользователям создавать события в любой день. Затем пользователь может щёлкнуть дни в календаре, чтобы увидеть запланированные события:

REBOL [title: "Schedule"]
view center-face gui: layout [
    btn "Date" [date/text: form request-date  show date]
    date: field
    text "Event Title:"
    event: field
    text "Time:"
    time: field
    text "Notes:"
    notes: field
    btn "Add Appointment" [
        write/append %appts.txt rejoin [
            mold date/text newline
            mold event/text newline
            mold time/text newline
            mold notes/text newline 
        ]
        date/text: "" event/text: "" time/text: "" notes/text: ""
        show gui
        alert "Added"
    ]
    a: area
    btn "View Schedule" [
        today: form request-date
        foreach [date event time notes] load %appts.txt [
            if date = today [
                a/text: copy ""
                append a/text form rejoin [
                    date newline
                    event newline
                    time newline
                    notes newline newline
                ]
                show a
            ]
        ]
    ]
]

Вот небольшое, но полностью функциональное кассовое приложение:

REBOL [title: "Minimal Cash Register"]
view gui: layout [
    style fld field 80
    across
    text "Cashier:"   cashier: fld 
    text "Item:"      item: fld 
    text "Price:"     price: fld [
        if error? try [to-money price/text] [alert "Price error" return]
        append a/text reduce [mold item/text "    " price/text newline]
        item/text: copy "" price/text: copy ""
        sum: 0
        foreach [item price] load a/text [sum: sum + to-money price]
        subtotal/text: form sum
        tax/text: form sum * .06
        total/text: form sum * 1.06 
        focus item
        show gui
    ]
    return
    a: area 600x300
    return
    text "Subtotal:"   subtotal: fld 
    text "Tax:"        tax: fld 
    text "Total:"      total: fld
    btn "Save" [
        items: replace/all (mold load a/text) newline " "
        write/append %sales.txt rejoin [
            items newline cashier/text newline now/date newline
        ]
        clear-fields gui
        a/text: copy ""             
        show gui             
    ]
]

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

REBOL [title: "Daily Total"]
sales: read/lines %sales.txt
sum: $0
foreach [items cashier date] sales [
    if now/date = to-date date [
        foreach [item price] load items [
            sum: sum + to-money price
        ]
    ]
]
alert rejoin ["Total sales today: " sum]

Вот пример полноэкранной презентации (слайд-шоу):

REBOL [title: "Simple Presentation"]
slides: [
    [
        at 0x0 box system/view/screen-face/size white [unview]
        at 20x20 h1 blue "Slide 1"
        box black 2000x2
        text "This slide takes up the full screen."
        text "Adding images is easy:"
        image logo.gif
        image stop.gif
        image info.gif
        image exclamation.gif
        text "Click anywhere on the screen for next slide..."
        box black 2000x2
    ]
    [
        at 0x0 box system/view/screen-face/size effect [
            gradient 1x1 tan brown
        ] [unview]
        at 20x20 h1 blue "Slide 2"
        box black 2000x2
        text "Gradients and color effects are easy in REBOL:"
        box effect [gradient 123.23.56 254.0.12]
        box effect [gradient blue gold/2]
        text "Click anywhere on the screen to close..."
        box black 2000x2
    ]
]
foreach slide slides [
    view/options center-face layout slide 'no-title
]

Вот приложение для базы данных деталей:

REBOL [title: "Parts"]
write/append %data.txt ""
database: load %data.txt
view center-face gui: layout [
    text "Parts in Stock:"
    name-list: text-list blue 400x100 data sort (extract database 4) [
        if value = none [return]
        marker: index? find database value
        n/text: pick database marker
        a/text: pick database (marker + 1)
        p/text: pick database (marker + 2)
        o/text: pick database (marker + 3)
        show gui
    ]
    text "Part Name:"       n: field 400
    text "Manufacturer:"    a: field 400
    text "SKU:"      p: field 400
    text "Notes:"      o: area  400x100
    across
    btn "Save" [
        if n/text = "" [alert "You must enter a Part name." return]
        if find (extract database 4) n/text [
            either true = request "Overwrite existing record?" [
               remove/part (find database n/text) 4
            ] [
               return
            ]
        ]
        save %data.txt repend database [n/text a/text p/text o/text]
        name-list/data: sort (extract copy database 4)
        show name-list
    ]
    btn "Delete" [
        if true = request rejoin ["Delete " n/text "?"] [
            remove/part (find database n/text) 4
            save %data.txt database
            do-face clear-button 1
            name-list/data: sort (extract copy database 4)
            show name-list
        ]
    ]
    clear-button: btn "New" [
        n/text: copy  ""
        a/text: copy  ""
        p/text: copy  ""
        o/text: copy  ""
        show gui
    ]
]

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

REBOL [Title: "Rebocalc" Authors: ["Carl Sassenrath" "Nick Antonaccio"]]
csize: 100x20  max-x: 8  max-y: 16
pane: []
xy: csize / 2 + 1 * 1x0
yx: csize + 1 * 0x1
layout [
    cell: field csize edge none [enter face compute face/para/scroll: 0x0]
    label: text csize white black bold center
]
char: #"A"
repeat x max-x [
    append pane make label [offset: xy text: char]
    set in last pane 'offset xy
    xy: csize + 1 * 1x0 + xy
    char: char + 1
]
repeat y max-y [
    append pane make label [offset: yx text: y size: csize * 1x2 / 2]
    yx: csize + 1 * 0x1 + yx
]
xy: csize * 1x2 / 2 + 1
cells: tail pane
repeat y max-y [
    char: #"A"
    repeat x max-x [
        v: to-word join char y
        set v none
        char: char + 1
        append pane make cell [offset: xy text: none var: v formula: none]
        xy: csize + 1 * 1x0 + xy
    ]
    xy: csize * 1x2 / 2 + 1 + (xy * 0x1)
]
enter: func [face /local data] [
    if empty? face/text [exit]
    set face/var face/text
    data: either face/text/1 = #"=" [next face/text][face/text]
    if error? try [data: load data] [exit]
    if find [
        integer! decimal! money! time! date! tuple! pair!
    ] type?/word :data [set face/var data exit]
    if face/text/1 = #"=" [face/formula: :data]
]
compute: has [blk] [
    unfocus
    foreach cell cells [
        if cell/formula [
            either cell/text = "formula" [
                cell/text: join "=" form cell/formula
                show cell return
            ][
                if error? cell/text: try [do cell/formula] [
                    cell/text: "ERROR!"
                ]
            ]
            set cell/var cell/text
            show cell
        ]
    ]
]
lo: layout [
    bx: box second span? pane
    text "Example: type '7' into A1, '19' into B1, '=a1 + b1' into C1" 
    text "Type 'formula' into any cell to edit an existing formula (C1)."
]
bx/pane: pane
view center-face lo

Процесс изучения языка программирования аналогичен изучению любого разговорного языка (английского, французского, испанского и т.д.). Если вы переезжаете из США в Испанию, вы можете быть уверены, что в течение года сможете адекватно говорить по-испански, даже если вам не будет предоставлено соответствующее структурированное обучение испанскому или руководство. Руководство, безусловно, помогает прояснить процесс, но ключевым важным компонентом является погружение. Погружение в код работает точно так же. Поначалу может быть болезненно и запутанно видеть или понимать совершенно чужой язык и среду, но если вы хотите научиться "говорить" на REBOL, необходимо сразу погрузиться в код. Запустите каждый пример в этом разделе и по ходу попробуйте изменить некоторые текстовые заголовки, метки кнопок, размеры текстовых полей и другие очевидные свойства, чтобы увидеть, как меняются программы. Привыкнуть к использованию интерпретатора REBOL, осознать, что примеры кода в этом тексте податливы, и открыть свой разум для перспективы и активности фактического набора кода REBOL - это важный первый шаг.

1.4 Основы кодирования REBOL

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

Каждая программа REBOL должна начинаться со следующего заголовка:

REBOL []

Функциональные слова выполняют действия над значениями данных. В следующих примерах функций отображаются некоторые значения данных (в данном случае текст) и запрашиваются полезные значения данных от пользователей (любой текст после точки с запятой в этих примерах является удобочитаемым "комментарием" и полностью игнорируется интерпретатором REBOL):

REBOL []
alert "Hello world"      ; "ALERT" это функциональное слово.
editor "Hello world"     ; "Hello world" это параметр - текстовые данные.
print "Hello world"      ; 
wait 2                   ; "Wait" это функция, "2" это данные.
request-date             ; Функции запрашивающие разные данные.
request-pass
request-text/title "What is your Name?"
request-file
request-list "Choose a color:" ["Red" "Green" "Blue"]
request ["Size:" "Small" "Medium" "Large"]
request-color

Обязательно вставьте КАЖДЫЙ пример кода в редактор REBOL и наблюдайте за выполнением каждой строки.

В REBOL вывод одной функции ("возвращаемое значение") может использоваться как вход ("аргумент" или "параметр") другой функции:

; Здесь функция "editor" редактирует любую дату, введённую пользователем:

editor request-date

; Здесь функция "alert" отображает любой текст, введённый пользователем:

alert request-text

В REBOL вы можете присвоить данные слову метки (также называемому "переменная"), используя символ двоеточия. После того, как данные присвоены словесной метке, вы можете использовать это слово где угодно, чтобы ссылаться на присвоенное значение:

REBOL []
balance: $53940.23 - $234
print balance
name: request-text/title "Name:"
print name
date: request-date
print date
alert "Click [OK] to continue"

Вы можете объединить или сцепить значения данных с помощью функции "rejoin". Попробуйте добавить эту строку в конец приведённой выше программы:

alert rejoin [name ", your balance on " date " is " balance]

В REBOL встроено множество полезных значений:

REBOL []
alert rejoin ["Right now the date and time is: " now]
alert rejoin ["The date is: " now/date]
alert rejoin ["The time is: " now/time]
alert rejoin ["The value of PI is " pi]
alert rejoin ["The months are " system/locale/months]
alert rejoin ["The days are " system/locale/days]

REBOL может выполнять полезные вычисления для многих типов значений:

REBOL []
alert rejoin ["5 + 7 = " 5 + 7]
alert rejoin ["Five days ago was " now/date - 5]
alert rejoin ["Five minutes ago was " now/time - 00:05]
alert rejoin ["Added coordinates: " 23x54 + 19x31]
alert rejoin ["Multiplied coordinates: " 22x66 * 2]
alert rejoin ["A multiplied coordinate matrix: " 22x66 * 2x3]
alert rejoin ["Added tuple values: " 192.168.1.1 + 0.0.0.37]
alert rejoin ["The RGB color value of purple - brown is: " purple - brown

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

1.5 Условные оценки

Условные оценки могут выполняться с использованием синтаксиса: "if (this is true) [do this]" ("если (это правда) [сделать это]":

REBOL []
if (now/time > 6:00am) [alert "It's time to get up!"]

Обратите внимание на естественную обработку значения времени в приведённом выше примере. Для использования этого значения не требуется специального форматирования или анализа. REBOL изначально "понимает", как выполнять соответствующие вычисления со временем и другими общими типами данных.

Используйте оценку "either" (либо), чтобы сделать одно действие, если условие истинно (true), и другое, если условие ложно (false):

REBOL []
user: "sa98df"
pass: "008uqwefbvuweq"
userpass: request-pass
either (userpass = reduce [user pass]) [
    alert rejoin ["Welcome back " user "!"]
][ 
    alert "Incorrect username/password combination"
]

В приведённом выше коде:

  1. Слово метка (переменная) "user" (пользователь) присваивается текстовому значению.
  2. Переменная pass присваивается некоторому тексту.
  3. У пользователя запрашивается комбинация имени пользователя и пароля, и результат этой функции помечается как "userpass".
  4. Для возвращаемого значения userpass выполняется условная оценка "either" (либо). Если значение userpass равно установленным переменным "user" и "pass", пользователь получает предупреждение с приветственным сообщением. В противном случае пользователь выдаёт предупреждение с сообщением об ошибке.

1.6 Ещё несколько полезных функций

Попробуйте вставить каждую отдельную строку ниже в консоль интерпретатора REBOL, чтобы увидеть, как каждую функцию можно использовать для выполнения полезных действий:

REBOL []
print read http://rebol.com  ; "read" извлекает данные из многих источников
editor http://rebol.com   ; встроенный редактор также может читать многие источники
print read %./          ; символ % используется для локальных файлов и папок
editor %./
write %temp.txt "test"  ; write принимает ДВА параметра (имя файла и данные для записи)
editor %temp.txt
editor request-file/only  ; уточнение "only" ограничивает выбор одним файлом
write clipboard:// (read http://rebol.com)  ; 2-й параметр в скобках
editor clipboard://
print read dns://msn.com  ; REBOL может читать многие встроенные протоколы
print read nntp://news.grc.com
write/binary %/c/bay.jpg (read/binary http://rebol.com/view/bay.jpg)
write/binary %tada.wav (read/binary %/c/windows/media/tada.wav)
write/binary %temp.dat (compress read http://rebol.com) ; СЖАТЬ ДАННЫЕ
print decompress read/binary %temp.dat  ; РАСЖАТЬ ДАННЫЕ
print read ftp://user:pass@website.com/name.txt    ; требуется пользователь/пароль
write ftp://user:pass@website.com/name.txt "text"  ; требуется пользователь/пароль
editor ftp://user:pass@website.com/name.txt  ; можно сохранять изменения на сервере!
editor pop://user:pass@website.com  ; читать все электронные письма в этом аккаунте
send user@website.com "Hello"  ; отправить электронное письмо
send user@website.com (read %file.txt)   ; отправить текст из этого файла по электронной почте
send/attach user@website.com "My photos" [%pic1.jpg %pic2.jpg pic3.jpg]
name: ask "Enter your name"  print name  ; отправить текст из этого файла по электронной почте

call/show "notepad.exe c:\config.sys"  ; запустить команду оболочки ОС
browse http://re-bol.com   ; открыть системный веб-браузер по умолчанию на странице
view layout [image %pic1.jpg]          ; просмотреть изображение
view layout [image request-file/only]  ; просмотреть изображение, выбранное пользователем
insert s: open sound:// load request-file/only wait s close s ; играть звук
insert s: open sound:// load %/c/windows/media/tada.wav wait s close s

rename %temp.txt %temp2.txt      ; изменить имя файла
write %temp.txt read %temp2.txt  ; копировать файл
write/append %temp2.txt ""  ; создать файл (или, если он существует, ничего не делать)
delete %temp2.txt
change-dir %../
what-dir 
list-dir
make-dir %./temp
print read %./

attempt [print 0 / 0]   ; проверять и обрабатывать ошибки
if error? try [print 0 / 0] [alert "*** ERROR:  divide by zero"]

parse "asdf#qwer#zxcv" "#"   ; разделить строки по набору символов
trim "  asdf89w   we   "     ;  удалить пробелы в начале и в конце
replace/all "xaxbxcxd" "x" "q"   ; заменить все вхождения "x" на "q"
checksum read %file.txt          ; вычислить контрольную сумму для проверки целостности
print dehex "a%20space"          ;  преобразовать из строки в кодировке URL
print to-url "a space"           ; преобразовать в строку с кодировкой URL
print detab "tab    separated"   ; конвертировать табуляции в пробелы
print enbase/base "a string" 64  ; преобразовать строку или бинарные данные в базу 64, 16, 2 
print encloak "my data" "my pass"  ; зашифровать и дешифровать данные (AES)
print decloak "µÜiûŽz®" "my pass"  ; fish и другие форматы также поддерживаются
read-cgi            ;аккуратно проанализировать все данные, отправленные из формы веб-страницы
for i 1 99 3 [print i]          ; считать от 1 до 99, с шагом 3

halt          ;  "HALT" останавливает закрытие консоли REBOL,
              ; так что вы можете увидеть распечатанные результаты.

Чтобы увидеть выполнение каждой из вышеперечисленных строк, вставьте их прямо в консоль REBOL, а не в редактор. При запуске кода непосредственно в консоли нет необходимости включать заголовок REBOL [] или функцию "halp" (остановкa):

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

  1. Чтение и запись данных в файлы на жестком диске, флэш-накопителе и т.д.
  2. Чтение/запись данных с/на веб-серверы и другие сетевые источники, системный буфер обмена, ввод данных пользователем и т.д.
  3. Чтение писем, отправка писем, отправка прикреплённых файлов по электронной почте.
  4. Отображение изображений.
  5. Воспроизведение звуков.
  6. Навигация и управление папками и файлами на компьютере.
  7. Сжатие, распаковка, шифрование и дешифрование данных.
  8. Запуск сторонних программ на компьютере.
  9. Чтение, синтаксический анализ и обратное преобразование между общими типами данных и значениями.

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

Вы можете получить список всех функциональных слов, набрав функцию "what" в консоли REBOL:

what                  ; нажмите клавишу [ESC]чтобы остановит вывод

Вы можете увидеть синтаксис, параметры и уточнения любой функции с помощью функции "help" (помощь):

help print
help prin
help read
help write

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

write %wordbrowser.r read http://re-bol.com/wordbrowser.r
do %wordbrowser.r

Научившись сочетать простые функции с условным оцениванием (if/then), а также с некоторыми методами обработки списков, вы сможете достичь действительно полезных программных целей, которые выходят далеко за рамки возможностей даже сложных программ офисного пакета (гораздо больше о обработке списка будет рассмотрено ниже).

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

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

2. Списки, таблицы и функция "Foreach" (по каждому)

2.1 Управление данными, подобными электронным таблицам

2.1.1 Предупреждение

ПРИМЕЧАНИЕ. Этот раздел руководства является самым длинным и сложным для понимания при первом чтении. Прочтите его один раз, чтобы познакомиться со всеми темами, и просмотрите структуры кода. Будьте готовы к этому - код станет сложным. Просто смеритесь, усвойте всё, что сможете, и продолжайте читать весь раздел. Позже вы вернётесь к нему более подробно, когда увидите, как все концепции, функции и шаблоны кода сочетаются друг с другом для создания полезных программ.

2.1.2 Блоки

Самые полезные бизнес-программы обрабатывают списки данных. Таблицы данных фактически обрабатываются программно как последовательные списки элементов. Список или "блок" данных создаётся в REBOL путём заключения значений в квадратные скобки:

REBOL []
names: ["Bob" "Tom" "Bill"]

Чтобы выполнить операцию/вычисление с/для каждого элемента данных в блоке, используйте функцию foreach. Синтаксис Foreach можно читать так: "foreach (именной_элемент) в (этом_именном_блоке) [выполнить эту операцию с/для каждого помеченного элемента]:

REBOL []
names: ["Bob" "Tom" "Bill"] ; создаём блок текстовых значений с именем "names"
foreach name names [print name]       ; печатаем каждое значение из блока names помещённое в name
halt

В этом примере печатается каждое значение, хранящееся во встроенном блоке "system/locale/months":

REBOL []
months: system/locale/months     ; устанавливаем переменную "months" к требуемому значению
foreach month months [print month]                ; печатаем значения каждого элемента помещённого в month
halt

Обратите внимание, что в приведённом выше примере переменные с именами "months" (месяцы) и "month" (месяц) можно заменить на любую другую желаемую, произвольно заданную метку:

REBOL []
foo: system/locale/months
foreach bar foo [print bar]                ; имена переменных произвольные
halt

Именование блока system/locale/months также не требуется. Без метки код короче, но, возможно, его немного сложнее читать:

REBOL []
foreach month system/locale/months [print month]
halt

Обучение чтению и мышлению в терминах "foreach item in list [do this to each item]" ("для каждого элемента в списке [сделайте это для каждого элемента]") - одна из наиболее важных фундаментальных концепций, которые необходимо усвоить в программировании. В этом тексте вы увидите множество повторяющихся примеров. Помните это каждый раз, когда вы видите слово "foreach".

Вы можете получить списки данных из множества различных источников. Обратите внимание, что функция "load" обычно используется для чтения списков данных. В этом примере печатаются файлы в текущей папке на жёстком диске:

REBOL []
folder: load %. 
foreach file folder [print file]
halt

В этом примере загружается список из файла, хранящегося на веб-сайте:

REBOL []
names: load http://re-bol.com/names.txt 
foreach name names [print name]     
halt

ПРИМЕЧАНИЕ: вы можете записать данные, необходимые для приведённого выше примера, на свой собственный веб-сервер, используя следующую строку кода. Обратите внимание, что функция "сохранить" обычно используется для записи списков данных:

REBOL []
save ftp://user:pass@site.com/folder/names.txt ["Bob" "Tom" "Bill"]

2.2 Некоторые простые алгоритмы списков (подсчёт, сумма, среднее, макс./мин.)

2.2.1 Подсчёт элементов

Функция "length?" подсчитывает количество элементов в списке:

REBOL []
receipts: [$5.23 $95.98 $7.46 $34]        ; список с пометкой "receipts" (квитанции)
alert rejoin ["There are " length? receipts " receipts in the list."]

Вы можете присвоить счётчику метку переменной и использовать значения позже:

REBOL []
month-count: length? system/locale/months 
day-count: length? system/locale/days
alert rejoin ["There are " month-count " months and " day-count " days."]

Другой способ подсчёта элементов в списке - создать переменную счётчика, изначально установленную на 0. Используйте цикл foreach для просмотра каждого элемента в списке и увеличения (прибавления 1) к переменной счётчика:

REBOL []
count: 0
receipts: [$5.23 $95.98 $7.46 $34]
foreach receipt receipts [count: count + 1]   ; увеличиваем  count на 1
alert rejoin ["There are " count " receipts in the list."]

Вот альтернативный синтаксис для увеличения переменной счётчика count:

REBOL [] count: 0 receipts: [$5.23 $95.98 $7.46 $34] foreach receipt receipts [++ count] ; увеличиваем count на 1 alert rejoin ["There are " count " receipts in the list."]

В этом примере подсчитывается количество месяцев в году и количество дней в неделе с использованием переменных счётчика:

REBOL []
month-count: 0
day-count: 0
foreach month system/locale/months [++ month-count] 
foreach day system/locale/days [++ day-count]
alert rejoin ["There are " month-count " months and " day-count " days."]

Переменные-счётчики особенно полезны, когда вы хотите подсчитать только определённые элементы в списке. В следующем примере подсчитываются только элементы с числовыми значениями:

REBOL []
count: 0
list: ["screws" 14 "nuts" 38 "bolts" 23]
foreach item list [
    ; Увеличивайте, только если тип элемента item целочисленный:
    if (type? item) = integer! [++ count]
]
alert rejoin ["The count of all number values in the list is: " count]

2.2.2 Суммы

Чтобы вычислить сумму чисел в списке, начните с присвоения переменной суммы 0. Затем используйте цикл foreach для увеличения суммы на каждое отдельное числовое значение. Этот пример начинается с присвоения метке "balance" (баланс) значению 0. Затем метка "receipts" (поступления) назначается списку денежных значений. Затем каждое значение в списке поступлений добавляется к балансу, и отображается этот подсчитанный баланс:

REBOL []
sum: 0        ; переменная суммы sum, изначально установленная на 0
receipts: [$5.23 $95.98 $7.46 $34]  ; Список с именем "receipts" (поступления)
foreach item receipts [sum: sum + item]         ; add them up
alert rejoin ["The sum of all receipts is: " sum]

Вы можете суммировать только те элементы в списке, которые содержат числовые значения, например, так:

REBOL []
sum: 0
list: ["screws" 14 "nuts" 38 "bolts" 23]
foreach item list [
    if (type? item) = integer! [            ; только если item целочисленное
        sum: sum + item                     ; добавляем item к сумме
    ]
]
alert rejoin ["The total of all number values in the list is: " sum]

2.2.3 Средние

Вычисление среднего значения элементов в списке - это просто деление суммы на количество:

REBOL []
sum: 0
receipts: [$5.23 $95.98 $7.46 $34]
foreach item receipts [sum: sum + item]
average: sum / (length? receipts)
alert rejoin ["The average balance of all receipts is: " average]

2.2.4 Максимум и минимум

REBOL имеет встроенные функции "maximum-of" (максимум-из) и "minimum-of" (минимум-из):

REBOL []
receipts: [$5.23 $95.98 $7.46 $34]
print first maximum-of receipts
print first minimum-of receipts
halt

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

REBOL []
highest: $0
receipts: [$5.23 $95.98 $7.46 $34]
foreach receipt receipts [
    if (receipt > highest) and (receipt < $50) [highest: receipt] 
]
alert rejoin ["Maximum receipt below fifty bucks: " highest]

2.2.5 Поиск

Функция "find" (найти) используется для выполнения простого поиска:

REBOL []
names: ["John" "Jane" "Bill" "Tom" "Jen" "Mike"]
if find names "Bill" [alert "Yes, Bill is in the list!"]
if not find names "Paul" [alert "No, Paul is not in the list."]

Вы можете определить позицию индекса найденного элемента в списке, используя функцию "index?":

REBOL []
names: ["John" "Jane" "Bill" "Tom" "Jen" "Mike"]
indx: index? find names "Bill"
print rejoin ["Bill is at position " indx " in the list."]
halt

Вы можете искать текст в каждом элементе списка, используя цикл foreach для поиска каждого отдельного значения:

REBOL []
names: ["John" "Jane" "Bill" "Tom" "Jen" "Mike"]
foreach name names [
    if find name "j" [
        print rejoin ["'j' found in " name]
    ]
]
halt

Уточнение "find/any" (найти/любой) можно использовать для поиска подстановочных знаков. Символ "*" позволяет частям поискового текста содержать случайные строки символов любой длины. Знак "?" символ позволяет выполнять поиск случайных символов заданной длины (в определённых позициях символа в поисковом запросе):

REBOL []
names: ["OJ" "John" "Joan" "Jan" "Major Bill" "MJO" "Mike"]
foreach name names [
    if find/any name "*jo*" [
        print rejoin ["'jo' found in " name]
    ]
]
print ""
foreach name names [
    if find/any name "j*n" [
        print rejoin ["'j*n' found in " name]
    ]
]
print ""
foreach name names [
    if find/any name "j??n" [
        print rejoin ["'j--n' found in " name]
    ]
]
halt

2.3 Сбор данных и функция "Copy"

При сборе ("агрегировании") значений в новый блок всегда используйте функцию "copy" (копировать) для создания нового блока. Вам нужно будет делать это всякий раз, когда создаётся подсписок или суперсписок значений на основе условных оценок, выполняемых для данных в базовом списке:

REBOL []
low-receipts: copy []             ; Создать пустой список с копией [], НЕ []
receipts: [$5.23 $95.98 $7.46 $34]
foreach receipt receipts [
    if receipt < $10 [append low-receipts receipt]     ; добавляем в пустой список
]
print low-receipts
halt

Например, НЕЛЬЗЯ использовать следующую строку (она не содержит слова "copy" при создании пустого списка):

low-receipts: []         ; НЕВЕРНО! - должно быть low-receipts: COPY []

То же самое верно и при создании значений пустой строки. Используйте функцию "copy" всякий раз, когда вы создаёте пустое текстовое значение, которое хотите изменить или добавить к:

REBOL []
names: copy {}                  ; Создали пустую строку с copy {}, НЕ {}
people: ["Joan" "George" "Phil" "Jane" "Peter" "Tom"]
foreach person people [
    if find person "e" [
        append names rejoin [person " "]    ; Это добавляется к пустой строке
    ]
]
print names
halt

2.4 Список функций сравнения

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

REBOL []
group1: ["Joan" "George" "Phil" "Jane" "Peter" "Tom"]
group2: ["Paul" "George" "Andy" "Mary" "Tom" "Tom"]
print rejoin ["Group 1: " group1]  
print ""   
print rejoin ["Group 2: " group2]
print newline
print rejoin ["Intersection:           " intersect group1 group2]
print "^/(values shared by both groups)^/^/"  
print rejoin ["Difference:             " difference group1 group2]
print "^/(values not shared by both groups)^/^/"
print rejoin ["Union:                  " union group1 group2]
print "^/(all unique values contained in both groups)^/^/"
print rejoin ["Join:                   " join group1 group2]
print "^/(one group tacked to the end of the other group)^/^/"
print rejoin ["Excluded from Group 2:  " exclude group1 group2]
print "^/(values contained in group1, but not contained in group2)^/^/"
print rejoin ["Unique in Group 2:      " unique group2]
print "^/(unique values contained in group2)"
halt

2.5 Создание списков на основе пользовательского ввода

2.5.1 Создание новых блоков и добавление значений

Вы можете создать новый блок, используя шаблон кода ниже. Просто назначьте метки переменных для "copy []":

REBOL []
items: copy []          ; новый пустой блок с именем "items"
prices: copy []         ; новый пустой блок с именем "prices"

Добавляйте значения в новые блоки с помощью функции "append" (добавить):

REBOL []
items: copy []
prices: copy []
append items "Screwdriver"
append prices "1.99"
append items "Hammer"
append prices "4.99"
append items "Wrench"
append prices "5.99"

Используйте функции "print", "probe" или "editor" для просмотра данных в блоке. Функция "print" просто печатает значения в блоке. Функция "probe" показывает структуру данных блока (квадратные скобки, заключающие значения, кавычки вокруг значений текстовой строки и т.д.). Функция "editor" открывает встроенный текстовый редактор REBOL с отображаемой структурой блока:

REBOL []
items: copy []
prices: copy []
append items "Screwdriver"
append prices "1.99"
append items "Hammer"
append prices "4.99"
append items "Wrench"
append prices "5.99"

editor items
editor prices

print rejoin ["ITEMS:   " items newline]
print rejoin ["PRICES:  " prices newline]

probe items
probe prices

halt

2.5.2 Принятие данных введённых пользователем

Вы уже познакомились с функцией "request-text" (запросит-текст). Она принимает ввод текста от пользователя:

REBOL []
request-text

Вы можете присвоить метку переменной данным, введённым пользователем, а затем использовать эти данные позже в своей программе:

REBOL []
price: request-text
alert price

Вы можете добавить текстовый заголовок к функции запроса, для чего воспользуйтесь уточнением "/title":

REBOL []
price: request-text/title "Input a dollar value:"
alert price

Вы можете добавить текстовый ответ по умолчанию, используя уточнение "/default":

REBOL []
price: request-text/default "14.99"
alert price

Вы можете комбинировать уточнения "/title" и "/default":

REBOL []
price: request-text/title/default "Input a dollar value:" "14.99"
alert price

Функция "ask" делает то же самое, но в текстовой среде консоли интерпретатора REBOL (вместо использования всплывающего оконного запросчика):

REBOL []
price: ask "Input a dollar value:  $"
alert price

2.5.3 Строительные блоки из данных, вводимых пользователем

Добавьте данные в блок, который был введён пользователем, используя шаблон кода ниже. Добавьте метку переменной введённых данных к метке блока:

REBOL []
items: copy []
prices: copy []
item: request-text/title/default "Item:" "screwdriver"
price: request-text/title/default "Price:" "1.99"
append items item
append prices price

В приведённом ниже примере используется цикл "forever" (навсегда) для многократного выполнения операций "request-text" (запрос-текст) и "append" (добавление). Условная оценка "if" (если) проверяет, вводит ли пользователь "" (пустой текст) в запросчик элемента. Если это так, он останавливает цикл, используя функцию "break" (прервать), и отображает данные в каждом блоке:

REBOL []
items: copy []
prices: copy []
forever [
    item: request-text/title "Item:"
    if item = "" [break]
    price: request-text/title "Price:"
    append items item
    append prices price
]
print "Items:^/"               ; Символы ^/ обозначают новую строку
probe items
print "^/^/Prices:^/"
probe prices
halt

Вы можете так же легко добавить введённые данные в один блок:

REBOL []
inventory: copy []
forever [
    item: request-text/title "Item:"
    if item = "" [break]
    price: request-text/title "Price:"
    append inventory item
    append inventory price
]
print "Inventory:^/"
probe inventory
halt

2.6 Сохранение и чтение данных блока в/из файла

Сохраните блок в текстовый файл с помощью функции "save" (сохранить). Помните, что в REBOL имена файлов всегда начинаются с символа "%":

REBOl []
inventory: ["Screwdriver" "1.99" "Hammer" "4.99" "Wrench" "5.99"]
save %inv.txt inventory
alert "Saved"

Загрузите заблокированные данные из сохранённого файла с помощью функции "load" (загрузка). Вы можете присвоить загруженным данным метку переменной, чтобы использовать её позже в программе:

REBOL []
inventory: load %inv.txt
print "Inventory^/"
probe inventory
halt

Вы также можете добавлять данные непосредственно в файл с помощью функции "write/append" (запись/добавление). При использовании функции "write/append" используйте функцию "mold" (пресс-форма), чтобы заключить каждое текстовое значение в кавычки, и функцию "rejoin" (воссоединить), чтобы отделить каждое значение пробелом:

REBOL []
forever [
    item: request-text/title "Item:"
    if item = "" [break]
    price: request-text/title "Price:"
    write/append %inv.txt rejoin [
        mold item " " mold price " "
    ]
]
inventory: load %inv.txt
print "Inventory:^/"
probe inventory
halt

2.7 Три полезные программы хранения данных: инвентаризация, контакты, расписание

Последняя программа, представленная выше, представляет собой хороший шаблон для практических приложений всех типов. Он хранит и отображает инвентарь и цены. Обратите внимание, что в заголовок была добавлена переменная "title" с текстом "Inventory". Хорошая практика - присваивать названия всем вашим программам:

REBOL [title: "Inventory"]
forever [
    item: request-text/title "Item:"
    if item = "" [break]
    price: request-text/title "Price:"
    write/append %inv.txt rejoin [
        mold item " " mold price " "
    ]
]
inventory: load %inv.txt
print "Inventory:^/"
probe inventory
halt

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

REBOL [title: "Contacts"]
forever [
    name: request-text/title "Name:"
    if name = "" [break]
    address: request-text/title "Address:"
    phone: request-text/title "Phone:"
    write/append %contacts.txt rejoin [
        mold name " " mold address " " mold phone " "
    ]
]
contacts: load %contacts.txt
print "Contacts:^/"
probe contacts
halt

Здесь он снова используется для хранения информации о расписании:

REBOL [title: "Schedule"]
forever [
    event: request-text/title/default "Event Title:" "Meeting with "
    if event = "" [break]
    date: request-text/title/default "Date:" "1-jan-2013"
    time: request-text/title/default "Time:" "12:00pm"
    notes: request-text/title/default "Notes:" "Bring: "
    write/append %schedule.txt rejoin [
        mold event " " mold date " " mold time " " mold notes " "
    ]
]
schedule: load %schedule.txt
print "Schedule:^/^/"
probe schedule
halt

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

2.8 Работа с таблицами данных: столбцы и строки

Столбцы в таблицах данных расположены в блоках в последовательном порядке. Отступы и пробелы помогают аккуратно отображать столбцы в визуальном макете "таблицы". Следующая таблица концептуально содержит 3 строки по 3 столбца данных, но весь блок по-прежнему представляет собой последовательный список из 9 элементов:

accounts:  [
    "Bob"   $529.23   21-jan-2013
    "Tom"   $691.37   13-jan-2013
    "Ann"   $928.85   19-jan-2013
]

Функция foreach в следующем примере предупреждает пользователя каждые три последовательных значения данных в таблице (каждая строка из 3 последовательных значений столбцов имени, баланса и даты):

REBOL []
accounts:  [
    "Bob" $529.23 21-jan-2013
    "Tom" $691.37 13-jan-2013
    "Ann" $928.85 19-jan-2013
]
foreach [name balance date] accounts [
    alert rejoin [
        "Name: " name ", Date: " date ", Balance: " balance
    ]
]

В этом примере отображается расчётный баланс для каждого человека на заданную дату. Отображаемая сумма представляет собой указанное значение "balance" (баланса) для каждой учётной записи за вычетом универсального значения "fee" (комиссия)):

REBOL []
accounts:  [
    "Bob" $529.23 21-jan-2013
    "Tom" $691.37 13-jan-2013
    "Ann" $928.85 19-jan-2013
]
fee: $5
foreach [name balance date] accounts [
    alert rejoin [name "'s balance on " date " will be " balance - fee]
]

Вот вариант приведённого выше примера, который отображает сумму значений во всех учётных записях:

REBOL []
accounts:  [
    "Bob" $529.23 21-jan-2013
    "Tom" $691.37 13-jan-2013
    "Ann" $928.85 19-jan-2013
]
sum: $0
foreach [name balance date] accounts [sum: sum + balance]
alert rejoin ["The total of all balances is: " sum]

Вот вариант, который вычисляет средний баланс:

REBOL []
accounts:  [
    "Bob" $529.23 21-jan-2013
    "Tom" $691.37 13-jan-2013
    "Ann" $928.85 19-jan-2013
]
sum: $0
foreach [name balance date] accounts [sum: sum + balance]
alert rejoin [
    "The average of all balances is: " 
    sum / ((length? accounts) / 3)
]

Вот вариант приложения "Расписание" из предыдущего раздела, немного изменённый с помощью функции "foreach" для форматирования более чётко распечатанных данных:

REBOL []
forever [
    event: request-text/title "Event Title:"
    if event = "" [break]
    date: request-text/title/default "Date:" "1-jan-2013"
    time: request-text/title/default "Time:" "12:00pm"
    notes: request-text/title/default "Notes:" "Bring: "
    write/append %schedule.txt rejoin [
        mold event " " mold date " " mold time " " mold notes " "
    ]
]
schedule: load %schedule.txt
print newpage                     ; "newpage" печатает очищенный экран
print "SCHEDULE:^/^/"
foreach [event date time notes] schedule [
    print rejoin [
        "Event:  " event newline
        "Date:   " date newline
        "Time:   " time newline
        "Notes:  " notes newline newline
    ]
]
halt

Вот программа "Инвентаризация" из предыдущего раздела, немного скорректированная для подсчёта количества предметов и расчёта суммы цен на инвентарь:

REBOL [title: "Inventory"]
forever [
    item: request-text/title "Item:"
    if item = "" [break]
    price: request-text/title "Price:"
    write/append %inv.txt rejoin [
        mold item " " mold price " "
    ]
]
inventory: load %inv.txt
count: 0
sum: $0
foreach [item price] inventory [
    count: count + 1
    sum: sum + to-money price
]
print newpage
print rejoin ["Total # of Items:  " count]
print rejoin ["Sum of Prices:     " sum]
halt

Вот вариант приложения "Контакты", которое ищет сохранённые имена и распечатывает любую совпадающую контактную информацию:

REBOL []
search: request-text/title/default "Search text:" "John"
contacts: load %contacts.txt
print newpage
print rejoin [search " found in:^/"]
foreach [name address phone] contacts [
    if find name search [
        print rejoin [
            "Name:      " name newline
            "Address:   " address newline
            "Phone:     " phone newline
        ]
    ]
]
halt

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

2.9 Дополнительные функции и методы работы со списком/блоком/серией

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

REBOL []

names: ["John" "Jane" "Bill" "Tom" "Jen" "Mike"]  ; список текстовых строк

print "Two ways of printing values, 'probe' and 'print':"
probe names   ; "Probe" похож на "print", но он показывает фактические состав данных
print names   ; "Print" пытается отформатировать отображаемые данные.

print "^/Sorting:"
sorted: sort copy names     ; "Sort" сортирует значения по 
                            ; возрастанию или убыванию.
probe names                 ; "Copy" предотвращает изменение блока names 
print sorted
sort/reverse names         ; Здесь блок имен отсортирован без
probe names                ; copy, так что он навсегда изменился.

print "^/Picking items:"
probe first names          ; 3 разные способы выбора 1-го предмета:
probe names/1 
probe pick names 1 
probe second names         ; 3 разные способы выбора 2-го предмета:
probe names/2
probe pick names 2

print "^/Searching:"
probe find names "John"           ;Как искать в блоке
probe first find names "John"
probe find/last names "Jane"
probe select names "John"         ; Найти следующее значение "John"

print "^/Taking sections of a series:"
probe at names 2
probe skip names 2                ; Пропускать каждые два пункта
probe extract names 3             ; Соберите каждый третий пункт

print "^/Making changes:"
append names "George"
probe names
insert (at names 3) "Lee" 
probe names
remove names
probe names 
remove find names "Mike" 
probe names
change names "Phil" 
probe names
change third names "Phil"
probe names
poke names 3 "Phil" 
probe names
probe copy/part names 2 
replace/all names "Phil" "Al"
probe names

print "^/Skipping around:"
probe head names 
probe next names 
probe back names 
probe last names 
probe tail names  
probe index? names 

print "^/Converting series blocks to strings of text:"
probe form names
probe mold names

print "^/Other Series functions:"
print length? names 
probe reverse names 
probe clear names
print empty? names
halt

Чтобы продемонстрировать лишь некоторые из вышеперечисленных функций, вот несколько практических примеров общих операций со списком, выполняемых с блоком контактной информации пользователя. Демонстрационный блок данных организован в виде 5 строк по 3 столбца данных (имя, адрес, телефон) или 15 последовательных элементов в списке с именем "users" (пользователи). Обратите внимание, что для сохранения структуры столбцов и строк пустые строки ("") помещаются в позиции в списке, где нет данных:

REBOL []
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]

append users ["Joe Thomas" "" "555-321-7654"]   ; добавить в конец 
                                                ; списка пользователей
probe users

probe (at users 4)                    ; круглые скобки не требуются

insert (at users 4) [
    "Tom Adams" "321 Way Lane Villageville, AZ" "555-987-6543"
]
probe users

remove (at users 4)                              ; удалить 1 элемент
probe users

; БУДЬТЕ ОСТОРОЖНЫ - строка выше разрушает структуру таблицы, удаляя
; элемент целиком, поэтому все остальные элементы данных переводятся 
; в неправильные столбцы. Вместо этого либо замените данные пустым 
; заполнителем или удалите также поля адреса и телефона:

remove/part (at users 4) 2                       ; удалить 2 элемент
probe users

change (at users 1) "Jonathan Smith"
probe users

remove (at users 1) insert (at users 1) "Jonathan Smith"
probe users
halt

Функция "extract" полезна для выделения столбцов данных из структурированных блоков:

REBOL []
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
] 
probe extract users 3             ; имена
probe extract (at users 2) 3      ; адреса
probe extract (at users 3) 3      ; телефоны
halt

Вы можете "pick" (выбирать) элементы в определённом индексном месте в списке:

REBOL []
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]
print pick users 1                  ; ПЕРВОЕ имя
print pick users 2                  ; ПЕРВЫЙ адрес
print pick users 3                  ; ПЕРВЫЙ телефон
;
print pick users 4                  ; ВТОРОЕ имя
print pick users 5                  ; ВТОРОЙ адрес
print pick users 6                  ; ВТОРОЙ телефон
;
indx: length? users                 ; позиция индекса ПОСЛЕДНЕГО элемента
print pick users indx               ; последний элемент
print pick users (indx - 1)         ; предпослдедний элемент
print pick users (random length? users)  ; случайный элемент
halt

Вы можете определить положение индекса, в котором найден элемент, с помощью функции "find" (найти):

indx: index? find users "John Smith"

В REBOL есть 4 способа выбрать элементы с таким переменным индексом. Каждый приведённый ниже синтаксис выполняет одно и то же. Это всего лишь варианты синтаксиса "pick":

print pick users indx
print users/:indx
print compose [users/(indx)]  ; заключить составные значения в круглые скобки
print reduce ['users/(indx)]  ; поставить галочку на неуменьшенных значениях

Обратите особое внимание на функции "compose" (составить) и "reduce" (уменьшить). Они позволяют преобразовывать статические слова в блоках в оцениваемые значения:

REBOL []

; В этом примере "[month]" печатается 12 раз: 

foreach month system/locale/months [
    probe [month]
]

; В этих примерах печатаются все значения за 12 месяцев:
foreach month system/locale/months [
    probe reduce [month]
]

foreach month system/locale/months [
    probe compose [(month)]
]

Вот полный пример, который запрашивает имя у пользователя, находит индекс этого имени в списке и выбирает имя, адрес и данные телефона для этого пользователя (расположенные в найденных позициях indx, indx + 1 и indx + 2):

REBOL [title: "Search Users"]
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]

name: request-text/title/default "Name:" "Jim Persee"
indx: index? find users name

print rejoin [
    (pick users indx) newline
    (pick users (indx + 1)) newline
    (pick users (indx + 2)) newline
]
halt

Вот версия, в которой используется код из программы "Контакты", которую вы видели ранее. Это позволяет вам создать свою собственную базу данных пользователей, а затем искать и отображать записи с помощью приведённого выше кода:

REBOL [title: "Search My Stored Contacts"]

; Этот код заимствован из программы "Контакты", которую мы видели ранее:

forever [
    name: request-text/title "Name:"
    if name = "" [break]
    address: request-text/title "Address:"
    phone: request-text/title "Phone:"
    write/append %contacts.txt rejoin [
        mold name " " mold address " " mold phone " "
    ]
]
users: load %contacts.txt

; Это вариант приведённого выше кода, который добавляет проверку ошибок в
; предоставить ответ, если искомый текст не найден в блоке данных:

name: request-text/title/default "Search For:" "Jim Persee"
if error? try [indx: index? find users name] [
    alert "Name not found" quit
]
print rejoin [
    (pick users indx) newline
    (pick users (indx + 1)) newline
    (pick users (indx + 2)) newline
]
halt

2.10 Сортировка списков и таблиц данных

Вы можете отсортировать список данных с помощью функции "sort" (сортировки):

REBOL []
print sort system/locale/months
halt

В этом примере отображается запросчик списка с отсортированными в алфавитном порядке месяцами:

REBOL []
request-list "Sorted months:" sort system/locale/months

Если вы отсортируете блок значений, состоящий из типов данных, которые понимает REBOL, значения будут отсортированы в соответствии с их типом (то есть в хронологическом порядке по датам и времени, в числовом порядке для чисел, в алфавитном порядке для текстовых строк):

REBOL []
probe sort [1 11 111 2 22 222 8 9 5]  ; сортировка ЧИСЛЕННО
probe sort ["1" "11" "111" "2" "22" "222" "8" "9" "5"]  ; сортировка БУКВЕТНО
probe sort [1-jan-2012 1-feb-2012 1-feb-2011]  ; сортировка ХРОНОЛОГИЧЕСКИ
halt

Чтобы отсортировать по первому столбцу в таблице, используйте уточнение "sort/skip" (сортировать/пропустить). Приведённая ниже таблица состоит из 5 строк по 3 концептуальных столбца, поэтому первый элемент каждой строки можно найти, пропуская каждые 3 значения:

REBOL []
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]
editor sort/skip users 3

Сортировка по любому другому выбранному столбцу требует реструктуризации данных в блоки блоков, которые чётко определяют структуру столбца. Например, эта "плоская" таблица, хотя и визуально понятна, на самом деле представляет собой последовательный список из 15 элементов данных:

users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]

Чтобы отсортировать его по столбцу, данные должны быть представлены следующим образом (обратите внимание, что концептуальные строки теперь разделены на дискретные блоки из 3 столбцов данных):

blocked-users: [
    ["John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"]
    ["Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"]
    ["Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"]
    ["George Jones" "456 Topforge Court Mountain Creek, CO" ""]
    ["Tim Paulson" "" "555-5678"]
]

Следующий код демонстрирует, как преобразовать плоский блок в такую структуру вложенных блоков строк/столбцов:

REBOL []
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]
blocked-users: copy []
foreach [name address phone] users [
    ; APPEND / ONLY вставляет блоки как блоки, а не как отдельные элементы
    ; Функция REDUCE (УМЕНЬШИТЬ) преобразует слова "name", "address", и "phone"
    ; к текстовым значениям: 
    append/only blocked-users reduce [name address phone]
]
editor blocked-users

Теперь вы можете использовать уточнение "/compare" функции сортировки для сортировки по выбранному столбцу (полю):

REBOL []
blocked-users: [
    ["John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"]
    ["Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"]
    ["Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"]
    ["George Jones" "456 Topforge Court Mountain Creek, CO" ""]
    ["Tim Paulson" "" "555-5678"]
]
field: 2              ; столбец для сортировки (в данном случае адрес)
sort/compare blocked-users func [a b] [(at a field) < (at b field)]
editor blocked-users  ; отсортировано по 2-му полю (по адресу)

Для сортировки в обратном направлении (т.е. по убыванию, а не по возрастанию) просто измените оператор "<" на ">":

REBOL []
blocked-users: [
    ["John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"]
    ["Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"]
    ["Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"]
    ["George Jones" "456 Topforge Court Mountain Creek, CO" ""]
    ["Tim Paulson" "" "555-5678"]
]
field: 2
sort/compare blocked-users func [a b] [(at a field) > (at b field)]
editor blocked-users

Вот полный пример, который преобразует плоский блок данных во вложенный блок блоков, а затем сортирует его по выбранному пользователем полю в выбранном восходящем/нисходящем направлении:

REBOL [title: "View Sorted Users"]
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]
blocked-users: copy []
foreach [name address phone] users [
    append/only blocked-users reduce [name address phone]
]
field: to-integer request-list "Choose Field To Sort By:" ["1" "2" "3"]
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
    sort/compare blocked-users func [a b] [(at a field) < (at b field)]
][
    sort/compare blocked-users func [a b] [(at a field) > (at b field)]
]
editor blocked-users

Вот версия вышеприведённой программы, в которой используется код из представленного ранее приложения "Контакты", который позволяет вам вводить свои собственные контактные данные "пользователей", а затем сортировать и отображать их, как указано выше:

REBOL [title: "Sort My Stored Contacts"]

; Этот код заимствован из программы "Контакты", которую мы видели ранее: 

forever [
    name: request-text/title "Name:"
    if name = "" [break]
    address: request-text/title "Address:"
    phone: request-text/title "Phone:"
    write/append %contacts.txt rejoin [
        mold name " " mold address " " mold phone " "
    ]
]
users: load %contacts.txt

; Это вариант приведенного выше кода:

blocked-users: copy []
foreach [name address phone] users [
    append/only blocked-users reduce [name address phone]
]
field-name: request-list "Choose Field To Sort By:" [
    "Name" "Address" "Phone"
]

; Функция "select" выбирает следующее значение в списке, выбранном
; пользователем. В этом случае, если переменная имени поля равна 
; "name", тогда переменная "field" установлена в 1. Если переменная 
; имени поля равна "address", для переменной "field" установлено 
; значение 2. Если field-name = "phone", Переменная "field" 
; установлена на 3:

field: select ["name" 1 "address" 2 "phone" 3] field-name

order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
    sort/compare blocked-users func [a b] [(at a field) < (at b field)]
][
    sort/compare blocked-users func [a b] [(at a field) > (at b field)]
]
editor blocked-users

Ещё раз обратите внимание, что REBOL правильно сортирует данные по типу. Если числа, даты, время и другие распознанные типы данных хранятся в виде строковых значений, сортировка будет осуществляться в алфавитном порядке для выбранного поля (потому что это подходящий порядок сортировки для текста):

REBOL []
text-data: [
    "1"    "1-feb-2012"  "1:00am"   "abcd"
    "11"   "1-mar-2012"  "1:00pm"   "bcde"
    "111"  "1-feb-2013"  "11:00am"  "cdef"
    "2"    "1-mar-2013"  "13:00"    "defg"
    "22"   "2-feb-2012"  "9:00am"   "efgh"
    "222"  "2-feb-2009"  "11:00pm"  "fghi"
]
blocked: copy []
foreach [number date time string] text-data [
    append/only blocked reduce [number date time string]
]
field-name: request-list "Choose Field To Sort By:" [
    "number" "date" "time" "string"
]
field: select ["number" 1 "date" 2 "time" 3 "string" 4] field-name
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
    sort/compare blocked func [a b] [(at a field) < (at b field)]
][
    sort/compare blocked func [a b] [(at a field) > (at b field)]
]
editor blocked

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

REBOL []
text-data: [
    "1"    "1-feb-2012"  "1:00am"   "abcd"
    "11"   "1-mar-2012"  "1:00pm"   "bcde"
    "111"  "1-feb-2013"  "11:00am"  "cdef"
    "2"    "1-mar-2013"  "13:00"    "defg"
    "22"   "2-feb-2012"  "9:00am"   "efgh"
    "222"  "2-feb-2009"  "11:00pm"  "fghi"
]
blocked: copy []
foreach [number date time string] text-data [
    append/only blocked reduce [
        to-integer number
        to-date date
        to-time time
        string
    ]
]
field-name: request-list "Choose Field To Sort By:" [
    "number" "date" "time" "string"
]
field: select ["number" 1 "date" 2 "time" 3 "string" 4] field-name
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
    sort/compare blocked func [a b] [(at a field) < (at b field)]
][
    sort/compare blocked func [a b] [(at a field) > (at b field)]
]
editor blocked

2.11 Файлы CSV и функция "синтаксического анализа" parse

Файлы "значения, разделённые запятыми" "Comma Separated Value" (CSV) - это универсальный текстовый формат, используемый для хранения и передачи таблиц данных. Электронные таблицы, системы баз данных, финансовое программное обеспечение и другие бизнес-приложения обычно могут экспортировать и импортировать табличные данные в формат CSV и обратно.

В файлах CSV строки данных разделяются разрывом строки. Значения столбцов чаще всего заключаются в кавычки и разделяются запятой или другим символом "разделителя" (иногда табуляцией, вертикальной чертой (|) или другим символом, который визуально разделяет значения).

2.11.1 Сохранение блоков табличных данных в CSV файлы

Вы можете создать файл CSV из блока данных таблицы REBOL, используя функцию "foreach". Просто объедините каждое формованное значение (значение, заключённое в кавычки) с запятыми, разделяющими каждый элемент, и новой строкой после каждой строки в длинную текстовую строку. Затем сохраните строку в файл с расширением ".csv":

REBOL [title: "Save CSV"]
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]
foreach [name address phone] users [
    write/append %users.csv rejoin [
        mold name ", " mold address ", " mold phone newline
    ]
]

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

2.11.2 Загрузка блоков табличных данных из CSV файлов

Чтобы импортировать файлы CSV в REBOL, используйте функцию "read/lines" (чтение/строки), чтобы прочитать содержимое файла, с одной текстовой строкой на элемент, сохранённый в результирующем блоке. Назначьте результату функции "read/lines" метку переменной. Используйте "foreach" и функцию "parse" REBOL, чтобы разделить каждый элемент в строках на индивидуальные значения. Соберите все полученные последовательные значения в пустой блок, и вы готовы использовать данные всеми способами, которые вы видели до сих пор:

REBOL [title: "Load CSV - Flat"]
block: copy []
csv: read/lines %users.csv
foreach line csv [
    data: parse line ","
    append block data
]
probe block
foreach [name address phone] block [
    alert rejoin [name ": " address " " phone]
]
halt

Первый параметр функции синтаксического анализа "parse" - это данные для анализа (в приведённом выше случае - каждая строка (line) файла CSV). Второй параметр - это символы-разделители, используемые для разделения каждого значения. Присвойте переменную выходу функции "parse", и вы можете ссылаться на каждое отдельное значение по мере необходимости (используя "pick" и другие последовательные функции). Приведённый выше код создаёт "плоский" блок. Чтобы создать блок блоков, в котором каждая строка файла CSV выделена в отдельный внутренний (вложенный) блок, просто используйте функцию append/only, как вы видели ранее:

REBOL [title: "Load CSV - Block of Blocks"]
block: copy []
csv: read/lines %users.csv
foreach line csv [
    data: parse line ","
    append/only block data
]
probe block
foreach line block [probe line]
halt

Уточнение Parse "/all" можно использовать для управления обработкой пробелов и других символов в процессе разделения текста (например, если вы хотите разделить данные запятыми, содержащимися в каждой текстовой строке в кавычках). Вы можете использовать функцию "trim" (обрезка), чтобы удалить лишние пробелы в значениях. Другие функции, такие как "replace" (замена), "to-(значение)" (в-значение) и условные оценки, например, могут быть полезны при преобразовании, исключении и иной обработке импортированных данных CSV.

Попробуйте загрузить данные учётной записи из Paypal или экспортировать значения отчётов из своего финансового программного обеспечения, и вы, вероятно, увидите, что наиболее распространённым форматом является CSV. Бухгалтеры и другие лица, использующие электронные таблицы для вычисления чисел, смогут мгновенно использовать файлы CSV в Excel и/или экспортировать данные рабочих таблиц в формат CSV, чтобы вы могли импортировать и использовать их в программах REBOL.

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

2.12 Две программы отчётов Paypal, Анализ

Взгляните на примеры кода Paypal, которые вы видели в этом тексте. Теперь вы сможете немного разобрать код:

REBOL [title: "Paypal Report"]

; Переменная, используемая для вычисления суммы, изначально 
; установлена на ноль долларов:

sum: $0

; Цикл foreach проходит через каждую строку в загруженном файле CSV,
; начиная со второй строки (первая строка содержит названия столбцов):
foreach line (at (read/lines http://re-bol.com/Download.csv) 2) [

    ; Сумма рассчитывается с использованием денежного значения в столбце 8:

    sum: sum + to-money pick (parse/all line ",") 8

]

; Пользователь получает предупреждение с итоговым значением:

alert form sum

Вот вся программа без комментариев:

REBOL [title: "Paypal Report"]
sum: $0
foreach line (at (read/lines http://re-bol.com/Download.csv) 2) [
    sum: sum + to-money pick (parse/all line ",") 8
]
alert form sum

Этот пример имеет дело с несколькими разными столбцами и выполняет условные оценки для полей имени и времени:

REBOL [title: "Paypal Reports"]

; Переменные, используемые для вычисления двух разных сумм, 
; изначально равны 0 долларов:

sum1: sum2: $0

; Цикл foreach проходит через каждую строку в загруженном файле CSV,
; начиная со второй строки (первая строка содержит названия столбцов):

foreach line at read/lines http://re-bol.com/Download.csv 2 [

    ; Первая сумма вычисляется с использованием денежного значения в столбце 8:

    sum1: sum1 + to-money pick row: parse/all line "," 8

    ; Если столбец имени (col # 4) содержит текст "Saoud", выведите
    ; составное сообщение. Текст сообщения состоит из даты
    ; (столбец 1), символы ", Сауд Горн: " и денежное значение 
    ; из столбца 8: 

    if find row/4 "Saoud" [print rejoin [row/1 ", Saoud Gorn: " row/8]]

    ; Если в столбце имени указано "Ourliptef.com", выполняем
    ; дополнительную условную оценку, проверяющая, соответствует ли 
    ; значение поля времени (столбец 2) - с полуночи до полудня. 
    ; Если да, прибавляем к sum2 денежное значение из столбца 8:
if find row/4 "Ourliptef.com" [
    time: to-time row/2
    if (time >= 0:00am) and (time <= 12:00pm) [
        sum2: sum2 + to-money row/8
    ]
]

]

; Информируем пользователя несколькими связанными сообщениями, отображающими суммы:

alert join "GROSS ACCOUNT TRANSACTIONS: " sum1
alert join "2012 Ourliptef.com Morning Total: " sum2

Вот вся программа без комментариев:

REBOL [title: "Paypal Reports"]
sum1: sum2: $0
foreach line at read/lines http://re-bol.com/Download.csv 2 [
    sum1: sum1 + to-money pick row: parse/all line "," 8
    if find row/4 "Saoud" [print rejoin [row/1 ", Saoud Gorn: " row/8]]
    if find row/4 "Ourliptef.com" [
        time: to-time row/2
        if (time >= 0:00am) and (time <= 12:00pm) [
            sum2: sum2 + to-money row/8
        ]
    ]
]
alert join "GROSS ACCOUNT TRANSACTIONS: " sum1
alert join "2012 Ourliptef.com Morning Total: " sum2

Чтобы увидеть данные, которые сортируют эти сценарии, взгляните на необработанные данные в файле Download.csv.

2.13 Некоторые перспективы изучения этих тем

Темы "Список" и "Табличные данные" - самые сложные разделы в первой половине учебного пособия. Скорее всего, для полного понимания им потребуется несколько раз их перечитать. Начните с беглого обзора и ознакомьтесь с основными языковыми структурами и общими шаблонами кода. Во время первого чтения вы должны знать, что продемонстрированные функции и фрагменты кода можно просто скопировать, изменить и вставить для использования в других приложениях. Вам не нужно запоминать или даже полностью понимать, как работает каждая строчка кода. Вместо этого более важно понимать, что функции и концепции блоков/таблиц, содержащиеся здесь, просто существуют и дают описанные результаты. Вы узнаете и усвоите детали только наизусть, в течение длительного периода чтения, изучения, копирования, изменения и, в конечном итоге, написания своих программ. Здесь много материала для обдумывания. Скорее всего, потребуется много часов практического программирования, чтобы полностью понять все это.

Вы можете найти небольшие фрагменты кода, которые предоставляют решения для первоначальных проблем и любопытства, которые побудили вас "научиться программировать". Уделите дополнительное время экспериментам с теми фрагментами кода, которые наиболее интересны и соответствуют вашим непосредственным потребностям. Скопируйте, вставьте и запустите примеры в интерпретаторе REBOL. Привыкайте к редактированию и изменению фрагментов кода, чтобы лучше познакомиться с синтаксисом. Измените метки переменных, введите новые значения данных и попробуйте перепрофилировать примеры кода, чтобы они соответствовали новым наборам данных. Попробуйте сломать рабочий код и придумайте, как это исправить. Привыкайте ИСПОЛЬЗОВАТЬ текстовый редактор REBOL и консоль интерпретатора. Пачкайте руки и погрузитесь в механику набора и запуска кода. Удобство работы с набором инструментов и рабочей средой - огромная часть битвы.

Вы выучите REBOL и все другие языки программирования точно так же, как вы изучаете любой разговорный язык: "говоря" на нем. Вы должны сначала имитировать (копировать/вставить код), а затем научиться составлять "фразы", ​​которые имеют смысл (редактировать, экспериментировать и переставлять слова), и в конечном итоге плавно писать большие композиции. Вы будете регулярно делать ошибки с синтаксисом функций, когда учитесь программировать, точно так же, как дети делают ошибки с грамматикой, когда учатся говорить. Вы научитесь, только творчески экспериментируя и преодолевая ошибки.

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

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

Для получения дополнительной информации об использовании списков и таблиц данных в REBOL см. http://www.rebol.com/docs/core23/rebolcore-6.html.

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

Вы уже видели, как можно использовать ряд функций для отображения и запроса информации от пользователя (print (печать), request-text (запрос-текст), request-list (запрос-список), editor (редактор) и т.д.). Для простых утилит этих функций ввода/вывода часто бывает достаточно, для создания функциональных сценариев. Для создания более сложных программ, которые позволяют как вводить все более сложные данные, так и повышать простоту использования, обычно используются "GUI" или Graphic User Interfaces (Графический Пользовательский Интерфейс). Графические интерфейсы пользователя - это окна, формы и экраны ввода данных, которые обычно содержат "виджеты" (элементы), такие как текстовые поля, кнопки, выпадающие списки, многострочные текстовые области, сетки данных, меню и другие узнаваемые визуальные компоненты. Запросы, которые вы видели до сих пор, представляют собой очень простые типы графических интерфейсов пользователя, но они принимают только отдельные единицы данных. Оконные графические интерфейсы позволяют пользователям просматривать и редактировать несколько полей данных на одном экране. Это обычно более эффективно и менее подвержено ошибкам, чем ответ на последовательные запросы ввода, и является "нормальным" интерфейсом, ожидаемым пользователями бизнес-приложений. Для кодирования графического интерфейса требуется довольно много базовых знаний в большинстве языков программирования. REBOL упрощает эту задачу (фактически, REBOL предоставляет самый простой способ создания графического интерфейса пользователя с помощью кода).

3.1 Основные принципы компоновки и виджеты

Чтобы создать окно программы, вставьте следующий код в редактор REBOL и нажмите [F5] для сохранения и запуска:

REBOL []
view layout [size 600x440]

Чтобы центрировать окно программы на экране компьютера, используйте "center-face" (центральная-грань).

REBOL []
view center-face layout [size 600x440]

Вы можете добавить заголовок (title), который появится в строке заголовка окна вашей программы:

REBOL [title: "My Program"]
view center-face layout [size 600x440  backdrop white]

Вместо слова "backdrop" (фон) вы можете использовать следующий код, чтобы изменить цвет по умолчанию для всех элементов в графическом интерфейсе. Это обеспечивает немного более чистое ощущение, чем серый цвет REBOL по умолчанию:

svv/vid-face/color: white

Вот как вы добавляете "виджеты" (элементы) (кнопки, текстовые поля, многострочные текстовые области, раскрывающиеся списки и т.д.) в окно вашей программы. Обратите внимание, что все в коде окна графического интерфейса пользователя по-прежнему заключено в квадратные скобки, но с отступом в 4 пробела. Отступы не требуются для разделов многострочного блока, но делают код более удобным для чтения, и это вполне ожидаемо. Также обратите внимание, что размеры окна автоматически соответствуют размерам содержащихся в нем виджетов:

REBOL [title: "My Program"]
svv/vid-face/color: white
view center-face layout [
    field "Type Here"
    area "Multi^/line^/text"
    text-list data ["first" "second" "third"]
    image logo.gif   ; this image is built into REBOL
    btn "Click Me"
]

Вы можете настроить размер, цвет и другие свойства виджета, включив модификаторы рядом с каждым виджетом. Обратите внимание, что окно графического интерфейса автоматически расширяется, чтобы соответствовать виджетам с изменённым размером:

REBOL [title: "My Program"]
svv/vid-face/color: white
view center-face layout [
    field 600 "Type Here"
    area 600 "Multi^/line^/text"
    text-list 600 data ["first" "second" "third"]
    image purple logo.gif
    btn red 100 "Click Me"
]

По умолчанию REBOL размещает виджеты друг под другом в окне программы. Вы можете выровнять виджеты по горизонтали, используя слово "across" (поперёк). Вы можете вернуться к вертикальному расположению по умолчанию с помощью слова "below" (ниже):

REBOL []
view center-face layout [
    across
    text-list 194
    text-list 194
    text-list 194
    below
    field 600
    area 600
    across
    text "" 368
    btn 50 "First"
    btn 50 "Next"
    btn 50 "Prev"
    btn 50 "Last"
]

Вы можете изменить начальную позицию по умолчанию для виджетов, размещённых на экране, используя слово "origin" (возникновение/начало), и отрегулировать интервал по умолчанию, используя слово "space" (пространство):

REBOL []
view center-face layout [
    size 251x251
    origin 0x0
    space 100x100
    across
    btn 50x50
    btn 50x50
    return
    btn 50x50
    btn 50x50
    origin 50x50
    btn 50x50
    btn 50x50
    return
    btn 50x50
    btn 50x50
]

Вы можете разместить виджеты в определённых координатах, используя слово "at" (в):

REBOL []
view center-face layout [
    at 20x50 btn
    at 70x100 btn
    at 130x150 btn
]

3.2 Вдыхание жизни в программы с графическим интерфейсом пользователя - выполнение действий

Поместите функциональные слова в блок (между квадратными скобками) после виджетов GUI, и действие этой функции будет выполняться всякий раз, когда виджет щёлкается мышью, отправляется с клавиатуры или активируется иным образом. Обратите внимание, что слово "value" (значение) содержит текущее/выбранное значение в каждом виджете:

REBOL [title: "My Program"]
svv/vid-face/color: white
view center-face layout [
    field "Type Here" [alert value]
    area "Multi^/line^/text" [alert value]
    text-list data ["first" "second" "third"] [alert value]
    image logo.gif [alert "Nice logo"]
    btn "Click Me" [alert "Clicked"]
]

Вы можете присвоить любому виджету метку переменной и изменить текст помеченного виджета, используя уточнение "/text", за которым следует двоеточие. Каждый раз, когда вы вносите изменения во внешний вид окна, вы должны использовать функцию "show" (показать) для обновления отображения:

REBOL []
view layout [
    size 600x400
    field1: field "field 1"  ; это поле помечено как "field1"
    btn "change field1's text" [

        ; Эти действия происходят при нажатии кнопки:

        field1/text: "You just changed field 1's text!"
        show field1
    ]
]

Вы можете прочитать текст из файла в текстовую область, используя функцию "read" (чтение). В REBOL именам файлов всегда предшествует символ процента ("%"):

REBOL []
view layout [
    a: area  ; this area is labeled "a"
    btn "Read" [

        ; При щелчке по btn текст "a" устанавливается на данные, 
        ; считанные из файла:
a/text: read %temp.txt
show a
]
]

Вы можете записать текст из текстовой области в файл, используя функцию "write" (записать):

REBOL []
view layout [
    a: area 
    btn "Save" [

        ; When btn is clicked, write to temp.txt file, the text in area:

        write %temp.txt a/text
        alert "Saved"
    ]
]

ВАЖНО: текстовые поля графического интерфейса могут отображать только текстовые ("string" (строковые)) значения. Обратите внимание, что следующая программа выдаёт ошибки, потому что значения, возвращаемые запросчиками, НЕ являются строковыми значениями, а являются другими типами данных, распознаваемыми REBOL (значения файла, даты, кортежа и т.д.):

REBOL []
view layout [
    btn "File" [
        f1/text: request-file
        show f1
    ]
    f1: field
    btn "Date" [
        f2/text: request-date  
        show f2
    ]
    f2: field
    btn "Color" [
        f3/text: request-color
        show f3
    ]
    f3: field    
]

При копировании форматированных текстовых значений в текстовую область используйте функцию "form" для преобразования данных в текстовую строку:

REBOL []
view layout [
    btn "File" [

        ; при нажатии btn установить текст поля f1 для выбранного имени файла

        f1/text: form request-file   ; FORM преобразует имя файла в текст
        show f1
    ]
    f1: field
    btn "Date" [

        ; установить текст поля f2 на выбранную дату:

        f2/text: form request-date   ; FORM преобразует значение даты в текст
        show f2
    ]
    f2: field
    btn "Color" [

        ; установить выбранный цвет для текста поля f3:

        f3/text: form request-color  ; FORM преобразует значение цвета в текст
        show f3
    ]
    f3: field    
]

Вы можете изменить другие свойства виджета, помимо текста. Измените положение координат виджета с помощью уточнения "/offset", измените его размер с помощью уточнения "/size", точно так же, как вы изменяете его текст с помощью уточнения "/text". Слово "face"(лицо/лик) позволяет виджету ссылаться на себя. В приведённом ниже коде виджет кнопки меняет своё положение, размер и текст при нажатии:

REBOL []
view layout [
    size 594x440 
    btn "click me" [
        face/offset: 200x300
        face/size: 150x50
        face/text: "I've moved and changed!"
        show face 
    ]
]

Слово "style" (стиль) позволяет создавать новые виджеты с предопределёнными свойствами и действиями. Здесь метка "green-button" (зелёная кнопка) определяется как зелёный виджет btn с текстом "click me" (щёлкните меня), который при нажатии переходит к случайной координате в диапазоне 580x420:

REBOL []
view layout [
    size 594x440 

    style green-button btn green "click me" [
        face/offset: random 580x420
        show face
    ]

    ; Слово "green-button" теперь относится ко всему вышеуказанному коду. Каждый
    ; "green-button" имеет те же свойства цвета и текста, а
    ; выполняет ОДНИ И ТЕ ЖЕ ДЕЙСТВИЯ при нажатии. 

    at 254x84 green-button
    at 19x273 green-button
    at 85x348 green-button
    at 498x12 green-button
    at 341x385 green-button
]

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

REBOL [title: "Sliding Puzzle"]

; Создайте графический интерфейс, расположенный по центру экрана пользователя:

view center-face layout [

    ; Определите некоторые основные параметры макета. "origin 0x0"
    ; запускает макет в верхнем левом углу окна,
    ; "space 0x0" означает, что нет пространства 
    ; между соседними виджетами и лежат "across" (поперёк)
    ; следующие друг за другом виджетов: 

    origin 0x0  space 0x0  across 

    ; В следующем разделе создаётся новая кнопка.
    ; виджет под названием "piece" с блоком действий, который
    ; меняет местами текущее положение кнопки с положением
    ; соседнее пустое пространство. Это действие запущено
    ; всякий раз, когда нажимается одна из кнопок:

    style piece button 60x60 [

        ; Строки ниже проверяют, была ли нажата кнопка
        ; примыкает к пустому пространству. "offset" (смещение)
        ; уточнение содержит позицию данного
        ; виджет. Слово "face" используется для обозначения
        ; выбранный в данный момент виджет. "empty" (пустая) кнопка 
        ; определяется позже (в конце макета графического интерфейса).
        ; Ничего страшного, что пустая кнопка ещё не определена,
        ; потому что этот код не оценивается до тех пор, пока
        ; весь макет построен и "view" (просмотрен):
distance: (face/offset - empty/offset)
if not find [0x60 60x0 0x-60 -60x0] distance [exit]

; По русски это читается как "вычесть позицию
; пустого пространство с позиции нажатой
; кнопки (позиции имеют вид пары 
; горизонтальных x вертикальных координат). Если это
; разница не 60 пикселей (точек) на одной из 4 сторон,
; тогда ничего не делать". (60 пикселей - это размер
; кнопки "piece", определённой выше.)

; Следующие три строки меняют местами символы
; нажатой кнопки с пустой кнопкой.
; Сначала создайте переменную для хранения текущего
; положение нажатой кнопки:

temp: face/offset  

; Затем перемещаем кнопку в положение
; текущего пустого пространства:

face/offset: empty/offset 

; Наконец, перемещаем пустое место (кнопку) на старую
; позицию, которую занимала нажатая кнопка:

empty/offset: temp
]

; Строки ниже рисуют кнопки стиля "piece" на
; графический интерфейс пользователя. Каждая из этих кнопок 
; содержит все кода действия, определённого для стиля piece выше:
piece "1"   piece "2"   piece "3"   piece "4" return
piece "5"   piece "6"   piece "7"   piece "8" return
piece "9"   piece "10"  piece "11"  piece "12" return
piece "13"  piece "14"  piece "15"

; Вот пустое место. Снимается его скошенный край
; чтобы он не выглядел как подвижная часть, а больше
; походил на пустое место:

empty: piece 200.200.200 edge [size: 0]
]

Вот вся программа без комментариев. Она крошечная:

REBOL [title: "Sliding Puzzle"]
view center-face layout [    
    origin 0x0 space 0x0 across   
    style piece button 60x60 [
        if not find [0x60 60x0 0x-60 -60x0] (face/offset - e/offset)[exit]
        temp: face/offset
        face/offset: e/offset 
        e/offset: temp
    ]
    piece "1"   piece "2"   piece "3"   piece "4" return
    piece "5"   piece "6"   piece "7"   piece "8" return
    piece "9"   piece "10"  piece "11"  piece "12" return
    piece "13"  piece "14"  piece "15"
    e: piece 200.200.200 edge [size: 0]
]

3.3 Справочник по языку графического интерфейса

Вот все основные слова GUI, встроенные в диалект GUI REBOL (называемые "VID"), которые вам следует знать. Первый блок "styles" (стили) содержит все доступные предопределённые виджеты. Слова макета влияют на то, как и где размещаются элементы, а также на другие параметры макета. Атрибутные слова регулируют внешний вид и функции виджетов. Фасеты стиля регулируют некоторые конкретные параметры, доступные для отдельных виджетов:

STYLES-WIDGETS: [
    face blank-face IMAGE BACKDROP BACKTILE BOX BAR SENSOR KEY BASE-TEXT
    VTEXT TEXT BODY TXT BANNER VH1 VH2 VH3 VH4 LABEL VLAB LBL LAB TITLE
    H1 H2 H3 H4 H5 TT CODE BUTTON CHECK CHECK-MARK RADIO CHECK-LINE
    RADIO-LINE LED ARROW TOGGLE ROTARY CHOICE DROP-DOWN ICON FIELD INFO
    AREA SLIDER SCROLLER PROGRESS PANEL LIST TEXT-LIST ANIM BTN BTN-ENTER
    BTN-CANCEL BTN-HELP LOGO-BAR TOG
]

LAYOUT-WORDS: [
    return at space pad across below origin guide tab tabs indent style
    styles size backcolor backeffect do
]

STYLE-FACETS--ATTRIBUTES: [
    edge font para doc feel effect effects keycode rate colors texts help
    user-data with bold italic underline left center right top middle
    bottom plain of font-size font-name font-color wrap no-wrap as-is
    shadow frame bevel ibevel
]

SPECIAL-STYLE-FACETS: [
    ARROW: [up right down left]  ROTARY: data  CHOICE: data  DROP-DOWN: 
    [data rows]  FIELD: hide  INFO: hide  AREA: hide  LIST: [supply map
    data]  TEXT-LIST: data  ANIM: [frames rate]
]

Вы можете получить приведённые выше списки слов, используя следующие строки кода:

probe extract svv/vid-styles 2
probe remove-each i copy svv/facet-words [function? :i]
probe svv/vid-words

По умолчанию все графические интерфейсы REBOL содержат текст "REBOL -" в строке заголовка окна. В Windows вы можете удалить этот текст с помощью следующего кода. Просто установите переменную "tt" для хранения текста заголовка, который вы хотите отобразить:

tt: "Your Title"
user32.dll: load/library %user32.dll
gf: make routine![return:[int]]user32.dll"GetFocus"
sc: make routine![hw[int]a[string!]return:[int]]user32.dll"SetWindowTextA"
so: :show show: func[face][so[face]hw: gf sc hw tt]

Виджетов и техник, которые вы видели до сих пор, достаточно для создания подавляющего большинства потенциально полезных оконных бизнес-приложений. Вот набор полезных фрагментов кода, демонстрирующих, как выполнять различные общие задачи в графическом интерфейсе пользователя и некоторых других доступных виджетах. Каждый фрагмент кода в этих примерах будет более подробно объяснён позже в руководстве. А пока просто вставьте и запустите эти примеры, чтобы увидеть, что они делают, и держите код под рукой для использования при необходимости:

REBOL [title: "GUI Reference"]
print "GUI Output:^/"
view center-face layout [
    h1 "Some More GUI Widgets:"
    box red 500x2
    drop-down 200 data system/locale/months [
        a/text: join "Month:  " value show a
    ]
    a: field
    slider 200x18 [bar1/data: value show bar1]
    bar1: progress
    scroller 200x16 [bar2/data: value show bar2]
    bar2: progress
    across 
    toggle "Click here" "Click again" [print value]
    rotary "Click" "Again" "And Again" [print value]
    choice "Choose" "Item 1" "Item 2" "Item 3" [print value]
    return
    x: radio y: radio z: radio
    btn "Get Radio States" [print [x/data y/data z/data]]
    return
    led
    arrow
    below
    code "Code text"
    tt "Typewriter text"
    text "Little Text" font-size 8
    title "Centered title" 500
]

; Слово "value" (значение) относится к данным, содержащимся в 
; активном в данный момент виджете:
view layout [
    text "Some widgets with values and size/color properties.  Try them:"
    button red "Click Me" [alert "You clicked the red button."]
    f: field 400 "Type some text here, then press the [Enter] key" [
        alert value                 ; ТАКОЙ ЖЕ КАК alert f/text
    ]
    t: text-list 400x300 "Select this line" "Then this one" "Now this" [
        alert value                 ; ТАКОЙ ЖЕ КАК alert t/text
    ]
    check yellow [alert "You clicked the yellow check box."]
    button "Quit" [alert "I don't want to stop yet!"]    
]

; List Widget:

y: read %.   c: 0   x: copy []
foreach i y [append/only x reduce [(c: c + 1) i (size? to-file i)]]
slider-pos: 0
view center-face layout [
    across space 0
    the-list: list 400x400 [
        across  space 0x0
        text 50 purple
        text 250 bold [editor read to-file face/text]
        text 100 red italic
        return box green 400x1
    ] supply [
        count: count + slider-pos
        if none? q: pick x count [face/text: none exit]
        face/text: pick q index
    ]
    scroller 16x400 [
        slider-pos: (length? x) * value
        show the-list
    ]
]

view layout [
    h3 "Just a few effects - fit, flip, emboss:"
    area 400x400 load http://rebol.com/view/bay.jpg effect [
        Fit Flip Emboss     ; вы можете разместить изображения на 
                            ; большинстве виджетов
    ]
]

effects:  [
  invert  contrast 40  colorize 0.0.200  gradcol 1x1 0.0.255 255.0.0
  tint 100  luma -80  multiply 80.0.200  grayscale  emboss  flip 0x1 
  flip 1x0  rotate 90  reflect 1x1  blur  sharpen  aspect  tile tile-view
]
view layout [
    area 400x400 wrap rejoin [
        "And there are MANY more effects:" newline newline form effects
    ]
] 

view layout [area effect [gradient red blue]]  ; градиенты - это 
                    ; плавный переход от одного цвета к другому
view layout [
    size 500x400
    backdrop effect [gradient 1x1 tan brown]
    box effect [gradient 123.23.56 254.0.12]
    box effect [gradient blue gold/2]
]

view layout [
    btn "Right/Left Click Me" [alert "left click"] [alert "right click"]
]

panels: layout [
    across
    btn "Fields"      [window/pane: pane1 show window]
    btn "Text List"   [window/pane: pane2 show window]
    return
    window: box 400x200
]
pane1: layout/tight [field 400 field 400 area 400]
pane2: layout/tight [text-list 400x200 data system/locale/days]
window/pane: pane1
view center-face panels

svv/vid-face/color: white
alert "New global background color is now white."

; Слово "offset" (смещение) относится к координатной позиции виджета.
; Слово "style" (стиль) создаёт новый виджет с указанным стилем и действиями:

view center-face layout [
    size 600x440 
    h3 "Press the left or right arrow key"
    key keycode [left]  [alert "You pressed the LEFT arrow key"]
    key keycode [right] [alert "You pressed the RIGHT arrow key"]
    btn #"a" "Click Me or Press the 'a' Key" [alert "clicked or pressed"]
]

; Вот небольшая программа для отображения всех кодов клавиш:

insert-event-func func [f e] [if e/type = 'key [print mold e/key] e]
view layout [text "Type keys to see their character/keycode"]

; Как обращаться к главному окну макета:

view gui: layout [
    btn1: btn "Button 1"
    btn2: btn "Remove all widgets from window" [
        foreach item system/view/screen-face/pane/1/pane [
            remove find system/view/screen-face/pane/1/pane item
        ]
        show gui
    ]
]

; "Feel" и "Engage" вместе обнаруживают события:

view layout [
    text "Mouse me." feel [
        engage: func [face action event] [
            if action = 'up [print "You just released the mouse."]
        ]
    ]
]

print "Click anywhere in the window, then click the text."
view center-face layout [
    size 400x200
    box 400x200 feel [
        engage: func [f a e] [     ; f a e = face action event
            print rejoin ["Mouse " a " at " e/offset]
        ]
    ]
    origin 
    text "Click me" [print "Text clicked"] [print "Text right-clicked"]
    box blue [print "Box clicked"]
]

movestyle: [                     ; общий код щелчка и перетаскивания
    engage: func [f a e] [
        if a = 'down [
            initial-position: e/offset
            remove find f/parent-face/pane f
            append f/parent-face/pane f
        ]
        if find [over away] a [
            f/offset: f/offset + (e/offset - initial-position)
        ]
        show f
    ]
]
view layout/size [
    style moveable-object box 20x20 feel movestyle
    at random 600x400 moveable-object (random 255.255.255)
    at random 600x400 moveable-object (random 255.255.255)
    at random 600x400 moveable-object (random 255.255.255)
    at random 600x400 moveable-object (random 255.255.255)
    at random 600x400 moveable-object (random 255.255.255)
    text "This text and all the boxes are movable" feel movestyle
] 600x440

; Следующий код блока создаёт повторяющийся цикл многозадачности в 
; графическом интерфейсе пользователя.
; Функция "within" (внутри) проверяет графические коллизии:

view center-face layout [
    size 400x400
    btn1: btn red
    at 175x175 btn2: btn green
    box 0x0 rate 0 feel [engage: func [f a e] [if a = 'time [
        btn1/offset: btn1/offset + 5x5
        show btn1
        if within? btn1/offset btn2/offset 1x1 [alert "Collision" unview]
    ]]]
]

view center-face layout [      ; следить за всеми движениями мыши
    size 600x440
    at 270x209 b: btn "Click Me - Aha!" feel [
        detect: func [f e] [
            if e/type = 'move [
                if (within? e/offset b/offset 59x22) [
                    b/offset: b/offset + ((random 50x50) - (random 50x50))
                    if not within? b/offset -59x-22 659x462 [
                        b/offset: 270x209
                    ]
                    show b
                ]
            ]
            e
        ]
    ]
]

; Чтобы перехватить другие события (этот пример перехватывает и отвечает на события закрытия):

closer: insert-event-func [
    either event/type = 'close [
        really: request "Really close the program?"
        if really = true [remove-event-func :closer]
    ] [event]  ; always return other events
]
view center-face layout [
    text "Close me"
    size 600x400
]

insert-event-func [  ; этот пример обнаружения события изменения размера окна
    either event/type = 'resize [
        fs: t1/parent-face/size
        t1/offset: fs / 2x2
        t2/offset: t1/offset - 50x25
        t3/offset: t1/offset - 25x50
        show gui  none
    ] [event]
]
svv/vid-face/color: white
view/options gui: layout [
    text "Centered in resized window:"
    across
    t1: text "50x50"
    t2: text "- 50x25"
    t3: text "- 25x50"
] [resize]

; Используйте "to-image", чтобы создать SCREEN SHOT ("фотографию" 
; экрана) любого макета:

picture: to-image layout [
    page-to-read: field "http://rebol.com"
    btn "Display HTML"
]
save/png %layout.png picture     ; сохранить изображение в файл
browse %layout.png

flash "Just waiting..."  wait 3  alert "Done waiting!"  unview
inform layout [btn "Click Me" [flash "Just waiting..." wait 3 unview]]

; Встраивать файлы (изображения, звуки и т.д.) в код:

alert "Select a picture from your hard drive:"
system/options/binary-base: 64
editor picture: compress to-string read/binary to-file request-file/only
view layout [image load (to-binary decompress picture)]

; Этот пример встроенного изображения был создан с помощью 
; приведённого выше сценария.

logo-pic: load to-binary decompress #{
789C018A0375FC89504E470D0A1A0A0000000D49484452000000640000001808
020000008360CFB90000001374455874536F667477617265005245424F4C2F56
6965778FD916780000033249444154789CD599217402310C86F7CE6227B1481C
1637874362B1382C1687C4A15168240A89C5A2B058ECDEBE47DFFA429276DCEE
10FDCD582F97267FD33F2D7CF47ABDCF32D1ED76E7F3F9ED76FB4EE0743A8D46
A3B6A683A80FFE540562381C1E8FC7144D12DBEDB6951C3B9D4E91648DC7E34C
41B925465D349C14A2CA230BA65EA729E27C3E37CCB43CB228905A3525B1DBED
9A4CED93851C7C193088A0667C0D0603FB5640BFDFB7F648C0D0836B1C41C22E
11D7EBF57038F074BFDF534429BE2693891B4626CE1C59BC7CB95CDC99EEF7FB
66B349F922D65A4B4A8DE0D0B547B9DD85212B6B4CB4D3E994B055FEE8943566
30134626BBDA64052C974BD757A637B1DA2E599959A05EE61F4032D62C55EFBC
6EED01878954188DC80AE714C07126D24F91BBBE6265A129B3D96C2A4085BB64
459FEBF51A1B2692E5A9FA17A428B562EBE595A1F29650AD5C6B9525FD4621E0
A95D73491606F9046C94101A06178B4518E19122023655DA184B03ECA15BE98E
6D9D302E536E8D2C96A5FF0061458FEE9EAA045958720EDCFC82CF145A9E2C7C
52BC6CF0503B8C2B2200DAACD24698A4B710361E6421930E05A85E9484BE51B3
0885AE9727CB22A5591981B73D1AC6A58D2ABD5892DF46C5993DCFF25BC8828E
14538AACEB3390A43C59D890213B5D2AA3D2AC3C59ABD54ACE2E85C29E36DE42
162B8C0AC47F0942B512972CCCF0D91170ED6594ECC130288549ED44744DE52C
771381C571D5AFEDB14B2E79CB022F13C834A056049EFCE35C2A7449877A2B00
2D872635082FEA2D267D8BC047AD910D3875CE9247078A826259FC8234F264E1
9FAD4AAC52015465D973193B3755B611B417FB562A0C66C77EF7001F5463FD83
2CF20F83B2B8E0C22DAE760FA556B32AAF87B86A18C18259CFAA3567C250C7C3
1AE72CD95350531BD93FAE3B6CEADB33188174FCBBD77B7B7A0841DAB6C3EBEE
F13DE8696B6455E222ADCE23F162ECF644064709A47AA8FD3632BFAD78EA5E92
D947500C3BB04CAD419F3D5B05580DC127118E3D2866CAFB8AC6CAFCEB68F895
56796455CF47AAD741F5B957D4D751245980BD569729B723D742A964558FFB4D
EAB6A440BF6ACE54157EB028F7A730B695BDF749D05EA9C1B612C4CF0F396EDC
8E943F5C020000000049454E44AE426082CAEBA2D78A030000
}
view layout [image logo-pic]

write/append %s ""  ; Очень компактная программа с графическим интерфейсом
view center-face g: layout [
    h3 "Name:"   x: field   h3 "Info:"   z: area wrap   across
    btn "Save" [do-face d 1  save %s  repend f [x/text z/text]]
    btn "Load" [
        c: request-list" Select:" extract (f: load %s) 2
        if c = none [return]
        x/text: first find f c  z/text: select f x/text  show g
    ]
    btn "New" [x/text: copy ""  z/text: copy ""  show g  focus x]
    d: btn "Delete" [
        if true = request "Sure?" [
            remove/part (find (f: load %s) x/text) 2 save %s f alert "ok"
        ]
    ]
]

; Некоторые примеры диалекта "рисование" для создания графики:

view layout [
    box 400x400 black effect [
        draw [
            pen red
            line 0x400 400x50
            pen white
            box 100x20 300x380
            fill-pen green
            circle 250x250 100
            pen blue
            fill-pen orange
            line-width 5
            spline closed 3 20x20 200x70 150x200
            polygon 20x20 200x70 150x200 50x300
        ]
    ]
]

view layout [
    box 400x220 effect [
        draw [
            fill-pen 200.100.90
            polygon 20x40 200x20 380x40 200x80
            fill-pen 200.130.110
            polygon 20x40 200x80 200x200 20x100
            fill-pen 100.80.50
            polygon 200x80 380x40 380x100 200x200
        ]
        gradmul 180.180.210 60.60.90
    ]
]
view layout [
    h3 "Draw On Me:"
    scrn: box black 400x400 feel [
        engage: func [face action event] [
            if find [down over] action [
                append scrn/effect/draw event/offset
                show scrn
            ]
            if action = 'up [append scrn/effect/draw 'line]
        ]
    ] effect [draw [line]]
]

pos: 300x300
view layout [
    scrn: box pos black effect [
        draw [image logo.gif 0x0 300x0 300x300 0x300]
    ]
    btn "Animate" [
        for point 1 450 4 [
            scrn/effect/draw: copy reduce [
                'image logo.gif 
                (pos - 300x300)
                (1x1 + (as-pair 300 point))
                (pos - (as-pair 1 point))
                (pos - 300x0)
            ]
            show scrn
        ]
        scrn/effect/draw: copy [
            image logo.gif 0x0 300x0 300x300 0x300
        ]
        show scrn
    ]
]

См. http://rebol.com/docs/easy-vid.html и http://rebol.com/docs/view-guide.html для получения дополнительной информации и примеров, демонстрирующих основные методы графического интерфейса.

3.4 Показательное сравнение

Чтобы быстро понять, насколько REBOL проще, чем другие языки, вот небольшой пример. Следующий код для создания основного окна программы с REBOL был представлен ранее:

view layout [size 400x300]

Он работает на всех типах поддерживаемых компьютеров и операционных системах одинаково.

Код для того же простого примера представлен ниже на языке C ++. Он делает то же самое, что и однострочник REBOL, описанный выше, за исключением того, что он работает только на компьютерах с Microsoft Windows. Если вы хотите сделать то же самое с компьютером Macintosh, вам нужно запомнить совершенно другую страницу кода C ++. То же самое верно для Linux или любой другой операционной системы. Чтобы делать очень простые вещи, нужно выучить огромные фрагменты кода, и эти фрагменты кода различны для каждого типа компьютера. Кроме того, вам, как правило, нужно потратить время на изучение самых элементарных вещей о синтаксисе кода и основ о том, как компьютер "думает", прежде чем вы даже начнёте создавать основы, такие, как код ниже:

#include <windows.h>

/*  Объявить процедуру Windows  */
LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);

/*  Превратите имя класса в глобальную переменную  */
char szClassName[ ] = "C_Example";

int WINAPI
WinMain (HINSTANCE hThisInstance,
         HINSTANCE hPrevInstance,
         LPSTR lpszArgument,
         int nFunsterStil)

{
    HWND hwnd;               
    /* Это заголовок для нашего окна */
    MSG messages;            
    /* Здесь сохраняются сообщения в приложение */
    WNDCLASSEX wincl;        
    /* Структура данных для оконного класса */

    /* Структура окна */
    wincl.hInstance = hThisInstance;
    wincl.lpszClassName = szClassName;
    wincl.lpfnWndProc = WindowProcedure;      
    /* Эта функция вызывается окнами */
    wincl.style = CS_DBLCLKS;                 
    /* Обработчик двойных нажатий */
    wincl.cbSize = sizeof (WNDCLASSEX);

    /* Использовать значок и указатель мыши по умолчанию */
    wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hCursor = LoadCursor (NULL, IDC_ARROW);
    wincl.lpszMenuName = NULL;                 
    /* Без меню */
    wincl.cbClsExtra = 0;                      
    /* Без дополнительных байтов после класса окна */
    wincl.cbWndExtra = 0;                      
    /* структура или экземпляр окна */
    /* Использовать цвет Windows по умолчанию в качестве фона окна */
    wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND;

    /* Зарегистрируйте класс окна. В случае неудачи выйдите из программы. */
    if (!RegisterClassEx (&wincl))
        return 0;

    /* Класс зарегистрирован, давайте создадим программу */
    hwnd = CreateWindowEx (
           0,                   
            /* Расширенные возможности для вариаций */
           szClassName,         
            /* Имя класса */
           "C_Example",       
            /* Текст загаловка */
           WS_OVERLAPPEDWINDOW, 
            /* окно по умолчанию */
           CW_USEDEFAULT,       
            /* Разрешение позиций окон */
           CW_USEDEFAULT,       
            /* где окно заканчивается на экране */
           400,                 
            /* Ширина программ */
           300,                 
            /* и высота в точках */
           HWND_DESKTOP,        
            /* Окно является дочерним по отношению к рабочему столу. */
           NULL,                
            /* Без меню */
           hThisInstance,       
            /* Обработчик экземпляра программы */
           NULL                
            /* Нет данных о создании окон */
           );

    /* Сделать окно видимым на экране */
    ShowWindow (hwnd, nFunsterStil);

    /* Запустите цикл сообщений. 
    Он будет работать до тех пор, пока GetMessage () не вернёт 0 */
    while (GetMessage (&messages, NULL, 0, 0))
    {
        /* Перевести сообщения виртуального ключа
            в символьные сообщения */
        TranslateMessage(&messages);
        /* Send message to WindowProcedure */
        DispatchMessage(&messages);
    }

    /* Возвращаемое программой значение 0 -
            значение, которое дал PostQuitMessage () */
    return messages.wParam;
}

/*  Эта функция вызывается Windows
        функция DispatchMessage ()  */

LRESULT CALLBACK
WindowProcedure (HWND hwnd, UINT message, 
    WPARAM wParam, LPARAM lParam)
{
    switch (message)                  
    /* обрабатывать сообщения */
    {
        case WM_DESTROY:
            PostQuitMessage (0);       
                /* отправить WM_QUIT в очередь сообщений */
            break;
        default:                      
            /* для сообщений, которыми мы не занимаемся */
            return DefWindowProc (hwnd, message, 
                wParam, lParam);
    }

    return 0;
}

Ух. Вернёмся в REBOL!

4. Быстрый обзор и разъяснения

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

  1. Для начала, REBOL имеет сотни встроенных функциональных слов, которые выполняют общие задачи. Как и в других языках, за функциональными словами обычно следуют передаваемые параметры данных. В отличие от других языков, переданные параметры помещаются сразу после функционального слова и не обязательно заключаются в круглые скобки. Для достижения желаемой цели функции располагаются последовательно, одна за другой. Значения, возвращаемые одной функцией, часто используются в качестве входных аргументов для другой функции. Знаки конца строки не требуются ни в какой точке, и все выражения вычисляются в порядке слева направо, а затем вертикально вниз по коду. Пустые пробелы (пробелы, табуляции, новые строки и т.д.) могут быть вставлены по желанию, чтобы сделать код более читабельным. Текст после точки с запятой и перед новой строкой рассматривается как комментарий. Вы можете выполнить значительную работу, просто зная предопределённые функции на языке и организовав их в удобном порядке.
  2. REBOL содержит богатый набор условных структур, которые можно использовать для управления потоком программы и действиями по обработке данных. If (Если), either (либо), и другие типовые структуры поддерживаются.
  3. Поскольку REBOL автоматически распознает и обрабатывает многие распространённые типы значений данных, вычисления, циклы и принятие условных решений на основе содержимого данных являются простыми и естественными для выполнения без каких-либо внешних модулей или наборов инструментов. Числа, текстовые строки, денежные значения, время, кортежи, URL-адреса, двоичные представления изображений, звуков и т.д. обрабатываются автоматически. REBOL может увеличивать, сравнивать и выполнять правильные вычисления для наиболее распространённых типов данных (т.е. интерпретатор автоматически знает, что 5:32 + 00:35:15 = 6:07:15 утра, и он может автоматически применять визуальные эффекты к необработанным двоичным файлам. данные изображения и т.д.). Доступ к сетевым ресурсам и Интернет-протоколам (http-документы, ftp-каталоги, учётные записи электронной почты, DNS-сервисы и т.д.) также можно получить напрямую, так же легко, как и к локальным файлам. Данные любого типа могут быть записаны и прочитаны практически с любого подключённого устройства или ресурса (например, "write %file.txt data" работает так же легко, как "write ftp://user:pass@website.com data", используя тот же общий синтаксис). Символ процента ("%") и синтаксис "%(/drive)/path/path/.../file.ext" используются кросс-платформенно для ссылки на значения локального файла в любой операционной системе.
  4. Любым данным или коду можно присвоить словесную метку. Символ двоеточия (":") используется для присвоения словесных меток константам, значениям переменных, вычисляемым выражениям и блокам данных/действий любого типа. После назначения переменные слова могут использоваться для представления всех данных, содержащихся в данном выражении, блоке и т.д. просто поставьте двоеточие в конце слова, и после этого оно будет представлять все последующие данные.
  5. Несколько фрагментов данных хранятся в "блоках", которые разграничиваются начальными и конечными скобками ("[]"). Блоки могут содержать данные любого типа: группы текстовых строк, массивы двоичных данных, другие закрытые блоки и т.д. элементы данных, содержащиеся в блоках, разделяются пробелом. Блоки могут автоматически обрабатываться как списки данных, называемые "сериями", и ими можно управлять с помощью встроенных функций, которые позволяют искать, сортировать, упорядочивать и иным образом организовывать заблокированные данные. Блоки используются для обозначения большинства синтаксических структур в REBOL (т.е. действий, являющихся результатом условных оценок, макетов виджетов графического интерфейса и т.д.).
  6. Функцию "Foreach" можно использовать для обработки списков и таблиц данных. Строки и столбцы, если данные в таблицах могут быть обработаны с использованием формата: "foreach [col1 col2 col3 ...] table-block [делать что-то для значений pof каждой строки]". Данные могут быть сохранены в файлы CSV путём повторного соединения формованного (цитируемого) текста и разделителей. Файлы CSV можно читать с помощью функций "read/lines" (чтение/строки) и "parse" (синтаксический анализ). Сортировка данных по столбцу с помощью функции сортировки/сравнения требует, чтобы строки сохранялись во вложенных блоках, а не в виде "плоских" последовательных списков элементов. Данные, хранящиеся как значения текстовой строки (заключённые в кавычки), сортируются в алфавитном порядке. Данные, хранящиеся или преобразованные в значения определённого типа, сортируются в соответствии с типом.
  7. Синтаксис "view layout [block]" используется для создания основных макетов графического интерфейса. Вы можете добавить виджеты в макет, просто поместив слова идентификатора виджета внутри блока: "button" (кнопка), "field" (поле), "text-list" (текстовый-список) и т.д. после каждого идентификатора виджета можно добавить цвет, положение, интервал и другие фасетные слова. Блоки действий, добавленные сразу после любого виджета, будут выполнять вложенные функции всякий раз, когда виджет активирован (то есть, когда виджет щёлкают мышью, когда нажата клавиша ввода и т.д.). Уточнения пути могут использоваться для ссылки на элементы в макете графического интерфейса (например, "face/offset" (грань/смещение) относится к положению выбранной грани виджета, "face/text" (грань/текст) - к его тексту и т.д.). Эти простые рекомендации можно использовать для создания полезных графических интерфейсов для ввода и вывода данных способом, который является родным (не требует каких-либо внешних инструментов) и намного проще, чем любой другой язык.

5. НЕКОТОРЫЕ ПРИМЕРЫ ПОЛНЫХ ПРИЛОЖЕНИЙ GUI

Примеры в этом разделе были представлены в самом начале урока в качестве демонстрации. На этом этапе обучения вы должны понимать каждый бит кода в каждой программе! Каждый пример в этом разделе документирован с подробными построчными объяснениями того, что делает каждая функция, переменная и языковая конструкция. Запустите каждый пример в интерпретаторе REBOL и прочтите каждую строку кода вместе со всеми комментариями. Обязательно обратите внимание на материал этого раздела - он один из самых формирующих во всем тексте. Приложения не только полезны в качестве основы для более персонализированной производственной части программного обеспечения, но и демонстрируемые здесь логика и шаблоны кода сформируют прочное фундаментальное понимание того, как создавать другие воображаемые части программного обеспечения.

5.1 Универсальная сохронялка текстовых полей

Такие маленькие программы образуют исходную основу для всех типов простых приложений для ввода данных. Введённые данные хранятся в формате файла CSV, поэтому их можно легко открыть с помощью электронной таблицы или другой программы:

REBOL [title: "Text Field Saver"]

; Слова "view layout" (вид макет) создают окно графического интерфейса пользователя:

view layout [

    ; Создаём три виджета с текстовыми полями, помеченными как f1, f2 и f3:

    f1: field
    f2: field
    f3: field

    ; Создаём виджет кнопки
    btn "Save" [

        ; Когда кнопка нажата, запишем в файл fields.csv объединённый
        ; текст. Уточнение "/append" (/добавить) функции "write" (запись)
        ; гарантирует, что данные ДОБАВЛЯЮТСЯ в конец существующего текстового файла,
        ; вместо того, чтобы стирать файл и записывать в него совершенно новые данные.
        ; Если файл fields.csv не существует, он создается:
write/append %fields.csv rejoin [

    ; Функция "mold" заключает текст в кавычки.
    ; Таким образом, объединённый текст, записанный в файл fields.csv
    ; включает текст из каждого виджета текстового поля выше,
    ; каждый разделён запятой и пробелом и завершён
    ; возврат каретки (новой строкой):

    mold f1/text ", " mold f2/text ", " mold f3/text newline

]

; Оповестить пользователя, когда данные были сохранены:

alert "Saved"

]
]

Вот вся программа без комментариев:

REBOL [title: "Text Field Saver"]
view layout [
    f1: field
    f2: field
    f3: field
    btn "Save" [
        write/append %fields.txt rejoin [
            mold f1/text ", " mold f2/text ", " mold f3/text newline
        ]
        alert "Saved"
    ]
]

5.2 Калькулятор

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

REBOL [title: "Calculator"]

; Создаём графическое окно:

view layout [

    ; Установите свойства макета так, чтобы виджеты размещались от края,
    ; рядом друг с другом, начиная с верхнего левого угла экрана:
origin 0  space 0x0  across

; Вот текстовое поле с названием "f". Его размер составляет 200 
; пикселей по горизонтали и 40 пикселей по вертикале. Размер 
; шрифта текста в поле установлен на 20. После этого полевого 
; виджета для перехода используется слово "return" (возврат).
;для начала новой строки:
f: field 200x40 font-size 20  return

; Слово "style"(стиль) ниже используется для создания нового 
; виджета под названием "btn",
; это кнопка размером 50 на 50 пикселей. При нажатии
; кнопка добавляет текст со своей лицевой стороны (то, что 
; написано на кнопке) (face/text) к виджету текстового поля выше,
; помечены как "f" (f/text), и обновляет изображение поля f с 
; помощью функции "show":
style btn btn 50x50 [append f/text face/text  show f]

; Под виджетом поля расположены 4 строки кнопок, на которых отображается либо
; числа или операторы на их "лице". Каждая из этих кнопок выполняет
; действия, определённые выше в определении "style" (добавляет свой
; текст в виджет поля f и обновляет его изображение):

btn "1"  btn "2"  btn "3"  btn " + "  return
btn "4"  btn "5"  btn "6"  btn " - "  return
btn "7"  btn "8"  btn "9"  btn " * "  return
btn "0"  btn "."  btn " / "   btn "=" [

    ; При нажатии кнопки "=" текст поля "f" устанавливается на
    ; результат выражения, отображаемого в тексте поля "f"
    ; (оценка выполняется с помощью функции "do"), и изображение
    ; обновляется с помощью функция "show". 
    ; Помните, что функция "form" используется для преобразования
    ; результата в значение текстовой строки, которое является 
    ; единственным типом данных, отображаемых виджетами текстовых полей. 
    ; Функция "attempt" (попытка) используется для предотвращения
    ; сбоя программы, если пользователь пытается ввести
    ; недопустимое выражение, такое как деление на ноль или
    ; неполные выражения (например, 1 +):
attempt [f/text: form do f/text  show f]

]
]

Вот вся программа без комментариев:

REBOL [title: "Calculator"]
view layout [
    origin 0  space 0x0  across
    f: field 200x40 font-size 20 return
    style btn btn 50x50 [append f/text face/text  show f]
    btn "1"  btn "2"  btn "3"  btn " + "  return
    btn "4"  btn "5"  btn "6"  btn " - "  return
    btn "7"  btn "8"  btn "9"  btn " * "  return
    btn "0"  btn "."  btn " / "   btn "=" [
        attempt [f/text: form do f/text  show f]
    ]
]

В следующем примере загружаются и анализируются текущие (действующие) обменные курсы доллара США с http://x-rates.com и пользователь может выбрать из списка валют для преобразования, а затем выполняет и отображает преобразование из долларов США в выбранная валюта. Этот пример, вероятно, будет слишком сложным для полного понимания на данном этапе руководства, но лучше запустить его в интерпретаторе REBOL и просмотреть код, чтобы распознать такие функции, как "read" (чтение), "find" (поиск), "parse" (синтаксический анализ), "attempt" и т. д., с которыми вы уже были знакомы. Посмотрите, сможете ли вы получить общее представление о том, что делает код:

REBOL [title: "Currency Rate Conversion Calculator"]
view center-face layout [
    origin 0  space 0x0  across
    f: field 200x40 font-size 20
    return
    style btn btn 50x50 [append f/text face/text  show f]
    btn "1"  btn "2"  btn "3"  btn " + "  return
    btn "4"  btn "5"  btn "6"  btn " - "  return
    btn "7"  btn "8"  btn "9"  btn " * "  return
    btn "0"  btn "."  btn " / "   btn "=" [
        attempt [f/text: form do f/text  show f]
    ] return
    btn 200x35 "Convert" [
        x: copy []
        html: read http://www.x-rates.com/table/?from=USD&amount=1.00
        html: find html "src='/themes/bootstrap/images/xrates_sm_tm.png'"
        parse html [
            any [
                thru {from=USD} copy link to {</a>} (append x link)
            ] to end 
        ]
        rates: copy []
        foreach rate x [
            parse rate [thru {to=} copy c to {'>}]
            parse rate [thru {'>} copy v to end]
            if not error? try [to-integer v] [append rates reduce [c v]]
        ]  
        currency: request-list "Select Currency:" extract rates 2
        rate: to-decimal select rates currency
        attempt [alert rejoin [currency ": " (rate * to-decimal f/text)]]
    ]
]

5.3 Редактор файлов

Далее приводится пример текстового редактора, который позволяет читать, редактировать и сохранять любой текстовый файл. Вот код макета графического интерфейса. Он состоит из текстового виджета заголовка "h1", отображающего текст "Text Editor:", виджета с полем шириной 600 пикселей с меткой "f", содержащего текст "filename.txt", виджета с областью 600x350 пикселей с меткой "a" и двух кнопки, выровненные по экрану (рядом друг с другом):

REBOL []
view layout [
    h1 "Text Editor:"
    f: field 600 "filename.txt"
    a: area 600x350 
    across 
    btn "Load" []
    btn "Save" []
]

А вот полный код, который заставляет его работать:

REBOL []
view layout [
    h1 "Text Editor:"
    f: field 600 "filename.txt"
    a: area 600x350 
    across 
    btn "Load" [

        ; Когда кнопка загрузки нажата, запросим имя файла у 
        ; пользователя и отобразим его в поле "f". Обязательно 
        ; незабываем обновить отображение поля с помощью функции 
        ; "show" (показать):
f/text: request-file
show f

; Чтобы загрузить файл, текстовая строка с именем файла,
; отображаемая в поле "f", должна быть преобразована из 
; текста в значение типа (file!) файл!:
filename: to-file f/text

; для текстового поля ("/text") виджета "a", присваиваем  (":") 
; значения считанные ("read") файла (filenane), и обновляем 
; его на экране ("show"):
a/text: read filename 
show a

]
btn "Save" [

; При нажатии кнопки сохранения запросим имя файла у 
; пользователя. Имя файла "по умолчанию", отображаемое в 
; запросчике, - это текст, отображаемый в настоящее время в 
; поле "f":
filename: to-file request-file/save/file f/text

; Запишим (write) в выбранный файл текст, содержащийся в 
; области текста ("/text") виджета "а":

write filename a/text

; Оповестить пользователя, что файл сохранён:

alert "Saved"

]
]

Вот вся программа без комментариев:

REBOL [title: "Text Editor"]
view layout [
    h1 "Text Editor:"
    f: field 600 "filename.txt"
    a: area 600x350 
    across 
    btn "Load" [
        f/text: request-file
        show f
        filename: to-file f/text
        a/text: read filename 
        show a
    ]
    btn "Save" [
        filename: to-file request-file/save/file f/text
        write filename a/text
        alert "Saved"
    ]
]

5.4 Редактор веб-страниц

REBOL может читать и писать на FTP-серверы (веб-сайты) так же легко, как и в локальные файлы. Все, что вам нужно знать, это имя пользователя/пароль учётной записи, расположение папки на сервере (часто "public_html") и имя файла для редактирования. Вот вариант вышеупомянутой программы, перепрофилированный как редактор веб-страниц:

REBOL [title: "Web Page Editor"]
view layout [
    h1 "Web Page Editor:"
    f: field 600 "ftp://user:pass@site.com/public_html/file.txt"
    a: area 600x350 
    across 
    btn "Load" [

        ; Не забудьте преобразовать текст имени файла в виджете 
        ; области "a" в тип данных URL!. Помещаем текст считанный из
        ; файла по URL адресу в текстовую область ("/text") виджета "a":
a/text: read to-url f/text 
show a

]
btn "Save" [

; Преобразуем текст из виджета "f" в значение URL и 
; записываем ("write") по соответствующему адресу данные из 
; текстовой области виджета "a":
write (to-url f/text) a/text
alert "Saved"

]
]

Вот вся программа без комментариев:

REBOL [title: "Web Page Editor"]
view layout [
    h1 "Web Page Editor:"
    f: field 600 "ftp://user:pass@site.com/public_html/page.html"
    a: area 600x350 
    across 
    btn "Load" [
        a/text: read to-url f/text 
        show a
    ]
    btn "Save" [
        write (to-url f/text) a/text
        alert "Saved"
    ]
]

5.5 Создатель инвентарного списка

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

REBOL [title: "Inventory"]
view layout [
    text "SKU:"
    f1: field
    text "Cost:"
    f2: field "1.00"
    text "Quantity:"
    f3: field
    across
    btn "Save" []
    btn "View Data" []
]

А вот код, запускающий всю программу инвентаризации:

REBOL [title: "Inventory"]
view layout [
    text "SKU:"
    f1: field
    text "Cost:"
    f2: field "1.00"
    text "Quantity:"
    f3: field
    across
    btn "Save" [
        write/append %inventory.txt rejoin [
            mold f1/text " " mold f2/text " " mold f3/text newline
        ]
        alert "Saved"
    ]
    btn "View Data" [editor %inventory.txt]
]

5.6 Сортировщик и просмотрщик инвентаря

Программа инвентаризации создаёт списки данных, которые выглядят следующим образом (сохранены в файле inventory.txt):

"932984729812" "1.00" "14"
"392328389483" "2.59" "93"
"602374822852" "4.92" "3"

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

REBOL [title: "Sort Inventory"]

; Загружаем блок данных из файла inventory.txt и помечаем его как  "inventory":

inventory: load %inventory.txt

; Создайте новый пустой блок с именем "blocked":

blocked: copy []

; Преобразуйте "плоские" данные в блоке "inventory" в
; "обблокированный" формат (со строками, выделенными как 
; отдельные блоки) в переменную "blocked". Помните, как было показано
; ранее, уточнение функции "append/only" (добавить/только) позволяет 
; легко это сделать. Просто запустите функцию "foreach" для каждых 3 
; элементов в блоке "inventory" и добавьте каждую группу из трёх 
; элементов в качестве нового блока в блок "blocked".
; Обратите внимание, что во время процесса "cost" (стоимость) и 
; "qty" (количество) преобразуются из строк в денежные и числовые 
; типы данных:

foreach [sku cost qty] inventory [
    append/only blocked reduce [
        sku 
        to-money cost
        to-integer qty
    ]
]

; Используем функцию "request-list" (запрос-списка), чтобы позволить 
; пользователю выбрать столбец для сортировки. Присвоим ответ 
; переменной "field-name":
field-name: request-list "Choose Field To Sort By:" [
    "sku" "cost" "qty"
]

; Функция "select" выбирает следующее значение в списке, выбранном 
; пользователем. В этом случае, если переменная field-name равна 
; "sku", переменная "field" устанавливается в 1. Если field-name = 
; "cost", для переменной "field" устанавливается значение 2. Если 
; field-name = "qty" ", то" поле "устанавливается на 3:
field: select ["sku" 1 "cost" 2 "qty" 3] field-name

; Используем функцию  "request-list" (запрос-списка), чтобы позволить 
; пользователю выбрать порядок сортировки. Присвоим ответ метке 
; "order" (порядок):

order: request-list "Ascending or Descending:" ["ascending" "descending"]

; Сортировка по соответствующему столбцу "field", по возрастанию или 
; убыванию, в зависимости от значения переменной "order":

either order = "ascending" [
    sort/compare blocked func [a b] [(at a field) < (at b field)]
][
    sort/compare blocked func [a b] [(at a field) > (at b field)]
]

; Используем функцию foreach для просмотра отсортированного блока 
; данных "blocked". Распечатаем 3 элемента в каждом блоке внутри 
; блока "blocked":

foreach item blocked [
    print rejoin [
        "SKU:  " item/1           ; 1-й элемент в блоке инвентарный номер.
        "  COST: " item/2         ; 2-й элемент стоимость.
        "  QTY: " item/3          ; 3-й элемент количество.
        newline                   ; переходим на новую строку
    ]
]
halt

Вот вся программа без комментариев:

REBOL [title: "Sort Inventory"]
inventory: load %inventory.txt
blocked: copy []
foreach [sku cost qty] inventory [
    append/only blocked reduce [
        sku 
        to-money cost
        to-integer qty
    ]
]
field-name: request-list "Choose Field To Sort By:" [
    "sku" "cost" "qty"
]
field: select ["sku" 1 "cost" 2 "qty" 3] field-name
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
    sort/compare blocked func [a b] [(at a field) < (at b field)]
][
    sort/compare blocked func [a b] [(at a field) > (at b field)]
]
foreach item blocked [
    print rejoin [
        "SKU:  " item/1 
        "  COST: " item/2 
        "  QTY: " item/3 
        newline
    ]
]
halt

5.7 Просмотр контактов

Вот приложение базы данных контактов, которое отображает пользовательскую информацию в виде таблицы:

REBOL [title: "Contacts"]

; Сначала создайте блок пользовательских данных. 15 значений - 5 строк, 3 столбца:

users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]

; Затем определите блок макета графического интерфейса. Белый фон, 
; виджеты, расположенные рядом друг с другом по горизонтали, новый 
; стиль текстового виджета  "header" (заголовок шириной 200 пикселей 
; и 3 из этих виджетов, содержащих соответствующие метки заголовков 
; для каждого столбца данных, за которыми следует новая строка 
; графического интерфейса:

gui: [
    backdrop white
    across
    style header text 200
    header "Name:" header "Address:" header "Phone:" return
]

; Используем функцию foreach, чтобы просмотреть каждую строку в блоке users:

foreach [name address phone] users [

    ; Для каждой строки в пользовательском блоке добавьте следующий 
    ; код в блок макета графического интерфейса:

    append gui compose [

        ; Добавляем новый виджет поля, содержащие имя, адрес и 
        ; значения телефона на каждой строки. Вышеупомянутая функция 
        ; "compose" преобразует слова в скобках ниже в оценённые 
        ; значения (текстовые данные в каждой строке вместо текста 
        ; "name", "address", "phone"):

        field (name) field (address) field (phone) return

    ]
]

; Покажем построенный блок GUI:
view layout gui

Вот вся программа без комментариев:

REBOL [title: "Contacts"]
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]
gui: [
    backdrop white
    across
    style header text black 200
    header "Name:" header "Address:" header "Phone:" return
]
foreach [name address phone] users [
    append gui compose [
        field (name) field (address) field (phone) return
    ]
]
view layout gui

Это приложение сочетает в себе некоторые методы, использованные в предыдущих двух примерах, для создания более красивого отсортированного отображения инвентаря:

REBOL [title: "Sort Inventory"]

; Сначала загружаем данные, созданные приложением инвентаризации. 
; Присвоим загруженный блок метке переменной "inventory":

inventory: load %inventory.txt

; Создаём новый пустой блок:

blocked: copy []

; Создаём блок блоков, чтобы данные можно было сортировать по 
; столбцам, при этом данные столбцов преобразовывались из текста в 
; денежные и целочисленные значения.
; Функция "reduce" (уменьшить) работает так же, как "compose" (составить),
; но не требует скобок.

foreach [sku cost qty] inventory [
    append/only blocked reduce [
        sku 
        to-money cost
        to-integer qty
    ]
]

; Используем запросчик "request-list" (запрос-список), чтобы получить 
; столбец сортировки от пользователя. Присвойте ответ пользователя 
; метке переменной "field-name":
field-name: request-list "Choose Field To Sort By:" [
    "sku" "cost" "qty"
]

; Использовал функцию "select" (выбрать), чтобы присвоить числовое 
; значение выбранному выше столбцу. Присвойте этот номер метке
; переменной "field":
field: select ["sku" 1 "cost" 2 "qty" 3] field-name

; Используем запросчик "request-list" (запрос-список), чтобы получить 
; от пользователя порядок сортировки. Присвойте ответ пользователя 
; переменной с именем "order" (порядок):

order: request-list "Ascending or Descending:" ["ascending" "descending"]

; Если выбранный порядок сортировки "ascending" (по возрастанию),
; отсортируем блок блоков в порядке возрастания на основе выбранного
; столбца "field". В противном случае отсортируйте его в порядке 
; убывания:
either order = "ascending" [
    sort/compare blocked func [a b] [(at a field) < (at b field)]
][
    sort/compare blocked func [a b] [(at a field) > (at b field)]
]

; Создаём блок GUI с текстовыми заголовками шириной около 200 пикселей: 

gui: [
    backdrop white
    across
    style header text black 200
    header "SKU:" header "Cost:" header "Qty:" return
]

; Для каждой строки в блоке блоков добавим 3 поля, содержащих данные, 
; в макет графического интерфейса:

foreach row blocked [
    append gui compose [
        field (form row/1) field (form row/2) field (form row/3) return
    ]
]

; Отобразим GUI макет:

view center-face layout gui

Вот вся программа без комментариев:

REBOL [title: "Sort Inventory"]
inventory: load %inventory.txt
blocked: copy []
foreach [sku cost qty] inventory [
    append/only blocked reduce [
        sku 
        to-money cost
        to-integer qty
    ]
]
field-name: request-list "Choose Field To Sort By:" [
    "sku" "cost" "qty"
]
field: select ["sku" 1 "cost" 2 "qty" 3] field-name
order: request-list "Ascending or Descending:" ["ascending" "descending"]
either order = "ascending" [
    sort/compare blocked func [a b] [(at a field) < (at b field)]
][
    sort/compare blocked func [a b] [(at a field) > (at b field)]
]
gui: [
    backdrop white
    across
    style header text black 200
    header "SKU:" header "Cost:" header "Qty:" return
]
foreach row blocked [
    append gui compose [
        field (form row/1) field (form row/2) field (form row/3) return
    ]
]
view center-face layout gui

5.8 Минимальная розничная касса и система отчётов о продажах

Пример ниже представляет собой простую программу POS ("Point of Sale" или кассовый аппарат розничной торговли):

REBOL [title: "Minimal Cash Register"]
view gui: layout [

    ; Создайте новый стиль виджета с именем "fld". Это просто 
    ; текстовое поле (field), 80 пикселей в ширину:
style fld field 80

; Разместите последовательные виджеты рядом друг с другом:

across

; Вот 3 текстовых метки и 3 текстовых поля для ввода, чтобы 
; пользователи могли вводить имя кассира (cashier), название 
; позиции (item) и цену (price). Поля ввода текста помечены 
; соответствующим образом:

text "Cashier:"   cashier: fld 
text "Item:"      item: fld 
text "Price:"     price: fld [

    ; Когда пользователь вводит цену, выполните следующие операции:
    ; Во-первых, убедитесь, что введённая цена может быть 
    ; конвертирована в денежное выражение. Если нет, предупредите 
    ; пользователя сообщением об ошибке и выйдите из блока действий:
if error? try [to-money price/text] [alert "Price error" return]

; В противном случае добавим текст цитируемого элемента и 
; цену в виджет области (через несколько пробелов):

append a/text reduce [mold item/text "    " price/text newline]

; Затем сотрём текст в полях ввода товара и цены:

item/text: copy "" price/text: copy ""

; Теперь вычислите промежуточный итог всех введённых 
; элементов. Обновите поля промежуточной суммы, налога и 
; общей суммы соответствующими вычисленными значениями:
sum: 0
foreach [item price] load a/text [sum: sum + to-money price]
subtotal/text: form sum
tax/text: form sum * .06
total/text: form sum * 1.06 

; Верните курсор в поле "item" и обновите отображение:

focus item
show gui

]

; В новой строке поместите виджет области размером 600 пикселей в 
; ширину и высотой 300 пикселей, помеченный буквой "a":
return
a: area 600x300

; В новой строке ещё 3 виджета с текстом и полями, помеченные 
; соответствующим образом, и кнопка с текстом "Save" (сохранить):

return
text "Subtotal:"   subtotal: fld 
text "Tax:"        tax: fld 
text "Total:"      total: fld
btn "Save" [

    ; Когда кнопка нажата, сделайте следующее: сначала создайте 
    ; цитируемый блок элементов в виджете области. Замените все 
    ; символы новой строки пробелом, чтобы все данные были в 
    ; одной строке. Обозначьте весь фрагмент данных "items":
items: replace/all (mold load a/text) newline " "

; Добавим данные о товарах, имя кассира и дату в файл 
; sales.txt (все они разделены символами возврата каретки 
; (новой строки)):
write/append %sales.txt rejoin [
    items newline cashier/text newline now/date newline
]

; Сотрём весь текст в каждом виджете поля, сотрём текст в виджете области и обновите отображение:
clear-fields gui
a/text: copy ""             
show gui             

]
]

Приведённый ниже код сообщает общую сумму продаж кассового аппарата за текущий день:

REBOL [title: "Daily Total"]

; Прочтите каждую строку блоком  из файла "sales.txt" и назовём её 
; "sales" (продажи): 

sales: read/lines %sales.txt

; Обозначим переменную, в которой будет храниться вычисленная сумма, 
; и сначала установим для неё значение $0:

sum: $0

; Используйте цикл foreach, чтобы просмотреть каждые 3 элемента 
; данных о продажах (помните, что каждая группа элементов, кассир и 
; дата были разделены новой строкой в приведённом выше коде):

foreach [items cashier date] sales [

    ; Если сегодняшняя дата присутствует в любой сохранённой записи данных:

    if now/date = to-date date [

       ; Используем цикл foreach для просмотра значений в данных "items":

        foreach [item price] load items [

            ; Добавляем значение цены к переменной sum:

            sum: sum + to-money price

        ]
    ]
]

; Информируем пользователя вычисленным sum:

alert rejoin ["Total sales today: " sum]

В отчёте ниже показано общее количество всех товаров, проданных в любой выбранный день любым выбранным кассиром:

REBOL [title: "Cashier Report"]

; Начинаем с запроса выбранной даты и имени кассира

report-date: request-date
report-cashier: request-text/title "Cashier:" 

; Читаем строки файла sales.txt:

sales: read/lines %sales.txt

; Готовим переменную для суммы:

sum: $0

; Используем цикл foreach для просмотра каждой записи о продажах:

foreach [items cashier date] sales [

    ; Выполним две условные оценки, чтобы выбрать любые записи, в 
    ; которых совпадают имя кассира и выбранная дата:
if ((report-cashier = cashier) and (report-date = to-date date)) [

    ; Для любой совпадающей записи добавляем стоимость к сумме (sum):

    foreach [item price] load items [
        sum: sum + to-money price
    ]

]
]

; Информируем пользователя о результате работы:

alert rejoin ["Total for " report-cashier " on " report-date ": " sum]

Вот все 3 программы без комментариев:

REBOL [title: "Minimal Cash Register"]
view gui: layout [
    style fld field 80
    across
    text "Cashier:"   cashier: fld 
    text "Item:"      item: fld 
    text "Price:"     price: fld [
        if error? try [to-money price/text] [alert "Price error" return]
        append a/text reduce [mold item/text "    " price/text newline]
        item/text: copy "" price/text: copy ""
        sum: 0
        foreach [item price] load a/text [sum: sum + to-money price]
        subtotal/text: form sum
        tax/text: form sum * .06
        total/text: form sum * 1.06 
        focus item
        show gui
    ]
    return
    a: area 600x300
    return
    text "Subtotal:"   subtotal: fld 
    text "Tax:"        tax: fld 
    text "Total:"      total: fld
    btn "Save" [
        items: replace/all (mold load a/text) newline " "
        write/append %sales.txt rejoin [
            items newline cashier/text newline now/date newline
        ]
        clear-fields gui
        a/text: copy ""             
        show gui             
    ]
]


REBOL [title: "Daily Total"]
sales: read/lines %sales.txt
sum: $0
foreach [items cashier date] sales [
    if now/date = to-date date [
        foreach [item price] load items [
            sum: sum + to-money price
        ]
    ]
]
alert rejoin ["Total sales today: " sum]


REBOL [title: "Cashier Report"]
report-date: request-date
report-cashier: request-text/title "Cashier:" 
sales: read/lines %sales.txt
sum: $0
foreach [items cashier date] sales [
    if ((report-cashier = cashier) and (report-date = to-date date)) [
        foreach [item price] load items [
            sum: sum + to-money price
        ]
    ]
]
alert rejoin ["Total for " report-cashier " on " report-date ": " sum]

5.9 Электронное письмо

Этот пример представляет собой законченный графический почтовый клиент, который можно использовать для чтения и отправки сообщений. Он имеет кнопку "Server settings", которая позволяет пользователю вводить все необходимые настройки учётной записи электронной почты:

REBOL [Title: "Little Email Client"] 

; Строка ниже создаёт окно графического интерфейса программы:

view layout [

    ; Эта строка добавляет текстовую метку к графическому интерфейсу:

    h1 "Send Email:"

    ; Эта строка добавляет кнопку в графический интерфейс:

    btn "Server settings" [

        ; При нажатии кнопки запускаются следующие строки. Эти строки 
        ; устанавливают всю информацию об учётной записи электронной 
        ; почты пользователя, необходимую для отправки и получения 
        ; электронной почты. Настройки берутся у пользователя с 
        ; помощью функции "request-text" (запрос-текст) и назначаются 
        ; их соответствующим местоположениям в системе REBOL:
system/schemes/default/host: request-text/title "SMTP Server:"
system/schemes/pop/host:     request-text/title "POP Server:"
system/schemes/default/user: request-text/title "SMTP User Name:"
system/schemes/default/pass: request-text/title "SMTP Password:"
system/user/email: to-email request-text/title "Your Email Addr:"

]

; Эта строка создаёт поле ввода текста, содержащее текст по 
; умолчанию "recipient@website.com". Этому виджету присваивается 
; имя "address":
address: field "recipient@website.com"

; Вот ещё одно поле ввода текста для темы электронного письма:

subject: field "Subject" 

; Эта строка создаёт большую, многострочную область ввода текста 
; для основного текста электронного письма:

body: area "Body"

; Вот кнопка со словом "send" (отправить). Функции внутри его 
; блока действий выполняются всякий раз, когда нажимается кнопка:
btn "Send" [

    ; Эта строка выполняет большую часть работы. Он использует 
    ; функцию REBOL "send" для отправки электронного письма. 
    ; Функция "send" с уточнением "/subject" принимает три 
    ; параметра. Ей передаётся текущий текст, содержащийся в 
    ; каждом поле, созданным выше (именуемое "address/text", 
    ; "body/text" и "subject/text"). Функция "to-email" 
    ; гарантирует, что текст адреса обрабатывается как значение 
    ; данных электронной почты:
send/subject (to-email address/text) body/text subject/text

; Эта строка информирует пользователя о завершении отправки письма:

alert "Message Sent."

]

; Вот еще одна текстовая метка:

h1 "Read Email:"

; Вот ещё одно поле для ввода текста. Здесь вводится информация об 
; учётной записи электронной почты пользователя.

mailbox: field "pop://user:pass@site.com"

; У этой последней кнопки есть блок действий, который читает 
; сообщения из указанного почтового ящика. Требуется всего одна 
; строчка кода:

btn "Read" [

; Функция "to-url" гарантирует, что текст в поле почтового 
; ящика обрабатывается как URL-адрес. Содержимое почтового 
; ящика читается и отображается с помощью встроенного 
; текстового редактора REBOL:

editor read to-url mailbox/text

]
]

Вот вся программа без комментариев:

REBOL [Title: "Little Email Client"] 
view layout[
    h1 "Send:"
    btn "Server settings" [
        system/schemes/default/host: request-text/title "SMTP Server:"
        system/schemes/pop/host:     request-text/title "POP Server:"
        system/schemes/default/user: request-text/title "SMTP User Name:"
        system/schemes/default/pass: request-text/title "SMTP Password:"
        system/user/email: to-email request-text/title "Your Email Addr:"
    ]
    a: field "user@website.com"
    s: field "Subject" 
    b: area
    btn "Send"[
        send/subject to-email a/text b/text s/text
        alert "Sent"
    ]
    h1 "Read:" 
    f: field "pop://user:pass@site.com"
    btn "Read" [editor read to-url f/text]
]

5.10 Планировщик

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

REBOL [Title: "Scheduler"] 
view center-face gui: layout [
    btn "Date" []
    date: field
    text "Event Title:"
    event: field
    text "Time:"
    time: field
    text "Notes:"
    notes: field
    btn "Add Appoinment" []
    a: area
    btn "View Schedule" []
]

Вот финальный код, который запускает всё приложение для планирования. Вся эта программа на самом деле не более сложна, чем инвентарь, кассовый аппарат и другие примеры графического интерфейса, которые вы видели выше. Есть ещё несколько виджетов и, конечно же, запросчик даты, позволяющий пользователю выбирать дату, но тот же графический интерфейс, чтение/запись файлов и управление блоками foreach перепрофилированы для ввода, сохранения, загрузки и отображения данных календаря:

REBOL [Title: "Scheduler"]
view center-face gui: layout [
    btn "Date" [date/text: form request-date  show date]
    date: field
    text "Event Title:"
    event: field
    text "Time:"
    time: field
    text "Notes:"
    notes: field
    btn "Add Appoinment" [
        write/append %appts.txt rejoin [
            mold date/text newline
            mold event/text newline
            mold time/text newline
            mold notes/text newline 
        ]
        date/text: "" event/text: "" time/text: "" notes/text: ""
        show gui
        alert "Added"
    ]
    a: area
    btn "View Schedule" [
        today: form request-date
        foreach [date event time notes] load %appts.txt [
            if date = today [
                a/text: copy ""
                append a/text form rejoin [
                    date newline
                    event newline
                    time newline
                    notes newline newline
                ]
                show a
            ]
        ]
    ]
]

5.11 База данных деталей

Вот приложение, которое позволяет рабочим добавлять, удалять, редактировать и просматривать изготовленные детали. Это ещё одно типичное приложение для хранения простых текстовых полей. Его можно использовать, как показано здесь, для сохранения информации о деталях, но, отредактировав всего несколько строк кода и текстовых меток, его можно легко адаптировать для хранения информации о контактах или любого другого типа связанных полей заблокированных текстовых данных. Обязательно обратите особое внимание на то, как используется виджет текстового списка, и особенно обратите внимание на то, как функция "pick" используется для выбора последовательных значений после найденного индекса:

REBOL [title: "Parts]

; Строка ниже записывает новый пустой файл данных на жёсткий диск, 
; если он ещё не существует. Если файл уже существует, эта функция 
; просто записывает в него пустую строку (т.е. оставляет файл в покое):
write/append %data.txt ""

; Эта строка загружает все сохранённые записи из файла базы данных:

database: load %data.txt


; Вот окно графического интерфейса:

view center-face gui: layout [

    ; Вот текстовая метка, чтобы проинформировать пользователя:

    text "Load an existing record:"

    ; Этот текстовый список (text-list) отображает отсортированный по 
    ; алфавиту список имён, найденных в базе данных (каждый четвёртый 
    ; элемент). Числовая пара указывает размер пикселя виджета:
name-list: text-list blue 400x100 data (sort extract database 4) [

    ; Следующая строка включена во избежание возможных ошибок. 
    ; При щелчке по элементу в текстовом списке мы сначала  
    ; проверяем, что выбранные данные (представленные словом 
    ; "value" (значение)) НЕ равны нулю. Если это так, выйдите из 
    ; блока действий виджета (слово "return" завершает процедуру 
    ; действия текстового списка):
if value = none [return]

; Следующий код находит выбранную деталь в загруженной базе 
; данных. Поля отображаются в графическом интерфейсе, затем 
; настраиваются так, чтобы отображать найденную деталь и 
; каждый из трёх элементов после неё в базе данных (название 
; детали (name) = поле 1, производитель (manufacturer) = 
; поле 2, каталожный номер (SKU) = поле 3, 
; примечание (notes) = поле 4):
marker: index? find database value
n/text: pick database marker
a/text: pick database (marker + 1)
p/text: pick database (marker + 2)
o/text: pick database (marker + 3)

; Обновите дисплей, чтобы отобразить изменённые текстовые 
; поля (обратите внимание на метку "gui", определённую выше - 
; она относится ко всему макету графического интерфейса 
; пользователя):

show gui
]

; Вот поля для отображения текста и несколько текстовых меток, 
; чтобы показать, что следует вводить в каждое поле:

text "Part Name:"       n: field 400
text "Manufacturer:"    a: field 400
text "SKU:"             p: field 400
text "Notes:"           o: area  400x100

; Слово "across" "в ширину" добавляет виджеты в графический 
; интерфейс рядом друг с другом, а не друг под другом, что 
; является поведением по умолчанию (следующие 3 кнопки появятся 
; рядом друг с другом):

across

; Вот кнопка графического интерфейса, позволяющая пользователю 
; сохранять содержимое текстовых полей в базе данных:

btn "Save" [

; При нажатии этой кнопки убедитесь, что в обязательном поле 
; содержится текст. Если нет, уведомите пользователя, а затем 
; выйдите из подпрограммы этой кнопки (слово "return" 
; завершает блок действия кнопки сохранения):
if n/text = "" [alert "You must enter a part name." return]

; Теперь просмотрите каждый четвёртый элемент в базе данных, 
; чтобы проверить, существует ли эта деталь. Если да, дайте 
; пользователю возможность перезаписать эту запись. Если они 
; ответят утвердительно, удалите старую запись из базы 
; данных ("remove/part" (удалить/часть) удаляет 4 элемента в 
; том месте, где находится выбранная часть). Если 
; пользователь не отвечает, выйдите из процедуры кнопки 
; сохранения:
if find (extract database 4) n/text [
    either true = request "Overwrite existing record?" [
       remove/part (find database n/text) 4
    ] [
       return
    ]
]

; Теперь обновите базу данных новыми данными и запишите их на
; жёсткий диск. Функция "Repend" добавляет вычисленные 
; переменные в скобки (в данном случае блок из 4 отдельных 
; текстовых строк, содержащихся в полях графического 
; интерфейса пользователя) к база данных:
save %data.txt repend database [n/text a/text p/text o/text]

; Обновите текстовый список (text-list), чтобы отобразить добавленную запись:
name-list/data: sort (extract copy database 4)
show name-list
]

; Эта кнопка позволяет пользователю очистить экран и удалить 
; запись:

btn "Delete" [

; При нажатии этой кнопки приведённый ниже код даёт 
; пользователю возможность удалить выбранную запись. Если 
; пользователь выбирает "yes" (да), функция "remove/part" 
; (удалить/часть) удаляет 4 элемента из базы данных в том 
; месте, где находится выбранная часть. База данных 
; сохраняется, и текстовые поля очищаются ("do-face" 
; запускает блок действий виджета "clear-button" выше, чтобы 
; очистить поля графического интерфейса пользователя), затем 
; список названий частей обновляется:

if true = request rejoin ["Delete " n/text "?"] [
    remove/part (find database n/text) 4
    save %data.txt database
    do-face clear-button 1
    name-list/data: sort (extract copy database 4)
    show name-list
]
]

; Эта кнопка позволяет пользователю ввести новую запись:
clear-button: btn "New" [

    ; При щелчке по этой кнопке установим для значения текста 
    ; каждого поля пустую строку:

    n/text: copy  ""
    a/text: copy  ""
    p/text: copy  ""
    o/text: copy  "" 

    ; Как всегда, при изменении каких-либо данных в графическом 
    ; интерфейсе необходимо обновить экран: 

    show gui
]
]

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

REBOL [title: "Contacts"]
write/append %data.txt ""
database: load %data.txt
view center-face gui: layout [
    text "Existing Contacts:"
    name-list: text-list blue 400x100 data sort (extract database 4) [
        if value = none [return]
        marker: index? find database value
        n/text: pick database marker
        a/text: pick database (marker + 1)
        p/text: pick database (marker + 2)
        o/text: pick database (marker + 3)
        show gui
    ]
    text "Name:"       n: field 400
    text "Address:"    a: field 400
    text "Phone:"      p: field 400
    text "Notes:"      o: area  400x100
    across
    btn "Save" [
        if n/text = "" [alert "You must enter a name." return]
        if find (extract database 4) n/text [
            either true = request "Overwrite existing record?" [
               remove/part (find database n/text) 4
            ] [
               return
            ]
        ]
        save %data.txt repend database [n/text a/text p/text o/text]
        name-list/data: sort (extract copy database 4)
        show name-list
    ]
    btn "Delete" [
        if true = request rejoin ["Delete " n/text "?"] [
            remove/part (find database n/text) 4
            save %data.txt database
            do-face clear-button 1
            name-list/data: sort (extract copy database 4)
            show name-list
        ]
    ]
    clear-button: btn "New" [
        n/text: copy  ""
        a/text: copy  ""
        p/text: copy  ""
        o/text: copy  ""
        show gui
    ]
]

5.12 Часы и отчёт о заработной плате

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

REBOL [title: "Time Clock"]

; Сначала проверим, был ли создан файл сотрудников (employees). Если 
; нет, создадим его. Этот список будет отображаться в виджете 
; текстового списка. Он содержит элемент "(Add New ...)" (добавить), 
; который будет использоваться, чтобы позволить пользователям 
; добавлять новые имена в список:

unless exists? %employees [write %employees {"John Smith" "(Add New...)"}]

; Инициализируйте значение текущего сотрудника (cur-employee) пустой строкой:

cur-employee: copy ""

view center-face layout [

    ; Вот виджет текстового списка, в котором отображаются имена 
    ; сотрудников, отсортированные по алфавиту. Он помечен как "tl1":
tl1: text-list 400x400 data sort load %employees [

    ; Когда пользователь выбирает имя из списка, установим 
    ; переменную "cur-employee" для хранения выбранного имени:
cur-employee: value

; Если пользователь выбрал "(Add New...)" (добавить),
; запустите код, необходимый для добавления имени в список 
; сотрудников:
if cur-employee = "(Add New...)" [

    ; Сначала запросите имя у пользователя и сохраните 
    ; указанное в кавычках значение в файле %employee:

    write/append %employees mold trim request-text/title "Name:"

    ; Перезагрузите новый список сотрудников в виджет
    ; текстового списка (text-list) и обновите отображение:

    tl1/data: sort load %employees  show tl1
]
]

; Виджет "key" не отображает что либо в графическом интерфейсе. 
; Он просто ждёт нажатия клавиши и выполняет желаемые действия:
key #"^~" [

    ; Сначала получаем имя удаляемого сотрудника и присвоим  ему 
    ; метку переменной "del-emp":

    del-emp: copy to-string tl1/picked

    ; Затем загружаем список сотрудников и присвоим ему ярлык 
    ; "temp-emp":

    temp-emp: sort load %employees

    ; Подтверждаем, что пользователь действительно хочет удалить сотрудника:

    if true = request/confirm rejoin ["REMOVE " del-emp "?"] [

        ; Найдём имя сотрудника в списке сотрудников, удалим 
        ; имя и присвойте метку переменной "new-list" 
        ; сокращённому списку сотрудников:

        new-list: head remove/part find temp-emp del-emp 1

        ; Сохраняем сокращённый список сотрудников в файл 
        ; %employee:
save %employees new-list

; Обновим виджет текстового списка и предупредим 
; пользователя о завершении действия:

tl1/data: sort load %employees show tl1
alert rejoin [del-emp " removed."]
]
]

; Вот кнопка, позволяющая сотрудникам приходить и уходить со смены:

btn "Clock IN/OUT" [

; Сначала убедимся, что имя выбрано. Если нет, предупредим 
; пользователя и выйдем из процедуры действия кнопки:

if ((cur-employee = "") or (cur-employee = "(Add New...)")) [
alert "You must select your name." return
]

; Объединим указанные в кавычках значения имени и времени в 
; квадратных скобках и начнём с символа возврата каретки. 
; Присвойте этому тексту метку переменной "record" (запись):

record: rejoin [
newline {[} mold cur-employee { "} mold now {"]}
]

; Подтвердим у сотрудника правильность выбранного имени и 
; времени:

either true = request/confirm rejoin [
record " -- IS YOUR NAME AND THE TIME CORRECT?"
] [

; Эта процедура сохраняет резервную копию текущего табеля 
; в папке с именем "clock_history". Сначала создаётся 
; папка, если её не существует:
make-dir %./clock_history/

; Затем создаётся имя файла путём повторного объединения 
; в имени папки, текущего дня и времени. Заменяются 
; недопустимые символы имени файла (например, "/" и ":" 
; не могут использоваться в именах файлов в большинстве 
; операционных систем). Текст существующего файла 
; %time_sheet.txt записывается в имя файла резервной 
; копии с отметкой о дате:
write rejoin [
    %./clock_history/ 
    replace/all replace form now "/" "--" ":" "_"
] read %time_sheet.txt

; Запишем новую запись в файл %time_sheet.txt:

write/append %time_sheet.txt record

; Предупредим пользователя о завершении операции:

alert rejoin [
    uppercase copy cur-employee ", YOUR TIME IS LOGGED."
]
] [

; Если пользователь отрицательно ответил на подтверждение 
; выше, предупредите пользователя, что действие отменено, 
; и выйдите из процедуры:

alert "CANCELED"
return

]
]
]

Вот вся программа без комментариев:

REBOL [title: "Time Clock"]
unless exists? %employees [write %employees {"John Smith" "(Add New...)"}]
cur-employee: copy ""
view center-face layout [
    tl1: text-list 400x400 data sort load %employees [
        cur-employee: value
        if cur-employee = "(Add New...)" [
            write/append %employees mold trim request-text/title "Name:"
            tl1/data: sort load %employees show tl1
        ]
    ]
    key #"^~" [
        del-emp: copy to-string tl1/picked
        temp-emp: sort load %employees
        if true = request/confirm rejoin ["REMOVE " del-emp "?"] [
            new-list: head remove/part find temp-emp del-emp 1
            save %employees new-list
            tl1/data: sort load %employees show tl1
            alert rejoin [del-emp " removed."]
        ]
    ]
    btn "Clock IN/OUT" [
        if ((cur-employee = "") or (cur-employee = "(Add New...)")) [
            alert "You must select your name." return
        ]
        record: rejoin [
            newline {[} mold cur-employee { "} mold now {"]}
        ]
        either true = request/confirm rejoin [
            record " -- IS YOUR NAME AND THE TIME CORRECT?"
        ] [
            make-dir %./clock_history/
            write rejoin [
                %./clock_history/ 
                replace/all replace form now "/" "--" ":" "_"
            ] read %time_sheet.txt
            write/append %time_sheet.txt record
            alert rejoin [
                uppercase copy cur-employee ", YOUR TIME IS LOGGED."
            ]
        ] [
            alert "CANCELED"
            return
        ]
    ]
]

Вот программа, которая создаёт отчёты о заработной плате и о часах, отработанных сотрудниками, зарегистрированные указанной выше программой, между любыми выбранными датами начала и окончания:

REBOL [title: "Payroll Report"]

; Запросим дату начала и окончания у пользователя:

timeclock-start-date: request-date
timeclock-end-date: request-date

; Инициализируем переменную "totals" (итого) пустой строкой. Эта 
; строка будет использоваться для сбора (агрегирования) всего текста 
; выходного отчёта:

totals: copy ""

; Загружаем список сотрудников и назначаем ему метку "names":

names: load %employees

; Загружаем всю сохранённую информацию табеля рабочего времени и 
; присвоим ей метку "log":

log: load %time_sheet.txt

; Используем цикл foreach для сбора информации журнала для каждого сотрудника:

foreach name names [

    ; Не пытаемся собрать информацию для записи "(Add New...)" в 
    ; списке:

    if name <> "(Add New...)" [

        ; Создаём новый блок для хранения информации отчёта для 
        ; текущего сотрудника, изначально содержащий только имя 
        ; сотрудника. Присвоим блоку метку переменной "times":
times: copy reduce [name]

; Используйте foreach для просмотра каждой записи в журнале 
; табеля рабочего времени:
foreach record log [

    ; Если имя текущего сотрудника совпадает с именем в 
    ; текущей записи в файле журнала, добавьте запись в отчёт:
if name = log-name: record/1 [

    ; Разделим дату и время в текущей записи 
    ; даты/времени (дата/время - второй элемент в каждой 
    ; записи журнала). Присвойте результату переменную 
    ; "date-time":

    date-time: parse record/2 "/"

    ; Преобразуем часть даты результата выше в значение 
    ; даты REBOL. Присвоим этому значению метку "log-date":
log-date: to-date date-time/1

; Преобразуем временную часть результата выше в 
; значение времени REBOL. Присвойте этому значению 
; метку "log-time":

log-time: to-time first parse date-time/2 "-"

; Если дата в текущей записи расписания находится в 
; пределах периода начальной и конечной даты, 
; выбранного пользователем, добавим текущую запись в 
; отчёт сотрудника:

if (
    (log-date >= timeclock-start-date) and 
    (log-date <= timeclock-end-date)
) [
    append times log-date
    append times log-time
] 
]
]

; Добавьте имя сотрудника и символ возврата каретки к 
; окончательному тексту отчёта:

append totals rejoin [name ":" newline]

; Инициализировать переменную суммы в значение 0:

total-hours: 0

; Просмотрим весь собранный блок "times", созданный выше, 
; чтобы вычислить время окончания - время начала. Помните, 
; что первым элементом в блоке было имя пользователя, поэтому 
; начните этот процесс со второго элемента:
foreach [in-date in-time out-date out-time] (at times 2) [

    ; Добавим красиво отформатированный текст, содержащий дни 
    ; и время начала и окончания, к окончательному тексту 
    ; отчёта:
append totals rejoin [
    newline 
    "    in: " in-date ", " in-time 
    "  out: " out-date ", " out-time "    "
]

; Добавим общее количество отработанных часов к сумме
; "total-hours" (общего количества часов). Обернём эту 
; процедуру в проверку ошибок, на всякий случай, если не 
; указано время начала или окончания:

if error? try [
    total-hours: total-hours + (out-time - in-time)
] [
    alert rejoin [
        "Missing login or Missing logout: " name
    ]
]
]

; Добавим общее количество часов сотрудника в окончательный 
; отчёт вместе с некоторым составным текстом и символами 
; возврата каретки, чтобы получить хорошо отформатированную 
; распечатку:
append totals rejoin [
    newline newline
    "    TOTAL HOURS: " total-hours
    newline newline newline
]

]
]

; Запишем окончательный отчёт в файл с именем, содержащим даты начала 
; и окончания, выбранные для отчёта, и откройте его с помощью 
; приложения Window "Блокнот" (для этого вы можете просто 
; использовать функцию REBOL "editor"):

write filename: copy rejoin [
%timeclock_report-- timeclock-start-date 
"_to_" timeclock-end-date ".txt"
] totals
call/show rejoin ["notepad " to-local-file filename]

Вот вся программа без комментариев:

REBOL [title: "Payroll Report"]
timeclock-start-date: request-date
timeclock-end-date: request-date
totals: copy ""
names: load %employees
log: load %time_sheet.txt
foreach name names [
    if name <> "(Add New...)" [
        times: copy reduce [name]
        foreach record log [
            if name = log-name: record/1 [
                date-time: parse record/2 "/"
                log-date: to-date date-time/1
                log-time: to-time first parse date-time/2 "-"
                if (
                    (log-date >= timeclock-start-date) and 
                    (log-date <= timeclock-end-date)
                ) [
                    append times log-date
                    append times log-time
                ] 
            ]
        ]
        append totals rejoin [name ":" newline]
        total-hours: 0
        foreach [in-date in-time out-date out-time] (at times 2) [
            append totals rejoin [
                newline 
                "    in: " in-date ", " in-time 
                "  out: " out-date ", " out-time "    "
            ]
            if error? try [
                total-hours: total-hours + (out-time - in-time)
            ] [
                alert rejoin [
                    "Missing login or Missing logout: " name
                ]
            ]
        ]
        append totals rejoin [
            newline newline
            "    TOTAL HOURS: " total-hours
            newline newline newline
        ]
    ]
]
write filename: copy rejoin [
    %timeclock_report-- timeclock-start-date 
    "_to_" timeclock-end-date ".txt"
] totals
call/show rejoin ["notepad " to-local-file filename]

5.13 Блоггер

Эта программа позволяет пользователям создавать и добавлять записи на страницу онлайн-блога. В графическом интерфейсе есть текстовые поля, которые позволяют пользователю вводить заголовок, ссылку и текст блога, а также кнопку для выбора файла изображения, который будет загружен и включён в запись блога. При нажатии кнопки "Upload" (Загрузить) создаётся файл HTML-кода, который загружается на веб-сервер пользователя вместе с изображением (вы узнаете больше о HTML позже в этом руководстве, а пока просто обратите внимание на то, что связка стороннего HTML-кода объединяется вместе со значениями, введёнными пользователем). Обязательно отредактируйте переменные ftp-url и html-url, чтобы они представляли фактическую информацию об учётной записи пользователя (для запуска этого примера вам понадобятся имя пользователя, пароль и папка на веб-сервере).

REBOL [title: "Blogger"]

; Сохраняем имя файла блога в переменной:

page: "blog.html"

; Сохраняем адрес учётной записи FTP с именем пользователя и паролем:

ftp-url: ftp://user:pass@site.com/public_html/folder/

; Сохраняем URL-адрес веб-страницы блога в переменной:

html-url: join http://site.com/folder/ page

; Создаём пустое изображение размером 1 пиксель для загрузки, если 
; пользователь не хочет загружать какое-либо изображение для данной 
; записи в блоге:

save/png %dot.png to-image layout/tight [box white 1x1]

; Вот графический интерфейс:

view center-face gui: layout [

    ; Отобразите URL-адрес блога в графическом интерфейсе (функция 
    ; "form" преобразует тип данных URL-адреса, созданный выше, в 
    ; строковый тип данных, необходимый для виджета h2):

    h2 (form html-url)

    ; Вот описательный заголовок и поле для ввода текста, где 
    ; пользователь может ввести заголовок для блога. Поле помечено 
    ; переменным словом "t":

    text "Title:"       t: field 400

    ; Заголовок и поле для ввода URL-ссылки для включения в блог:

    text "Link:"        l: field 400

    ; Заголовок и кнопка выбора изображения для включения в блог. При 
    ; нажатии кнопки появляется запросчик файла. Текст кнопки 
    ; настроен на отображение выбранного имени файла:
text "Image:"       i: btn 400 [i/text: request-file show i]

; Поле заголовка и текста для текста блога:

text "Text:"        x: area  400x100

; Размещаем на экране горизонтально (across) все следующие виджеты:

across

; Вот кнопка графического интерфейса, которая выполняет большую 
: часть работы:

btn "Upload" [

    ; Когда кнопка нажата, сначала попробуем прочитать 
    ; существующий текст блога (источник HTML). Если он доступен 
    ; для чтения, присвойте этим данным метку переменной 
    ; "existing-text" (существующий-текст). Если он не читается, 
    ; создайте папку и пустой файл блога на FTP-сервере, затем 
    ; присвойте переменной "existing-text" пустую строку:

    if error? try [existing-text: read html-url] [
        make-dir ftp-url
        write (join ftp-url page) ""
        existing-text: copy ""
    ]

    ; Назначьте переменной "picture" (изображение) выбранное выше 
    ; имени файла изображения ("last split-path" удаляет часть 
    ; пути к каталогу в имени файла (т.е. %/C/folder/myimage.jpg 
    ; будет обрезан до %myimage.jpg)):

    picture: last split-path to-file i/text

    ; Запишите изображение на FTP-сервер:

    write/binary (join ftp-url picture) (read/binary to-file i/text)

    ; Напишем следующий объединённый HTML-код в файл блога на 
    ; FTP-сервере:
write (join ftp-url page) rejoin [

    ; Сначала добавим заголовок, заключённый в теги "h1":

    {<h1>} t/text {</h1>}

    ; Теперь добавьте ссылку на загруженное изображение, за 
    ; которой следуют 2 тега разрыва (новой) строки HTML:
{<img src="} picture {"><br><br>}

; Добавим дату и время, а затем 5 пробелов, без разрывов 
; (переноса) строки:

now/date { } now/time { &nbsp; &nbsp; }

; Добавьте тег ссылки (поскольку выше не было разрывов 
; строк, эта ссылка отображается в той же строке, что и 
; дата и время в блоге), за которым следуют 2 разрыва 
; строки:

{<a href="} l/text {">} l/text {</a><br><br>}

; Поместите текст блога в таблицу, занимающую 80% экрана. 
; Отцентрируйте таблицу на экране (так, чтобы она была 
; сделана с отступом). Используйте тег "pre", чтобы 
; убедиться, что текст отформатирован в точности так, как 
; он был введён пользователем (с возвратами каретки и 
; пробелами, введёнными в виджет области выше), и 
; используйте тег "strong", чтобы выделить текст 
; полужирным шрифтом. После всего этого раздела HTML 
; поставьте разрыв строки, а затем тег "hz" 
; (разделительная линия):
{<center><table width=80%><tr><td><pre><strong>}
    x/text 
{</strong></pre></td></tr></table></center><br><hr>}

; Добавьте уже существующий HTML-код блога под новой записью:

existing-text

]

; После обновления блога откройте URL-адрес блога в браузере:

browse html-url

]

; Добавьте кнопку графического интерфейса, чтобы открыть 
; URL-адрес блога в браузере:
btn "View" [browse html-url]

; Эта кнопка графического интерфейса пользователя позволяет 
; пользователю редактировать существующий файл блога (данные 
; HTML, созданные этой программой), используя встроенный 
; текстовый редактор REBOL. Помните, что редактор REBOL имеет 
; возможность читать и сохранять данные непосредственно в/из 
; файлов FTP, поэтому этот редактор позволяет пользователю 
; фактически редактировать и ВНЕСТИ ИЗМЕНЕНИЯ в файл блога на 
; FTP-сервере:

btn "Edit" [editor (join ftp-url page)]

]

Вот вся программа без комментариев:

REBOL [title: "Blogger"]
page: "blog.html"
ftp-url: ftp://user:pass@site.com/public_html/folder/
html-url: join http://site.com/folder/ page
save/png %dot.png to-image layout/tight [box white 1x1]  ; blank image
view center-face gui: layout [
    h2 (form html-url)
    text "Title:"       t: field 400
    text "Link:"        l: field 400
    text "Image:"       i: btn 400 [i/text: request-file show i]
    text "Text:"        x: area  400x100
    across
    btn "Upload" [
        if error? try [existing-text: read html-url] [
            make-dir ftp-url
            write (join ftp-url page) ""
            existing-text: copy ""
        ]
        picture: last split-path to-file i/text
        write/binary (join ftp-url picture) (read/binary to-file i/text)
        write (join ftp-url page) rejoin [
            {<h1>} t/text {</h1>}
            {<img src="} picture {"><br><br>}
            now/date { } now/time { &nbsp; &nbsp; }
            {<a href="} l/text {">} l/text {</a><br><br>}
            {<center><table width=80%><tr><td><pre><strong>}
                x/text 
            {</strong></pre></td></tr></table></center><br><hr>}
            existing-text
        ]
        browse html-url
    ]
    btn "View" [browse html-url]
    btn "Edit" [editor (join ftp-url page)]
]

5.14 Групповой чат FTP

Этот пример представляет собой простое приложение для чата, которое позволяет пользователям отправлять мгновенные текстовые сообщения туда и обратно через Интернет. "Комнаты" ("rooms") чата создаются путём динамического создания, чтения, добавления и сохранения текстовых файлов через ftp (для использования программы вам потребуется доступ к доступному ftp-серверу: адрес ftp, имя пользователя и пароль. Больше ничего не требуется настраиваться на сервере). Это обеспечивает неограниченное количество приватных пространств для разговоров, которые можно использовать для соединения любого количества людей или команд в группе. Эта программа полностью запускается в консоли REBOL (т.е. нет графического пользовательского интерфейса - это весь печатный текст), поэтому её можно использовать даже на простых устройствах, не поддерживающих графический интерфейс. Он включает в себя защищённый паролем доступ для администраторов для удаления содержимого чата. Это также позволяет пользователям приостанавливать действие на мгновение и требует имени пользователя/пароля для продолжения (["secret" "password"] в данном примере).

REBOL [title: "FTP Chat Room"]

; Следующая строка получает URL-адрес текстового файла на веб-сервере 
; пользователя для использования в чате. Имя пользователя ftp, 
; пароль, домен и имя файла должны быть введены в указанном формате:
webserver: to-url ask trim/lines {
    URL of text file on your server (ftp://user:pass@site.com/chat.txt):  
}

; Следующая строка запрашивает имя пользователя:
name: ask "Enter your name:  "

; Следующая строка записывает некоторый текст в файл веб-сервера 
; (полученный выше), указывая, что пользователь вошёл в чат. 
; Уточнение "/append" добавляет к существующему тексту в файле 
; веб-сервера (в отличие от удаления того, что уже есть). При 
; использовании "rejoin" текст, записываемый на веб-сервер, 
; представляет собой объединённое значение имени пользователя, 
; некоторого статического текста, текущих даты и времени и возврата 
; каретки. Если файл не существует, он создаётся. На веб-сервере 
; может быть автоматически создано любое количество файлов "комнаты":

write/append webserver rejoin [now ": " name " has entered the room.^/"]

; Теперь программа использует "вечный" (forever) цикл, чтобы 
; постоянно ждать ввода пользователя и делать с этим вводом 
; соответствующие действия:
forever [

    ; Сначала прочтём сообщения, которые в данный момент находятся в 
    ; текстовом файле "webserver", и присвойте этому тексту 
    ; переменное слово "current-chat":
current-chat: read webserver 

; Очищаем экран

prin "^(1B)[J"

; Отображаем приветствие и некоторые инструкции:

print rejoin [ 
    "--------------------------------------------------"
    newline {You are logged in as: } name newline 
    {Type "room" to switch chat rooms.} newline
    {Type "lock" to pause/lock your chat.} newline
    {Type "quit" to end your chat.} newline 
    {Type "clear" to erase the current chat.} newline 
    {Press [ENTER] to periodically update the display.} newline 
    "--------------------------------------------------" newline
]

print rejoin ["Here's the current chat text at: " webserver newline]
print current-chat

; В строке ниже функция "ask" (спросить) используется для 
; получения текста от пользователя. Возвращённому тексту (тексту, 
; введённому пользователем) присваивается метка "entered-text" 
; (введённый-текст), и он объединяется с именем пользователя и 
; текстом "says" (говорит). Это подготовит его к добавлению в 
; файл веб-сервера и отображению в чате. Обратите внимание, что 
; пользователь должен сначала ответить на функцию "ask", прежде 
; чем может произойти оценка повторного присоединения:
sent-message: copy rejoin [
    name " says: "
    entered-text: ask "You say:  "
]

; Приведённая ниже структура "switch" (переключатель) 
; используется для проверки команд в тексте, введённом 
; пользователем. Если пользователь вводит "quit" (выйти), 
; "clear" (очистить), "room" (комната) или "lock" 
; (заблокировать), происходят соответствующие действия:

switch/default entered-text [   ; "switch" обрабатывается как несколько "if"

    ; Если пользователь набрал "quit" (выйти), остановите цикл 
    ; навсегда (выйдите из программы):

    "quit" [break] 

    ; Если пользователь набрал "clear" (очистить), стереть 
    ; текущий текстовый чат. Но сначала спросите у пользователя 
    ; имя пользователя/пароль администратора:
"clear" [

    ; "if/else" выполняет то же действие, что и "either" (не 
    ; рекомендуется): 

    adminu: ask "Admin Username:  "
    adminp: ask "Admin Password:  "
    if/else ((adminu = "secret") and (adminp = "password")) [
        write webserver ""
    ] [
        ask trim/auto {
            ^LYou must know the administrator password to clear
            the room!
        }
    ]
]

; Если пользователь набрал "room" (комната), запросил новый 
; адрес FTP и запустил некоторый код, который был представлен 
; ранее в программе, используя только что введённую 
; переменную "веб-сервер", чтобы эффективно изменить 
; "комнаты" чата:
"room" [

    ; Добавьте сообщение в файл чата, указывающее, что 
    ; пользователь покинул чат:

    write/append webserver rejoin [
        now ": " name " has left the room." newline
    ]

    ; Получите URL-адрес нового текстового файла чата (адрес 
    ; новой комнаты). Используйте старый адрес в качестве 
    ; отображаемого URL-адреса по умолчанию:
webserver: to-url ask rejoin [
    {New Web Server Address (} to-string webserver {):  }
]

; Отобразить сообщение в только что выбранном текстовом 
; файле чата, показывая, что пользователь вошёл в чат:

write/append webserver rejoin [
    now ": " name " has entered the room." newline
]

]

"lock" [

; Вывести пользователю сообщение о том, что программа 
; будет приостановлена:
ask trim/lines {
    ^LPress [Enter] to resume.  You'll need the correct
    username and password to continue.
}

; Не продолжайте, пока пользователь не получит правильный 
; пароль:
forever [

    ; Цикл while ниже постоянно запрашивает у 
    ; пользователя пароль до тех пор, пока он не станет 
    ; правильным:
while [
    adminu: ask "Admin Username:  "
    adminp: ask "Admin Password:  "
    ((adminu <> "secret") or (adminp <> "password"))
][
    ask "^LIncorrect password - look in the source!"
]

; После того, как пользователь ввёл правильное имя 
; пользователя и пароль, выйдите из цикла навсегда и 
; продолжите программу:
break

]
]
][

; Следующая строка является случаем по умолчанию для 
; структуры переключателя: пока введённое сообщение не 
; пустое ([Enter]), напишите введённое сообщение на 
; веб-сервер (добавьте его к текущему тексту чата):
if entered-text <> "" [
    write/append webserver rejoin [sent-message newline]
]
]
]

; После выхода из цикла "forever" (навсегда) сделайте следующее:
prin "^(1B)[J"   ; очистка экрана
print "Goodbye!" 
write/append webserver rejoin [now ": " name " has closed chat." newline]
wait 1

Основная часть этой программы выполняется в цикле "forever" и использует структуру условного "переключателя" ("switch"), чтобы решить, как реагировать на ввод пользователя. Это классическая схема, которую можно настроить для соответствия множеству общих ситуаций, в которых компьютер постоянно ожидает и отвечает на действия пользователя в командной строке.

5.15 Напоминание для группе

Эта программа работает по тем же принципам, что и приложение FTP Chat. Он хранит напоминания для группы пользователей в текстовом файле, доступном по FTP. В отличие от приложения FTP-чата, эта программа использует функции графического интерфейса и фокусируется на всплывающих окнах, когда группе отправляются новые сообщения. Пользователи могут запустить программу в любое время, чтобы ввести новое текстовое напоминание в виджет текстовой области. Программу также можно запустить и свернуть для работы в фоновом режиме и прослушивания напоминаний. В режиме прослушивания окно графического интерфейса пользователя будет всплывать с сообщением, когда кто-то ещё в группе добавляет новое напоминание. Систему можно использовать для обмена напоминаниями о событиях, заметками, списками задач или любой другой актуальной и конфиденциальной информацией. Это приложение требует только, чтобы кто-то из группы имел доступ к FTP-серверу. Как и в случае с программой FTP-чата, на сервере ничего настраивать не нужно. Все, что требуется, - это имя пользователя и пароль, а также подключение к Интернету.

REBOL [title: "Group Reminder System"]

; Эта переменная настроена для хранения имени пользователя, пароля, 
; папки, файла и URL-адреса FTP-сервера, на котором хранятся тексты 
; напоминаний:

group-url: ftp://user:pass@site.com/public_html/reminders.txt

; Здесь функция списка запросов позволяет пользователю выбирать между 
; добавлением нового текста напоминания или прослушиванием в фоновом 
; режиме. Ответ пользователя хранится в переменной "menu":

menu: request-list "" ["Create New Reminder" "Listen For Reminders"]

; Приведённый ниже образец кода был продемонстрирован ранее в 
; программе "Детали". Он создаёт новый файл, если файл не существует. 
; В противном случае ничего не происходит:

write/append %reminders.txt ""

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

if menu = "Create New Reminder"  [

    ; Создайте окно графического интерфейса пользователя с текстом 
    ; заголовка, виджетом области и кнопкой:

    view center-face layout [

        h3 "Add a new reminder for the group:"
        a: area wrap
        btn "Submit" [

            ; При щелчке запустите следующий код с проверкой ошибок, 
            ; на всякий случай, если подключение к Интернету 
            ; недоступно:

            if error? try [

                ; Попытка записать текст виджета области в файл FTP:
write/append group-url mold a/text

; Если есть ошибка записи, предупредите пользователя и 
; выйдите:

] [alert "ERROR: Not Saved (check Internet connection)" quit]

; В противном случае предупредите пользователя с 
; сообщением об успешном завершении и завершите:

alert "Saved"
quit

]
]
]

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

if menu = "Listen For Reminders" [

; Напечатаем ожидающее сообщение в консоли. Это можно свести к 
; минимуму:

print "Listening for reminders..."

; Повторяем следующий код бесконечно:

forever [

; Подождём несколько секунд, чтобы сэкономить много трафика:

wait 5

; "Attempt" (попытка) похожа на "if error? try" (если ошибка? 
; попробуем) - она просто ничего не делает, если возникает 
; ошибка. Смысл использования следующего кода - убедиться, 
; что программа продолжает работать, если подключение к 
; Интернету отключено или файл FTP не может быть прочитан по 
; какой-либо другой причине:
attempt [

    ; Прочтите текст в FTP-файле и присвойте ему ярлык 
    ; "reminders" (напоминания):
reminders: read group-url

; Если новое напоминание было добавлено кем-либо в 
; группе, текст "reminders" (напоминания) не будет 
; соответствовать тексту, хранящемуся в файле 
; reminders.txt:

if reminders <> read %reminders.txt [

    ; В этом случае обновим файл reminders.txt новым
    ; текстом, прочитанным из файла FTP:

    write %reminders.txt reminders

    ; Создайте пустую строку для хранения текущих 
    ; напоминаний:

    remind: copy {}

    ; Измените порядок всех напоминаний в файле FTP, 
    ; чтобы сначала были самые новые сообщения, а затем 
    ; прокрутим их с помощью функции "foreach":
foreach reminder (reverse load reminders) [

    ; Добавьте каждое напоминание к пустой текстовой 
    ; строке, созданной выше, разделённой двумя 
    ; пустыми строками:
append remind rejoin [reminder newline newline]

]

; Откроем окно графического интерфейса пользователя и 
; отобразим форматированный текст напоминания в 
; виджете области:

view center-face layout [
h3 "Reminders for the group:"
area remind
]

]
]
]
]

Вот вся программа без комментариев:

REBOL [title: "Group Reminder System"]
group-url: ftp://user:pass@site.com/public_html/reminders.txt
menu: request-list "" ["Create New Reminder" "Listen For Reminders"]
write/append %reminders.txt ""
if "" = read group-url [write group-url {""}]
if menu = "Create New Reminder"  [
    view center-face layout [
        h3 "Add a new reminder for the group:"
        a: area wrap
        btn "Submit" [
            if error? try [
                write/append group-url mold a/text
            ] [alert "ERROR: Not Saved (check Internet connection)" quit]
            alert "Saved"
            quit
        ]
    ]
]
if menu = "Listen For Reminders" [
    print "Listening for reminders..."
    forever [
        wait 5
        attempt [
            reminders: read group-url
            if reminders <> read %reminders.txt [
                write %reminders.txt reminders
                remind: copy {}
                foreach reminder (reverse load reminders) [
                    append remind rejoin [reminder newline newline]
                ]
                view center-face layout [
                    h3 "Reminders for the group:"
                    area remind
                ]
            ]
        ]
    ]
]

5.16 Универсальный генератор отчётов для Paypal и любых других данных таблицы CSV

В этом примере создаются отчёты по столбцам и строкам данных в таблице .csv. Сценарий демонстрирует типичные операции с файлом CSV с использованием функций parse, foreach и простых последовательностей. Код выполняет суммирование по столбцам и выборочные вычисления по указанным полям на основе условных оценок, поиска и т.д.. Практические возможности создания отчётов по столбцам и строкам, такие как эта, являются простым и полезным навыком в REBOL. Используя базовые комбинации методов, показанных здесь, легко превзойти возможности электронных таблиц и других "офисных" инструментов отчётности.

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

  1. Войдите в свою учётную запись Paypal
  2. Нажмите Моя учётная запись -> История -> История загрузок.
  3. Выберите диапазон дат
  4. Выберите "Разделённые запятыми - Все действия" (где указано "Типы файлов для загрузки").
  5. Сохраните файл в %Download.csv.

На момент создания этого примера кода загрузка CSV-файла автора содержала следующие поля (от "A" до "AP" - 42 столбца).

Дата, время, часовой пояс, имя, тип, статус, валюта, брутто, комиссия, нетто, с адреса электронной почты, на адрес электронной почты, идентификатор транзакции, статус контрагента, статус адреса, название позиции, идентификатор позиции, сумма доставки и обработки, страхование Сумма, Налог с продаж, Имя варианта 1, Значение варианта 1, Имя варианта 2, Значение варианта 2, Сайт аукциона, Идентификатор покупателя, URL-адрес товара, Дата закрытия, Идентификатор условного депонирования, Идентификатор счета-фактуры, Идентификатор ссылочного Txn, Номер счета-фактуры, Пользовательский номер, Квитанция Идентификатор, баланс, адресная строка 1, адресная строка 2 / район / район, город / город, штат / провинция / регион / округ / территория / префектура / республика, почтовый индекс / почтовый индекс, страна, номер контактного телефона

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

Сценарий прост для понимания:

REBOL [title: "Paypal Reports"]

; Сначала прочтите строки CSV-файла (как описано ранее в разделе 
; CSV/parse этого руководства):
filename: request-file/only/file %Download.csv
lines: read/lines filename

; Первая строка содержит названия столбцов. Давайте преобразуем эту 
; строку CSV в блок, используя функцию parse/all:
labels: copy parse/all lines/1 ","

; Удалите лишние пробелы с каждой метки в блоке "labels" (метки):

foreach label labels [trim label]

; Теперь преобразуем строки CSV в блок данных REBOL. Сначала создаём 
; пустой блок для хранения данных:
database: copy []

; Значения данных начинаются со второй строки CSV-файла. Используйте 
; "foreach" и "parse/all", чтобы разделить отдельные значения в 
; каждой строке и собрать их в блок блоков (как описано в разделе 
; CSV/parse):
foreach line (at lines 2) [
    parsed: parse/all line ","
    append/only database parsed
]

; Для первого отчёта давайте составим список всех людей в столбце 
; "Name". Найдите порядковый номер столбца "Name" в блоке "labels". 
; Мы будем использовать это, чтобы выбирать значения имён из каждой 
; строки:

name-index: index? find labels "Name"

; Создайте пустую текстовую строку для сбора имен:

names: copy {}

; Прокрутите базу данных, выбирая элемент в позиции индекса имени из 
; каждой строки

foreach row database [
    append names rejoin ["Name:  " (pick row name-index) newline]
]

; Отобразите результаты:

editor names

; Теперь давайте рассмотрим каждую транзакцию, в которой поле имени 
; включает "Netflix". По сути, это та же процедура, что и выше, 
; только теперь с добавленной условной оценкой, использующей функцию 
; "find" (найти). И для этого отчёта мы хотим увидеть уплаченную 
; сумму. Установим переменную для ссылки на позицию индекса столбца 
; "Net":
net-index: index? find labels "Net"
amounts: copy {}
foreach row database [
    if find/only (pick row name-index) "Netflix" [
        append amounts rejoin ["Amount:  " (pick row net-index) newline]
    ]
]
editor amounts

; Теперь давайте получим сумму каждой транзакции Netflix в период с 
; января по декабрь 2012 года. Даты хранятся в столбце с надписью 
; "Дата" в формате, например: "26.12.2012" (месяц/день/год). Нам 
; нужно будет преобразовать это во внутренний формат REBOL 
; (день-месяц-год).
date-index: index? find labels "Date"

; Вы видели ранее, как собирать суммы. Начнём с установки значение 
; переменной sum равной 0. Будем добавлять значения к ней, чтобы 
; получить результат:

sum: $0

; Теперь прокрутим базу данных и выполните несколько условных оценок 
; для каждой строки:

foreach row database [
    if find/only (pick row name-index) "Netflix" [

        ; Мы будем использовать "parse", чтобы разделить текст даты 
        ; символом "/":

        date: parse (pick row date-index) "/"

        ; Выберем месяц, используя значения system/locale/months

        month:  pick system/locale/months to-integer date/1

        ; Затем переставим и соединим части даты, используя "rejoin":

        reb-date: to-date rejoin [date/2 "-" month "-" date/3]

        ; Если дата находится в выбранном диапазоне, добавим значение 
        ; к сумме (sum):
if ((reb-date >= 1-jan-2012) and (reb-date <= 31-dec-2012)) [
    sum: sum + (to-money pick row net-index)
]
]
]

; Покажем результат:
alert form sum

Вот вся программа без комментариев:

REBOL [title: "Paypal Reports"]
filename: request-file/only/file %Download.csv
lines: read/lines filename
labels: copy parse/all lines/1 ","
foreach label labels [trim label]
database: copy []
foreach line (at lines 2) [
    parsed: parse/all line ","
    append/only database parsed
]
name-index: index? find labels "Name"
names: copy {}
foreach row database [
    append names rejoin ["Name:  " (pick row name-index) newline]
]
editor names
net-index: index? find labels "Net"
amounts: copy {}
foreach row database [
    if find/only (pick row name-index) "Netflix" [
        append amounts rejoin ["Amount:  " (pick row net-index) newline]
    ]
]
editor amounts
date-index: index? find labels "Date"
sum: $0
foreach row database [
    if find/only (pick row name-index) "Netflix" [
        date: parse (pick row date-index) "/"
        month:  pick system/locale/months to-integer date/1
        reb-date: to-date rejoin [date/2 "-" month "-" date/3]
        if ((reb-date >= 1-jan-2012) and (reb-date <= 31-dec-2012)) [
            sum: sum + (to-money pick row net-index)
        ]
    ]
]
alert form sum

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

REBOL [title: "Reports"]
filename: request-file/only/file %filename.csv
lines: read/lines filename
labels: copy parse/all lines/1 ","
foreach label labels [trim label]
database: copy []
foreach line (at lines 2) [
    parsed: parse/all line ","
    append/only database parsed
]

Вы можете использовать эту строку кода для просмотра списка меток столбцов в файле CSV:

editor labels

Затем присвойте переменную(и) метке(ам) столбца, для которой вы хотите выполнить вычисления:

name-index: index? find labels "Name"
date-index: index? find labels "Date"
net-index: index? find labels "Net"

Задайте любые начальные переменные, необходимые для выполнения вычислений (сумма, макс/мин и т.д.) или для хранения блоков собранных значений:

sum: $0
names: copy {}
amounts: copy {}

Используйте цикл(ы) "foreach" для выполнения условных оценок и желаемых вычислений для каждой строки данных:

foreach row database [
    append names rejoin ["Name:  " (pick row name-index) newline]
]
foreach row database [
    if find/only (pick row name-index) "Netflix" [
        append amounts rejoin ["Amount:  " (pick row net-index) newline]
    ]
]
foreach row database [
    if find/only (pick row name-index) "Netflix" [
        date: parse (pick row date-index) "/"
        month:  pick system/locale/months to-integer date/1
        reb-date: to-date rejoin [date/2 "-" month "-" date/3]
        if ((reb-date >= 1-jan-2012) and (reb-date <= 31-dec-2012)) [
            sum: sum + (to-money pick row net-index)
        ]
    ]
]

Отображение всех вычисленных или собранных результатов:

editor names
editor amounts
alert form sum

Конечно, приведённые выше вычисления включают в себя довольно много оценки и обработки данных, чтобы продемонстрировать полезные многократно используемые фрагменты кода (например, преобразование дат в значения REBOL, которыми можно разумно управлять). В большинстве случаев простые суммы, средние значения, поиск и другие общие вычисления потребуют гораздо меньше кода. Вы можете ещё больше сократить код, используя числа для обозначения столбцов. Для простых отчётов редко требуется намного больше кода, чем в первом примере в этом руководстве:

sum: $0
foreach line at read/lines http://re-bol.com/Download.csv 2 [
    sum: sum + to-money pick (parse/all line ",") 8
]
alert form sum

5.17 Просмотр и использование кода, который вы видели, для моделирования новых приложений

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

  1. Добавьте столбцы в блоки данных в существующих приложениях (например, возможно, поля "электронная почта" и "мобильный", "домашний", "рабочий" в примерах контактов или поля "имя" и "заголовок" в сообщения в приложении напоминания) .
  2. Добавьте новые вычисления в примеры отчётов (возможно, функции средних, максимальных и минимальных значений в столбцы отчётов Paypal). Примените шаблоны кода CSV, сортировки, вычислений и отчётов, которые вы видели, к столбцам данных, экспортированных из других настольных приложений и веб-сайтов.
  3. Применяйте новые и более сложные условные операции к вычислениям (возможно, применяйте налог только к товарам в файле Paypal, которые были отправлены из определённого штата, или применяйте плату к ценам на товары, произведённые определённым производителем в приложении базы данных "Детали").
  4. Создайте несколько небольших приложений с графическим интерфейсом, которые сохраняют файлы CSV, которые можно импортировать в электронную таблицу (возможно, добавьте функцию экспорта CSV к примерам "Контакты" или "Части", и особенно попробуйте экспортировать вычисленные отчёты в виде файлов CSV).
  5. Добавьте функции списка в программу электронной почты (возможно, разрешите выбирать адреса электронной почты из списка контактов или отправлять электронные письма группе пользователей).
  6. Добавьте полезные функции в программы редактирования файлов и веб-страниц (возможно, попробуйте сохранить историю редактируемых файлов или учётные данные для входа на FTP).
  7. Объедините программы инвентаризации и кассового аппарата, чтобы пользователи могли выбирать предметы инвентаризации из раскрывающегося списка в приложении кассового аппарата.
  8. Добавьте уникальные процедуры вычислений в приложение калькулятора.
  9. Поэкспериментируйте с функциями синтаксического анализа "parse" и поиска "find" (возможно, попробуйте добавить функцию поиска в расписание и программы электронной почты или отправить заранее сформированные ответы на сообщения электронной почты, содержащие заданную строку текста в теме).
  10. Поэкспериментируйте с печатью и отображением отформатированных данных в консоли и в полях графического интерфейса (возможно, создайте почтовую программу, которая анализирует заголовок каждого сообщения электронной почты и отображает каждую часть в отдельно напечатанных строках в консоли или в отдельных виджетах области в графическом интерфейсе).
  11. Добавьте функции обмена изображениями и файлами в приложение Group Reminders.
  12. Добавьте изображения к сохранённым записям в приложении базы данных деталей.
  13. Создавайте версии приложений FTP-чата и отчётов Paypal с графическим интерфейсом. Используйте виджеты текстового списка для отображения сообщений в чате и для выбора имён столбцов и параметров вычислений в отчётах.
  14. Поэкспериментируйте с макетами графического интерфейса. Размещайте текстовые заголовки, поля, кнопки, области, текстовые списки и т.д. таким образом, чтобы данные можно было согласованно и удобно размещать на экране, а также создавали интуитивное взаимодействие с пользователем и естественные шаблоны рабочего процесса.
  15. Поэкспериментируйте, как можно использовать цвета, шрифты, размеры виджетов, изображения и другие косметические функции для изменения внешнего вида приложений.

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

6. Пользовательские функции и модули импортированного кода

6.1 "Do", "Does" и "Func"

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

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

some-actions: [
    alert "Here is one action." 
    print "Here's a second action."
    write %/c/anotheraction.txt "Here's a third action."
    alert "Writing to the hard drive was the third action."
]

do some-actions

Новые служебные слова также могут быть определены с помощью слов "does" (делает) и "func" (функц). "Does" включается непосредственно после определения метки слова и заставляет блок оцениваться каждый раз, когда встречается слово:

more-actions: does [
    alert "Counting some more actions:  4" 
    alert "And another:  5"
    alert "And finally:  6"
]

; Теперь, чтобы использовать эту функцию, просто введите слово метки:

more-actions

Вот полезная функция для очистки экрана командной строки в интерпретаторе REBOL.

cls: does [prin "^(1B)[J"]

cls

6.1.1 "Func"

Слово "func" создаёт исполняемый блок таким же образом, как "does", но дополнительно позволяет вам передавать ваши собственные заданные параметры вновь определённому функциональному слову. Первый блок в определении функции содержит имя (имена) передаваемых переменных. Второй блок содержит действия, которые необходимо выполнить с этими переменными. Вот синтаксис func:

func [имена передаваемых переменных] [
     действия, которые необходимо предпринять с этими переменными
]

Это определение функции:

sqr-add-var: func [num1 num2] [print square-root (num1 + num2)]

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

sqr-add-var 12 4      ; выводит  "4",  квадратный корень 12 + 4  (16)
sqr-add-var 96 48     ; выводит  "12", квадратный корень 96 + 48 (144)

Вот простая функция для отображения изображений:

display: func [filename] [view layout [image load to-file filename]]

display (request-file)

6.2 Возвращаемые значения

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

concatenate: func [string1 string2] [join string1 string2]

string3: concatenate "Hello " "there."
print string3

Вы также можете использовать слово "return" (возврат), чтобы завершить функцию с возвращаемым значением. Это может быть полезно при выходе из цикла (функция "for" выполняет операцию подсчёта - это будет подробно рассмотрено в следующем разделе этого руководства):

stop-at: func [num] [
    for i 1 99 1 [
        if (i = num) [return i]
        print i
    ]
    return num
]
print stop-at 38

6.3 Область видимости

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

x: 10

change-x-globally: func [y z] [x: y + z]

change-x-globally 10 20
print x

Вы можете изменить это поведение по умолчанию и указать, что любое значение обрабатывается как локальное для функции (не изменяется во всей остальной части вашей программы), используя уточнение "/local":

x: 10

change-x-locally: func [y z /local x] [x: y + z]

change-x-locally 10 20     ; внутри функции x теперь 30
print x                    ; вне функции x по-прежнему 10

Вы можете уточнить способ работы функции, просто поставив перед необязательными аргументами операции косую черту ("/"):

compute: func [x y /multiply /divide /subtract] [
    if multiply [return x * y]
    if divide   [return x / y]
    if subtract [return x - y]
    return x + y
]

compute/multiply 10 20
compute/divide 10 20
compute/subtract 10 20
compute 10 20

6.4 Документация по функциям

Функция "help" (справка/помощь) предоставляет информацию об использовании любой функции, включая пользовательские функции:

help for
help compute

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

doc-demo: func ["This function demonstrates doc strings"] [help doc-demo]
doc-demo

Допустимые типы данных для любого параметра могут быть перечислены в блоке, а строки документации также могут быть включены сразу после любого параметра:

concatenate-string-or-num: func [
    "This function will only concatenate strings or integers."
    val1 [string! integer!] "First string or integer"
    val2 [string! integer!] "Second string or integer"
] [
    join val1 val2
]

help concatenate-string-or-num
concatenate-string-or-num "Hello " "there."  ; это работает правильно
concatenate-string-or-num 10 20              ; это работает правильно
concatenate-string-or-num 10.1 20.3          ; это вызовет ошибку

6.5 Выполнение импортированного кода

Вы можете "сделать" ("do") модуль кода, содержащийся в любом текстовом файле, если он содержит минимальный заголовок "REBOL []" (включая файлы HTML и любые другие файлы, которые могут быть прочитаны через встроенные протоколы REBOL). Например, если вы сохраните предыдущие функции в текстовом файле с именем "myfunctions.r"

REBOL []    ; ЭТОТ ТЕКСТ ЗАГОЛОВКА ДОЛЖЕН БЫТЬ ВКЛЮЧЁН В НАЧАЛО ЛЮБОГО 
            ; ФАЙЛА REBOL

sqr-add-var: func [num1 num2] [print square-root (num1 + num2)]
display: func [filename] [view layout [image load filename]]
cls: does [prin "^(1B)[J"]

Вы можете импортировать и использовать их в своём текущем коде следующим образом:

do %myfunctions.r

; теперь вы можете использовать эти функции так же, как и любую 
; другую встроенную функцию:

sqr-add-var
display
cls

Вот пример функции, которая воспроизводит звуковой файл ".wav". Сохраните этот код как C:\play_sound.r:

REBOL [title: "play-sound"]  ; вы можете добавить дополнения в заголовок

play-sound: func [sound-file] [
    wait 0
    ring: load sound-file
    sound-port: open sound://
    insert sound-port ring
    wait sound-port
    close sound-port
]

Затем запустите приведённый ниже код, чтобы импортировать функцию и воспроизвести выбранные файлы .wav:

do %/c/play_sound.r

play-sound %/C/WINDOWS/Media/chimes.wav
play-sound to-file request-file/file %/C/WINDOWS/Media/tada.wav

Импортированные файлы могут содержать определения данных и любой другой исполняемый код, включая тот, который содержится в дополнительных вложенных исходных файлах, импортированных с помощью функции "do". Любой код или данные, содержащиеся в исходном файле, оцениваются, когда файл вызывается функцией "do".

6.6 Разделение формы и функций в графическом интерфейсе пользователя - приложение Check Writer

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

Например, все следующие кнопки выполняют несколько разных действий. Первая кнопка считает от 1 до 100, а затем предупреждает пользователя сообщением "Готово", вторая кнопка считает от 101 до 200 и предупреждает пользователя о завершении, третья кнопка считает от 201 до 300 и предупреждает пользователя о завершении ( функция "for" выполняет операцию подсчета):

view layout [
    btn "Count 1" [
        for i 1 100 1 [print i]
        alert "Done"
    ]
    btn "Count 2" [
        for i 101 200 1 [print i]
        alert "Done"
    ]
    btn "Count 3" [
        for i 201 300 1 [print i]
        alert "Done"
    ]
]

Блоки действий каждой кнопки выше могут быть сведены к общей функции, которая считает от начального числа до конечного числа, а затем предупреждает пользователя. Этой функции можно присвоить метку, и тогда только эта метка должна использоваться в блоках действий виджета графического интерфейса пользователя:

count: func [start end] [
    for i start end 1 [print i]
    alert "Done"
]
view layout [
    btn "Count 1" [count 1 100]
    btn "Count 2" [count 101 200]
    btn "Count 3" [count 201 300]
]

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

action1: func [args] [
    actions
    actions
    actions
]
action2: func [args] [
    other actions
    other actions
    other actions
]
action3: func [args] [
    more actions
    more actions
    more actions
]
view layout [
    widget [action1]
    widget [action2]
    widget [action3]
]

Вот простой пример написания чека, который берет суммы и имена, введённые в виджет области, и создаёт блоки текста для записи в чек. Одна функция создана для "вербализации" введённых сумм для печати на чеке (она преобразует числовые значения в их устный английский эквивалент, т.е. 23482194 = "Двадцать три миллиона, четыреста восемьдесят две тысячи, сто девяносто четыре"). Другая функция создана для перебора каждого бита необработанных данных проверки и для создания блока, содержащего имя, числовую сумму, озвученную сумму, некоторый текст памятки и дату. Код графического интерфейса состоит всего из 4 строк:

REBOL [title: "Check Writer"] 
verbalize: func [a-number] [
    if error? try [a-number: to-decimal a-number] [
        return "** Error **  Input must be a decimal value"
    ]
    if a-number = 0 [return "Zero"]
    the-original-number: round/down a-number
    pennies: a-number - the-original-number
    the-number: the-original-number
    if a-number < 1 [
        return join to-integer ((round/to pennies .01) * 100) "/100"
    ] 
    small-numbers: [
        "One" "Two" "Three" "Four" "Five" "Six" "Seven" "Eight"
        "Nine" "Ten" "Eleven" "Twelve" "Thirteen" "Fourteen" "Fifteen"
        "Sixteen" "Seventeen" "Eighteen" "Nineteen"
    ]
    tens-block: [
        { } "Twenty" "Thirty" "Forty" "Fifty" "Sixty" "Seventy" "Eighty"
        "Ninety"
    ]
    big-numbers-block: ["Thousand" "Million" "Billion"]    
    digit-groups: copy []
    for i 0 4 1 [
        append digit-groups (round/floor (mod the-number 1000))
        the-number: the-number / 1000
    ]    
    spoken: copy ""
    for i 5 1 -1 [
        flag: false
        hundreds: (pick digit-groups i) / 100
        tens-units: mod (pick digit-groups i) 100
        if hundreds <> 0 [
            if none <> hundreds-portion: (pick small-numbers hundreds) [
                append spoken join hundreds-portion " Hundred "
            ]
            flag: true
        ]
        tens: tens-units / 10
        units: mod tens-units 10
        if tens >= 2 [
            append spoken (pick tens-block tens)
            if units <> 0 [
                if none <> last-portion: (pick small-numbers units) [
                    append spoken rejoin [" " last-portion " "]
                ]
                flag: true
            ]
        ]
        if tens-units <> 0 [
            if none <> tens-portion: (pick small-numbers tens-units) [
                append spoken join tens-portion " "
            ]
            flag: true
        ]
        if flag = true [
            commas: copy {}    
            case [
                ((i = 4) and (the-original-number > 999999999)) [
                    commas: {billion, }
                ]
                ((i = 3) and (the-original-number > 999999)) [
                    commas: {million, }
                ]
                ((i = 2) and (the-original-number > 999)) [
                    commas: {thousand, }
                ]
            ]
            append spoken commas
        ]
    ]
    append spoken rejoin [
        "and " to-integer ((round/to pennies .01) * 100) "/100"
     ]
    return spoken
]
write-checks: does [
    checks: copy []
    data: to-block a1/text
    foreach [name amount] data [
        check: copy []
        append/only checks reduce [
            rejoin ["Pay to the order of: "  name  "   "]
            rejoin ["$"  amount  newline]
            rejoin ["Amount: " verbalize amount newline]
            rejoin ["Sales for November    "  now/date  newline newline]
        ]
    ]
    a2/text: form checks
    show a2
]
view layout [
    text "Amounts and Names:"
    a1: area {"John Smith" 582 "Jen Huck" 95 "Sue Wells" 71 "Joe Lask" 38}
    btn "Create Check Data" [write-checks]
    a2: area
]

Одно из очевидных преимуществ использования функций заключается в том, что вам не нужно помнить или даже понимать, как они работают. Вместо этого вам действительно нужно только знать, как их использовать. В приведённом выше примере вам не нужно точно понимать, как работают все вычисления в функции "verbalize". Вам просто нужно знать, что если вы наберёте "verbalize (число)", он выдаст текст, представляющий указанную сумму в долларах. Вы можете использовать эту функцию в любой другой программе, где она будет полезна, в блаженном неведении о том, как происходит волшебство вербализации. Просто вставьте приведённый выше код вербализации и используйте его, как любую другую функцию, встроенную в язык. Это верно для любой функции, которую вы создаёте, или любой функции, которую создаёт кто-то другой. Такая работа с функциями помогает значительно улучшить возможности повторного использования кода и мощность языка. Это также поощряет чёткие структуры кода, которые другим будет легче читать.

Примечание. Алгоритм вербализации частично заимствован из статьи по адресу http://www.blackwasp.co.uk/NumberToWords.aspx (код C #)

6.7 Полнофункциональное приложение для обмена групповыми заметками

В предыдущем разделе приложения "Групповые напоминания" и "FTP-чат" продемонстрировали, как сохранять текстовые сообщения на веб-сервер, чтобы ими можно было поделиться с группой подключённых пользователей. Это приложение основано на той же идее, но добавляет ряд полезных функций. Отображаемый список сообщений может обновляться вручную или автоматически с любым выбранным интервалом. Повторяющийся звуковой сигнал может воспроизводиться как сигнал будильника, чтобы уведомить пользователей о новых сообщениях (звуковой сигнал создаётся с кодом: call "echo ^G"). Звуковые уведомления можно включать и выключать. Сообщения помечаются датой и временем и пронумерованы. Сообщения можно стирать индивидуально по номеру, или можно удалить сразу весь список заметок. Можно создать неограниченное количество частных "комнат", просто изменив URL-адрес. Новый файл будет автоматически создан по введённому URL-адресу, если он не существует.

Все функции в этой программе реализованы как отдельные функции. Функция "update" (обновления), в частности, вызывается не только тогда, когда пользователь нажимает кнопку обновления, но и другими функциями каждый раз, когда вносятся изменения, требующие обновления дисплея (при стирании сообщений, при включении автообновления и т.д.). Обратите внимание, что макет графического интерфейса остаётся чистым, и каждый виджет просто вызывает функцию:

REBOL [title: "Group Notes"]
url: ftp://user:pass@site.com/public_html/Notes
beep: false   autoupdt: false   call ""
write/append %notes.txt ""
update: does [
    if error? try [notes: copy read/lines url] [write url notes: ""]
    if ((beep = true) and (notes <> read/lines %notes.txt)) [
        loop 4 [call "echo ^G" wait 1]
    ]
    write/lines %notes.txt notes
    display: copy {}
    count: 0
    foreach note reverse notes [
        either note = "" [
            note: newline
        ] [
            count: count + 1
            note: rejoin [count ") "note]
        ]
        append display note
    ]
    a/text: display
    if a/text = "" [a/text: copy " "]
    show a
]
autoupdate: does [
    either autoupdt: not autoupdt [
        time: request-text/title/default "Refresh display (seconds):" "10"
        b/text: join "Auto Update:  " autoupdt  show b
        forever [
            either autoupdt = true [wait to-integer time update] [break]
        ]
    ] [
        b/text: join "Auto Update:  " autoupdt  show b
    ]
]
submit: does [
    if f/text = "" [focus f  return]
    if error? try [
        write/lines/append url rejoin [
            "^/^/" now " (" n/text "):  " f/text
        ]
    ] [alert "ERROR: Not Saved (check Internet connection)" return]
    update
    f/text: copy ""  show f  focus f
]
erase: func [arg] [
    if true = request rejoin ["Really erase " arg "?"] [
        write/lines to-file replace/all replace form now "/" "--" ":" "_"
            notes
        if arg = "all" [write url ""]
        if arg = "" [
            indx: (
                3 * to-integer request-text/title/default "Index:" "1"
            ) - 2
            remove/part at notes indx 3 
            write/lines url reverse notes 
        ]
        update
    ]
]
changeurl: does [
    url: to-url u/text  
    update
    focus f
]
setbeep: does [
    beep: not beep  
    p/text: join "Beep: " beep
    show p
]
insert-event-func [either event/type = 'close [quit][event]] 
view center-face layout [
    h3 "Current notes for:"
    u: field 600 form url [changeurl]
    a: area 600x260
    h3 "Name:"
    n: field 600
    h3 "New Note:"
    f: field 600 [submit]
    across
    btn "Update" [update]
    b: btn join "Auto Update: " autoupdt [autoupdate]
    p: btn join "Beep: " beep [setbeep]
    btn "Erase" [erase ""]
    btn "Erase All" [erase "all"]
    do [update focus f]
]

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

7. Несколько полезных инструментов визуализации данных

7.1 Отображение и сортировка данных с использованием графических сеток в виде электронных таблиц

Как вы уже видели в примере "Контакты", можно динамически создавать графические интерфейсы пользователя с использованием циклов foreach для отображения данных в полях графического интерфейса. В следующих главах мы будем опираться на такие примеры:

REBOL [title: "Display a Table of Data"]

; Создаём 500 строк из 3 столбцов случайных данных:
x: copy[] for i 1 500 1[append x reduce [i random "abcd" random 1-1-2012]]

; Покажем это:
grid: copy [across space 0]
foreach [indx text date] x [
    append grid compose [
        field 50 (form indx)
        field 70 (form text)
        field 110 (form date)
        return
    ]
]
view center-face layout [
    across
    g: box 230x200 with [pane: layout/tight grid pane/offset: 0x0]
    scroller [g/pane/offset/y: g/size/y - g/pane/size/y * value show g]
]

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

REBOL [title: "Table/Grid/Listview Example"]

headers: ["Numbers" "Text" "Dates"]

x: [
    [1   "1"   1-1-2012]
    [11  "11"  1-2-2012]
    [2   "2"   1-3-2012]
]

do decompress #{
789CC518C98EDB36F4AEAF78500FB11228B2074D0AA8B3A01F904BAF860EB245
D9CAD0A2225133720CFF7B1F77D24BE324456A60C626F9F69D1C5B4A86015E4A
3A922778B3199B7445D9FA1996F6670E8763118D21E004CB298735EBF6B02CCE
4EC9AEE3FB74E8CA3581A5B7C8615E447B8D3741B46674DCB5430E94B41BBE7D
822D292BD20F91FA36FC25F8E118F5A423258706341E2C23C04FD975A4ADC0C7
819E7C664D0BCB781B237C9C713271888BA83821CD9946F0B7A355734AEFCDB0
1F38D945949595DA22550EF5D8AE615995BC4CEB8612C87AF26524032FB4644D
0D7AC783CACD9E5C65ACA5FB4C625B8842A9C5B9309D26253ED672FA5B08E3B0
2CD8B065AF401BB9161A0FE50BF945320B56C2A6E1194C2885B45C4D4BFECBCD
56A3BDAE98CAC5303870D663C82C606682B29E929388331FC12908D265119C0B
529F610E33839EC22241D2211517C28E1E740DFEAB279835F00E3E271060845C
14B2B40826554FAA11D3CE91B2B09E8E3692FE3D68FE37775D720A29D75BE851
CAE9C47EDA78B5D51D819CD24E43119B4E8B7A72BAB29EA7CA4546DBBA21B432
0A92866F490F128CF55814726819F7D69E4062335BB35D57F618F79A5A092BA4
35C3DA5582A49CC02388E54A2F9594C50FD0B9BF484719CEF8B4889E0956E075
CF28350AAA555AEED8D85A4F0E69C7B01ACB2F0CBB00C6B85A1D3EBAF4C0EC58
6A44BB5584D0F798030666AE2B05CD842F0CB32CA01788AF7F0C14F5E8C9D07C
25E9A66F6C25EB48BF262D2F37C468213A970043DAB268672F0D79CD501942DA
B4C6369475654BB2452680E04F9194F3692E516983012BB6B345EE2FE02D383E
279077B9E598DD2131AFDF4948AE85E9D18A15AAE951F5B4F6BB4D92681369CC
126D54363D2C3EFA6C25CCD84AE564EEA314A0561D6F18561B212FD2142A032D
F76CE4E0357865CBC259B56E30D92B46861FB1A3C4F0DC03336B14A1A7AF750A
EF1709B22DAB2AA54D4B0CD3B014600B9E647F6E50890DE698A927A29567BCE1
58552A529723C5C6FE5755A9E163C082228BC46F79EC178F9D4F89BDF4E9DA45
B4E0040F22447BC2C7BE5501DA92D754910C6BFB597B083C278ABBAE478E80C2
8F639D15ED4030B7A5CB44EE4E5280C48777992B9CB363588AFF2343FD8D7439
0345348FE1E76C8407BC1F09DC3FDA0E6007AFBF150B88552B535A42FC141721
0D254A60095FFD33E52512A355FA5D7A7F123CEA9EEDBE2B38844B6E60A3C90B
BB7AC4D138B399A1206C970086CECC882E7792DBEDF929B4A625736E5161B20C
855126358089D5C66F0C3BAC5463D7511C08825E475E48FB84718B851F9BA1C8
75EC8F0C1BDFEBB6E1A2D2FA7B77BFCFDFCBBF0F1F9404122FD7E8EF5489370A
B6ACC52BC297DC28E2F31026CD4DB600991A6EEDA2DD8256BB20D4DD1C99CF3D
01E4A1EA2F6BDBBC3C1692F717116E44CCA6036D2A34F65E8C2DDA0CED062BBD
9D06B0DF12BF3B3762E701DE8C1DF635373D288238318AFB831E46826B442DF9
BB99643DAAF29CDF88E90B6070E1F1012ED5A230287CA94BCAD38ABD62540DBC
94C2E31493034E69753D104F3C214D8AF1A0653C0463573F7CC5D140F8B96E7A
8CD4966C4A8E898CB54F51C442EFD14F02648C3BE90ED36BC335621AEA01165A
7966D44E2CEEA5DD5B2860642E12ADF3299DF0EC766A9764F24FF0F7454ADBAB
525D3BB926D329BC91E8F2FE35796485D04E399DC44E288939C1E21EED2FBCAC
530112C4101C9CA78E36826FC691FC8E10AE2F86B748CC4B0187C11A6CDB6075
E11F5EB590A8854173DD9FB640F3A95828F5C905448C59A46A785064B04EF7AE
C4B093023363508A81D1543E6C0AB5AC69A26BE8CD5C6F190F19664AB59B4B58
7869D53532E873A691AAC3E0B6182AF9EDA1514EDD29CCA70FF3C88E856E86C6
A39919783FCEFD693A89BE3511FFD0A86DEE25105CF0641F066B66D1B5223DD8
B9195A154429B5151F4ABC350D628693CF5FE666211DAF59C36AE49CB5560B25
2476315861F83FAB1E0B2BECDCD1B71EBD9C2C6650B03E39887FDB23621E72C3
F888C301354F5B4396E319D48450701DF028E604317059979EB1928C0C79FBB6
2173F1DC44B9D156E916F7312CFE9850D72014A5B91FECD483D32366E2536C1F
19F797225CE5A1BE8EA9FB9A0B07FF2E2D6DBF2694828C5E0D2015B74969A1CF
FDF7B34E101F44D4AE90721C3D35507DF3702AE799C15E211C6139A5AA53F45A
7199BB71DC05971DF4C3801AF6C08E7DFAA299EBBBBE78E5A0DA7C3A2FFC7BF9
5B252884CED06E385EE3FA4CF6E26FCD2AA43A765884DC7304A40BF35A118089
F12404BC0CD761894BCF68DECDAF039F5356D0C7C87BF1B6CFC2764FBD093BD5
CC83F03FD2FC2B1641170000
}
view center-face gui: layout gui-block

Другие параметры, такие как раскрашивание строк и указание типов данных столбца с использованием сжатого кода таблицы, приведённого выше, показаны в следующем примере. Это очень полезный инструмент для ввода, визуализации и демонстрации данных:

REBOL [title: "Table/Grid/Listview Example With Expanded Features"]

headers: ["Numbers" "TEXT (Note Sort)" "Dates"] ; НЕОБХОДИМЫЕ ЭТИКЕТКИ КОЛОНКИ

; ВСЕ ЭТИ ДОПОЛНИТЕЛЬНЫЕ ПАРАМЕТРЫ НАСТРОЙКИ *** НЕОБЯЗАТЕЛЬНЫ ***:

x: [[1 "1" 1-1-2012][11"11"1-2-2012][2"2"1-3-2012]]  ; некоторые данные по умолчанию

colors: [blue black red]    ; укажем цвета столбцов
empty-space: 235            ; размер пустой области графического интерфейса для отображения под сеткой
svv/vid-face/color: white   ; цвет фона графического интерфейса по умолчанию

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

gui-block: {
    h3 "RIGHT-CLICK/DRAG HEADERS TO RESIZE COLUMNS.  RESIZE WINDOW..."
    text "Click headers to sort (note that sort is DATA-TYPE SPECIFIC)."
    text "Notice Arrow Keys, PgUp/PgDn Keys, Scroll Bar, and highliting"
    text "Click any cell to edit data. Buttons load and save data to HD."
}

; Следующая строка автоматически помещает сетку в окно графического 
; интерфейса с изменённым размером.

insert-event-func [either event/type = 'resize [resize-fit none] [event]]

do decompress #{
    789CC518C98EDB36F4AEAF78500FB11228B2074D0AA8B3A01F904BAF860EB245
    D9CAD0A2225133720CFF7B1F77D24BE324456A60C626F9F69D1C5B4A86015E4A
    3A922778B3199B7445D9FA1996F6670E8763118D21E004CB298735EBF6B02CCE
    4EC9AEE3FB74E8CA3581A5B7C8615E447B8D3741B46674DCB5430E94B41BBE7D
    822D292BD20F91FA36FC25F8E118F5A423258706341E2C23C04FD975A4ADC0C7
    819E7C664D0BCB781B237C9C713271888BA83821CD9946F0B7A355734AEFCDB0
    1F38D945949595DA22550EF5D8AE615995BC4CEB8612C87AF26524032FB4644D
    0D7AC783CACD9E5C65ACA5FB4C625B8842A9C5B9309D26253ED672FA5B08E3B0
    2CD8B065AF401BB9161A0FE50BF945320B56C2A6E1194C2885B45C4D4BFECBCD
    56A3BDAE98CAC5303870D663C82C606682B29E929388331FC12908D265119C0B
    529F610E33839EC22241D2211517C28E1E740DFEAB279835F00E3E271060845C
    14B2B40826554FAA11D3CE91B2B09E8E3692FE3D68FE37775D720A29D75BE851
    CAE9C47EDA78B5D51D819CD24E43119B4E8B7A72BAB29EA7CA4546DBBA21B432
    0A92866F490F128CF55814726819F7D69E4062335BB35D57F618F79A5A092BA4
    35C3DA5582A49CC02388E54A2F9594C50FD0B9BF484719CEF8B4889E0956E075
    CF28350AAA555AEED8D85A4F0E69C7B01ACB2F0CBB00C6B85A1D3EBAF4C0EC58
    6A44BB5584D0F798030666AE2B05CD842F0CB32CA01788AF7F0C14F5E8C9D07C
    25E9A66F6C25EB48BF262D2F37C468213A970043DAB268672F0D79CD501942DA
    B4C6369475654BB2452680E04F9194F3692E516983012BB6B345EE2FE02D383E
    279077B9E598DD2131AFDF4948AE85E9D18A15AAE951F5B4F6BB4D92681369CC
    126D54363D2C3EFA6C25CCD84AE564EEA314A0561D6F18561B212FD2142A032D
    F76CE4E0357865CBC259B56E30D92B46861FB1A3C4F0DC03336B14A1A7AF750A
    EF1709B22DAB2AA54D4B0CD3B014600B9E647F6E50890DE698A927A29567BCE1
    58552A529723C5C6FE5755A9E163C082228BC46F79EC178F9D4F89BDF4E9DA45
    B4E0040F22447BC2C7BE5501DA92D754910C6BFB597B083C278ABBAE478E80C2
    8F639D15ED4030B7A5CB44EE4E5280C48777992B9CB363588AFF2343FD8D7439
    0345348FE1E76C8407BC1F09DC3FDA0E6007AFBF150B88552B535A42FC141721
    0D254A60095FFD33E52512A355FA5D7A7F123CEA9EEDBE2B38844B6E60A3C90B
    BB7AC4D138B399A1206C970086CECC882E7792DBEDF929B4A625736E5161B20C
    855126358089D5C66F0C3BAC5463D7511C08825E475E48FB84718B851F9BA1C8
    75EC8F0C1BDFEBB6E1A2D2FA7B77BFCFDFCBBF0F1F9404122FD7E8EF5489370A
    B6ACC52BC297DC28E2F31026CD4DB600991A6EEDA2DD8256BB20D4DD1C99CF3D
    01E4A1EA2F6BDBBC3C1692F717116E44CCA6036D2A34F65E8C2DDA0CED062BBD
    9D06B0DF12BF3B3762E701DE8C1DF635373D288238318AFB831E46826B442DF9
    BB99643DAAF29CDF88E90B6070E1F1012ED5A230287CA94BCAD38ABD62540DBC
    94C2E31493034E69753D104F3C214D8AF1A0653C0463573F7CC5D140F8B96E7A
    8CD4966C4A8E898CB54F51C442EFD14F02648C3BE90ED36BC335621AEA01165A
    7966D44E2CEEA5DD5B2860642E12ADF3299DF0EC766A9764F24FF0F7454ADBAB
    525D3BB926D329BC91E8F2FE35796485D04E399DC44E288939C1E21EED2FBCAC
    530112C4101C9CA78E36826FC691FC8E10AE2F86B748CC4B0187C11A6CDB6075
    E11F5EB590A8854173DD9FB640F3A95828F5C905448C59A46A785064B04EF7AE
    C4B093023363508A81D1543E6C0AB5AC69A26BE8CD5C6F190F19664AB59B4B58
    7869D53532E873A691AAC3E0B6182AF9EDA1514EDD29CCA70FF3C88E856E86C6
    A39919783FCEFD693A89BE3511FFD0A86DEE25105CF0641F066B66D1B5223DD8
    B9195A154429B5151F4ABC350D628693CF5FE666211DAF59C36AE49CB5560B25
    2476315861F83FAB1E0B2BECDCD1B71EBD9C2C6650B03E39887FDB23621E72C3
    F888C301354F5B4396E319D48450701DF028E604317059979EB1928C0C79FBB6
    2173F1DC44B9D156E916F7312CFE9850D72014A5B91FECD483D32366E2536C1F
    19F797225CE5A1BE8EA9FB9A0B07FF2E2D6DBF2694828C5E0D2015B74969A1CF
    FDF7B34E101F44D4AE90721C3D35507DF3702AE799C15E211C6139A5AA53F45A
    7199BB71DC05971DF4C3801AF6C08E7DFAA299EBBBBE78E5A0DA7C3A2FFC7BF9
    5B252884CED06E385EE3FA4CF6E26FCD2AA43A765884DC7304A40BF35A118089
    F12404BC0CD761894BCF68DECDAF039F5356D0C7C87BF1B6CFC2764FBD093BD5
    CC83F03FD2FC2B1641170000
}

; ДОБАВИТЬ ЛЮБЫЕ ВИДЖЕТЫ И / ИЛИ КОД GUI, ЧТОБЫ ПОЯВЛЯТЬСЯ ПОД СЕТКОЙ, ЗДЕСЬ:

append gui-block [

    ; ЗАМЕНИТЕ 'BTN' НА 'KEY', ЧТОБЫ СКРЫТЬ КНОПКИ И ПО-прежнему 
    ; ИСПОЛЬЗОВАТЬ ЯРЛЫКИ КЛЮЧЕЙ КЛАВИАТУРЫ
    ; ИЗМЕНИТЕ/УДАЛИТЕ КНОПКИ И/ИЛИ ЯРЛЫКИ КЛАВИАТУРЫ ПРИ 
    ; НЕОБХОДИМОСТИ:
text "" return
btn "Insert (Ins)" keycode [insert] [add-line] 
btn "Remove (Del)" #"^~" [remove-line]  
btn "Move (CTRL+M)" #"^M" [move-line]
btn "Grow (+)" #"+" [resize-grid 1.333]
btn "Shrink (-)" #"-" [resize-grid .75]
btn "Fit (CTRL+R)" #"^R" [resize-fit]
return
btn "Load Blocked (CTRL+O)" #"^O" [load-blocked/request %blocked.txt]
btn "Save Blocked (CTRL+S)" #"^S" [save-blocked/request %blocked.txt]
btn "Load Flat (CTRL+U)" #"^U" [load-flat/request %flat.txt]
btn "Save Flat (CTRL+F)" #"^F" [save-flat/request %flat.txt]

; ЗАГРУЗИТЕ ДАННЫЕ ПО УМОЛЧАНИЮ * ФАЙЛ ЗДЕСЬ (вместо того, чтобы 
; указывать его в приведённом выше коде): не то, чтобы функции 
; "load-flat" и "save-flat" загружали "плоские" блоки, которые 
; представляют собой просто длинные последовательности значений 
; данных. Функции load-block и save-blocked загружают блоки, строки 
; которых заключены внутри выделенных блоков. Все эти функции 
; предоставляют необязательное уточнение "/request", которое 
; позволяет пользователю выбрать файл:
;  do [load-blocked %blocked.txt] 

]
view/options center-face gui: layout gui-block [resize]

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

7.2 Создание графиков, графиков и диаграмм с помощью "Q-Plot"

Полный набор графических инструментов REBOL будет подробно рассмотрен в следующих разделах этого руководства. На данный момент диалект "q-plot" Мэтта Личолая представляет собой простое решение для создания столбчатых, линейных, круговых и других диаграмм из данных блоков. Вы можете скачать и запустить диалект q-plot следующим образом:

REBOL []
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r

После загрузки q-plot построить график блока значений очень просто:

view quick-plot [
    600x400                     ; установить размер окна программы
    bars [5 3 8 2 10 3 4 9 5 7] ; установить тип графика и данные 
                                ;для построения
]

Вот полный пример гистограммы, используя приведённый выше код:

REBOL [title: "Minimal Bar Graph Example"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view quick-plot [
    600x400
    bars [5 3 8 2 10 3 4 9 5 7]
]

Обратите внимание, что синтаксис диалекта q-plot очень похож на сборку REBOL на диалекте VID для создания графических интерфейсов пользователя. Основное отличие состоит в том, что "view layout" (просмотр макета) заменён на "view quick-plot" (просмотр быстрый-график). Вот пример, демонстрирующий, как настраивать цвета, добавлять текстовые метки и прикреплять шкалы для каждой оси данных XxY:

REBOL [title: "Another Bar Graph Example"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view center-face quick-plot [
    600x300
    y-min 0                         ; минимальное значение для 
                                    ; отображения по оси y
    fill-pen blue                   ; установить цвет заливки
    pen green                       ; установить цвет контура и текста
    bar-width 80                    ; установить ширину полосы
    bars [5 3 8 2 10 3 4 9 5 7]
    pen black                       ; установить цвет контура и текста
    label "Fat Bars"                ; при желании добавить ярлыки
    y-axis 11                          
    x-axis 10
]

Так же просто создать круговые диаграммы. Вот пример круговой диаграммы с двумя указанными частями круговой диаграммы, "exploded" ("взорванными") для выделения (код "explode [3 5]" является необязательным):

REBOL [title: "Exploded Pie Chart"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view center-face quick-plot [
    400x400
    pie [10 3 6 1 8] labels [A B C D E] explode [3 5]
    title "Exploded Sections C and E" style vh2
]

Также легко создавать и линейные графики:

REBOL [title: "Simple Line Graph Example"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view quick-plot [
    400x400
    line [1 2 4 8 16 32 64 128 256]
]

Вот более сложный пример линейного графика, который демонстрирует, как построить предопределённый блок данных, как добавить заголовок, как добавить текст со шрифтом и определённым положением (вверх и над процентным соотношением окна) и как прикрепить масштабируемая сетка для отображения:

REBOL [title: "Another Line Graph Example"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
my-block: copy [2]
loop 10 [append my-block (2 * last my-block)]
option-font: make face/font [
    size: 30
    style: [italic bold underline]
    name: font-serif
]
view center-face quick-plot [
    500x500
    scale linear 
    line [(data: copy my-block)]
    title style vh1 "Linear scale"
    x-axis 7 border
    y-axis 7 border
    x-grid 7
    y-grid 7
    text font option-font "Formatted Text" color red up 50 over 40
]

Вот расширение программы "Paypal Reports", которое вы видели ранее в этом тексте. Он отображает все общие числа транзакций с помощью гистограммы и составляет круговую диаграмму транзакций с Саудом Горном:

REBOL [title: "Paypal Reports Charts"]
transactions: copy []
saoud: copy []
dates: copy []
foreach line at read/lines http://re-bol.com/Download.csv 2 [
    row: parse/all line ","
    append transactions to-integer row/8
    if find row/4 "Saoud" [
        append saoud to-integer row/8
        append dates replace row/1 "/2012" ""
    ]
]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
view quick-plot [
    594x400
    bars [(data: copy transactions)]
    label "All Paypal Transactions"
]
view center-face quick-plot [
    495x530
    pen blue
    pie [(data: copy saoud)] labels [(data: copy dates)] explode [1 2 3]
    title "Saoud" style vh2     
]

Этот пример демонстрирует, как включить несколько графиков в одно окно. Выравнивание по умолчанию для нескольких графиков - вертикальное. Уточнение "multi-plot/across" (многосюжетный/поперечный) раскладывает их по горизонтали:

REBOL [title: "Multi-Plots"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r
m-plots: multi-plot/across 594x200 [ 
    [
        title "2 to a Power"
        pen green 
        line [0 2 4 8 16 32 64 128] 
    ]
    [
        title "Rizing"
        pen white
        line [0 5 10 15 20 25] 
    ]
    [
        title "Falling"
        pen blue    
        line [25 20 15 10 5 0] 
    ] 
]
view m-plots

В этом примере показано, как размещать графики на субпанелях для удобного переключения:

REBOL [title: "Switching Plots"]
if not exists? %q-plot.r [write %q-plot.r read http://re-bol.com/q-plot.r]
do %q-plot.r    
window: layout [
    vh2 "Switching Plots"
    guide 
    pad 20
    button "Sine Wave"   [graph/pane: plot1 show graph]
    button "Parabola"    [graph/pane: plot2 show graph]
    button "Cubic Curve" [graph/pane: plot3 show graph]
    return
    box 2x204 blue
    return
    graph: box 354x204 coal
]
data1: copy []
data2: copy []
data3: copy []
for i -400 400 .5 [
    append data1 sine i
    append data2 (i * i)
    append data3 (i ** 3)
]
graph-size: 350x200
plot1: quick-plot [
    (graph-size)
    line [(data1)]
    title "Sine Wave"
]
plot2: quick-plot [
    (graph-size)
    line [(data2)]
    title "Parabola"
]
plot3: quick-plot [
    (graph-size)
    line [(data3)]
    title "Cubic Function"
]
plot1/offset: 2x2
plot2/offset: 2x2
plot3/offset: 2x2
graph/pane: plot1
view window

Имейте в виду, что вместо загрузки и запуска диалекта q-plot из внешнего файла вы можете включить сжатый код q-plot непосредственно в свою программу:

REBOL [title: "Q-Plot Compressed"]
do decompress #{
789CED7DFB93E3B871F0EFFB57E0A6F2D5483BC779DDAD1F5ADF6DD93EA77229
3B71EE6C5752AA498A92A0197A299126A91DCA17FFEFE9071E0D02943433BB4E
3E27AABB5989041A40BFD16800DFFDEA17FFFC6B357FA5E0F3BBA22BF54C9DFD
CBAE58BE57BF2DAB4E7D53E4A55E7667F4FE9BBC83D7376FB2BFD78BECF6FAFA
969EFE41376D516D67EAFAF2E6F29A1EFD7D8170FEDF9FB21A605C36F4ECE7BB
EEA16A00FA6FF2AE53BF2E960F5599170CF8BBE2FEA16BE1DD64395500F8E673
84AE7E733928F7FB36BF07C03FFCBED52A6F55AE1665053D5D6BBD2AB6F7EA3B
1ACB1FBEFD46ADB8DB974A7DAFB5D27F361D51EBAA512BDDE545D9FE8520FE76
D7D4558B307FDB541F8A15C0557FA2E1E7DB95D279BB575DA576D09E01492010
5A872D165B6E9481FDB2DA6CF4B60360BFABB26F2AF5D557F4587EFE0146AAB2
2C7AFE1BBDC2C7FF9EFD7B261F5FA8DFD72BC0FAD5AFFA9A3AE446B2AA963B6C
2CEF00F711B85F578F004E25E0FD7CB5526D972FDF43838BBC51CB87BCE9DAA8
E06BF59D7EAC9AF7AA576575AFDA655EC278A91D1EEB3F146D5735FB999A5F5F
02E1D5FCE63AFB462F912D6ED4D9B7DBA2037C39A42DF216DAABB66AB327E4A9
F56EBBC49EB767778AE021945B80F2A580F28BBC2D9640E81A40B40ABB037DB8
9435BE801A3FF6357EF8E543BEBD879680668DEE76CD16C859E6FB6AD7A96AF1
4700F3B9DAE408057AD6D6F006495CE8E6F22F02E8976AFE26FBC77C4B2CAECE
7E59EA7C0B3077B55A56C82040872553BAFD5CE5AB15BC03E4E8AC06C6D64D38
A437D04101EC875F564D03DD802AB9AA9B6A51EA8D7A2CBA0755ADD7AD66A6EA
1EB48A286A3F3D52AFE9545D15DB8EFAB205742EA133BAC1CA40D3560EE647D0
811F253B10345FE60B5DB604EF43DE14D5AE558BDDBD5A17BD0EC0FD18C0FD54
20E7E734FCCDAEEC8A2C20AC44C24FD4FCF64654FA4DB105215A225E33C02A36
DAE84DF50118A65AABFBB25AC03720F72A40E54F018A68FA87EF090F4C712478
BE75342E3630B80F5AD5800CCD4AE2F3518C02051D8F6B46C112FEC0F70EF500
B039FC2531F18800454784B58A10F1BAA94B8D785D528F5AEC12B5FF3981643E
591047D7858E01DE10A104C090EDAE2CCB0968BA0794E317071068F5EA8E40FE
6A034A0E74F026CBDA2C2B8D1ACDB2426B9D6555C3B2EC9052168B262771F68F
F4070D10E605B2D606342CE8A13B5F03A4A7035DB8811279598A17DDBE067D3A
37A27FE79EAF2AE8129889F97D93D70FC5B2B5DAA105990416BCDF15BE7007D8
D7AB6CB75D69B019DB6A2B44A2DDD575D574C3C73046BD45553E280DE3CDCBB6
025C38ED492F1153AF48D7D3D3CCF466861A421B56FA0CF0C1AAFD0FDF67DFAE
40B3ABBFC37FAC65FBFC83BAB9FCE20BB459B757F0DFCD4FC13CCEBEB89EDDFC
0407D529D0DBEAEF4065BAEED097B7A09C871FF3E26CA59765DEE83316005006
7907225A966AA1D10EADD0E6A086E00E9A5A60BB1F8A56C17F5BB085AC00EB06
08083A82E104B5D4BAA936AEEA62B758A0764756837A46FE88FC4A4D48090385
F754BFCD37647ACFA12B024007D4D5EDF6BC8376BAA65AED966848B7FAD18001
B12E8094C65C5D4E4DD541A7573B1664543DEF6C8F1F4149B3615F361A581014
1380B2C6089B478B8C83841A2D5A70675B48B515562DB497A6C6F7BA43C8A019
DEEB7D123DD00964A2515A196A81C2CEF6D90699DA1537CFF27EF0AC4F94EB07
E53AF6BFC2DF19A8C5563C244D9D6D1BF108747EF658ACBA07F16C9F81DF908B
07FDF0419DB5C59F6573EB5D590E9F35BA05DD2E1E0086C0D0351BD9A5955EE7
502A5B57DB402AEDF3655556617F41E6860F1D10E075F17857C725ABC76D0C13
C8A86334F419BF0830133DEA333000191980A060EA2913A5EDF6C1D3D57E9B6F
8A65541A9E67F5D222F000438D7E4C1DE35101758CBD0496754F1E1FF4D608D1
739A606A7978331221610AE6CBBC5B3E78ED7C0516352FDD4FE26D45DCAC481E
D4DE7C5F15EB35FC435869EC9755F667DD54501E547C4DCC4B5F3ABDA9B345F9
9E95B36FBC58AB89E3BAA99A1BC7CE3DBA83E1B6EF8B9A04D80D82140D3B6C00
E0519F833B9097A041567B6020E0D0A20B75F233F036201243F827DD83CE8549
C7B6DD0195487F234A906A8414FAD6839B55EED0DD8032A00B040014CDA6AA75
032AD7F9CD383634ACA849518EB185DC330114BD2F40DB5F8670BEA9C83B0457
A8510D4F6E406F376441CB42AFB813971F71FC1A342E366035A39A1B05E99EDC
09BE1235585F0DDEB11452FD75D1B484C862B3DB64D5DA5688CA03B551D6DE19
4E8C01E2C7F21AF40B3C0960937A6F00268B3F3EC08456CD05DCE128467BEC5B
DA2263044C3EFCDC454FC32777A76307EC5EB362DF353DACBF6534BD8ABF8D30
28C8E3DC586BF7E4190C8AF50D09F2FE290C8AED7F0ACCC7A318EDF1A767D024
765ABDACB62773E8DF1A9E4EE45036ACF3DEABD0FE800AEDC738B41F5112FD11
C4F74F540D23F0F013203E3988D10E7F6206356DDD3C9740C89ABD5721FD0115
72884049213981404F928C2710E8B064F49F4A32F0738C62C714B2E860A9B7F7
DDC3BB319590009EA8BCAE764DF77048558D4BB7F4C7806CE8A4B9B0356A410A
2ED54DB1D4EDE714E1E4701CBA7BCBB26A752BDD60F4E5ECA8EDFC2EEA9C6813
AA04F39234574E26F9A2354A3633EEC095F9FD5ADD5C5F4FD5D7761E334DDA3F
314DEA9A9D3E6E12448535CC6EF571F4F15402E057D94A2F8B4D5E7A3670D0D2
E3CBB77B3537E3F94A5D9B11C2B7148323AC9B6B3509D0313D9525A9A90937F0
F55718B59C5A503FE39FD0A6AD9364BDB7C15C013DFC4505E47DC83F681F7F69
8BFB38E48F1F24E46462C6E0A83A8571D871998730A6E9A9D2107669D9546DAB
7002972C6C1B370D5FA8B03300E874E109BB135024CD223CCD9CA989E0128E72
5CEDA7C4D3C84452263996BEAC36F5CE85DE8359EA50E91FE536C75D18210E40
E102D975521C82426EB6FC3A41B41154E1A7589BB67F063C7EB495ADBECFBBB1
B1862001EB5FAB78301307024B5C593EBFB0A47FED517F3285C306042606A3BF
53C1A4BDC030A96E50B3E2A2424331C3AE52ED43F508862DEFCE5BB5D0B4EA83
337F2CFA6EC836B655DB691A88448E8876F05ACDCC9B750E28A9B90C4281A45F
DFC95A18E448F2668FBC299F4F42B385F27BA3A612167246BEFAA3E8C2185B4A
B843B0D0EAF5E5ED1B94D2F0CD1885ECFA2E93D17DB5219C678D4E4DC5C84474
B1D114459E9FBB8812FC3DB78125FC7BEEE24EF8867D21FA1BF4F97C187E6ADC
2313880A7F9D9BB894F9E7DCC5A7DC3043F0961D1C5FE023431F4FA914DA8681
AC57FC1EFF9EB05EE02283E17AAA9AE06A3886E12BB56AF247BBC8323D04E815
0B02AE62B647837FD6032127E6B33B75F60BEBCDEC6D500B571FA05767AECE20
60B8CF6AC4172EF8C1FF30FAA2E1E5B70C676FF89810C283173C685843443E61
44F78072FD41377B8CF5DFA313BEAB3FB7EE9209F93908A61554B3CB5DD3EC19
E455C802C1B306CD0434B1E300A37966F11DCF0E68583303C3F184353508620D
1AAAD4EB4EC5958B6DABB1545E9426F4AECE912A52F68BEDB2D19C6710889B69
10399444CE3E301CE8857A5D014E960F4C85A417FD1615E579C7AC854E0DC5F4
0D7127F9BA030CB86E20D2FBD03291E100E0B119B2E4264F8EE86EE51CC7824B
8DF7A4CEA01CB84E9E27EA6E7A64A230823EDBE001CB69283609502B7EA0291B
E0125426B36F629E98E8C45CE607DCC9DE1B71777FF6D97D53AC8E0B212B5735
C7A17DE61F6F1B16625EB505548A77910C7A39736CEA110EA37FBEF8BD3A1919
C45918C137E1F247CD5A6B592163ED300F8106F4EAD34B308D392553D6F374BF
C0DC9084396C837D9E0A09B5FE41A8035E85A89F4978523356ADF04200B2F457
093738086C57E5AC4F6086D0551B45515F4D323B44FF985AA1C106521889D4C4
767FAAA484523FA777D343848E804FAC4B40E27310AC1FF40E18A18459D3C055
240C320F5F30E90605088F8151090A84FAEAC5687A0AAA3E01BA46474625704A
10786EFEAFFFD3FFD5340F6B4E52B02FB5F347ACEE8508EBF509F1B6F8CDD46D
2CD0F060A0F64F576B42A3459A2CA293E1F0133992636ED4DA5D8267622B7AFD
1CD64B18633F9D0BE105E67336C419184F46BDB3997165985F4A5E8F89383DC2
BB928BF3BE783613BB0C88147B6FF2E6FD69EC6DE60FC8DFFBEC416372AB637A
14C7DA7C47F4B000141FC1D4BE550F94B6C646AE061E874696796BD203AA6D69
03C03861A69FB7AAE8F4A6152070CEDC740888D26A6C4A22CFB168E13A986081
A39797A89D0D608C3E99D99D7932555F8170DD0D15B8488961864E84A6458A8C
592D4A144AE5A3784A0C525EFC0BE412CB059EEA8ECA09B7EEF499BC8332A245
3C201298438B03036C22340B7D6C8E6EC061D6F03BD7747A1DC74D11A47FD373
C473388ED07F4EC04A4C4522B0EA18DC4378B7A234F3042307E9F6CD9815908E
556F1CAB50B78CCEC0C6156FA7FBEEA8E245B31CA3C88EE088DF0490DA8E9279
5303198E88554A622033E54D7ED2898212456022050A8FAD58D5981A6BDE36D5
6EBBCAAED5A40065CFBAFE18B7489EB85002756C45004E7128D6190CFDC24BA4
51BDF17CED39AEDF617AA73DC08809D244170DA7091F08319B8E39FD7337676C
DFF2D3BBF1813172C0B27A69416934389A0E4D6A50BF588711458E54F9826F29
693A07146D31AC881989642ED0F2A0D053A400A6EF404A4CB4C2459EB2C51477
5ED8C24C6555E1081F0BB1F8F31611F840B649A389420C71AA2A556BC88E6A30
03958023D666B03E06F87583937653BB622CB691C4BD9CE203CC3E41F1DACF9B
37C957230B31F8F92930F7E4269419B34C50B3E45AFE104A385645D4CAC7D050
2946152DDFCD6BCFA649AF6DFF2287ED096ED99E750347369C01E01924CF92F6
DE217BF984E47F68BC2261788541B1818B1BD1C85342153FFA78F634E6CDE707
223CBB86C2B14F1BD6FFDE88C373CCCECB020F317A8E74FEF4A002FDE1DC84EA
A15C1E15F3C1CAC2BCAA61E6F2807BE6CAEA913322EE1405AF8B56E65170687A
74BD01C1B404A74540ADC9AD008EEFD88FC1F158CAD990A825F0C75103D4053B
D9096631D42D37C5095E615F6726973378C1FD77D91FC13B21CF38BA99301593
60950054C2974A08CE1A198910F2339CB8CC6DFD9B3B0132E5455F98A686331B
D9D88B02B811D70F65759C574D6EB8DBB1891A95DD13182FB826B8FB04FD8DC9
AAA96ADA5A407B60C4563C4A6AD2252D05B4D13A0713D4F0532C9E2264BBCAF7
E7CC84D802F273ABEF116854E9C4E0CF912594E41C043F6E5985A111FF255657
12CD9FD0E2103AB2F03458AF4F9269802B927B831EC496F7FF8CEF87FB6833DC
E54AAB631F0B81479DFA09AB8ACC30FC341A2E89F92743E618F443A824D61CE2
92961F3F292A9FC78B2C472FC7DFC44F0D994ECF6EFCC00C0BD774756D512AD5
046E27232BF5412F3B988100A639C72FA86F4C0255A4EFC15B6316E82D7D0FDE
B265A097F83578678D03BD1D6411468388A6D2EC54C6515C13C43D840EDCB449
79375A6CBAA1B566DC91F8A0E5E48DE390DB21080556BDD1359098370B020083
C31F08457F09CA233BBC1BE0EE090BB1D265A1BF30ABFD0F74BCAC9FD2F566F3
0CB8E2BBB6A10D6EC6B75080AFA29329849173202DABAB3D4FF40AF3CF5048E7
546262CB82DA4CCCCDA9DD78F95C2AA9F158FA6B1ECE15A57586A88CEBEF71AE
892541DFED6A53494D0F22D80D05D10890B0B3E895F6DD89F518057247E1344D
278A3A5842199AF4661E17BB73BC6B12E5F5903BE71D2FB7C9D27E0B02651EDA
2C405C94B875A56EDF60229F01123A5B020C785CB730479B4BC0B791DB25E7FC
BEEAEBD0A91BD27CE0DF99A0905CFA7A2382B4864FA6C3445F8F90305CE82891
90BF97387A83D914851B4DAA8A18FAA159666A3197987228398269EE0C7060D7
E1740635529721ABCC68AFB3C04F6A0942D4A428D113AB13FA12BB86C5F360E7
B07D3EDC8D2C9E07E587F3344C6DA36DF947676867E458E414A000558E478850
4531F90A26715ED8D2119C68F10C8901262D2317655767ECF6719486C3912E23
EF53258905228305DC06EBA158889DD75EA4AE128AE00233380FC65C5F4998AC
015836A2B4C3F4240C0689C7D4B8E410E063F0E9C01ABBDC71BDBA27E4B5EA0C
03B2188FF5447B3B0236F363446D76798DC1DFB2AD68BB3182A7D3124C38D69C
C471EEAAC4A3DBDBC4FF672805F7C566B0215724D3D768CE47A6CEFB1A07A67B
3ED52BE1AA16145E48EE15602E3D96CBC69E3C27B13207CBC89C7D769DF07A2D
FB275A50F1ACE442F0E9D4A54F63E79F903B77D4DF5F54FD5D2CA0077CE68187
39C2D4A7BB6D0A4F08C8C0E7B37EC12B11653227B7FCD5024DF678028F4F7F3A
817FF6F2701456618C5359A31557F99E9BFABF40D5C70B5445DD7B891FE319E5
7F78A02A1542D078F00FDA1252EFC4D276950F384F4D30FB638B53035C64C30E
C1D378A6ED9874E6779CC970066E8D9A04337020A995ABBBB997A75889E575AD
E9308CC11CC22808C0956D7BAA06BF4FD08887A22A7F03C1BC97C4F212A8B9E7
5375800ACD5637AD9DE783B9F0137ACBA9ACA5E19D70BAECC769BAD8E8A511F0
31C27342A91EB6E61F2FB0849F431C2670E43D19246F569E16AB4B4D80901A49
5372C07E0FB5D4FFBE98D728A0016AFE3F8B7F914A0CCE688A5C2B1A24BB574B
AA143956380EDCEDED9DA91F6868EC31F52003E065804776450FDA2B942C318A
F621AF41C4F0102EA8AA7EE0DFD6CAB4FBCDA22AED7622516BBF31B349B7EC0F
55F181C1EE58C568B6490714F5C3738978C5BDE7FD31F0A7EEA259A6CD2DA47D
F5F33B39C1095E785569272D752790165A694130935B55775737A365F6AECCED
A199E578984BCCFE9E77DC8F4B54E8ED11A1D85ABE41CFA17D31F47E74D3F7F8
7EC760D3B74C44C4ADDF322BE7C806F003A991A1AE3CB2195C36E9B6844BE0FF
3D1BC36576D0607BB81CEBA7DD241E7422D82A2EB1167761C4580DBA364A4121
2B82D50E6C1EA7B4DEFEA4CDE3812A71654FE5DA017FE216F200E0D816F24121
A7C15E8F12F680BD07CB25FB91DE4E3E68D1EC054F8F3E05D86E2A4FF2C700B8
DD692E2B5F85427411F2D26B4FB9E772CF481F046A93E864EF783075EC91243C
754C9B9A174D350F4E31D17CD9C97CE210945EBC36C9F583EA26831EDDAA647D
F13E05A04EF9D3D1ACDCA0892D6EBCEF3304194EFADAC7A203A36AFC88D8B12F
9A25481B7CE674C8ABF90DCD581FE204AF1CBD66FE3010FCFD3408AB22DF60D0
C442B0BF9F0685D5AA180CFD7E1A8C7FA514400FC3FC7E1A103E7C5A74C461F5
664CBC2238ECBB0E784A0670FCDE8DD8B97AD0F96A5857BAACF8E7ADF5025D48
C56D92A7F7A2E7D6ABC52DCBBC3EA186EEE5C0731CBA83662FBF3BB440F09943
A9313796859EDEE8AEE6791B465F9B6ED805F376868D6604CABDE20AF4E6227C
33EC37B276D84CDC7766999775DF058F3FEA680860AC70B8C2951BD9D5FE2E00
968E36D8C2BD6950D61AA28D565C42BC99ADA3E1606364920C3F0B9735B704FF
341DFD8056E0DF0422534AB8C6814DE0EFDEA0771AE2E4489D8BA80EB79ED2F6
1D2D3353799CAB48341EAA7291AE3282798F0D8377EE0F3711AE14480DFC7F98
7F36E6EBAADCDF575B83108309468B453EBD1A20BF70DA76B008E3E7ABC14AB1
7FDEE00964923267F81B66FCB8C24E5DF6EB327C9F41189838F3E903620547F7
F5A018872EDC396CDBDD66A1299E45D3F716CF99C7C882397D7F34E9B88129D1
8A2D65BBDBA81A0FBAC70ED3DE4F3E2BE55635B743AEC1104C3B1BB832F755B9
0AADA71E24685765F1218C082E1AB9D2C1B8DF860F1EC1A50D936BEB5D539721
9C0F4555EA3040F8FE217F5F0C1AAB1EB7830EAD9A7C113C2A75BE0E1E2CC103
D4616B5D1E82D9E4F718090B9E6DF30F61DCB86A70E939F412FFB40B2B61F8A5
ED06B09BAA4A4E072DE5ECF6065CFA1607F7CC68B66B0B81C4B8AF89240220BE
DC8A7764ED9A4A23BF5CA8610EBEE81F32930F319D08DA38CEC48872968BC5AF
B0CD83F90947D75AFCA9EE42398DD432EED1C4E20D7C7E402ACEAA58C839D74C
0E38A1AC04FE83FC354F0C507ECDD1BEF0090FA2273CE7B0B596D50EF9C01390
F681E0DC5CF92040DEC16476A5EF1BAD5BD554E69A03195FFB37323B2DAF93B9
3D6D5D55ABB65205DE81516C776D444A4406116C3E101CEA14FD136DE1C47936
544BCFAF815E48243BE9236D131502D9E5ED2EC882D15B545C182E5F623EE1CD
4F1015D304BB3A5C99925FFC88825FE9823D53BF2DF89CA1E8FD9EDF2FABD112
497396E20A3A4E631A2E1C052C333CB00E3FA90E4F1A28FC662ABA7D9BEAB72F
E67B1F176439FAA86318E933B0CBF5299DB6E50EF59AD3E5FECAA87F921C1F85
F6F62D1EB7BFD46DCB71544D5344BFE86E0EDF7777ED94459B58E4C5F5F5EDCA
39132C98E9E856F37A66926A85AE43CD972CDD43E9E6F54172E1676F8B1DA216
7E003FAF0F660F87C47A3DAA58F7AF131C4608C59C3A73F1414F21E13DDF72D0
D12D36F64603663073E3D44867C7D92BECD56BEEECD00ABCE68E8EF4F3A84133
AA32C14407AA5BAF1819F1F54855B3BC0506DA285A32C16F0E9DB72CD423D51C
2D192ACAC3655F8ED831C82172EA5860F0730A5EB1B1A85882A02F20E658D5B2
AA5654AF4C9F3E67924775D354CD3BD5357BDA2F600DAB9F808CAC988C34CB79
F396F17943A7D99D4949F4C97E702CCDCC1C28164BDF79FCAD0997D1F7B8BAEF
A85D5B770F06DE67F8EDCEDE66A368CF3ECF0E3157CE46D95803E089D7AC59DD
A5261CCBB6A7455A28F4A1E3116DFD026F0873DBF9D7F6CA935D8BC8C8FD9D6D
04C104CCA3F5E933B7A91ADFE39D2734B1E3A70868596DC187DB76624A1886DB
E7C64F1649CB0EE6E09D20F5D1F382CD749ABBBB4F3009A6B9ED7119C8DF7913
6CEE157D8CA96A2AD8842B40F6D70428C98A32BF251C7B169CC26BCF763E7D59
701CF2450AB23D5238D5C018331EC2AADDAB43076744200F0F9B4E1DE60E4ED5
D87A5918DBC06333FD622C6A7977CDCF84CE5B12F734E1F59C26A78281F7630C
DC2719B83FC4C0FD3106EE4F66E0F135CB00D5FD0803F7A30CDC3F8D81FB5319
B81FB259BC1ADA3F8F81FB218FC490ED2AE14762E014560F8F9592964EE55AFC
6B58D7ECFC8FB88F32DF3902E658C4837CCBF5D40294FA43B1A645161B5540C7
AE6B005ADED9CBFAF063088A70E39565936AFF3305B8C44F8A6F5096D1C76429
82C9331E194AA7D562961C58EDC7143E831CBE093662AE36C8602AFF663A2527
1C26B26364313DFB5A71C7467BF6A4B62FB86D15371E10C8D3E7FA24FA88A8C3
C7254F72C88CC123EC3CA842031F192EFEA153B4FDD59C6A38EA1FBEC5CB5629
EB30BAAED9ADF6D9BB87BB4AF923B1FF50E8C7AB6F9AFCD1E7840DB7062C37AB
564483BFDDD6BBCE5E348D773FF364115A188DFCF262B44B52B3D95E31798EE4
567DA73391C1EF2FAB94D7537249652E0544C4E1D61ECA30E4CCBE636958EEF5
D83E343FAC4FB421CDBD90F75D0E639BEE8BDD1D3B009ABA03133FFDF8652307
EE21F1D7460EABA49F377CC9E34F7C9410BC68F6A9870331218203238CAEEEE4
1E251E46B7737249EACBB58037781032EA8CCFCE6FBCBB6F3977A6BE90E3311B
40AF3DC1C28B3F6927D83A5FEA2BDEE91C28028677F365F010C3A7748FE67C01
33E150796EC187C27D23DB2E6BF36D9BB5BA29FCEA41026D83DB4617652E963F
C2DB45EF9B7C2F38C7DF46DAE885DB4E831F7FFB685B941FC45A89BC83F4CB37
97F49FD8C2175C46FAC59B67F3D5F3B877701969F492F9016CCE40F20C31CE1F
8233A74FC902FD46AFED3E11BE73BBD9953A3857F47774418739DC0DDF9232A5
ED48AAAA79BEC79136A3B74FEA81284486226C96D388409FCF40CB9F1B1FF61C
E78D93E8D2A204AAA7EA3FE3702156A7438273F2BF8675E2DB9000CA0810EC47
00E1C4BAA9C6636E48D6359C811857733416861B945F549D0CB887FB117A926A
E2F7338BC8082DACF3D9AA8CF5F9B93969D53C84665B3528BF0FCAF32F732A9C
2D2FC4C13D46927292EFFC9C8FFA9B0C8408C9776E6E6611EF783C7729DA2214
77469AC58892A730EC0F76A03FD081FE651D0871466C0EE8E21CE3188CE5FB04
0B0C683456627FA8046E541B9D5A4899B45B4342B1A43D082C953681838FE2F3
1B073E630E25058B45EF4862E9686EB3A3DA378996DF40C30426DCDFCC7871B5
01D514C6E3AF515B77B87669F6BFBB9CFA6C906561B6975A5D42F7B611287FF3
DA68D7FD0168A2B4806D36AD3AE8E6B719863ADFD526CDDEECD298D3968ACF60
2C66DF444AD2719D9124DCEF47E56D16508DEB230B0A1C25B9311E5D3C559F04
5B6F6515D763DF0989520E7F58D1373FCDA8CF4DF21E7522D8240242456E35BD
496C048907728011DC4E0CC70AB61BEE4DE8CD4FA5FE2AB4ED3C7C2578F84862
8BC7C221552A60FC42FB3E6561CC521315B70B4AAEB81F7B3350DC01A00976C8
75C62431700DDB03037A2AA3423468A7BAA92E5F6D24C54594BF6F568E6BF902
01ABC358589D0E9B98EB056C576C89A9D4AD12DAFE20B4FD516847CDD0E4D821
E0D348ED8FDBA8C93E009600C11E961D1DFD729C8E6E178D54B861CACAAA7BAE
7819E0B3D01EC1F44AE00CD3525830DC05DC2638404C91B4DA34159330F23E80
013F8FC2E8837EF4613FFAD3FAD107FDE8C37EF4A7F563C13EB7A5943FBA02E1
F85FF161B13CBDB18DF389C6465CF93C188B7D357147DE48EAEABEF3268DD670
DA6A43CB816DD159D5837319FE7A4CEDCA06EFB8493EBB86D2EC518A6952E726
C3522B6D8A76E9FA82C0F0C150F7C3171B3502F08325B7762986861B521D6268
77AA836994D2E1FAF0D4BBE1F373F3D31C9E1C10D1EB55DE9FE874ABD9AE3868
C73C9EB214B94E792591EA96C093A301B6C166CA636B68612DB010E6392E2D0E
C66B0D0C42C5EF4F07EA162D0D648F17CB4C440BDA14CCDA591A02B411C661D8
8716C277D13022732B4DD6C9BFB0679471F7C2E248A7B8CA563F8A2AA303C212
B6A81CD0113FD63A8A0921314E5F4A7CACA3967204BCA395AAE9FD91945B5C8C
F5C59BCDE44B63D512EFF607DE1D74D10F7AE7CE26A4DFB18E4DB6385EAF3F50
4FE8DCC45B67F712EF9CD24DD5331A35F14A68943415ED3BB34CC00BD4B817D8
AE40DFE3325D53EDEE1F5CB414A3BE112CABFB12CD582D4BFBBE113EB644AD2C
340CACED78996E5BD11159EBAA2CAB478C4A17712EC4C8540A3F2045726A15D5
382D3AF3DB46D7B8726ED622298A0DD3245A46C7845017D17E125CAFBBCDE90F
1695A6195C93C2549F5D97EAB74D78C00A8508B13399281D0B9582061BB5C285
9EEF7EF58B7FFEB5A88FF47BE411EC3907A3B5012453DF152ED63649033A7C85
E5CFE137676C20D967B633572BAD6BB5C0442E5A04104B10C3ADC2D43C823D14
91743EA54D0A21A8302DEEBB265FF2711972D77A70759BEC3E1D4286693AEF3C
CCE15A28F78862B034DACFD41937CA87C8982624AA373B78BBD0AE69EECB84D2
EEA7678706C658E3B316A4DC48DEA0C074B409FEF91BCDCDC6F76AD7B8950D5E
D2A1B381D9D2D0B85EDCD630330786E0D3BBE8141919699E2A71DC8C88321F48
82960047CFBB247C3E51D039F62A64FAC901555F8699C7F106996A35D1EBB536
9C8BE7B934FCD62F34E3111833D30FE2761FAF0A3944B4B4ADD8A7A4B62C1BE2
204895B7F61E11780D92CEEDA3AA894F3AAAADB059199132E46F69B67B8367C1
319832662FCF2A5DBBE0BCABB99F893352A39A839111AA9271B00857E16BC9FB
6B73C1F5638E6A91051CD59D55E3D682E197AD51056223C55B65AA1871D9B562
29C3E4B1D1E125F38416F9FD365F94B4E3C574B5DA6C803AED59423190987698
6D06FA86D77E3959CB29775A8AA56944B34BABEBB7CE90D035DEC64E97F91E2C
09F2022EFDFA83A8702889BB2CDE9A84313E1F81A6CF3481C27B6B0A770C7678
57E6DA141CE8D5038B92A6BB350CA12F3679A7C1B75802A1CD693D0C6E509A77
11E1DD356002F0686122E0C30DB9D974DADC0207FAE3CB37E072F6183602C6D2
974A7D4FA90EB4903F046986B329567864508982FFA5ADDD151B9380E73732B9
D6DBA877787C0D6D69401A59D6A2815C26D0323C9676224FC1C4136D3335F912
D363CC7198546B3A1D6E050E6085E9A8F695BA1E9C52101F72E429E5D56BD514
F705A660583853E6838908B5C0AC907EC57D1AACAC26181E4895558B3FE2A1AE
B6CF8133313C9872D9E8BC33A7743247436DD46840DA479C2DBA84895850F163
2A2562FE64BB8577109530A8B8EEAFA357B8EB57EEDF090019959BCE8F22499E
4FD8B425926413D1624FA6275CFE66EF4A6774BFE252F40FE58A6CA0F5227337
A19F7C91903A93FE1752AED4401E0FCEBB41BC7D2771EBB9C853F15152791749
CEFBF4CF0CEDACE26981662B4DC93945B08D9042F789E27CE61F2DB1DECBF20D
EE3612A8C29F6DB49F911472C9474AE1A05B7FCAD26E410D8CE6B36C9B8C7B80
131DC21F4E20B69AF75702F3304E0D1C2E556C4075545D5E66D41F6FF4F0A561
454C0532954CDC14277DFC529E5E2D72666C57664EA5D0CFA4D7CC7899B37DCB
F49F7679F9CE55335872434B24DAC5A6F03B8228BC67322F6890B7389BA0156D
DAB515704EC8D2412FA91D8F2599A4811FB9A1CF9235EEA7AC2F7E247710C6DF
1C216703CFD4CC06860B61F6742366EAF40E43C71BE75C2C2864F92556F6B7C2
ADA2DD47A35992714374DCF189EDC803F2718FD36D0A2F82516781D204A2A083
92621BC3CC76BA17B2267EC2C93D63D2B06982FFACB08564BD32D09921E2E808
3D36B3B34499544AA968C90AC4A9DC7B881D044A462891BCB5E04AEA994C7D39
55EE92B1D89CC42AE339AD401303FF2281A6C3437180D3572944837ADE50AE4F
807EF006138C264CBA7DADDF193632A3C27B7A79E68FA9AD9BEA83D3E7617DAB
DCD9EDC3DDD9D2004E68A78971CAAC3D90783B70136724D3B113E74572AAB8ED
09E92CDBA9E921352BE5595AA18B31B32351463324DA654372452292CA467FD2
20F8DE7176BE8CB29A42054C703E45FC126E92F1195CF36C3ED55BEFD0BCBA7B
F55FAB2C8BF4F3A50000
}
view quick-plot [
    400x400
    line [1 2 4 8 16 32 64 128 256]
]

Полное руководство обо всех функциях q-plot доступно, загрузив и запустив учебник "ez-plot" с сайта rebol.org:

write %ez-plot.r read http://www.rebol.org/library/scripts/ez-plot.r
do %ez-plot.r

7.3 Рисование диаграмм с использованием исходного кода графического интерфейса

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

REBOL [title: "Simplest Bar Chart Maker"]
data: [12 3 9 38 1 23 18]
gui: copy [backdrop white]
foreach val data [append gui compose [box blue (as-pair (val * 10) 40)]]
view layout gui

В приведённом ниже коде добавлен ряд функций, таких как текстовые метки, полосы случайного цвета и трёхмерный вид с использованием кнопок вместо виджетов в виде прямоугольников:

REBOL [title: "Simple Bar Chart Maker"]
data: [12 3 9 38 1 23 18]
labels: [Jan Feb Mar Apr May Jun Jul]
gui: copy [backdrop white across]
repeat i length? data [
    append gui compose [
        text bold 30 (form labels/:i)
        button random white (as-pair (data/:i * 12) 40) (mold data/:i)
        return
    ]
]
view layout gui

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

REBOL [title: "Simple Bar Chart Maker"]

data: [12 3 9 38 1 23 18]
labels: ["Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul"]
height: 11
width: 50

gui: copy [
    backdrop effect [
        gradient 1x1 180.255.255 255.255.100 grid 10x10 220.220.189
    ]
    across
]
foreach val reverse data [
    append gui compose [
        button random white (as-pair width (val * height))
    ]
]
chart: to-image layout gui
gui2: [
    backdrop white
    style txt text bold (width)
    tabs 20
    across image (chart) effect [rotate 180] return tab
] 
foreach label labels [append gui2 compose [txt (label)]] 
view center-face layout gui2

7.4 Создание 3D-графиков с помощью r3D

Учебное пособие по 3D-библиотеке "r3D" Эндрю Хоадли находится на http://re-bol.com/rebol.html#section-9.6. Хороший пример использования r3D в бизнесе можно получить, запустив этот код:

do http://www.rebol.net/demos/BF02D682713522AA/histogram.r

В следующем примере показано, как редактировать код в histogram.r для создания трёхмерных графиков с использованием блока ваших собственных данных. Это может обеспечить хороший эффект для отображения графиков данных в настройках презентации. Вы можете использовать клавиши asdfghqwerty для настройки положения камеры в 3D-виде. Просто загрузите свои данные в блок "graph-data":

REBOL [title: "3D Graph"]

; Здесь вы помещаете свой блок данных для построения графика.
; Первый столбец содержит метки для каждого значения.
; Второй столбец содержит фактические значения для графического 
; представления. 

graph-data: [
    ["Jan"  11.0]
    ["Feb"  22.0]
    ["Mar"  25.0]
    ["Apr"  55.0]
    ["May"  35.0]
    ["Jun"  75.0]
    ["Jul"  20.0]
    ["Aug"  33.0]
    ["Sep"  21.0]
    ["Oct"  55.0]
    ["Nov"  65.0]
    ["Dec"  45.0]
]

; Этот сжатый код и есть модуль r3D. Его никогда не нужно трогать:

do to-string decompress 64#{
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
Rf8G28Udt8glAAA=
}

; Этот сжатый код является некоторой начальной подготовкой для 
; построения графика. Его тоже не нужно трогать:

do decompress #{
789CED564B6F9B4010BEF7574C4F6912110763471187F6901E7388A2283D581C
D6B0B669318B96C5C1F9F59D7DC00EC458E9BDC896601EDFCC7C33B3000090B2
3D97EC45B2B26E6308A2DB9BDB2F30941F8DFC84E23D867039943F0AF187298D
343FA540A853620DD407C8F2BA2AD8F191AD791183920DA7E25756349C885351
8846F2CC98D7560146F3266491C568501D619518D196ABE0C138C4B069CA1456
D6D4E033C58283468755C6D37CCF8AAF49AF9D152265056468606409AC7A5DBE
A1CEDFE10E2B4164C955234B98234391FE4132E9B11C7A20154BFDFBB44768CC
6F90C3332E8BA18B0D323FEB120D5C969FF0980F3C4CDD37E1D94AC281073E8C
CA208AC80E88D53515C2F0AE8FB41FA73A6FAE5A31696673D10F9BB97899A174
242C9BBD491167AAE0E556ED7EC056B26A17E8ECBD599B36B216384F169C286A
C5AB18BE1974089C1E2E61064A046EC27C14EFB866B2C659E3DAD780C01556BF
44887B64EAD21B6E84E42CDD6100258F2439478537D497E73CB60EB3F947230C
1DEC45C60BC7A08CB260BF68F70B73A7F4CE17487A57B46E8AF96BADC9B94FDE
DF90665F414419EE228AF56F9E2A8CC6B326C5ED83B459739BC72823B2C01437
1962B2AA42CEED1478501F696CDFB7B02BEBDAF1EECD88873DB72C3795A87395
8BB203065283313394B5F4E1481FDEC7A94C5F236077CEBAA7C21EAE83A77FC0
5EF58D0CF19F90BA9FA4D0856189B14942D7CC655D69D9819BB38D34F41969E7
F245E6ACDC167A71A411B84ED8E408A4DEB836BA230079797820F4E22306C351
5838676F69E8A3035A79587473B108F2064AA1466F0B2CDCCD8A1C668E8A0B94
C3DB2EC7694F289393FB3EBD8DA32D2B74EC1DCFB73B455FB824D3874F66D9CF
844996EC46B7E0C9680610FB2779BBD233B3E395B7EA976ED7931B6DB29674EB
491127E6AC4E25E72542F41D3A44B665268099876E77269C51DDE2D702BE6DDA
F0FE165792D541C572E9D5B390DCCFE1038E23ED798A34B8D0B9D0708EB7104E
9434ECDADDB86B533CBFDA0F8AFF3C0F78C61760AD645E6EFDAB283943E7D401
7DF27CB64BA2BA3CC817C387E3A9FB40F80B7F5D1FA3080B0000
}

; Здесь находится определяемый пользователем макет графического 
; интерфейса. Если вы хотите использовать другие ключевые элементы 
; управления или виджеты, такие как ползунки, для управления углом 
; обзора и размером 3D-вида, поместите этот код здесь. Переменные 
; cameraTrans_ и cameraLookat_ производят все настройки:
out: layout [
    origin 1x5
    at 0x0 scrn: box 400x360 black effect [draw RenderTriangles]
    across
    text "" #"a" [cameraTransx: (cameraTransx + 10) update show scrn]
    text "" #"s" [cameraTransx: (cameraTransx - 10) update show scrn]
    text "" #"d" [cameraTransy: (cameraTransy + 10) update show scrn]
    text "" #"f" [cameraTransy: (cameraTransy - 10) update show scrn]
    text "" #"g" [cameraTransz: (cameraTransz + 10) update show scrn]
    text "" #"h" [cameraTransz: (cameraTransz - 10) update show scrn]
    text "" #"q" [cameraLookatx: (cameraLookatx + 10) update show scrn]
    text "" #"w" [cameraLookatx: (cameraLookatx - 10) update show scrn]
    text "" #"e" [cameraLookaty: (cameraLookaty + 10) update show scrn]
    text "" #"r" [cameraLookaty: (cameraLookaty - 10) update show scrn]
    text "" #"t" [cameraLookatz: (cameraLookatz + 10) update show scrn]
    text "" #"y" [cameraLookatz: (cameraLookatz - 10) update show scrn]
]

; Попробуйте изменить указанные ниже переменные, чтобы повлиять на 
; то, будут ли метки и значения отображаться в нижней части каждой 
; трёхмерной полосы на графике, и будут ли данные отображаться в 
; цвете или в черно-белом цвете:

DisplayLabel: true
DisplayValue: true
ColouredLabels: false

; Следующие 2 строки обновляют и показывают дисплей. Не меняйте их:

update 
view out

7.5 Использование Google Chart API

Google.com предоставляет мощный и красивый генератор диаграмм, к которому любой может бесплатно получить доступ по адресу http://developers.google.com/chart/. Синтаксис для подготовки данных диаграммы и отображения результатов значительно упрощён с помощью сценария REBOL на http://reb4.me/r/google-charts.r, написанного Крисом Россом Гиллом. Чтобы использовать его, просто включите строку "do http://reb4.me/r/google-charts.r" в свой скрипт или вставьте следующий код:

REBOL[title: "Google Chart API"]
do decompress #{
789C8D56EB6FDB3610FF2CFD1517015BBAAEB6E2B4EB5A0D4390BED26ED99A65
6DF710848092684B0D256A2495D85DF7BFEFEE28594ED002FD6253C77BFCEECD
F3E74F5E9F421A066F6AA76402D189D62B25E169258C83E3B3575118BC93C6D6
BA4DE060BE982FC2E0B8779536C8FBB432B575BAABA481736DEDECA4560A055E
EA065555CE75491C1BBA58E1C5BCD04DDC89958CBD8D0BB6612F445B5E9C138C
3078261C0A3E9E1DF7ABD9E1C1C1A33078BEEE3432259016C49D85C18B9A707E
B5621D33A6DAB90983B3DE74DAE2D5BF27B29506355910F0F6FC149C065114D2
5A7095845D072D79F85F189CD6B91166935020825379255502FBB95CD52D6A42
D299126EA94D8354A11412DE6C3AB4942EFBB670181A6874D9A352A7B582B216
4A16043578A61B5163E0D281663928A0CA02AE654E1CA775215B42BD5F14B37C
33B3220CB2300B43762C81DE4A0265B476E0D0A8851FC04AE7EA7665C3A0111D
7158902D62D61D5E16DA481871210BC19ED9BAE9101E9F0BADB4F14785D99B4E
D61F4BE104EA21635049D561F2C36C80B04D2AA39B8BAEB6739F084E2E538FC2
3060A43E9AAA6E2544AA88804EF17A831FEB4D04B613E672B8B45833412E0C44
F9958D004F311658FD41B74E282456CCD0D5C8DBDD8F000FF11233825F44A720
442EA2B88501452361FF21ED8CC6D4A7B9D2C5E55E861665B1FD228F02664830
64AD936B07FC89642F2DCCCA8EECA0F3F7983E1443E686CA2CD6ADDAB0830131
26D0884B097758C5374024BA2A35E4755BC21DB2BD25671E2AE21E915A696ACC
622ADACD6C842B7C65316DCCE780DBB313F06E13975276E02978755D617B40DA
52B9885A1D0D372C36C955A2C536ECA8C507D38335FF152F60E171069514E556
3BA11E2A6D447E85096288947282276B47E360EBC911300BFE64A991655F48FF
9191B29DE21C4ADDE9D9C3053A560E98F984A3E6F8C9D367CF5F9CBC7CF5D3CF
A7BFFCFAFAECB7F3DFDFBC7DF7C79F7FFD2DF2A294CB5555BFBF544DABBB7F8C
75FDD5F57AF3E1607178FFC1770FBF7FF438425B01ABBE81BBED9B5C9A3D6875
2B87D006F5923F07D846BADEB4105D44148C006994E93534750B0B6639207A57
17971E3452BF05A37B4C3AFA7197583892DBB24A0BE18A2A036EB3A936252643
386CCC1BC124DD46BED7688DAA9C6512F6632AA3A9A947DF7C87A7AEC7B0A2EE
186D2050DD3B5688FFBE7220A2EE31B293D84AE48A92EDCA554730C8736D779D
44575006BB127DE41F2A2C1C75B34AAE3D6F9C8C6E62BFEB6B690A81B944A11D
8434614680ECF5279DA709B207D6199C6F43008C6CF495849D3078F16D1E7D98
F876D264AF6B0C3336C752F4CA8FCEA3F85A9BD2E795248252167523D41EA699
1216E3B0E08CCE179CECC00710A7FC3436C91CDF71C9403AF3AC1E4270CBE864
2918421AE5AB28031E93F851E019D7099E842F2FD4A0555D22C1E2D5CA88B296
2D712A12A3A8D0024869607AFE2C1D0165B70A8207FA186F9AAE5F1872DC2454
C7D36ADB23CD313A5681EDF3CFCA85C117248BCF9F4CD760D533EFDAB9E51675
C0E8D5CD0E9AAA9CE31EF344F76B221ED65FAC442E7DB9F4ADA2F7001AC142F5
4AA761EA155B7CC4C0A09D217BB67186D15736B636D7BB1FB1DE54CAFE453689
76F72B2B8EEE4590EC0C3E4E1FE34C27561FF05BFCBCAE239F7A766547827971
6E471FFD3DBB3FA0703750ECAAFE7853F5BD41D6F4F273603E29914DBB97A370
254DCE4FB1A550145F99F7AB64B761872531F0F1A6CE253797DF0D7E4CDD4DFC
88924DBD1D1DFBB843615826E368BA8B2DF17544DB15A21FBDAB30AA71FE594B
ED4A8BB3FE801FF70F0FD6870F68703B7EC6EDD333241C7337A00EC784A75938
F46FE2A71DB615471FFF8591C881EF153B9AF04B4B497CCC10B2013DB6BCA395
489B9EDE81C3538EDF580C974ED9C46C81904EDF250C111BCB6BDB0C636D7339
EE9039FF638D5279C63C6938BD141248475C0886089C7986E867D50D8FB391BB
D03B8FC4A115C6B727B34EC569B726D424E35B702BE219BD690CE55662B96385
2E9883C2BCE5C8AB1D16BA19EAD00F167E2E73D9414B81F0A9C077F5FFE11722
94EF0C0000
}

Этот код позволяет на простом диалекте REBOL создавать и отображать диаграммы:

REBOL [title: "Google Chart Introduction"]
do http://reb4.me/r/google-charts.r
probe chart [
    title: "Chart Example"
    type: 'line
    size: 350x150
    labels: ["Red" "Green" "Blue"]
    data: [50 40 10]
    colors: reduce [red green blue]
    area: [color solid 244.244.240]
]
halt

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

http://chart.apis.google.com/chart?cht=p3&chs=350x150&chd=t:50,40,10&chtt=Chart%20Example&chco=ff0000,00ff00,0000ff&chl=Red|Green|Blue&chf=bg,s,f4f4f0

Вы можете просмотреть результаты функции REBOL "chart" (диаграмма) с помощью функции "browse":

REBOL [title: "Google Chart Introduction"]
do http://reb4.me/r/google-charts.r
browse chart [
    title: "Chart Example"
    type: 'pie
    size: 350x150
    labels: ["Red" "Green" "Blue"]
    data: [50 40 10]
    colors: reduce [red green blue]
    area: [color solid 244.244.240]
]

Или просмотрите его прямо в графическом интерфейсе, загрузив результаты в виджет "image" (изображение):

REBOL [title: "Google Chart Introduction"]
do http://reb4.me/r/google-charts.r
my-chart: chart [
    title: "Chart Example"
    type: 'line
    size: 350x150
    labels: ["Red" "Green" "Blue"]
    data: [50 40 10]
    colors: reduce [red green blue]
    area: [color solid 244.244.240]
]
view layout [image load my-chart]

Изменить свойства легко:

REBOL [title: "Another Google Chart"]
do http://reb4.me/r/google-charts.r
my-chart: chart [
    title: "Chart 2"
    type: 'pie
    size: 500x400
    labels: ["John" "Paul" "Sue"]
    data: [35 55 10]
    colors: reduce [orange purple pink]
    area: [color solid 225.225.245]
]
view layout [image load my-chart]

Вы можете включить код REBOL непосредственно в блок диаграммы для выполнения расчётов, форматирования данных и т.д.:

REBOL [title: "Example by Chris Ross Gill"]
do http://reb4.me/r/google-charts.r
clipdata: {Adsense Revenue^-300
Sponsors^-500
Gifts^-50
Others^-58}

browse chart [
    title: "Revenue"
    size: 650x300
    type: 'pie

    ; извлекаем необработанные данные
    data: parse/all clipdata "^/^-"
    labels: extract data 2
    data: extract/index data 2 2

    ; форматируем данные и метки
    sum: 0
    forall data [sum: sum + data/1: to-integer data/1]
    forall data [change data round 100 * data/1 / sum]
    forall labels [
        labels/1: rejoin [labels/1 " " data/(index? labels) "%"]
    ]
]

Дополнительную информацию о REBOL Google Chart API можно найти на странице http://www.ross-gill.com/page/Google_Charts_and_REBOL.

7.5.1 Работа с API других веб-сайтов

Крис создал ряд других веб-API, которые упрощают взаимодействие с популярными сайтами, такими как Twitter, Facebook и Etsy. Учебное пособие по использованию REBOL с Etsy API доступно здесь. Вы можете узнать больше об использовании REBOL с API Twitter и упрощении использования Rest, OAuth и других протоколов на http://www.ross-gill.com/page/REBOL.

7.6 Использование приложения для работы с электронными таблицами "Nano-Sheets"

В разделе вводных демонстраций этого текста вы видели небольшое приложение для работы с электронными таблицами под названием "Rebocalc". Идея этого приложения была расширена в статье Стива Ирвина и Стива Ширемана на http://www.devx.com/opensource/Article/27454. Получающееся в результате приложение "Nano-Sheets" действительно полезно в производственных ситуациях и может быть улучшено с помощью простого кода REBOL. Сохранение, загрузка, печать и другие функции уже доступны в приложении Nano-Sheets. Вы можете создавать свои собственные функции, которые используют любую из возможностей языка REBOL для обработки данных ячеек (математика, графика, синтаксический анализ, собственные диалоги и интерфейсы GUI, подключение к Интернету, файловые и сетевые протоколы для подключения к источникам данных и т.д.). Эта программа и бесплатный интерпретатор REBOL настолько малы, что их можно легко отправить, даже по электронной почте, тем, кому она может понадобиться. Для работы с электронными таблицами, созданными с помощью этого инструмента, не требуется установка больших или дорогих офисных программ.

Вот немного изменённая версия с полосами прокрутки для сеток любого размера. К функциям суммы столбцов и сумм строк также добавлены включённые возможности:

REBOL [Title: "Nano-Sheets Spreadsheet"]
cell-size:  125x20
sheet-size: 10x50
window-size: 800x450
scalar-types: [
    integer! | decimal! | money! | time! | date! | tuple! | pair!
]
protect 'scalar-types
sheet: lay: current-file: sheet-code: none
cells: copy [] 
sheet-buttons: copy []
use [
    buttons== cell== sheet-code==
    id val text action face style
] [
    id: val: text: action: face: none
    style: 'btn 
    buttons==: [
        'buttons into [
            any [
                set val word! (id: val) 2 [
                    set val string! (text: val)
                    | set val block! (action: val)
                ] (
                    repend sheet-buttons [id text action]
                    append lay/pane face: make-face/size/offset style
                        cell-size
                        cells/:id/offset
                    if 'button = style [face/edge/size: 1x1]
                    if not none? face [
                        face/text: text
                        face/action: action
                        face/style: style
                    ]
                )
            ]
        ]
    ]
    cell==: [
        set id word! into [
            opt 'formula set val [block! | path!](cells/:id/formula: :val)
            | opt 'value set val [string! | scalar-types] (
                set cells/:id/var cells/:id/text: val
            )
        ]
    ]
    sheet-code==: ['do set sheet-code block! (do sheet-code)]
    sheet==: [
        (sheet-code: none  clear sheet-buttons)
        opt sheet-code==
        any [buttons== | cell==]
    ]
]
if link? [
    hilight-all: func [face] [
        either empty? face/text [unlight-text] [
            highlight-start: head face/text
            highlight-end: tail face/text
        ]
    ]
]
clear-cell: func [cell] [set cell/var cell/text: cell/formula: none]
clear-sheet: does [
    foreach [id cell] cells [clear-cell cell]
    clear next find lay/pane last cells
    show lay
]
compute: does [
    unfocus
    foreach [id cell] cells [
        if cell/formula [
            if error? try [cell/text: do cell/formula] [
                cell/text: "ERROR!"
            ]
            set cell/var cell/text
            show cell
        ]
    ]
]
cur-cell: does [either in-cell? [system/view/focal-face] [none]]
empty-cell?: func [cell] [
    all [
        none? cell/formula
        any [
            none? cell/text
            all [string? cell/text empty? cell/text]
        ]
    ]
]
enter: func [face /local data] [
    if empty? face/text [exit]
    set face/var face/text
    data: either #"=" = face/text/1 [next face/text][face/text]
    if error? try [data: load data] [exit]
    if scalar? :data [face/formula: none  set face/var data  exit]
    face/formula: either formula? face/text [compose [(:data)]] [none]
]
event-func: func [face event /local f] [
    if all ['key = event/type in-cell?] [
        switch event/key [
            F2    [if in-cell? [show-formula system/view/focal-face]]
            up    [move up]
            down  [move down]
        ]
    ]
    event
]
formula?: func [text] [#"=" = text/1]
in-cell?: has [f] [all [f: system/view/focal-face 'cell = f/style]]
load-sheet: func [file [file! url!]] [
    clear-sheet
    parse load/all file sheet==
    current-file: file
    show lay
    compute
]
move: func ['way /local pos] [
    pos: find cells cur-cell
    cell: pick switch way [
        up    [enter cur-cell  skip pos negate sheet-size/x * 2]
        down  [enter cur-cell  skip pos sheet-size/x * 2]
    ] 1
    if not object? cell [cell: none]  ; if 'A1 = cell [cell: cells/A1]
    if cell [focus cell]
]
new-sheet: does [
    clear-sheet
    current-file: none
    show lay
    focus second cells
]
use [not-vals] [
    not-vals: reduce [none ""]
    no-val?: func [cell-val] [find not-vals cell-val]
]
open-sheet: func [/with file [file! url!]] [
    if not file [
        if %none = file: to-file request-file [exit]
    ]
    load-sheet file
    focus second cells
]
save-sheet: func [/as /local file buffer] [
    if any [not file: current-file  as] [
        if %none = file: to-file request-file/save [exit]
    ]
    if all [file <> current-file  exists? file] [
        if not confirm join file { already exists. 
            Do you want to write over it?} [exit]
    ]
    buffer: copy []
    if sheet-code [repend buffer ['do sheet-code]]
    if not empty? sheet-buttons [repend buffer ['buttons sheet-buttons]]
    foreach [id cell] cells [
        if not empty-cell? cell [
            repend buffer [
                cell/var reduce [any [cell/formula  get cell/var]]
            ]
        ]
    ]
    save file buffer
    current-file: file
]
scalar?: func [val] [find scalar-types type?/word :val]
set-cell: func [id val /local cell] [
    cell: select cells id
    cell/text: form val  enter cell  show cell
    compute
]
show-formula: func [face] [
    if face/formula [
        face/text: join "=" mold/only face/formula
        focus face
    ]
]
ctx-html-export: context [
    out-buff: make string! 10'000
html-template: {
<html>
<!--Page generated by REBOCalc-->
<head>
<title>$title</title>
<style type="text/css">
html, body, p, td, li {font-family: arial, sans-serif, 
    helvetica; font-size: 10pt;}
table, tr {border-collapse: collapse;}
th, td {font-size: 12px; border: 1px solid #C0C0C0; 
    padding: 0.5em 0.5em 0;}
th {background: #8E806E; font-weight: bold; 
    text-align: center; color: #404040;}
</style>
</head>
<body bgcolor="white">
$table
</body></html>
}
    emit: func [data] [repend out-buff [reduce data newline]]
    set 'emit-html func [/to file /local val] [
        clear out-buff
        emit <table>
        repeat row 1 + sheet-size/y [
            emit [<tr><th> either 1 = row [""] [form row - 1]</th>]
            repeat col sheet-size/x [
                emit either 1 = row [[<th> col-lbl col </th>]] [
                    [
                        <td width="110"> any [
                            get/any mk-var col row - 1 ""
                        ] 
                        </td>
                    ]
                ]
            ]
            emit </tr>
         ]
        emit </table>
        out-buff: replace copy html-template "$table" out-buff
        write %rebolcalc-out.html out-buff browse %rebolcalc-out.html
    ]
]
avg: average: func ["Arithmetitc mean" block [any-block!]] [
    remove-each val block [no-val? val]
    either empty? block [0] [divide  sum block  length? block]
]
gcd: func ["Greatest common denominator" m [integer!] n [integer!]] [
    either (m // n) = 0 [n] [gcd n (m // n)]  ; Euclid's algorithm
]
geo-mean: func ["Geometric mean" block [any-block!]] [
    either empty? block [0] [(product block) ** (1 / length? block)]
]
median: func [
    "Returns the number in the middle of a set of numbers sorted by value"
    block [any-block!] /local len mid
] [
    block: sort copy block
    len: length? block
    mid: to integer! len / 2
    either odd? len [
        pick block add 1 mid
    ][
        (block/:mid) + (pick block add 1 mid) / 2
    ]
]
mode: func [
    "Returns the most frequently occurring value in the block"
    block [any-block!]
    /local last-item result high-count count
][
    block: sort copy block
    result: last-item: first block
    count: high-count: 1
    foreach item next block [
        either item = last-item [count: count + 1] [
            if count > high-count [
                high-count: count
                result: last-item
            ]
            last-item: item
            count: 1
        ]
    ]
    if count > high-count [result: last-item]
    result
]
product: func [
    "Multiplies all the values in the block"
    block [any-block!] /local result
][
    remove-each val block [no-val? val]
    result: 1
    foreach value block [result: result * value]
    result
]
sum: func [
    "Adds all the values in the block"
    block [any-block!] /local result
][
    remove-each val block [no-val? val]
    result: 0
    foreach value block [result: result + value]
    result
]
sum-rows: func [
    "Adds a range of row values"
    row start end
][
    total: 0
    for i start end 1 [
        do rejoin ["total: total + " row i]
    ] 
    total
] 
; =sum-rows "b" 1 4 
sum-cols: func [
    "Adds a range of column values"
    col start end
][
    total: 0
    for i start end 1 [
        do rejoin ["total: total + " i col]
    ] 
    total
]
; =sum-cols 2 #"b" #"e"
col-lbl: func [col] [form to char! 64 + col]
cell-name: func [col row] [join col-lbl col row]
mk-cell-size: func [col] [
    either any [none? col-widths  col > length? col-widths] [cell-size] [
        as-pair col-widths/:col cell-size/y
    ]
]
mk-var: func [col row] [to lit-word! cell-name col row]
sheet: [
    origin 5x5 space 1x1 across
    style cell field cell-size edge none with [formula: none] [
        enter face  compute  face/para/scroll: 0x0
    ]
    style label text cell-size white rebolor bold center
    style menu button 60x20 silver edge [size: 0x0] shadow off with [
        font/colors: [0.0.0 0.0.128]
    ]
    menu "New"  #"^n" [new-sheet]  menu "Open" #"^o" [open-sheet]
    menu "Save" #"^s" [save-sheet] menu "Save As" #"^a" [save-sheet/as]
    menu "HTML" #"^t" [emit-html]
    text 10 "" text "(Press [F2] to edit cell formulas)"
    return
]
repeat row 1 + sheet-size/y [   ; +1 accounts for header row
    repend sheet ['label (as-pair 30 cell-size/y) either 1 = row [
            ""
        ][
            form row - 1
        ]
    ]
    repeat col sheet-size/x [
        append sheet compose/deep either 1 = row [[label (col-lbl col)]] [
            [cell with [var: (mk-var col row - 1)]]
        ]
    ]
    append sheet 'return
]
lay: layout sheet
foreach face lay/pane [
    if 'cell = face/style [
        repend cells [face/var face]
        set face/var none
    ]
]
focus second cells
insert-event-func :event-func
gui: [
    across
    g: box window-size with [pane: lay pane/offset: 0x0]
    scroller as-pair 16 window-size/y [
        g/pane/offset/y: g/size/y - g/pane/size/y * value show g
    ]
    return
    scroller as-pair window-size/x 16 [
        g/pane/offset/x: g/size/x - g/pane/size/x * value show g
    ]
]
view layout gui

Попробуйте сохранить приведённый ниже текст в текстовый файл, а затем загрузить его как электронную таблицу в Nano-Sheets, чтобы увидеть, как реализованы некоторые полезные функции. Это "родной" внутренний формат, который Nano-Sheets использует для сохранения данных. Вы также можете использовать изменённую версию, указанную выше, для сохранения и загрузки плоских и заблокированных таблиц данных REBOL, а также стандартных файлов CSV, совместимых с традиционными приложениями для работы с электронными таблицами:

do [
    right-now: does [now/time/precise] 
    circle-area: func [diameter] [diameter / 2 ** 2 * pi]
] 
buttons [
    A10 "Random-test" [
        set-cell 'a11 circle-area random 10 1E-2
    ] 
    C10 "test-2" [set-cell 'c11 right-now] 
    A5 "Check" [print "check"] 
    F16 "Done" [quit]
] 
A1 ["test"] 
C1 [[now/date]] 
D1 [3] 
E1 [$200.00] 
F1 [1x2] 
E2 [[d1 * e1]] 
A11 [19.63] 
C11 [0:46:00.171]
C12 [[c11 + 1]]

По мере того, как вы узнаете больше о кодировании REBOL, вы сможете легко изменять и расширять возможности программы Nano-Sheets способами, которые было бы очень сложно или невозможно выполнить с помощью других приложений для работы с электронными таблицами.

8. Использование REBOL для создания презентаций

8.1 REBOL как презентационное программное обеспечение

Функции REBOL GUI позволяют использовать простые методы компоновки изображений и текста с помощью различных шрифтов, стилей, цветов, градиентов, а также возможность создавать анимацию, добавлять звуки и использовать другие элементы, которые полезны для создания захватывающих аудио/визуальных презентаций. Графические возможности REBOL предоставляют продвинутые методы для создания изображений из кода, которые сложно спроектировать даже с помощью сложного программного обеспечения для работы с графикой (тема для более позднего периода в этом тексте). В большинстве простых случаев такие приложения, как PowerPoint и Photoshop, можно легко заменить простым кодом REBOL.

8.2 Некоторые базовые идеи макета и простая структура кода для презентаций

Презентации слайд-шоу часто отображаются в полноэкранном режиме. В REBOL размер экрана сохраняется в значении "system/view/screen-face/size", поэтому следующий код создаёт полноэкранное белое поле, которое закрывает графический интерфейс при нажатии:

REBOL [title: "Empty White Screen"]
view center-face layout [
    at 0x0 box system/view/screen-face/size white [unview]
]

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

REBOL [title: "Slide 1"]
view center-face layout [
    at 0x0 box system/view/screen-face/size white [unview]
    at 20x20 h1 blue "Slide 1"
    box black 2000x2
    text "This slide takes up the full screen."
    text "Adding images is easy:"
    image logo.gif
    image stop.gif
    image info.gif
    image exclamation.gif
    text "Click anywhere on the screen to close..."
    box black 2000x2
]

REBOL [title: "Slide 2"]
view center-face layout [
    at 0x0 box system/view/screen-face/size effect [
        gradient 1x1 tan brown
    ] [unview]
    at 20x20 h1 blue "Slide 2"
    box black 2000x2
    text "Gradients and color effects are easy in REBOL:"
    box effect [gradient 123.23.56 254.0.12]
    box effect [gradient blue gold/2]
    text "Click anywhere on the screen to close..."
    box black 2000x2
]

REBOL [title: "Slide 3"]
view/options center-face layout [
    at 0x0 box 600x400 [unview]
    at 20x20 
    text "This slide is smaller, and as simple as can be"
    text "Click anywhere on the screen to close..."
] 'no-title

Эта программа использует q-plot для создания графических изображений ежемесячных продаж и расходов, а затем отображает их в полноэкранной презентации, которую можно показать группе:

REBOL []
do %q-plot.r
save/png %sales.png to-image quick-plot [
    600x400
    bars [5 3 8 2 10 3 4 9 5 7]
]
save/png %expenses.png to-image quick-plot [
    600x400
    line [9 2 4 1 7 3 8 14 10 5 6]
]
view center-face layout [
    at 0x0 box system/view/screen-face/size white [unview]
    at 20x20 h1 blue "February Sales:"
    image load %sales.png
]
view center-face layout [
    at 0x0 box system/view/screen-face/size white [unview]
    at 20x20 h1 blue "February Expenses:"
    image load %expenses.png
]

Создать структуру слайд-шоу для отображения последовательных макетов так же просто, как создать блок вложенных блоков, содержащих код графического интерфейса, а затем использовать цикл foreach для отображения каждого макета:

REBOL [title: "Simple Presenter"]
slides: [
    [
        at 0x0 box system/view/screen-face/size white [unview]
        at 20x20 h1 blue "Slide 1"
        box black 2000x2
        text "This slide takes up the full screen."
        text "Adding images is easy:"
        image logo.gif
        image stop.gif
        image info.gif
        image exclamation.gif
        text "Click anywhere on the screen for next slide..."
        box black 2000x2
    ]
    [
        at 0x0 box system/view/screen-face/size effect [
            gradient 1x1 tan brown
        ] [unview]
        at 20x20 h1 blue "Slide 2"
        box black 2000x2
        text "Gradients and color effects are easy in REBOL:"
        box effect [gradient 123.23.56 254.0.12]
        box effect [gradient blue gold/2]
        text "Click anywhere on the screen to close..."
        box black 2000x2
    ]
    [
        at 0x0 box 600x400 [unview]
        at 20x20 
        text "This screen is smaller, and as simple as can be"
        text "Click anywhere on the screen to close..."
    ]
]
foreach slide slides [
    view/options center-face layout slide 'no-title
]

Вы можете упростить кодирование утомительно повторяющихся элементов макета, разместив только уникальные элементы графического интерфейса на каждом слайде. Виджеты и код макета для отображения на всех слайдах можно вставить в цикл "forever". В этом примере разделяются заголовок, цвет/эффект текстового поля фона и код макета для отображения на каждом уникальном слайде, а также добавляются элементы управления вперёд и назад, полосы (чёрные поля) и цвета для отображения на каждом слайде. В этом примере функция "compose" используется для вставки данных, содержащихся в круглых скобках. Чтобы создать свои собственные слайды, всё, что вам нужно сделать, это отредактировать уникальный код, который будет отображаться в каждом отдельном макете слайда (строка заголовка и уникальный код графического интерфейса для каждого слайда):

REBOL [title: "Presenter"]
slides: [
    "Slide 1 - A Few Basics"  
    [
        text "By default these slides are white and full screen."
        text bold "Adding images is easy:"
        image logo.gif
        image stop.gif
        image info.gif
        image exclamation.gif
        text {
            Press the space bar, right arrow key, or left click screen
            for the next slide.  Press the left arrow key, or right
            click screen to go back to previous slide.  Press the 'X'
            key to quit...
        }
    ]
    "Slide 2 - Colors and Gradients" 
    [
        at 0x90 box as-pair system/view/screen-face/size/1 220 effect [
            gradient 1x1 tan brown
        ]
        at 20x70 text "Colors and gradient effects are easy in REBOL:"
        box effect [gradient 123.23.56 254.0.12]
        box effect [gradient blue gold/2]
        text {
            Left arrow key or right click screen to go back, 'X' key to
            Quit...
        }
    ]
    "Slide 3 - A Simple Window"
    [
        text "This slide is as simple as can be."
    ]
    "Slide 4 - Lots of Stylized Text"
    [
        across
        text "Normal"
        text "Bold" bold
        text "Italic" italic
        text "Underline" underline
        text "Bold italic underline" bold italic underline
        text "Serif style text" font-name font-serif
        text "Spaced text" font [space: 5x0]
        return
        h1 "Heading 1"
        h2 "Heading 2"
        h3 "Heading 3"
        h4 "Heading 4"
        tt "Typewriter text"
        code "Code text"
        below
        text "Big" font-size 32
        title "Centered title" 200
        across
        vtext "Normal"
        vtext "Bold" bold
        vtext "Italic" italic
        vtext "Underline" underline
        vtext "Bold italic underline" bold italic underline
        vtext "Serif style text" font-name font-serif
        vtext "Spaced text" font [space: 5x0]
        return
        vh1 "Video Heading 1"
        vh2 "Video Heading 2"
        vh3 "Video Heading 3"
        vh4 "Video Heading 3"
        label "Label"
        below
        vtext "Big" font-size 32
        banner "Banner" 200
    ]
    "Slide 5 - Live Code"
    [
        h3 "Remember, These Slides Are Live, Fully Functional GUIs!"
        box red 500x2
        bar: progress
        slider 200x16 [bar/data: value show bar]
        area "Type here"
        drop-down 200 data reduce [now now - 5 now - 10]
        across 
        toggle "Click" "Here" [alert form value]
        rotary "Click" "Again" "And Again" [alert form value]
        choice "Choose" "Item 1" "Item 2" "Item 3" [alert form value]
        radio radio radio
        led
        arrow
        return
    ]
]
indx: 1
forever [
    slide: compose [
        size system/view/screen-face/size
        backdrop white [
            if indx < ((length? slides) / 2) [indx: indx + 1 unview]
        ] [
            if indx > 1 [indx: indx - 1 unview]
        ]
        at 20x20 h1 blue (pick slides (indx * 2 - 1))
        box black as-pair (system/view/screen-face/size/1 - 40) 2
        (pick slides (indx * 2))
        box black as-pair (system/view/screen-face/size/1 - 40) 2
        key #"x" [quit]
        key #" " [
            if indx < ((length? slides) / 2) [indx: indx + 1 unview]
        ]
        key keycode [right] [
            if indx < ((length? slides) / 2) [indx: indx + 1 unview]
        ]
        key keycode [left] [
            if indx > 1 [indx: indx - 1 unview]
        ]
    ]
    slide: layout slide
    view/options center-face slide 'no-title
]

Важно понимать, что, поскольку эти слайды состоят из живого, работающего кода REBOL, они могут содержать гораздо больше, чем просто статический текст и изображения. Может быть включено любое полнофункциональное приложение большой сложности. Вы можете, например, включить диаграммы, таблицы или даже рабочую электронную таблицу, которая выполняет оперативные вычисления для мгновенно обновляемых данных. Используя несколько простых инструментов, которые вы видели до сих пор, вы можете легко создавать презентации, которые практически невозможно использовать при использовании традиционных программ для презентаций.

8.3 Использование панелей вкладок и меню для представления информации

Хотя это и не традиционный формат слайдов, простой и эффективный способ представления нескольких макетов экрана - использование виджета панели вкладок. Следующий код содержит панель вкладок, а также полезный виджет меню, созданный Ричардом Смолаком. Здесь демонстрируются те же 5 примеров слайдов, что и в предыдущем разделе. Меню можно использовать для всплывающих небольших текстовых предупреждений, запросов и вариантов выбора, которые изменяют состояние экранов в презентации. Просто скопируйте сжатый код панели вкладок (и код меню, если необходимо) плюс код "insert-event-func", а затем поместите код слайда в блок "tab-panel data". Синтаксис, необходимый для использования дополнительных меню, должен быть интуитивно понятным, если изучить приведённый здесь пример кода. Эти два инструмента с графическим интерфейсом не только являются удобным способом отображения страниц данных презентации, но и полезны для создания всех типов приложений, требующих значительного пространства на экране:

REBOL [title: "Tab Panel and Menu Presentation"]

; Tab Panel Widget: 

do load decompress #{
789CBD586973E2B816FDCEAF50577FE86428C7989824506F2695349DADB37496
4E4FA09C2A63CBE0C6D8C4368DC9BCF9EFEF5E495E65B2CCAB9AA4005BBAD2DD
8E8EAEF45714AF3CF799AA33338A6948868D47253647CADCF4A9D7238E6951B2
74E309EBE05D6648CD1E999953CABB79CFA312384E44E31E6925ED9668A2F698
0A517CCC441F1577666297179836890365E4FA66B82236B582D93CA4514476F4
8F7F35E8D973D8FF76F8DC69FEB8D56767E387ABBED63D39383B884FC79713EB
E4B8D38F2EEF9ED4A3969BCCFBBF76AE7E9E2E4757ABB079D3D28FADC99F8DDB
6F0FA77BB3FBF1D3D199F7703C0906B3BEDEBCE99EFE6A1F8EBFF6EFC70F27EE
FDF2E2707C39681F5EDF256A337A1ADCEB874FB7CF897DF2E5E2A9D171EFBA4D
4775FC78767D6D1E4D6EA6D6F5D765D4D793E3D9E0AC3B4816E777E7CFA74F17
9FA7AD9B2FC9F27C104C8F0E7FBA17DD4B6B7BFAE3B6D1FC71393BD8BB9AF447
30CF73D3695D7C3B595EDEDE3C58BB5F4EA3CF5DB77564FF79D78CC0BC643038
39F8BAFB5DBB3EBF98B9FDD1FDFC9BAD37BE3E07CBF3D3D5C21B3C75CD8BE6F7
B3B63538BE9C3E9CF8FABDF7BDD554E943AB75A21FDF6996EA6DDF5F7DFEDE3A
38D8D54E0F9AD303F8FBBDF17716F40812DD237AA293ACC90ABC20EC113FF0A9
6833C4AF4311003CCDF058C85DA3F4F3A884D40ECD258065E15B64E8109350A3
200E30000001B61C750ED8F1630561A38E82D0A6E17E4910B4AA0814953A0EB5
004B08872002E0D024A6BECD6CDF7054748428446B6D12E600BE6E94A767ED91
AA6D1A85F98D3A6525EF8B1110225553C6A169BBA00780AE9176A7BDC53F3A69
EBFA565BDFDE6A6FEFBDC72CA31478235D67754B8C27B0038ABB6996023F4E25
E1B1104D9159ADDDDA129F0A0E34AD92F1F5AB5546493A0998F266DC507FCC16
7D012744F502CBF448E43965C844403AD60444CA19B383A55F690259CFE955C0
55789685D594C55464B91E89A8071926A227620F4E602D229E06D7B769B24F1C
F84D65945190B0C1A016463B151DAE039E0D51D6B46237F071587956E335ABD4
BA900BD949B064B3A15CA9CFA805B1F1D2425D9300173B7F279F98AE72BCDF11
6D474DF78499EBE3982012DFCDCA0C76E0C495A12200E868BA684A1282544A61
0593DF902D09409C50D4558FB4DB521777013B5B521F2EB9D4D091675A531908
68433DD5F129EAE84EB5299D9321268B0CE7D4271BC5286C82739EA7D4B4839F
B820BB408C2DD88535A30AB35A3E0B8305C44AEBEC6D693B5DF874C876755CF9
DD581FBF8C97EAE227C7568A1F0063A71AE6F706B166C914FDAEF39720A7831D
8CD2351DD8927F8023E1977F5E89C98B4BAF48ED9C373323855B3D12870BDE52
A01EA4A31EB1CDD864CF0864781D99D31E31C33058463D22A257D9C83826C0D9
8F436C3320313B5B6D0DF6A64E879BB104BD28202C441D64E8D3A5CAB5396E18
C5C4876D17348DA3FC2975C90F84E97C54E686637A00AA5C927FBBBECB20375F
91D94ABCA5AA33C7E4FD2E2F2AF524470F9205EE3C850D2DA6F348CC3FCCC33F
33936C9BCA85EB2386FF7C3B28CF62E4560A0B916786D2BE6B14048B2572FA0A
254C757B2D0DC92320DE7213380ACA66C1A2F08398E4CC372CC0464BA5B84340
FB0B0C6A668AD050988BA79F238B29C2868A2E16373214E15B4EDC98A6120BE4
11C80AF9657A0B1223563C73455C3B210EB32E9ECD59260AD49136959323529B
271BE60087F28255184367F378B52FEC2EAC3DD854C016A9192012CCA8C41DA6
0F21057C71BBE1812D8B0F063E7237A23874FDF107D6802E0D47B05F4E3F90FF
9260F413F8049FC4986C32EEF2279610836C485C042EE0907D3E217CF5C8984F
2FF335C8328D4561F80A166BE4A151CD0F62552A65F3A1AF741FECE3B891444A
0C04E1AF1158F8E8279F41EAAE752255CA025DA733CB0278B05E234ABC4D2182
841D13EA94E5D8038AE0894B9B306A061AC10F194DA2ED25DBDB6FD109168680
1AD3F5186F67CB0E57417D6E11DAD80B4A5E9B2EAF61320E2A170B355E426F8F
540B2B116DDCAF7161B5B014635C8F4D0AC37C8187418F5AA0BAF41578888843
04AB343A6CB1F4C806FE6CCA9E3E2AC5FE9A6E991BDF1F711622B613088F7051
41DDE999515C0EE127B45D9A8E510F9258136F24E6A61B6629D4B228BD3C2791
AA844DE08768EACEDF502CA4A501E7119C2D63CD824CAA34DFDBCA19CE02C1FA
36D83CBF112D81A3729328DA0E948505695828305C4DC81F8443A202A4628899
9B69D9C139282DEEE0A83B86A8BC8848D30A8328AA348A81DB499B4473445C95
B1983EA2ED26DA2ED910956E7B937814F2149A31253B35D067DB51E54099FE3B
FC52EB8DA74511232493093581B2F30305475A9D0AAC364B5250DC626D5B697D
6524AF72CAEF2F42B36A9A844501323CD265B23522AF2FBDEAFB9A24416E27FF
284BFF384788D37D5EABBE3751B5E156FEFF704B50A8B3EEDF499451A60385E8
AD44694B65025FE26B2E205E3EDBD4DFFD08C190724EE2B70FC4618CD324A5D3
A438D463C8F7457FF5CA2E3D6AA64F901026A816CB06E9A28FAB6682EBC89785
5B722F33BE7A7512A45727B083BFE9EA846354A14F0BD34322C16A1B7FE45A75
E2DA9438B51E48875B474D2959EAB20332C26B104715672EDCB91CB0AC7A6355
719D252271E33581023FC4A583949BF42AA2974AD4F5BF78DFCA6A222CCA3476
67E4517F1C4F8AB42BEE71B212B5519C3FBB421348CB2F8AA1469124D3E9544D
45B4AAFC7EB2A24BED6185F6895D3C56EC6681E0D19722F1E2222A0808FD42AE
DCDCCE9B5352AD9F242DF8D39BF1F202AF8C29121C96803E1D2343D7788D3775
6AB266B86033AC6DD8D628F1617D8E44D4AA550C141EFF917995973065A7A088
D1BB50C4D45CE2554CABB36AED18F94E614D8C15A87813FC7A43452753A4D130
FEFE1F50628086731B0000
}


; Menu Widget:

do load decompress #{
789CB51B6B73DAB8F67B7E85BA3B770237430CE4D12EDD6EC6109A92266D499A
B629E3CE1810C6606C6A9BE074BBFFFD9E23C9B66CCB84A477C384C8B2747474
5E3A0FE5EF5118D516D45DB5C8C29C53E20D6774143E23839D6F357B615A3468
119F8E5723CABABED55EE260329C5B646D875342C71665FD8E678EC9F1E1EF7F
DB9FDAEFAFD6F5B76796A7C3CFBBEB9B69F7C68256BB87CFD71DFD161F3EFFF0
8EBE6047FB6CDCFE78D3D5F58BB3F7930FE1D5CE7C0DBD9DF6ECFAF5F93B787D
7CDED775AB77A9EB1F5C0D5EE8C7F07AFC11BEDE2F11ECB103E35F37F7BE7CFF
EADE20C09DE6DCE9F63F5D1DBA67172F34ED85A6FFB8D1FB7A27B27A33FFD38B
BDABAFE7DF17E1B43B5B4DA7B06AD7BABE3A773AEFDAD7FE7BC07074D5DDB1E7
776BC4D25B2EDB00BBFBEE1656F7ACC08AACAF67572BBDD9A6BDC6E7B6ADEB6F
BAD6FCC5E91B5B3F8F2EBFF6FBD142BFA6D6E71D58EDEAF8C7CDF2EB25ECA0D3
D1DFBEE95E754EEFDF74FA7AB7FBEEA67376DFD1BF9FDDF6FBFA69FBEEF6D68F
AE7A17BADEEB0DDB9DE69DFEFEEDCE17EB6BEFB6F7767D7EEE0125EF69AF1FE8
EDB61E5993FAE5349AF5828B4B86E17A31E97FBC9E863F7AFACDFBFACCBA38EF
996DA0FBCE6D70F5E58D7DE67EE94CFBFDD7A3D3C6C58B8B2BFDB2DB7D7D7C7A
AD03B5FBB7FAC5DC72BF1EDE6B07EDBECE5873F3E9FDD5DBA3CE6DAFF7EA1F89
D9A3291DCD413CFCF9969CD6D9578F73FAB47BFAFCF8E2DFE1F44D3DE6F4F97C
4FDFEBAE7AB0F41A609DBE99CED7FDB6A5B7EDFECAB4AE0F7B20017A74DBB7EC
B707CF3F009C7EFFEDF4F38ED75D74901A97D7AF7FD43B9FDAC8ACB3CBCE9ED5
EDB65FB70F6F75ABDFEF358EEE74EB3540EEEA7BF3E0CB878F079ED6EE3292ED
E46966C06F10DE3BF60FAA2DCC20A4BED01BAE6313131489290EEFFD561B798E
E7B788EBB954F44C3C3714DA88CD64643276E898A379D2E94D26018509F5A89E
F4055373ECAD33500DF117155640C7A6043D009C5BA41135F2EB359E37F71BC7
2FF61B4707DF6A39706BCF1F838948A18CCDD0240397AE356CC18E6D3F08894B
A39098BE15A42D2399623ACBA92981F856C3E9ACB7305F1A04066A42827BA0F1
421BAE6C678C2B52F22769366AE7A65B6BD6EB8719A839C8CDA323522324E9C9
8C34A4A70C62793C8C1C41BC498617410838B5083029E5376C8A711824A236C9
304960164F9E50EAC492004D69333EE5DC9AACDC11190019E069CF90374B41C8
40F896A6ED9F90898603C84034B4A895B4C81E9F0C2D79CFF1D078603C4C1E13
4CBD35991428912388EDDA612B2BC42D62BAF764C0C48B6D3F850A2C75BD90F0
57032180CD83E3FDE6C1C17EB3719C8E5C9A2E6037F2960029ED659445B5497B
3C9F9A2350389B0C0DC2C553DA84ED06D40F4968DA0E419082DCE688E6642704
D6B7889DE9E3CA243130A337857E3835F93BE9503DFC63BF513FDE6FFCF13C47
098EBB1B1666ACA73688393302C5D1B1B8A0DD301DDB725B64D7A193303B7269
FAA618894D80E99BCB13344F4E003E806F5B364C6C46F5EC342E1087D1214883
45C35AC84408D7B271C5CCD8D82C813E64FAB9551812FCC9625F2AEC02E01DF5
5381378967E44730E9311D986A82D4C2C628500F39A9312DCCF571F55300E1B6
5A0315A321CC4184156354C0346F19DA9E1B2858BF6152B85872ABA10629E858
59DBAE202AE873153850A9470DF25FA19F553065F5E870DB353927033AF2DC31
0940EE4FD4AB732563D695D9AB7887822E894CEE7285CDC2803EB28BD231D1D8
41B6CB0D7D7610EB33B6C55B9C29654CE1164931B130B6B8E244136ABBB4C1DB
9968A9B292E2527C47B91992B62AA6E4ACA51A0DEA5AA6456531A70A090DC083
008B661674842998B776952FF2241547139A5BC50B0500159756CB92B5401555
3AA81E0C16C99BE715945963DB1DD3088413FEAA5E8324536722AC066F0F5468
E2671BAD8E1905F82C574B0D740EC8A9996BF35E2956ADD2D342FE240AFECBBA
2C84447DEAC89FB2132823289B7549880C3B53F2C63D4356738426A1852702B0
2B4B27D079F6B67476A981798C7979BC71119C2E5A41C4A7743CC8346C50B89C
77365D935D90135CA9A63CB232847CE078933F78368E1C2FA0357A077B41FF9A
3D96EF84CF015FC61ED393C47C2003180483B75FF1270D1F1E82B5F4BD110D82
9AB70A1128C74405F90138F8B3721D00C59578B080839C84F68212148D3B5471
D198D37BC2AC5730F23DC7A93936F84302E5F07E097230454C986A3E843EFEB0
990F8C7B900CDEB236A6211D852DE0E153362F1C71C11B92CE7E5004721018F9
64B128210DF723C980A3B8C51AC6D698288542DAD2368BFD0243CADF95BD3118
D56ADC8E174EDFF8B336EDB0FCDC509E9ACC53564E508129F6E57BB2CFF293DC
16A74888274623AAC3895129F1C6C12456AB85D0EC5BEDE543364F0C38AED7A3
669A504093A5F1A093B0539687E5B17DE6FD6C621232C47F172B27B485E5636D
49DA26B643333665E8CC334A0516973957D04F1A393165ECE087184B49719460
A4927859B40C96AA81AF8470311AEC7510FAECAF961E7C136F38C3497C804FC3
95EFB22DB3C050E107A003844A9E49E70C672067312A22E152879F887D8B7E1E
69C638705459582D7248299ECC0B49CEEFF8EC640F041E44EF24CE56404BF42D
CD702A3A97A2C55D23DE74BC91E9B0E65D92E0F55760C4590BED329F0381E842
38D7E355DC0A5643DE630727AC319ACE79C35E58ECEFD0F44547B8607FC1F2B3
BF3402591CF377AE07E6920F172C8D43C9241309188340C95CC1E76C5200D8C0
3616B305741942DC847004B605C76BE0C1793440C0102F7B2EA960536BE0C304
1CB49F04BD4614C2219066FE8C543818EC010055783FB79706093D42DD315F98
E076EC30E16192DB40DA275282BB0CB2181B29E15BE4203A48C99BA65114C1F0
D843E72476C2B82E9818976C769D779937B28B001F31CBC41457925254052DBF
860E07F92884A84C3D2E982AF7BCE03C6F4CC0629E2A3172D4013700F52A753B
8DE4D478446E67DBAC4E2E7768A4CA9718F0ACDDFFEDF237C277AE2529B08CD0
81AEC7B2561766881D29A9962BD4834AF95DAE278951455A0C7641D1CDA14349
0535BE45427F45AB06A3D380E5F976E1B4421B205E8136EDD6E21E768E560D31
1CB50C8C0419B0D2D33318892704FE45FC9FC5C376C180900A5A91643536335C
A0D5B45DEB191FF73BEB46B75274C7F3C144910A33300900AEFB0916C21211BE
6134093F992D90314DED81412AD2A1C3625926B43CAB08946F908A8864719DAA
34384E59A1809C24CBB22E84CF90348C814853E26C8E471604235AE674147C67
843C89896A89A3125A598783BD0A3D5EF293B5466887E553EA92547738DC1683
4B2713748F4129C08B4A8A08E09B3051355E368E23396F9BF568004D9040ED1E
629383FC0EA4CC2CB7EC04F896D169F2127A588D0A9F732E15FAC580557C1CC9
9B15B86379536BBC64E2CEAB214390217349FE634D2216E2EF2F5D2B373BDEEE
C85B2CC117073F3B0AC1E813166B8283432A65468347A8CDA323A3CA13DCE835
958E96B2E3D5BCB7581EDE97E606D4E97289090119A439DB20DCCE218599A886
1B3807E4D7F27975C2EB8A63EAD80BB057BE9A710058BD6DA91EB5EDEE13574B
AA6E091396E6A9C18F06A7B774DF19F73B9F9FE1C6281F156448554A146EB213
B2E06306B6A047B19F5724C2450E4975E2DDF73CF42A73DA90398B44A089669C
0C061299659283319226955720BC7C098208F649B3B5E653EA12982B83238599
1730331008E51444442251B31819C11191A90E31A9111A1D47EA92C92C0820D3
796D4CE9920CC6BEB9164715BA4360E2175695D4F741C3F7EB464E81F2A0C462
780A2AC2EF0C6C0CF184B16A9680572C80BE65410FCAB51A04741C0903B14C82
03C1D6C79666088F20C88C848B25FE7AF815E0976D902C150B68C3A0B2F89A1F
6A35FA7D653A2719416AC822AACC673C2ABFFFA40C7F31CA57E6FCE532154FBE
A36A663CDB3809FA0A5E025BD4099AF2792D3EAD300742928A0A12F4CFD2AA73
EE4D5C3F75B96CB08045890FC12F803303ADCB5BD9FCCE2B0E78F8E1F484235A
257FBE2249975B65647193DD6FCCAC7307AE74048421C9415D0107074C638DF9
C2E2C89D79109E0C7621F0406292DD4CEE1A3D6003F1C97B5CD98F4F594ED30C
057166A52319756AA5D4512D414A324A55456745D127EC4CD6A1243CBC050AB3
9D95E4FF4AD3829BC98E32A3F1B43F999448221B97C4C9998D0D54BD6011D5DD
6A188AF594F42A2D5DA1D92AABA115B44E594C2D8CDA504A2D8C55153F846F28
46F3334B7889E8472A0AADEA19B19F5A26CF42602A251B65374614EF44910B5E
436CC7E3F624F89732B0E08F6385AB4AFE22F50D69676081642294B44410F994
60414C3DB55993463063A95C406B8AE23BDBF231D90CC756C249A8B261726C60
6D7ECBA9049B8D3B4DB8868800B2B68CB91DF3ED2FF2105F1E5824DE285BA526
AF524B567900403AA7C5FD8274261279E3EC8DE51625F03DEE76D41E02BCF12D
1792ADF6C7252E4563C3E027D432CAB7AF7205124A3C412CCB5040B5E47BE492
C4BF379A3C38DB376A7A2AFD4F117C99E319A1378781E7AC42CAE4E0D1A4FE05
8A72992B5FF297CE8F47B24B5C87C983528EDED2C9D85CB7FA3F5E6099C49536
F022CA2E9EB03B26E25E49DEE530480A41EDBCB15B1D83CDA3B62FD32519371E
3DE7E6C57724A452D79DE9AC681C28452C32F5E154264A874DE5A2257EADCAB3
D9E0EA32D7B6D4DB85D80FFDDDE28D8D2D02CE38B0F35CE75E89166C92DF81D6
3077A118C0D3CF6C448953C4CA354A09CEE5A6B39F48DC39DDDD63E96783D1BB
126189870B2344A31313738CD047067B301206D57082610CA04FE970E3E72789
5AB9B21082C0B250E90CAC16A9B550949054CAA85415520119C92F14D7DAD574
5CC48A9248E12F887D2E1988B9F6C7E602C14AF9E1681516135F49EA2BAE0FA6
9FF20CE813EFB666925752112549812B2EDC6E4838F2DC5C116F6EEB9B8AD414
922E5FBC493FE2F0B96FF174791EF55C82262DC6AA39086B15B0CE7172B3FD79
98ABA6EFC3E153CE52F67E6BA696D239CEDD0DF87AF5FDE7F00B1FE27BEC5CF8
A3C0E64285E25FA474194DE3320FC6DFAEA1222DB7A18CBE3CA9A04CCCB14A13
D697E25889D79AF68863825F961FAD28A8F0407412B00B0149448A28629A9B9D
1808B25E128C4A858A5D9E5E4DF5261B84A605906DEEB6F0DB9095B20CAEB44C
55B67D80774148784D32D7C9CA5DB93E5E8FCC750A4615FA45ED30D39B629231
F0F11E25C3CEBBC41D92F412C30EBBE4C13204995B1EE9058FF45E867C3323BD
1921DFCDE0D1C84E725067FE312577BBE0A1A23262A03552BB943AED99FFA7B0
852B5B6902F7D81278B735E3D8DAB21F2B7BEF7238920BA3EEF3C3EFE597B082
ACA8316A33B194FC0F4EDCEB9C891B73123B8BC7D62C1358A4898F38807E8040
79F1CE5F8A5253AAD26477BA18373653EE49BB44B5891369E93EE37F084A7659
93B0D222BC8C7CA8DC1378B5CCE66E41B81A99A54D01B8042696F69F80E28B52
24C4202067732BD6487628BE646727D7B20587528E714B85025B2DAAB6505CE9
F69674D5077ACCF13873A72BBDFD1697FB714A72A52D003F4943CBA2C141A2A1
B30D5E5D6C99F9D04301C9244363202209FC77431B040804940CD96F930C307B
8BB782C12732E21B663C8F5D82914872B36B38325AB011E325D6BFBD0989FF97
78E79FFF0120649CE6573C0000
}

insert-event-func [ 
    either event/type = 'resize [
        mn/size/1: system/view/screen-face/pane/1/size/1 
        my-tabs/size: system/view/screen-face/pane/1/size - 15x30
        show [mn my-tabs]  none
    ] [event]
]

view/options center-face layout  [
    across space 0x0 origin 0x0
    mn: menu with [ 
        size: 470x20 
        data: compose/deep [
            " File " [
                "Open" # "Ctrl+O" [request-file]
                "Save" # "Ctrl+S" [request-file/save]
                bar
                "Exit" [quit]
            ]
            " Options " [
                "Preferences" sub [
                    "Colors" [alert form request-color]
                    "Settings" [request-text/title "Enter new setting:"]
                ]
                "About" [alert "Menu Widget by Cyphre"]
            ]
        ]
    ]
    below
    at 10x25 my-tabs: tab-panel data [
        "1" [
            h1 "Slide 1 - A Few Basics"
            text "By default these slides are white and full screen."
            text bold "Adding images is easy:"
            image logo.gif
            image stop.gif
            image info.gif
            image exclamation.gif
            text {
                Press the space bar, right arrow key, or left click screen
                for the next slide.  Press the left arrow key, or right
                click screen to go back to previous slide.  Press the 'X'
                key to quit...
            }
         ]
        "2" [
            h1 "Slide 2 - Colors and Gradients" 
            at 0x90 box as-pair system/view/screen-face/size/1 220 effect[
                gradient 1x1 tan brown
            ]
            at 20x70 text "Colors and gradient effects are easy in REBOL:"
            box effect [gradient 123.23.56 254.0.12]
            box effect [gradient blue gold/2]
            text {
                Left arrow key or right click screen to go back, 'X' key
                to quit...
            }
        ]
        "3" [
            h1 "Slide 3 - A Simple Window"
            text "This slide is smaller, and as simple as can be."
        ]
        "4" [
            h1 "Slide 4 - Lots of Stylized Text"
            across
            text "Normal"
            text "Bold" bold
            text "Italic" italic
            text "Underline" underline
            text "Bold italic underline" bold italic underline
            text "Serif style text" font-name font-serif
            text "Spaced text" font [space: 5x0]
            return
            h1 "Heading 1"
            h2 "Heading 2"
            h3 "Heading 3"
            h4 "Heading 4"
            tt "Typewriter text"
            code "Code text"
            below
            text "Big" font-size 32
            title "Centered title" 200
            across
            vtext "Normal"
            vtext "Bold" bold
            vtext "Italic" italic
            vtext "Underline" underline
            vtext "Bold italic underline" bold italic underline
            vtext "Serif style text" font-name font-serif
            vtext "Spaced text" font [space: 5x0]
            return
            vh1 "Video Heading 1"
            vh2 "Video Heading 2"
            vh3 "Video Heading 3"
            vh4 "Video Heading 3"
            label "Label"
            below
            vtext "Big" font-size 32
            banner "Banner" 200
        ]
        "5" [
            h1 "Slide 5 - Live Code"
            h3 "Remember, These Slides Are Live, Fully Functional GUIs!"
            box red 500x2
            bar: progress
            slider 200x16 [bar/data: value show bar]
            area "Type here"
            drop-down 200 data reduce [now now - 5 now - 10]
            across 
            toggle "Click" "Here" [alert form value]
            rotary "Click" "Again" "And Again" [alert form value]
            choice "Choose" "Item 1" "Item 2" "Item 3" [alert form value]
            radio radio radio
            led
            arrow
            return
        ]
    ]
] [resize]

8.4 Show.r - полезная система построчной презентации

Карл Сассенрат (создатель REBOL) выпустил простой и производительный инструмент для презентаций по адресу http://www.rebol.com/notes/devcon07-carl.zip. Вот код (вам нужно только отредактировать заголовок и фоновое изображение - скопируйте и вставьте остальные):

REBOL [
    Title: "Slideshow Presenter"
    Author: "Carl Sassenrath"
    Version: 3.0.3
]

file:       system/script/args
if not file? file [file: request-file/only]
title-line: "Presentation Title"                               ; EDIT THIS
diags:      %diags.r
save/png %logo.png svv/image-stock/2   ; demo image
back-image: %logo.png                                      ; AND EDIT THIS
author-mode: off
time-need:  1:00

;-- Configuration --------------------------------------------------------

page-size: system/view/screen-face/size
;page-size: 800x600
;page-size: 1024x800
;page-size: 740x480
big-size:  page-size/x > 1000
sect-size: page-size/x / 4 - 40
text-size: (round/to page-size/y / 32 4) - 1 ;pick [28 20] big-size
margin: round page-size/x / 10
h2-size: as-pair page-size/x - margin - 30 text-size * 2
h3-size: h2-size - (text-size / 2)
origin: page-size / 11
def-in: round page-size/x / 7
spacing: page-size / 100x200

;-- Styles ---------------------------------------------------------------

back-image: load back-image

stylize/master [

    vh2: vh2 h2-size left top font [
        size: text-size + 6
        name: "arial black"
        style: [underline]
        color: 240.220.60
        shadow: 3x3
    ]

    vh3: vh2 h3-size left middle font [
        color: white
        style: [bold] ; underline]
        size: text-size
        name: "arial"
    ]
    para [origin: 10x2]
    effect [merge gradmul 96.96.96 128.128.128]

    txt: vtext page-size/x - margin - 50 font [
        style: 'bold
        size: text-size
        shadow: 2x2
    ]

    txtb: txt page-size/x - margin - 50 font [
        color: sky + 30
    ]

    txti: txt italic white effect [merge colorize red]

    code: tt page-size/x - (margin * 2) - 50
        black snow edge [size: 2x2 color: gold]
        ;snow coal edge [size: 2x2 color: gray]
        font [
        size: round (text-size * .8)
        style: 'bold
        colors: [0.0.0 0.0.80]
    ] as-is para [origin: margin: 12x8]
]

bullet: to-image make face [
    size: text-size / 2 - 1 * 1x1
    color: 160.0.0
    edge: make edge [color: black size: 0x0]
    effect: [oval gradmul 1x1 255.255.255 0.0.0 oval]
]
shift-bullet: text-size - bullet/size/y * 0x1

;bold: make face/font [name: "Arial Black" size: 480]
;scale: page-size/x / 800 / 17

backdrop: layout [
    size page-size
    across
    at 0x15 box as-pair page-size/x text-size * 2
        edge [size: 0x2 color: gold] 
        effect [merge grid navy gradmul 200.120.100 128.128.128]
    origin 10x20
    banner font-size text-size + 4 italic title-line white 
    ;font-color silver
    return
]

backdrop/image: back-image
backdrop/effect: [gradmul 1x-1 50.50.90 100.120.140 fit]
backdrop: to-image backdrop

end-mark: make face [
    offset: page-size * 0x1 + 40x-20 size: 140x3 ; -140x80
    effect: [gradient maroon green]
]

pan-mark: make face [
    offset: page-size * 0x1 + 40x-20 size: 140x3
    effect: [gradient maroon purple]
]

;-- Scanner --------------------------------------------------------------


*scanner*: context [

;-- Variables:
text: none
part: none
code: none
title: none
out: [] ; holds output block

;-- Emitters:
emit: func ['word data] [
    if block? word [word: do word]
    if string? data [trim/tail data]
    repend out [word data]
]

emit-section: func [num] [
    emit [to-word join "sect" num] text
    title: true
]

;--- Text Format Language:
rules: [
    [to "^/=start" skip to newline (author-mode: true) | none]
    some parts
]
parts: [
    ;here: (print here)
    newline |

    spaces

    ;--Document sections:
    "===" text-line (emit-section 1) |
    "---" text-line (emit-section 2) |
    "###" to end (emit end none) |

    ;--Special common notations:
    "***" para opt newline (emit bullet3 part) |
    "**" para opt newline (emit bullet2 part) |
    "*" para opt newline (emit bullet1 part) |
    ":" define opt newline (emit define reduce [text part]) |
    "#" para opt newline (emit enum part) |
    "!" para (emit txti part)|
    "[" example (emit code detab code) | ; trim/auto
    ";" thru newline |  ; comment
    ";===" to "===" |

    "==" output (emit output head insert code "  ") |
    "=image" file (emit image text) |
    "=all" (emit all true) |
    "=intro" (emit intro true) |
    "=diagram" some-chars (emit diagram text) |
    "=pad" num (emit pad num-n) |
    "=skip" num (clear back back tail out) num-n [to "===" thru newline] |
    "=" some-chars | ; ignore unknown options


    ;--Defaults:
    para (emit para part) |
    skip
]

space: charset " ^-"
nochar: charset " ^-^/"
chars: complement nochar
spaces: [any space]
some-chars: [some space copy text some chars]
text-line: [copy text thru newline]
;par:       [copy part some chars newline]
para: [copy part some [chars thru newline]]
;example:   [copy code some indented] ; | some newline indented]]
example:   [thru newline copy code to "^/]" skip thru newline]
indented:  [some space chars thru newline]
output:    [
    copy code indented any [
        "==" copy text indented (append code head insert text "  ")
    ]
] ; compensate for ==
define:    [
    copy text to " -" 2 skip any space para
]
file:      [
    spaces copy text some chars thru newline (text: to-file trim text)
]
num:       [
    spaces copy text any chars (num-n: either text [to-integer text][1])
]
num-n: 0

;-- Export function to scan doc. Returns format block.
set 'scan-doc func [str] [
    clear out
    parse/all detab str rules
    copy out
]
]

;-- Load it up -----------------------------------------------------------

doc-text: read file
if find doc-text "=author" [author-mode: on]
doc: scan-doc doc-text
;?? doc halt
;do diags ;;; NASTY!

;-- Globals --------------------------------------------------------------

this-page: doc ; points to current page position
title:   select doc 'title
options: select doc 'options
time-left: 1.0
time-start: now/time
back-flag: false

;-- Helpers -------------------------------------------------------------

title-of: :second
next-page: does [this-page: any [find next this-page 'sect1 this-page]]
back-page: does [
    this-page: any [find/reverse back this-page 'sect1 this-page]
]
limg: :load-image
load-image: func [file][
    either exists? file [
        limg file
    ][
        make image! reduce [page-size / 3 200.0.0]
    ]
]

;-- Page Builder ---------------------------------------------------------

build-page: has [page out emit bull count] [

    at-once: author-mode
    intro: false
    in-sect: false
    count: 0

    ; Slide title line and indentation:
    out: compose [
        across space spacing
        at (origin)
;        vh2 (title-of this-page) return
;        indent margin guide
    ]
    emit: func [blk] [append out compose blk]
    bull: func [depth] [
        emit [pad (20x0 * 2 * depth + shift-bullet)]
        emit [image bullet effect [key 0.0.0]]
        emit [pad (-8x0 - shift-bullet)]
    ]

    foreach [type data] this-page [
        switch type [
            sect1 [
                if in-sect [break]
                in-sect: true
                emit [
                    vh2 (data) return
                    indent (margin) guide
                ]
            ]
            sect2 [emit [pad 0x4 * spacing vh3 (data) return]]
            para [emit [txt (data) return]]
            code [
                emit [
                    pad to-integer margin / 3 code (trim/auto data) return
                ]
            ]
            bullet1 [bull 1 emit [txtb (data) return]]
            bullet2 [bull 2 emit [txtb (data) return]]
            enum [
                emit [
                    txtb (join count: count + 1 [". " data]) return
                ]
            ]
            pad [emit [pad (data * text-size * 0x1)]]
            txti [emit [txti (data) return]]
            define [
                emit [
                    pad 30 txt def-in no-wrap (data/1) txtb (data/2) 
                    return
                ]
            ]
            diagram [
                type: layout/tight blk: get to-word data
                emit [
                    pad (as-pair margin 30)
                    ;panel (type/size) [(blk)]
                    return
                ]
            ]
            image [
                data: load-image data
                emit [
                    pad (
                        as-pair 
                        page-size/x - data/size/x / 2 - margin
                        text-size
                    )
                    image (data)
                    return
                ]
            ]
            intro [
                intro: true
                at-once: true
            ]
            all [at-once: true]
        ]
    ]
    out: layout/tight out
    offs: 100x100
    if intro [
        foreach face out/pane [
            if face/style = 'txt [offs: 110x120]
            face/offset: face/offset + offs
        ]
    ]
    out/pane
]

;-- Show page:
show-page: func [out] [
    ; Do we step through items one at a time?
    either any [at-once back-flag] [
        items: []
    ][
        items: copy next out
        clear next out
    ]
    screen/pane: out
    if at-once [show-end-mark]
    show screen
    back-flag: false
]

pan: []

show-end-mark: does [
    time-used: now/time - time-start
    either time-need - time-used <= 0:00 [
        end-mark/effect/3: red
        end-mark/size/x: 140
    ][
        end-mark/size/x: (
            to-decimal (time-need - time-used) / to-integer (time-need)
        ) * 140
    ]
    append screen/pane end-mark
]

;-- Traverse pages:
next-item: does [

    if not empty? pan [
        append panel pan/1
        remove pan
        if empty? pan [
            append screen/pane pan-mark
        ]
        show screen
        exit
    ]

    either not empty? items [
        if items/1/style = 'panel [
            panel: items/1/pane
            pan: copy panel
            clear panel
            append screen/pane items/1
            remove items
            next-item
            exit
        ]
        if items/1/style = 'image [
            append screen/pane items/1
            remove items
        ]
        append screen/pane items/1
        remove items
        if empty? items [show-end-mark]
        show screen
    ][
        next-page
        show-page build-page
    ]
]

back-item: does [
    pan: []
    back-page
    back-flag: true
    show-page build-page
]

;-- Handle Keystrokes:
do-key: func [key] [
    if key = escape [quit]
    switch key [
        #" " [next-item]
        #"^(back)" [back-item]
        down [next-item]
        up [back-item]
        page-down [next-page show-page build-page]
        page-up [back-item]
    ]
]

count-slides: has [n d] [
    n: 0
    d: doc-text
    while [d: find/tail d "^/==="] [n: n + 1]
    n
]

this-page: doc ;next-page halt

;-- Build screen and event handler:
screen: [
    size page-size
    across
]

if not author-mode [
    append screen [
        at (as-pair page-size/x - 150 20) t1: txt form now/time
        at (origin) guide
        v1: vh2 "Show07.r Information and Setup" return
        vh3 200 "Version:" 
        vh3 gold form system/script/header/version return
        pad 0x30
        vh3 200 "File: " vh3 gold form file return
        vh3 200 "File size:" vh3 gold reform [
            round (511 + size? system/options/script) / 1024 "K"
        ] return
        vh3 200 "Slides: " vh3 gold form count-slides return
        pad 0x30
        vh3 200 "Page size: " vh3 gold form page-size return
        vh3 200 "Font size: " vh3 gold form text-size return
        vh3 200 "Head font: " vh3 gold form v1/font/name return
        vh3 200 "Body font: " vh3 gold form t1/font/name return
        vh3 200 "Origin: "   vh3 gold form (origin) return
        vh3 200 "Margin: "   vh3 gold form margin return
        vh3 200 "Spacing: "   vh3 gold form spacing return
    ]
]

screen: layout/tight screen

screen/image: backdrop
screen/color: navy
view/new screen

insert-event-func func [face event][
    switch event/type [
        key [do-key event/key]
        down [next-item]
        alt-down [back-item]
        close [quit]
    ]
    event
]

items: []
if author-mode [show-page build-page]

do-events

Использовать указанную выше программу очень просто. Сохраните её как %show.r. Отредактируйте переменные "title-line" и "back-image". При запуске программы запрашивается файл. Сохраните следующий текстовый файл как show-example.txt и загрузите его в show.r по запросу (вы также можете проверить текстовые файлы, включённые в загрузку, по адресу http://www.rebol.com/notes/devcon07-carl.zip - он содержит три актуальные презентации, созданные Карлом). Клавиши пробела, мыши и курсора управляют перемещением содержимого слайда:

===Presentation Title

=intro

    ---Nick Antonaccio

    Operating Manager
    Merchants' Village, LLC
    Pittston, PA  2013

===A Main Slide Header

    ---A Sub Header
    (Some sub text)

=pad
    #Numbered item 1
    #Numbered item 2
    #Numbered item 3
    #Numbered item 4

===Another Main Slide

    ---Another Sub Header

    *Bullet Item 1:
        **Sub Bullet Item 1a
        **Sub Bullet Item 1b
        **Sub Bullet Item 1c
    *Bullet Item 2:
        **Sub Bullet Item 2a
        **Sub Bullet Item 2b
        **Sub Bullet Item 2c
    *Bullet Item 3
    *Bullet Item 4

===A Third Main Slide

    ---Subheader:

=image rebol.gif

=pad

=image info.gif

===Slide 4

    ---Subheader 4:
    Topic 1 - idea 1
    Topic 2 - idea 2
    Topic 3 - idea 3
    Topic 4 - idea 4

[
    A preformatted text block:

        Idea 1:  some thoughts
        Idea 2:  some thoughts
        Idea 3:  some thoughts
        Idea 4:  some thoughts
]

===Slide 5

    Some Text

    ---Definitions:

    :Idea 1 - definition of Idea 1
    :Idea 2 - definition of Idea 2
    :Idea 3 - definition of Idea 3
    :Idea 4 - definition of Idea 4

=all

---Last Idea 1
---Last Idea 2
---Last Idea 3

###

Notes: nothing below the 3 pound characters appears in the presentation.

Поскольку эта программа полностью представляет собой код REBOL, вы можете вносить в неё изменения и добавлять функциональные возможности по мере необходимости. И так же, как и со всеми другими программными инструментами REBOL, вы можете передавать, устанавливать и быстро отправлять по электронной почте всю программу вместе с интерпретатором REBOL даже пользователям с компьютерами с низким энергопотреблением и низкой пропускной способностью Интернет-соединения. Программа будет работать в любой операционной системе, поддерживаемой REBOL, и вся установка бесплатна для копирования, использования, изменения и т.д..

8.5 Создание "снимков экрана" графических интерфейсов

При создании презентаций может быть полезно иметь снимки экрана программ и/или создавать графические макеты с использованием графического диалекта REBOL. Вы можете использовать функцию "to-image" для создания изображений из макетов графического интерфейса пользователя и функцию "save/png" для сохранения данных изображения в файл ".png":

REBOL [title: "Create Images From GUI Code"]
save/png %image1.png to-image layout [
    size 600x400
    backdrop white
    h1 blue "Slide 1"
    text "Here's a red box:"
    box red "I'm red!"
]
browse %image1.png

Вот расширенный графический пример Джона Никласена (используемый здесь диалект "draw" (рисование) будет рассмотрен более подробно позже в этом руководстве):

REBOL [title: "Shiny Black Button"]
sz: 200x400
img: make image! sz
img/alpha: 255
draw img compose [
    pen none
    fill-pen linear (as-pair 0 sz/y / 2) -50.5 70.5 90.0 1.0 1.0
            45.45.47 9.9.11 108.113.117
    circle (as-pair sz/x - 115 sz/y / 2) 70.5
    fill-pen 1.0.5
    circle (as-pair sz/x - 115 sz/y / 2) 68.5
    reset-matrix
    fill-pen linear (as-pair sz/x - 115 sz/y / 2) -60.5 30.5 45.0 1.0 1.0
            161.164.169 161.164.169 89.94.100
    box 
        (as-pair sz/x - 115 - 26 sz/y / 2 - 26) 
        (as-pair sz/x - 115 + 26 sz/y / 2 + 26) 10.0
    fill-pen 1.0.5
    box 
        (as-pair sz/x - 115 - 22 sz/y / 2 - 22) 
        (as-pair sz/x - 115 + 22 sz/y / 2 + 22) 6.0
    reset-matrix
    fill-pen linear (as-pair 0 sz/y / 2 - 26) -50.5 100.5 90.0 1.0 1.0
            1.0.5.255 200.214.226.224 200.214.226.128
    shape [
        move 154x200
        arc 16x200 68.0 68.0
        arc 154x200 -149.0 68.0
    ]
]
save/png %black-btn.png to-image layout [
    backdrop 1.0.5
    image img
]
browse %black-btn.png

8.6 Встраивание двоичных ресурсов (изображений, звуков, файлов и т.д.) в код

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

REBOL [Title: "Simple Binary Embedder"]

system/options/binary-base: 64
file: to-file request-file/only
data: read/binary file
editor data

Используйте "load (data)", чтобы использовать любые текстовые данные, созданные вышеуказанной программой. В этом примере используется текстовое представление изображения на http://musiclessonz.com/test.png, закодированное с помощью программы выше:

picture: load 64#{
iVBORw0KGgoAAAANSUhEUgAAAFUAAABkCAIAAAB4sesFAAAAE3RFWHRTb2Z0d2Fy
ZQBSRUJPTC9WaWV3j9kWeAAAAU1JREFUeJztlzEOgzAQBHkaT7s2ryZUUZoYRz4t
e9xsSzTjEXIktqP3trsPcPPo7z36e4/+3qO/9y76t/qjn3766V/oj4jBb86nUyZP
lM7kidKZPFE6kydq/Pjxq/nSElGv3qv50vj/o59++hNQM6Z93+P3zqefAw12Fyqh
v/ToX+4Pt0ubiNKZPFE6Ux5q/O/436lkh6affvrpp38ZRT/99Ov6+f4tPPqX+8Ps
/meidCZPlM7kidKZPFE6kydKZ/JE6UyeKJ3JE6UzeaJ0Jk+UzuSJ0pk8UTMmvn8L
j/7l/nC7tIkonekLdXm9dafSmeinn376D/rpp5/+vv1GqBkT37+FR/9yf7hd2kSU
zuSJ0pk8UTqTJ0pn8kTpTJ4onckTpTN5onQmT5TO5InSmTxROpMnasbE92/h0b/Q
//jR33v09x79vUd/73XvfwNmVzlr+eOLmgAAAABJRU5ErkJggg==
}
view layout [image picture]

Приведённая ниже программа позволяет вам сжимать и вставлять файлы в ваш код. Эта программа сжатия BINARY RESOURCE EMBEDDER будет упоминаться много раз на протяжении всего руководства. Сохраните его в файл .r, чтобы его можно было запустить позже:

REBOL [Title: "Binary Resource Embedder *** SAVE THIS PROGRAM ***"]
system/options/binary-base: 64
editor picture: compress to-string read/binary to-file request-file/only

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

to-binary decompress {compressed data}

Например:

REBOL []
image-compressed: load to-binary decompress 64#{
eJzrDPBz5+WS4mJgYOD19HAJAtL/GRgYdTiYgKzm7Z9WACnhEteIkuD8tJLyxKJU
hiBXJ38f/bDM1PL+m2IVDAzsFz1dHEMq5ry9u3GijKcAy0Fh3kVzn/0XmRW5WXGV
sUF25EOmKwrSjrrF9v89o//u+cs/IS75763Tv7ZO/5qt//p63LX1e9fEV0fu/7ap
7m0qZRIJf+2DmGZoVER5MQiz+ntzJix6kKnJ6CNio6va0Nm0fCmLQeCHLVMY1Ljm
TRM64HLwMpGK/334Hf4n+vkn+1pr9md7jAVsYv+X8Z3Z+M/yscIX/j32H7sl/0j3
KK+of/CX8/X63sV1w51WqNj1763MjOS/xcccX8hzzFtXDwyXL9f/P19/f0vxz4f2
OucaHfmZDwID+P7Hso/5snw8m+qevH1030pG4kr8fhNC4f/34Z89ov+vHe4vAeut
SsdqX8T/OYUCv9iblr++f67R8pp9ukzLv8YHL39tL07o+3pekn1h/dDVBgzLU/d3
9te/Lki4cNgBmA6/lO+J/RPdzty8Rr5y94/tfOxsX6/r8xJK0/UW9vlH93/9oAzR
e09yKIUBVbT9/br/U/m7x6CU98VAAJS2ZPPF/197eEDhtfs9vX9rDzc6/v3qzUyo
nJA/dz76Y77tHw+w3gXlbEMpDKihza/+7/o/c3+DU54tDwsobR2/fXR/qYXBiV8T
t3eDEmpA/d9LDASK0y/tnz+H/Ynmt78E1vti7lAKA6pouxz/X7v+uR045ZFdRE6x
1q21pG7NiSzx1f5R40pvvdNn+oB1P4Onq5/LOqeEJgCemFy1KQgAAA==
}
view layout [image image-compressed]

Вы будете регулярно использовать программу для внедрения бинарных ресурсов. Хорошей идеей будет сохранить его сейчас на рабочем столе или в легкодоступном месте на жёстком диске.

8.7 Воспроизведение звуков

Воспроизведение звуковых файлов .wav в REBOL очень просто:

insert s: open sound:// load %/c/windows/media/tada.wav wait s close s

Как и в случае с изображениями, вы можете использовать программу "Binary Resource Embedder" для сохранения звуковых файлов в виде кода в ваших программах. В этом примере загружается и воспроизводится встроенный звук:

REBOL []
my-sound: load to-binary decompress 64#{
    eJxtlHtMU1ccx6vGR3kWLMWpm5pskc2pi3MkqFNYJsh0oigV5KEULK2lpdDb9vbe
    9r56W1pogSKltBQqSGGAlYeimAlDkU1H1KgssrhBmFNhjAECHcZl2bVo1LjfyTm/
    nO/vdf45n9joyMjphTTaoQj2Tq4QWB1Io9HmUSt8Fc2z59GWUEp6KpB6k8pC3zJs
    7nzTvXbB/sdhmMdjcx5744q9SkHf1l4f/LLhq+K33jZnOI6/3gZBVAjq8Qj6Mkzl
    ek5KwBEpL11saGyrANhx4kKHzUBgmFKhUKKIAgBAlNRqNaSayiO1pEqeLeTzxZjJ
    nA+KAK2ByBaidle1XoGVnrLjx3kSlULEOSaBIBmkMZkL9Lo8owYUZYqyRLwMMVJo
    NqgkoL7MQoqOiXUVzpPl1a7OiyXS6K2RBxJiE1KgitrTteYyq0ktStgbE3MwTUha
    7BXFmDxHjpGITE5Unu++dLaupc2UFTLfd5PEZi8xObp7W/Ts8N3Jx3npXMjqarQA
    8XFHpJBUeAwscNRZ8wwF1a7auvIzVWRCwCL/XY6B4c5CWGYol+8LWfk52X63t1Ka
    mnw0Izk+XkCcbDpVKueKUdvpRofVfulms3TlIh8mODQ7fjpx2359ERTFZB3u+Gt2
    MH+zr8+mgwJMZ2/vajYeDt2aYr72U7eNUDf+es+yYQFzQcQvs+MTmQH0zaTTsued
    j/HHT2b6vloc6L0+71Z/hw2RykVHvt6bVtTZd+ecraCq3sT28fNnFD4bdQ/sWhwQ
    xHG14HH7iHtPpi6ELgn0CnFNTQybdwSviYLs39TYyqwN565cq+evoQctDvv+6cjT
    ns/ogQGH6r4tVwh0PWMjzq1egb5rqqfHJhq3+Pt9kOW61l2jh3TOG0Pt/BV01sL9
    A+7H/5xi+TGWpl/pq0cScxruP2qI8g70W2Zxj7ovb/cJYCS0D49dMXC5up4H13OC
    vFkLEodnhv8t8WUE+AsHn9w27totPj94XbXCZ6l31uj45MOkJUz65tbJiZGahJ1p
    rv6fcZYXa/7eodlHtLplXqyFkXenJ7qSwz4VNlxuTlrK8NvYNDPqrl9LDcq4Pzk5
    aIyNFpdftITSmfQP256NuH9jL2L6MhVDk2Mdiojt8WBZniB8bWha7f2JOyf2BLO2
    4b1jk301pAxAeFtWegfvbJgan36ofc/Hj7G/cuCPvlaYx04SZPCT4lKExvqLbRXg
    wS+jBZbv+vuuNhWDieuCAoN3EP0zU5PnOcu9vEO41Tdu9zp1AOc4N4PDPpSYlV/T
    dtaBcWJi+YbWH+/e7HAoD6zyZyyP0t9z//1nq2Cjj/8nKaXtP1ytOyHn8TkZ3DRu
    ugjWWU9WFqMivhCznunq6W42Z4Yt8wrYwGt++NR9x5K0/t33I0TFrZ1tTj3ATU7l
    iUFQli2SwFrTiaJ8jdZgrmpou+CqkOz+yD94XZK568Hvt5rwo3u+iEmR6IttZUYS
    yBRkyhFCDcnEmdkArMkvsTlqapx1VeWGnCNRYWHhidKi+pbW+tJ8pVwuQ3G1BoVz
    eOkcbg6caywoyMWVIKTEqf9ozNPrcrUYLMuRABBO6nUkppACAIzpqLQ8LYmpIBAE
    FRAEwbASwQmCwFSwgpIgJarW6inTUfUakiTVOAJTMkJoNCSuAmVSQCqHUdwTec4X
    tRrHVEpYqaKMIg9OqNVqAvcgh9RoczUkgSKeGMUlRKWkBqpQah6Gzkko/oJjOE7g
    L2FFAc2DM8zTjHoZDKsw4vlEDwyRF5xUKZVUj7lagsCxOfYR/wHwfuhW/AUAAA==
}

insert port: open sound:// my-sound wait port close port

8.8 Запуск кода в отдельных процессах

Для фоновых звуков и других элементов презентации может быть полезно запускать разделы кода как отдельные программы, которые не блокируют и не мешают работе вашей основной программы. Вы можете легко добиться этого, написав отдельный код в текстовый файл, а затем используя функцию "launch" (запуск) для его запуска. Функция "launch" открывает новый экземпляр интерпретатора REBOL для запуска загруженного файла. Запущенный код выполняется как совершенно отдельная и независимая программа. Обязательно включите заголовок REBOL в сохранённую программу:

REBOL []

write %playsound.r {
REBOL []
insert s: open sound:// load %/c/windows/media/tada.wav wait s close s
}

launch %playsound.r

8.9 Запуск приложений командной строки

Функция "call" выполняет команды в операционной системе вашего компьютера (например, команды DOS и Unix). Это может быть действительно полезно при создании презентаций любого типа. В приведённом ниже примере открывается Блокнот Windows для редактирования текстового файла "C:\YOURNAME.txt", созданного ранее (если не указывать параметр "/show", программа запускается в скрытом окне):

call/show "notepad.exe c:\YOURNAME.txt"

В следующем примере открывается программа Windows Paint для редактирования изображения, которое мы загрузили ранее в руководстве:

call/show "mspaint.exe c:\bay.jpg"

Вот пример, который встраивает исполняемую программу в код, распаковывает и записывает программу на жёсткий диск, а затем запускает её с функцией вызова:

program: load to-binary decompress 64#{
eJztF11sU2X03K4VqJsrkZJp6OzchhFJsx8qDB9od1fHdIO6ds7AgJX2jttyey/p
vWUjJuNnmNhMibzwaCSLi+EBE1ziGIkBGh0BSYTwwAMme9Dk4kgkgSiKcj3nu7es
QrKFhMUQOcn5+c7fd875+vXe27FJAg4AbIiGAQwWIwZMEbqTcmODN5xRdmRi6aoy
Z83YogngLlaNtV+s6kV7q9KelHeu9LYqQTXt7e/v97UqLcLuqKJIvriShnAIoJ0r
gXvPn+StlDAF5dyzHLwAdlw4TZ1Mm7oQvWDu7jKLslsxBc4KQ30bb9bMHF3F/D5j
MFAHEIbHD+cwb88s9riSEIjvK7EKogZs//bxAvQmYlqM5JsOUwHPWFgEAYDTvqTp
eYdy1Fn5Sh/O96h9nLrrDcD4IpQm7UOkWL/nt6MlqMvxrkl+GVWS7xqWalzDzqGz
9rbyD5ehpmnl+ezt3M/RSPe7Q9/ajeh5+9Ztm3vKh9xoM7SaimLUR18C2JKf+Kg2
APoJwzDOuiAF+hHU/pHXryObdLyP+y2kEhx7UaLfo0gq/RJa60/n88Ndrpz7FmqG
u5bk3L8zwdWXc0+jdOYXkn4lnYfW++/qOPLyDz7BfH3jTXVnplx949inhPvnSgw/
8RSIHM7P8PdSUYtxlxSkONE+o/u7EkNElMbpcuRKUhTjmLH/iHbDQQ7DHqL77zbh
oQxeRa9duBQHkRj+HnIdr7y/e178AvmmnHt5VQAmaNo59/EZ8QSJAY7EURJvMu2x
KipYj2CaEToYve2eYYiwl4rWY6jN8RWF5XtsuWSyhO7aJG8XXQFkNdWYIqIHK8nH
8FOSFJMoteEfZfQEo1SNCPCW2/BTjWK1uXkp9dDDegjrDqpkAUtiJhNp4ma3qUrx
MG6dqkyFMQ2ExQmaxgU2c/07D2ZJsCz3Q68Xh76Cvac2pZwi8jCO8rIZd4jielmc
uHxmsEMe1vMBZJf0YY8Pda95yH5p+tWrI86XMZbTE5a1gVlXFKyryeowp0Cy4Wf+
hdSrWGp26N008hW4XnS6/OBS7MnUVHoK0osoTV+22qF56c95qKdtZBzB66J/imSc
/Rmsg/KDdHFbA9O3RrZWByD/qPf1KTCwze3y2KCbn9vnP4ExoItiwr11zvncqq6+
oXGV//XVa5qCzXxL6M3ZfBfMZyFPBvywgD3FGDjLnGVl83o4T+HJAZ/PFxWTqrcj
GxerHljRqyL9sWXxqU2/nkHki1H4HDkvJeM7vZooeLdnNU2R10K34G1XdgveTmE7
vmv7fNDcFY1u3ABpNa5J6rZd9MouqGpjw6z1GLXn6vDxV/s9o1cYvcroNUanGP2J
UZ3RG4zeZPQ2o3cY/YtRqCdqZ3Qho6WMuhitYHQZ0pr6mRr21Zvv03VFuuMoX0Gd
VqT7BlupKFoXw8eo/8yynUR+HvEa4g3EPxEXYuwSxOWIaxADiGHEBKKGeADxCOIx
a1wXkE81zH/ut0OdG0LtjQ2+hCSBzLUKWoeSyErC+pickIQgfAmhgaSG319xPEvo
ioQ6Ld9D0CL04ddZQuknaxA4W1hRtXeySa0DXWM7BHjDFhHkhLUKYs2cJTcrA0H4
mmtXYgk+m1GVTBBOsVVbXJGDsNTWKexIqpqQ4aWYqgbps4LPCDFNMPcLYXQpldrC
g0bcVHcKcQ220DqyB4PTHYKWScZVgCGsw/LBEgHWsjYLZR2zRTMxWZUwfaFwOAot
SXVXTIuLM9V/ZeuSMw/UxW/s4KOF6W2GNjmp8Uo6rci8ImsZRVLxG+1hZWhgrlv6
/4F/ABcSIgQAEAAA
}
write/binary %program.exe program
call/show %program.exe

Функция "call" имеет множество опций, которые позволяют вам отслеживать, контролировать и использовать вывод из внешних приложений командной строки. Наберите "help call" в интерпретаторе REBOL для ознакомления. Для получения дополнительной информации см. http://rebol.com/docs/shell.html.

8.10 Создание простых анимаций

Вы можете разместить свои виджеты в заданных координатах (координаты XxY указывают на X пикселей выше и Y пикселей вниз):

REBOL []
view layout [
    size 600x400
    at 200x250 btn "button 1"
    at 300x350 btn "button 2"
]

Измените положение помеченного виджета с помощью уточнения "/offset", за которым следует двоеточие:

REBOL []
view layout [
    size 600x400
    at 20x20 btn1: btn "button 1" 
    at 100x20 btn "change button 1's position" [
        btn1/offset: 300x250
        show btn1
    ]
]

Вы можете создать координату из двух отдельных чисел, используя функцию "as-pair" (вставить/F5):

REBOL []
x-size: 600
y-size: 400
x-position: 20
y-position: 20
view layout [
    size (as-pair x-size y-size)
    at (as-pair x-position y-position) button1: btn "button 1"
]

Чтобы создать повторяющийся цикл, просто скопируйте строку ниже, которая начинается со слова "box" (коробка) и закрывающие 3 квадратные скобки, в свой код графического интерфейса. Все, что находится внутри этих скобок, будет повторяться постоянно. Это простой способ создать непрерывное движение:

REBOL []
view layout [
    size 600x440
    btn1: btn red
    box 0x0 rate 0 feel [engage: func [f a e] [if a = 'time [
        btn1/offset: btn1/offset + 2x2
        show btn1
    ]]]
]

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

REBOL []
view center-face layout [
    size 600x440 
    text "Press an arrow key"
    key keycode [left]  [alert "You pressed the LEFT arrow key"]
    key keycode [right] [alert "You pressed the RIGHT arrow key"]
]

Поместите выполняемые движения в квадратные скобки после теста if:

REBOL []
direction: "down"
view layout [
    size 600x440
    btn1: btn red
    box 0x0 rate 0 feel [engage: func [f a e] [if a = 'time [
        if btn1/offset/2 > 420  [direction: "up"]
        if btn1/offset/2 < 1    [direction: "down"]
        if direction = "down"   [btn1/offset: btn1/offset + 0x5]
        if direction = "up"     [btn1/offset: btn1/offset - 0x5]
        show btn1
    ]]]
]

Используйте REBOL функцию "within?" (внутри?) для проверки графических коллизий (то есть, когда графика соприкасается или обменивается координатами):

REBOL []
direction: "down"
view layout [
    size 600x440
    btn1: btn red
    at 20x350 btn2: btn green
    box 0x0 rate 0 feel [engage: func [f a e] [if a = 'time [
        if btn1/offset/2 > 420  [direction: "up"]
        if btn1/offset/2 < 1    [direction: "down"]
        if direction = "down"   [btn1/offset: btn1/offset + 0x5]
        if direction = "up"     [btn1/offset: btn1/offset - 0x5]
        show btn1
        if (within? btn1/offset btn2/offset 1x1) [alert "Collision!"]
    ]]]
]

Эта простая программа демонстрирует некоторые из наиболее важных методов анимации, обсуждаемых здесь. Лови падающие куски:

REBOL [title: "Catch"]    
alert "Arrow keys move left/right, up goes faster, down goes slower"
random/seed now/time   
speed: 11   score: 0
view center-face layout [
    size 600x440   backdrop white   across
    at 270x0 text "Score:"  t: text bold 100 (form score)
    at 280x20  y: btn 50x20 orange
    at 280x420 z: btn 50x20 blue
    key keycode [left]   [z/offset: z/offset - 10x0  show z]
    key keycode [right]  [z/offset: z/offset + 10x0  show z]
    key keycode [up]     [speed: speed + 1]
    key keycode [down]   [if speed > 1 [speed: speed - 1]]
    box 0x0 rate 0 feel [engage: func [f a e] [if a = 'time [
        y/offset: y/offset + (as-pair 0 speed)  show y
        if y/offset/2 > 440 [
            y/offset: as-pair (random 550) 20   show y
            score: score - 1
        ]
        if within? z/offset (y/offset - 50x0) 100x20 [
            y/offset: as-pair (random 550) 20   show y
            score: score + 1
        ]
        t/text: (form score)  show t
    ]]]
]

Ваша способность создавать интересные анимации ограничивается только творческим применением движения.

8.11 Простая структура анимации для презентаций

Простая структура анимации, специально разработанная для облегчения работы по перемещению и изменению размеров элементов графического интерфейса пользователя в презентациях, созданная Джеффом Крейсом, доступна по адресу http://www.cs.unm.edu/~whip/presentation.r (обязательно загрузите демонстрационный файл по адресу http://www.cs.unm.edu/~whip/test-prez.r). Краткое обсуждение этого инструмента доступно в списке рассылки REBOL по адресу http://www.rebol.org/ml-display-thread.r?m=rmlBYZS.

8.12 Использование анимированных изображений в формате GIF

Еще один простой способ работы с анимацией в REBOL - использование стиля "anim" в графическом интерфейсе пользователя. "Anim" берет серию кадров неподвижного изображения и воспроизводит их по порядку как анимацию с заданной скоростью. Базовый формат:

view layout [
    speed: 10
    anim rate (speed) [%image1.gif %image2.gif etc...]
]

Следующий скрипт преобразует анимированный .gif в папку, заполненную изображениями отдельных кадров:

REBOL []

gif-anim: load to-file request-file
make-dir %./frames/
count: 1

for count 1 length? gif-anim 1 [
    save/png rejoin [
        %./frames/ "your_file_name-" count ".png"
    ] pick gif-anim count
]

Этот следующий сценарий преобразует каталог изображений (например, выше или любую другую серию изображений) во встраиваемый блок кода REBOL. Он ищет все изображения с именем [ %your_file_name-1.* Your_file_name-2.* etc ...]:

REBOL []

system/options/binary-base: 64    
file-list: read  %./frames/
anim-frames-block: copy []
foreach file file-list [

    ; Unique portion of file names for your image frames go here.
    ; Leave out this check if you instead want to convert all
    ; files in the directory:

    if find to-string file "your_file_name-" [ 
        print file
        uncompressed: read/binary file
        compressed: compress to-string uncompressed
        append anim-frames-block compressed
    ]
]

editor anim-frames-block

Вот пример вывода:

anim-frames-block: [64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zCHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxqjufmZWdnWxS/OsPJcODoPLtKprUcW9TPLXJT
V7LdFZIZuMx/Ll/rrJCkC3NZ1ztd6SpVCG+L363EsXpCTvhmtovzVCWurr7R6jG7
rzZarKFpd8XTS77Z1/Xu7Qn+vunr6+/v725rqv6nm/Oj4Or2Ll7jvDUOa8+e6FX3
3uYjbPz0fN/RKjbeWcU+Z5do2qfN2lWaelnXfbveKwkz7ytLqu0qBK6Xed1cyfhG
TC58xeujhyuF422FXxQeOPybbR1nzbbP18+khtXvu/H95Ns7Gzdv5ZtfaVX64fjZ
crf/d6xPvV7XmJ7PZ1/x/ueXm/nXrOfVZKyZ+DL8nt85zhWzqu8LPosvPyYZEdW8
QrJjvjdj3TOFJuXQFVEVEl0iC9L49pVJJvZcnR7XLn/w+ux64XUpizrvbF0R1PFx
4QvB3s29OxLylB9tW9Cj9+vEol5NLk+5ia7vLB74GvxbETxZRklSqI+HyWNpR7ri
VbkJtreOp05nF1O/EeGW9C01/RqjmVrF3l7PZxnfPStv12qxsjBYAwBolvDW2AQA
AA==
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxqjufmZWdnW6hqBUwQfnxuvkPltxaJLSsuLOTt
ZWPdIPzSaal3vZUth6nWhZUsq7NsrUqzQ9f47K17qyWmdW1T2txFsreLdW/Pydu6
rXe2mHrsYuf3j86uLn95Z1/Qf6ZnWeUGD2e38V/3WVOh9viYkfzh3Fvmb1Iap+oq
P7OUKH64ocH2tsisGfkvTy7nXi6nG/n11dGZzLv3RQt8On3c19zY7e8stbyDCxtf
h0rLZBZuKjyYFrv6jsLdZ8xr99lGi3wueRLuGN6+zqSq7MW1700y/hHle4o/PhP8
5Xt+397f3z88Pj3ff/++v79/vGdnYbAGAJfEqNM/BAAA
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blri2cIVNC+GU2Hp6elcEX0tnsbLfPpNs++9mTE57fRcyepfJZxfFgUsdNWU
s51l8ihoma+8XatU6cOQVaHCca6zQh+GrYvlrWOVnvbgxrzUo/POzrz2JmpuLuu+
VuntT+9ML316T3VWuf79HXX/t/GuKTJIPBj5UW7bzB0fko75frwVGzP1ffIRa934
tpiQp88O9Zq3q84pL3qwq593uZ621dus61NCJ097K/714b7l3tf1bAv03jfNmv/v
264t3wu2Hn0r9973y6uiy2aql235hJeef35hovexONmK8jc3rzapXLeL03r+6cXl
1fHn9+39/f3D49Pz/ffv+/v7x+fX98/v3////1NWFgZrALxatNdHBAAA
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxKZMWCZWdnSuW+urOSId11nkP+rx6JLS8C2l0n
y6XO2PLyUovvXDtTCdNXV5pCl8YtnRn68tq6qOVNX6tKdW4uT+ud5sv9RTt6Xt79
Vz3a4Stu7Cq7+OitZ/i7i3tza5n4tCo+3JzWdniTz5oI1cfHNOVXt2pWqp87VaPv
LZf1413C3s7pdmKys0rSL88PZGbbe+vzva1rY3+/PV32+sCubRtnnd0rkJdwj/0h
0wyemh2p644UC7fl7H778NGh3vO6fKbGX1/f2Jx9/9ze3d/fPzjczSvvv2/Pz88v
Lq+Oj7dTYLAGANdbpyswBAAA
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxqjufmZWdnWxS/unNy8/Lz8x2auWR/BTVeXOwi
Khe7y2Sl47KAiVamXApZV5b4rnWSXbVVO3RB3OF/PN7X1G9usjnfdXdl2dpz2/IK
D339VZZ3fVfZ2kdnd5uqx++t+/9tqvaMlWfXh3IrT7sZ/jHxaHim0zWtSqOnM6a9
FDtbU26cfkDPvrlNc1dm6kVTb22Lv5alaYfm5C+qu3OrNPfa+tzj13Ijv+XemZzI
zv9n+oq7Kye6f9+js2Fz5IFZx4PK+MR+JSy/sTn7/rm9u7+/f3C4m/m7pACDNQAX
yZ/iJgQAAA==
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWendyiezhkdy8zHemsfm9O5LG6m7zHGqjWKRCMo7MY+h4
Z/IrYGXwMp65dq2rAl6FrGJbG3fUKuB12DrPvVqs2gFvwlelHZ/ku3qadvSilMP7
9kqW653fWvay6ezq67rxS6r/P1qjPWPDg4Nu/N+/rvyh9/iYt7zzNs0So6enpi2M
cuuRNLp3qJH/d6hNlEnY+eXS09l6w0qzLq+PPP7s98yy3N2Fp5+dvTtVN78lqf77
u5XTi3wfHpYVj5lTnX3xfsHkeDe98qrS11catc/PK7D+/u74fnNpHv19e35+fnF5
dfz5fXt/f//w+PR8//37/v5mYGJisAYARqapGj4EAAA=
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8loBjYyMaj5ToqJNHjqOV0zsq/l56RnnjPlcq9t6Zy8+Nx8w+okFq8vywK6XDvl
ZGdNeR7Uyb9oUY7X55dH2INX7trCZbr62oIYSa+vv65mRDRHs05rrRR7GLU09+K+
v5LmD++sKuW/d3R2+YO4fbUn//G+MV+bsKpF9JzvnSKDx/vbhJ3DTkbo3j5coB2v
F72z4MzWubrBbLJWL25fWuZv7/d6y4q0bdMNj6udub7mzYnGuVV+v6qK8k/sl/We
l7Nb/+Ojyv5ytX0yFq/2LnRdfW3P79ef515b73/9nFRGSVPJ00c2fXwSf9685y1d
7B9ft/fu53ei/f3/5xnVtie8f33//P79wEKATeNBA4tYxoNGDrUVD5p4zF48aBZw
00h0ZGRksAYAd264o18EAAA=
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5tUvVi5Yia1eG5edqbPtPjSnpWBy/0YDCvDvvwsXh7Q6TL5
kI1UYGbQMv65Wq2nAl6FrApd++vIrA8HmRc4smbxni59cH294d46Vu2tOQc3OzDO
cc2+ujZiZ9zjc6mvr+hFNGV+/rT31bUX9xuTTybFWllsTFzXI5uv6xO2yXe3m669
nrfIxrAzDaLqx9bc2Jx8aVZ90bWcWYZXr6xj39+W++NT4K1VuZ9LeqPfpM2cWHj8
ytmQHx/u79b9zSf3e9un5iOth/QkYnd9fHVy/fSydbWl5e8PBbYHLreJ+1Oyv1d1
cX5tVe2Li+94t/X7y9b9Wf5y4mx3u5919d/Orr1+s8jyovr9ZFYpjol1XGYvHjQL
uGk8bBEJy3jYKpG24mGbTNmLh+0KbRqPOoTYWBisAbfrxM90BAAA
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxqjufmZWdnWxRHhRwIfu46z6Hx1xSJLSsuLOTt
1XLdFfDy0mIfTqu5t4xfOayKWMt04NRVretrAvc3yWqVrTm/LnqlUuusba9Ct6aL
ctQ4mL+9syt3+jHWgO+Nd/fVPXxm88p8Q8y+Gl7/q5Il667sZjp7S0drqm7UHP/T
UrJ7LNc/2zFFOXudlNWyG9uzvs6yO1NgEj29V3RXH2/1tzfTthVv9lt52+zdvcXZ
zPZ/rb99OKfvLF+vu+d50Xaju3b3bSutnj+fsTx4/sra6pK3N9fed2Op/2uR/OZ5
+/pQf7GKiJ37tlb905I3LVw7s//St1W7NgW8f/l1+41qZr6O+MxvjuH3m3jMXjxo
FnDTeNgiEpbxsFUibUViGyMjgzUAhlm/D2kEAAA=
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJGcFnIAgdVr2kGybtEJDernZmpnfsqp9P48bn5tvr/ZKSuPApY4Koo
Fzvry8OgZb6Sdq1Sog9DZjJlh/l6mLz2ZeDfU3c3SuClwzQm+RWsC6bqOC7JOrwo
Vnv72uht1gfbeK0n6MWtKW/8pbrj2/uI7QU/F9Vmf14XMbfnolxpjWlR3GGbyXZb
a3ZufLY619b5H8+vnNRL8z7K6ciWbnG80B7Y3SZrrZF7bVN+ee6q6uKr9/ZFM8/X
qfnx7s6xYPGrs+7oPXrWzex83qes6svaa+v/n9OrtUp9fX9ve7j/ux8fP3x61rjY
vLZ6b+iNdzsPre/9l5a86itjv21cXGXk5p+Wx+fVM3K9CK15v7MtwZlL74RCAp+b
xsMWkbCMh60SaSsetsmUvXjYrtCm8ahDZVrGo06NPFEBBmsAOJHArHoEAAA=
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4a8uKBYvd+6Wd
i/54bFp8YjKf9yqTzk2ph6ZqxZ4S4dj87Mw00+J7IjM3Pz/Xa1v674jElecXJrom
yq3NKFbwWC4/PSiE68FB5llMay/1aJkuClobLhqyV2pa9vUp8SeZBLjL1t7czDM7
S9ViukrMlpCNYj2V5YlB03x/7/uzu3RpQqsjL5tdjYFhyIF8yfehWT82Rmz3VxXf
9rvi0+VJs8zdv8lsLYo/NK2b699pqS93r20wLu/lrTbNvbYt3/rcWmv9x5f2prb7
1VZbvHxwrPO1n94u8+IzB/XV+/VsTEpfXl5pn+9Xbf3l6b2J1cHP+6psKhc/43zk
d99Cs/qrXW17eW3Nl7Jfp1aff17zb2/Rjz8/v8uWMf1aGt/IobbiQROP2YsHzQJu
Gg9bRMIyHrZKpK142CZT9uJhu0KbxqMOlWk7Eh0YrAGyBMCKdgQAAA==
} 64#{
eJxz93SzsEwMZwhn+M4AAg1g3ACmGsCsBjDnPxj/B1P/waz/YM4oGAXDBij+ZAHT
OiAClCfYOf4zMHLIeGxYcLCZQ1gr5sSGhYfbBZS95nhsXHS0W8I4686JjYuP9ys4
d8l4blpycrJG8KqYk5uWnp5ukHxqjufmZb79XEWvrlROfnRuvn21F4tXSOOFNptu
JttVBisuzfURtJsrdfXBleWhnHFLZ5VqX18V18lnImW6JmwT/yamD1ofHG9tZbi0
TLV6ytrbOwqeHkrNCtePaiypntX7u+z9rTml7OIxWiZrbhy2kbbm45IsTDrevTDu
GM/PgptrkzWj360qefhi9nLH+b09VUa3Z62zPN+zNkLt7fVt+eK21tHf8w40Jv7S
Oxv148Pxg73y1898t4h4Pnvh9rh5c9S+XjZbH/5+757K7y/22bc716+Lzn168ln4
db/1917kfwvbOH+6/zzLD8ez7p/X9/u1/d+fiEq2+Joe3owHjRxqKx408Zi9eNAs
4KbxsEUkLONhq0SaqACDNQAYMLy/ZgQAAA==
}]

А вот пример того, как записать файлы в этом блоке обратно на жёсткий диск и отобразить их в графическом интерфейсе:

; Write files:

count: 1
make-dir %./frames/
for count 1 length? anim-frames-block 1 [
    write/binary rejoin [
        %./frames/ "frame-" count ".gif"
    ] to-binary decompress pick anim-frames-block count
]

; Create file list, with frames in numerical order:

file-list: read %./frames/
animation-frames: copy []
for count 1 length? file-list 1 [
    append animation-frames rejoin [
        %./frames/ "frame-" count ".gif"
    ]
]

; Display that file list as an animation:

view layout [
    anim: anim rate 10 frames animation-frames
]

Вот пример, который объединяет вышеупомянутые анимированные файлы GIF с обычной анимацией графического интерфейса пользователя (значения "/offset" настраиваются с помощью циклов "for"):

view center-face layout [
    size 625x415
    backcolor black
    anim: anim rate 10 frames load animation-frames
    btn "Run Animation" [
        for counter 0 31 1 [
            anim/offset: anim/offset + (as-pair counter 0)
            show anim wait .05
        ]
        for counter 0 24 1 [
            anim/offset: anim/offset + (as-pair 0 counter) 
            show anim wait .05
        ]
        for counter 0 31 1 [
            anim/offset: anim/offset + (as-pair (negate counter) 0)
            show anim wait .05
        ]
        for counter 0 24 1 [
            anim/offset: anim/offset + (as-pair 0 (negate counter))
            show anim wait .05
        ]
    ]
]

8.13 И это только начало

По мере того, как вы узнаете больше об общем кодировании REBOL в этом тексте и за его пределами, вы улучшите свою способность создавать экраны графического интерфейса, которые точно соответствуют вашему творческому видению. Если вы хотите создавать более сложные визуальные макеты, в центре вашего внимания должен быть графический интерфейс и графическое программирование. На данный момент понимания базовой структуры и концепции переключения между слайдами, добавления графики, текста, анимации, звука и т.д., а также определённых готовых инструментов, таких как сценарий Карла show.r, более чем достаточно для создания эффективных презентаций.

Другие инструменты, такие как средство создания файлов .swf REBOL Flash, о которых будет рассказано далее в этом руководстве, предоставляют дополнительные мощные возможности для создания и распространения презентаций.

9. Makedoc и другие полезные инструменты для повышения производительности REBOL

9.1 Makedoc.r - Конструктор HTML-документов

Сценарий презентации show.r, продемонстрированный ранее, на самом деле является всего лишь вариантом другого полезного сценария REBOL под названием "Makedoc". Makedoc используется для быстрого и простого создания HTML (веб-страниц) с использованием простого формата текстовой разметки. Используя минимальные шаблоны символов, Makedoc позволяет пользователям размещать заголовки, подзаголовки, нумерованные списки, изображения и другие общие элементы страницы. Makedoc также автоматически создаёт оглавление с гиперссылками для всех тем и подтем. Вот небольшой пример синтаксиса:

Page Title

    By:  Author Name
    Some descriptive info about the page.

===Main Section Header

Main section text.


---Sub Header

Sub section text.

# Automatically numbered item 1

# Automatically numbered item 2

# Automatically numbered item 3

Here's how to include an image:

=image %image.png

Сохраните приведённый выше текст как файл с именем example.txt, затем загрузите и запустите сценарий makedoc.r с http://www.rebol.org/library/scripts/makedoc2.r. При появлении запроса выберите файл example.txt, и Makedoc преобразует его в красиво отформатированную HTML-страницу.

На самом деле весь этот учебник написан в формате Makedoc. Исходный текст доступен по адресу http://www.re-bol.com/business_programming.txt.

Дополнительная информация и инструкции по использованию Makedoc доступны на http://www.rebol.net/docs/makedoc.html.

Makedoc интегрирован в CGI-скрипт Sitebuilder, поэтому вы можете создавать и редактировать страницы на веб-сайте, используя только браузер с простым синтаксисом Makedoc. Гораздо больше о создании и использовании программ CGI для веб-сайтов будет подробно рассмотрено в следующих разделах этого руководства.

9.2 Улучшенный текстовый редактор

Функция текстового редактора "editor", встроенная в REBOL, очень проста. Он даже не позволяет отменять/повторять действия, когда возникают ошибки при наборе текста. Следующая версия позволяет использовать несколько таких полезных функций. После выполнения приведённого ниже кода просто используйте функцию "editor", как обычно, и новые функции будут добавлены. Обратите внимание, что процедура загрузки заключена в блок, выполняемый функцией "attempt" (попытка), на случай, если подключение к Интернету недоступно:

if not exists? %e [
    attempt [
        write %e read http://re-bol.com/e
        do %e
    ]
]

; editor none

Вот программа в сжатом формате для использования в ситуациях, когда подключение к Интернету недоступно. Этот скрипт использует код отмены/повтора Романо Паоло Тенка для добавления функций [ctrl]-z и [ctrl]-y:

REBOL [Title: "Advanced Editor"]
do undo: decompress #{
789CAD58DD6EA33A10BEA64F61F55C546A85C856DA1BBAE7F441908FE4829358
4B2002A709FBF4677E6C6C2034ED9E8DD406EC99F137BF9E49D936565FAC7833
4DC5FF8ABBE4D4546D7A50975C7CDB6C3642BC88A66DB4F85B9C9ADA1C8CD595
7044AAAA72B13D35A528B6AAD412B913B315AAAE45611A818BE20149418415DB
5AED525C7BE59DBDA9B44496C434BDEEAC286BAD3ADACB8849C1527B1C788580
02447D7915A5EAB425467798C72CC6871FA2D6CDCEEE5FC55EAB2A0805909D3E
B4EF7ABE8EE2C6B75C5865EAB00B9B40209DDE3B6D977A8F2073964C1881CC74
BD9D08A2E7DA343AAD4D0F14685D5876A8169495E9ECF00A80BA932608CE5804
F0A71E0EEA48E7FF75FFEFAF7BE138E165B8179DF62F27DE49C15824E365DB76
42F7E55DD26B2B1EF6A636BBBDC56DA7973B5D646D530FC0422BDAD8BD06BEC3
1110455E293032901FDFD8A37B78E7B5DEAACEDB64649992E8A68A0DEE086414
4C183E88448C5105E025593F83A77C1627E4ABBB040DEA1DA5DF7503E164BB5A
1C6B659AA946B899F57BB3056554338862865F2CF421F74A39551D243AB74F04
9790665D5B47A7B36A93631D07F143B2CD01CC6C3C339E4313D44E87384245A5
6BAB4456B7A502046D2FECE1887FCFE42D78C845BBDD422CA4B64D1942C4F784
2CEE10246052A6F0A988C25648602B981AE9B241FCF80765C243010BB260FC0C
9F22525786559DC523BB5195D6B46844A711A481C08C1A447F5647E6F31A8AA3
DAE9F474E4EFAA3D7304A5BDF9A57D608F0B39C7103E8AD43B91A3ACDA69513C
8BC7F04A64B2D8A0DB0141EEFC098FECDE6BF52E00CCE964B67D48A5A88A70F6
2BB0FFF8943B5BCEA3ACEBDAEE152AC48065375448594C934A16CAAE1453E912
0E374CB36D193155297ADDC01E59380F8BA47FE6942DF70A20A023482D6F3ABC
5360B1C79870856BA4192F113A8C56982445618C808D99C8820CDCEB5A97D657
3DF892A14E9CDBAE8A0038AF8395391D58DCE868F02E078CFF7E149B4BFA4D8E
AC18283799918979E0B672E058B55A634E5345E7902DDE54F9334590DE37B810
12173E1DE6F394A901774C987061C204811D73F8609F009F68C86C94070BC6B0
BA64F59C93133CCE70E3490F21A25E1CC67119D3230973ECDB835E155EBCE91D
D49614614DCCA0B1775939A180CDAB4CE4128A3576DA95804CE836E0C704AAA1
B61002E4685DA5FE2643D48031EA4B92D9D5E26F002F89D33E2677F77F76C462
EFD23B848C2B668E56FA1626A20B699C704A2457FB87711374F943AAA3A53FA3
FA4241B188EE58ADCFAB02A7E8518F2B7517849DBA6654E5666576747CC984D4
A70ABC2EC3AAB737688D8B6D5B9E7A566D6B74CD0695DE321763277A5D31FBA2
0636FA8CA11B690C3D115F7245D4B951AB169D862D91A30A8F2470DEB300EDC9
DDB6EBD1403226FE8F36D7C3E323C71D550F4C37CEBD217AD948AF1EEA6F4D3F
6D500C76545C6A737C6B5557E55916E1A37924A54AB28E6F19D68B7273DB1210
3C936CFC20BEA63936F6C39DE6C01B2B0C1DE757A34E73AC2151297A9E35ACAE
25A5FA130571B112D59F4EA90F732A61A808C763FC3F5903F68A0CFC024934F6
1C91955746552EE761381C6B0787CE5746D464325652A98E67BC519D9D8E134A
46D8718CBB0D99CBF0127274F8EFA1BF81CECF95AB08613E5B4407C5D008319A
FBA360F9EA0F09B35F0F6410D3DBCE343B671C8453C496087D37CE945FBC24FE
D44540FFA56B775D478BF43006E08443F2D9C26BC8D898EF469F1FB3C5B43A5F
A6A171AC976C8DAC82AE2E32228D1EF78FF7E30F29530761C71B1B03400BF709
CDCDCAB418113EBB4107EAA7CAFA123A853AECFB1000BA0C7FD2D9F0F8488F92
47DB9831BBE49E2015C423AFCB1A82AC614DD6907B02963544B2B88CE5938980
DAB3F830A221ACDF3F84FA144853F15D5E138220B913FF08E7532077030A4555
BF6FCF3E7769C49602AF8D7E80CBF79061648807B85DB7B858DA0BDFC7B4F21F
C9601ECC1F140000
}
base-color: 230.230.255  base-effect: []
ctx-edit: mold :ctx-edit
changes: [
    {style tx vtext bold 40x22 font [colors: [0.0.0 200.200.200]]} 
    {style tx text 40x22 font [colors: [0.0.0 170.170.170]]}
    {vtext} {text}
    {btn-enter} {btn} {btn-cancel} {btn} {btn green} {btn} {btn red + 50}
    {btn}
    {[tabs: 28 origin: 4x4]}
    {[tabs: 28 origin: 4x4] with [
            undo: []
            colors: [254.254.254 255.255.255]
        ]}
    {Ctrl-V - paste text}
    {Ctrl-V - paste text^/^-^-Ctrl-Z - undo^/^-^-Ctrl-Y - redo}
]
foreach [original changed] changes [replace/all ctx-edit original changed]
ctx-edit: do ctx-edit

Написание/редактирование кода - это основная деятельность, которой занимаются программисты, поэтому возможность редактировать напрямую с помощью интерпретатора REBOL - очень полезная возможность. Одним из преимуществ использования простого встроенного редактора является то, что он работает на любой платформе, поддерживаемой REBOL. Он работает таким же образом в Windows, Mac, Unix или других операционных системах, которые могут быть вам незнакомы. Возможность редактировать сценарии обеспечивает мгновенную среду для разработки переносимого программного обеспечения на любой ОС. Крошечный интерпретатор REBOL - это всё, что вам нужно. Metapad (http://liquidninja.com/metapad/download.html) - отличный сторонний редактор, который можно использовать для кодирования REBOL в Windows. Щёлкните Параметры -> Настройки -> Основное внешнее средство просмотра и установите путь к REBOL (обычно C:\Program Files\rebol\view\rebol.exe).

9.3 Конструкторы графического интерфейса пользователя и средства обучения

Основное различие между REBOL и другими инструментами программирования заключается в том, что REBOL, как правило, требует меньше строк кода для достижения аналогичных целей. Диалект графического интерфейса пользователя особенно лаконичен по сравнению с другими решениями. Ввод "view layout [button]" делает то, что требует страницы или более кода в других средах. REBOL также имеет встроенную справку, доступную для всех языковых конструкций. Вы можете искать забытые функции, искать синтаксис любой функции или перечислять содержимое любого объекта с помощью функции "help", перечислять все доступные слова с помощью функции "what", быстро тестировать короткие фрагменты кода в консоли и текстовый редактор и используйте функцию автозаполнения консоли, чтобы запомнить написание и ускорить набор текста. Более подробно об этих и других функциях улучшения работы будет рассказано позже в этом руководстве.

В результате изначально простого рабочего процесса большинство разработчиков REBOL склонны писать код, используя простой текстовый редактор, а не раздутую интегрированную среду разработки ("IDE"). Как правило, быстрее и эффективнее держать копию интерпретатора REBOL открытой и просто писать код REBOL, чем использовать тяжёлые сторонние конструкторы графического интерфейса для создания интерфейсов или полагаться на сложные функции IDE для запоминания и управления большими API-интерфейсами. Одним из приятных преимуществ REBOL является то, что он крошечный и мгновенно устанавливается на любой компьютер. Никаких других инструментов для разработки или распространения программного обеспечения REBOL не требуется.

Тем не менее, может быть полезно иметь несколько посторонних инструментов, помогающих изучать и изучать язык, а также создавать простые визуальные макеты без написания кода. Следующие ниже примеры не являются сильными отраслевыми приложениями, но могут быть полезны для быстрого проектирования и обучения, для ускорения макета презентации и т.д.:

http://www.rebol.org/view-script.r?script=vid-build.r

http://www.rebol.org/library/scripts/layout-1.8.r

http://www.rebol.org/view-script.r?script=rebolide.r

10. Проблемы и примеры из реального мира: почему "Программирование"> Офисное программное обеспечение

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

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

Но это только начало. Еще одно большое преимущество специализированных приложений перед электронными таблицами, например, состоит в том, что они позволяют полностью контролировать ввод и отображение данных. Легко проверить данные на наличие ошибок с помощью простого кода проверки формы, прежде чем неверные данные будут постоянно сохранены, и нужно будет отображать только основные категории значений. Вы также можете легко создавать автоматические резервные копии данных, чтобы информация никогда не была случайно перезаписана или стерта навсегда. Вы можете гарантировать, что защищенные поля данных никогда не будут затронуты во время ввода данных, а будут доступны для просмотра и редактирования только при необходимости. Вы можете выбрать, чтобы пользовательские интерфейсы были четко разложены и помечены так, как вы считаете нужным, и настаивать на порядке рабочего процесса, в котором вводятся данные. Коллекции небольших процедур ввода данных, отчетов и других элементов повседневной работы можно собирать и запускать из основного приложения с графическим пользовательским интерфейсом, данные можно сохранять, читать и совместно использовать между каждым компонентом, а другие функции автоматизируются за кулисами, чтобы пользователи видели и взаимодействовать с данными точно так, как необходимо. Ваша способность использовать код для ускорения использования, уменьшения количества ошибок и выполнения вычислений и манипуляций с данными полностью ограничивается вашими собственными интересами, способностями и приоритетами. Ваша палитра прекрасных элементов управления данными и мощных инструментов будет продолжать расти по мере того, как вы узнаете больше, а потенциальные возможности, которые вы можете включить, ограничены только вашим собственным опытом, пониманием и творческими усилиями.

10.1 Расширенная программа инвентаризации

Вы уже были свидетелями того, насколько просто создать пример программы инвентаризации ранее в этом руководстве:

REBOL [title: "Inventory"]
view layout [
    text "SKU:"
    f1: field
    text "Cost:"
    f2: field "1.00"
    text "Quantity:"
    f3: field
    across
    btn "Save" [
        write/append %inventory.txt rejoin [
            mold f1/text " " mold f2/text " " mold f3/text newline
        ]
        alert "Saved"
    ]
    btn "View Data" [editor %inventory.txt]
]

Это приложение создаёт файл данных, который можно легко открыть как электронную таблицу в Excel, просмотреть как текст в Word, импортировать в базу данных Access и т.д. фактически, эта программа и другие программы в этом тексте позволяют пользователям выполнять многие из те же цели, что и типичное офисное программное обеспечение. Вы можете получать столбцы данных, введённых пользователями, отображать таблицы в визуальной сетке, сортировать и выполнять вычисления по столбцам/строкам, создавать диаграммы из значений (и при необходимости вы можете создавать другие сложные функции) и т.д. разработка даже самых простых. Однако пользовательские приложения с графическим интерфейсом позволяют значительно улучшить контроль над управлением данными.

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

REBOL [title: "Real World Inventory"]

; Скрипт позволяет редактировать данные вручную с помощью функции 
; "editor" (редактор). Сюда добавлен улучшенный редактор из 
; предыдущего раздела этого руководства, позволяющий 
; отменить/повторить действие при возникновении ошибок ввода:

if not exists? %e [
    attempt [
        write %e read http://re-bol.com/e
        do e%
    ]
]

; Это код, убирающий слово "REBOL" из строки заголовка 
; графического интерфейса в Windows. Это делает программу более 
; профессиональной. Вы можете скопировать и использовать эти пять 
; строк в любой программе:

tt: "Real World Inventory"
user32.dll: load/library %user32.dll
gf: make routine![return:[int]]user32.dll"GetFocus"
sc: make routine![hw[int]a[string!]return:[int]]user32.dll"SetWindowTextA"
so: :show show: func[face][so[face]hw: gf sc hw tt]

; Мы можем выбрать автоматическое создание и сохранение с любым 
; желаемым именем файла в любом месте:

make-dir %/c/merchants-inventory/
datafile: %/c/merchants-inventory/merchants_mv_inventory.csv
write/append datafile ""
prcnt: 1.0
scanmode: false
roundmode: false

; Эта процедура резервного копирования используется для 
; автоматического сохранения инкрементных версий файла 
; инвентаризации, чтобы никакие изменения не были потеряны:

backup-data3: func [msg] [
    write (
        to-file bak-file: replace form datafile "_mv_inventory" rejoin [
            "_mv_inventory-backup--"
            now/date "_"
            replace/all form now/time ":" "-"
        ]
    ) read datafile
    if msg = "msg" [alert (join "Data has been backed up to " bak-file)]
]

; Эти процедуры проверяют, что соответствующие значения введены в 
; поля стоимости и количества:

check-errors-f2: does [
    if (
        (f2/text = "0.0") or (f2/text = "") or
        (error? try [to-decimal f2/text])
    ) [
        alert {
            *** ERROR: Enter a decimal in cost field 
            (set mode to 'key' if using keyboard).
        }
        focus f2 show f2
        return 0
    ]
    return 1
]
check-errors-f3: does [
    if ((f3/text = "") or (error? try [to-integer f3/text])) [
        alert "*** ERROR: Enter an integer in quantity field." 
        focus f3 show f3
        return 0
    ]
    return 1
]

; Эта процедура сохраняет данные и автоматически выполняет резервное 
; копирование. Попутно общее значение по умолчанию сбрасывается в 
; поле стоимости, и пользователю сообщается, что процесс прошёл 
; успешно:

enter-item: does [
    if check-errors-f2 = 0 [return]
    if check-errors-f3 = 0 [return]
    backup-data3 ""
    write/append datafile rejoin [
        mold f1/text " | "  mold f2/text " | "  
        mold f3/text " | "  mold f4/text " | " 
        mold {} " | " mold {} " | " mold {} newline
    ]
    request/timeout/ok "Done" 00:00:01
    set-face f1 ""
    set-face f2 ".99"
    set-face f3 ""
    set-face f4 ""    
    focus f1
]

; Эта процедура позволяет пользователю при желании вручную 
; редактировать файл данных. На всякий случай сначала создаётся 
; резервная копия. Попутно рассчитывается и отображается общая сумма 
; инвентаризации, а файл данных проверяется на целостность:
view-data: does [
    backup-data3 ""
    total: 0
    inv: read/lines datafile
    if error? try [
        foreach line inv [
            ln: parse line "|"
            line-total: (to-decimal ln/3) * (to-decimal ln/2)
            total: total + line-total
        ]
        alert join "Total Inventory: $" total 
    ][
        alert {
            *** ERROR: Improperly formed data in database.  
            Check each line.
        }
    ]
    editor datafile
]

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

calc-percent: does [
    if error? try [
        if scanmode [f2/text: calculate-barcode-price f2/text show f2]
        f2/text: form (prcnt * (to-decimal f2/text))
        if roundmode [f2/text: form (round/to (to-decimal f2/text) .01)]
        show f2
    ] [alert "*** ERROR: Enter a decimal in cost field."]
]
set-percent: does [
    if error? try [
        prcnt: to-decimal request-text/title/default
            "Default Percent:" form prcnt
    ] [alert "*** ERROR: Enter a decimal."]
]

; Программа позволяет вводить цены вручную или извлекать их из 
; специального формата штрих-кода, который можно ввести с помощью 
; USB-сканера. Это позволяет пользователю переключать режимы ввода:

scan-mode: does [
    scanmode: not scanmode
    either scanmode [
        b2/text: "Mode: Scan"
    ] [
        b2/text: "Mode: Key"
    ]
    show b2
]

; Эта процедура позволяет пользователю переключаться между 
; автоматическим округлением вычисленных оптовых значений выше или 
; использованием точных дробных значений:

round-mode: does [
    roundmode: not roundmode
    either roundmode [
        b1/text: "Round: On"
    ] [
        b1/text: "Round: Off"
    ]
    show b1
]

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

calculate-barcode-price: does [
    scanned-price: to-integer (trim/all copy (at copy f2/text 8))
    final-price: rejoin [
        to-string to-integer (
             (scanned-price - (scanned-price // 100)) / 100
        )
        "."
        either (scanned-price // 100) < 10 [
            rejoin ["0" scanned-price // 100]
        ] [
            scanned-price // 100
        ]
    ]
    final-price
]

; Вот окно графического интерфейса программы:

view/options center-face layout [
    size 240x400

    ; Пользователи могут выбирать из специализированных категорий 
    ; товаров, имеющихся в этом бизнесе, с помощью быстрого выбора из 
    ; списка:
text "Item:" [
    picked-item: request-list "Items" [
        "1 - Food" "2 - CVS" "3 - Bread" "4 - Electronics" "5 - Coke"
        "6 - Drinks" "7 - " "8 - " "9 - " "10 - "
    ]
    f1/text: copy at picked-item (
        (index? find/only picked-item " - ") + 3
    )
    show f1
]

; Все специальные процедуры, созданные ранее, выполняются путём 
; ввода данных в поля, нажатия кнопок и иного взаимодействия с 
; простыми виджетами:

f1: field "Food"
text "Cost"
f2: field ".99" [calc-percent check-errors-f2]
across 
btn "%" #"^x" [set-percent]
b1: btn "Round: Off" [round-mode]
b2: btn "Mode: Key" [scan-mode]  
below
text "Quantity:"
f3: field [check-errors-f3]
text "Description:"
f4: field
text 100 ""
across
btn 95 "Enter [CTRL+Z]" #"^Z" [enter-item]
btn 95 "View Data" [view-data]
do [focus f1] 
] [resize]

Эта программа обеспечивает специализированный рабочий процесс, который является максимально быстрым, эффективным и защищённым от ошибок, и он удовлетворяет точным требованиям, необходимым для ввода данных в уникальной среде, для которой они были созданы. Специальные модификации - это просто дополнения к общей программе инвентаризации, представленной в начале руководства.

10.2 Чековый принтер

В одном из предприятий этого автора квитанции об арендной плате первоначально предоставлялись с использованием традиционной бумажной книги квитанций. Это был процесс, подверженный ошибкам, бумажные квитанции могли быть повреждены и потеряны, а поиск информации по квитанциям позже был слишком трудоёмким. Первоначально было разработано очень быстрое решение путём копирования всех полей бумажных квитанций в форму с графическим интерфейсом пользователя (аналогично примеру "Generic Text Field Saver"). Эта простая программа превратилась в следующий сценарий, который проверяет правильность ввода данных, запрашивает подтверждение перед сохранением, автоматически сохраняет резервные копии контрольного журнала, обеспечивает простой ввод с использованием таких функций графического интерфейса, как раскрывающиеся списки выбора и автоматический ввод даты и времени, а также красиво отображает форматированный текст квитанции для печати:

REBOL [title: "Cash Receipt"]
make-dir %/M/merchant/documents/
make-dir %/M/merchant/documents/cash_receipts/
make-dir %/M/merchant/documents/cash_receipts/history/
write/append %/M/merchant/documents/cash_receipts/cash_receipts.txt ""
write/append %/M/merchant/documents/cash_receipts.txt ""
view center-face layout [
    across
    style field field 400
    text 50 right "Name: "  name: field return
    text 50 right "Booth: "  booth: field return
    text 50 right "Amount: "  amount: field "$" return
    text 50 right "" paytype: drop-down "Cash" "Check" "Credit" "Other"
        text right 15 "#:" num: field 270 return
    text 50 right "Signed: "  signed: field return
    text 50 right "Date"  date: field 400 (form now) return
    text 50 right "Note: "  note: area "Rent for ..." return
    indent 405 btn 50 "SAVE" [
        if error? try [
            to-integer booth/text to-money amount/text to-date date/text
        ][
            alert {
                ERROR: booth must be a number, amount must be valid money
                amount, date must be a date/time in the default format
                (1-jan-2011/12:00:00-4:00).  Make sure there are no
                additional spaces in the data.  ENTER ANY OTHER
                INFORMATION IN THE "NOTE" FIELD.
            }
            return
        ]
        unless true = request "Confirm Save" [return]
        backup-receipt: rejoin [
            %/M/merchant/documents/cash_receipts/history/  
            now/date "_" 
            replace/all copy form now/time ":" "-"
            ".txt"
        ]
        write 
            backup-receipt 
            read %/M/merchant/documents/cash_receipts/cash_receipts.txt
        write/append 
                %/M/merchant/documents/cash_receipts/cash_receipts.txt 
                receipt-data: reduce [
            newline mold name/text "  "
            mold booth/text "  "
            mold amount/text "  "
            mold paytype/text "  "
            mold num/text "  "
            mold signed/text "  "
            mold date/text "  "
            mold note/text "  "
        ]
        write/append %/M/merchant/documents/cash_receipts.txt receipt-data
        cur-receipt: rejoin [
            %/M/merchant/documents/cash_receipts/
            name/text "_"
            now/date "_" 
            replace/all copy form now/time ":" "-"
            ".txt"
        ]
        write/append cur-receipt reduce [
            newline newline newline
            "             M E R C H A N T S '    R E C E I P T" 
            newline
            "             ____________________________________"
            newline newline newline newline
            "      Name:  " name/text "  " newline newline
            "     Booth:  " booth/text "  " newline newline
            "    Amount:  " amount/text "  " newline newline
            "  Pay Type:  " paytype/text "  " newline newline
            "    Number:  " num/text "  " newline newline
            " Signed by:  " signed/text "  " newline newline
            "      Date:  " date/text "  " newline newline
            "      Note:  " note/text newline newline newline newline
            newline newline newline newline
            "             X _____________________________________________"
            newline newline newline newline
            "             X _____________________________________________"
        ]
        alert "This receipt is not valid until signed by both parties!"
        call/show rejoin ["notepad " to-local-file cur-receipt]
    ]
]

Используя данные, сохранённые вышеприведённой программой, приведённый ниже скрипт можно использовать для создания отчёта, в котором рассчитывается общая арендная плата, собранная между выбранными датами:

REBOL [title: "Total "] 
start-date: request-date
end-date: request-date
receipts: load %/m/merchant/documents/cash_receipts/cash_receipts.txt
total: $0
foreach [name booth amount type number signed date notes] receipts [
    date: to-date first parse date "/"
    if ((date >= start-date) and (date <= end-date)) [
        ; print name print date
        total: total + to-money amount
    ]
]
alert form total

Вот программа, которая отображает всю историю арендной платы для каждого клиента с заметным указанием тех, кто в настоящее время задолжал арендную плату:

REBOL [title: "Rent History and Currently Due Report"]
due-dates: copy ""
total-money: 0
grand-total-monthly: 0
booths: load %booths.txt
foreach [a b c d e f g] booths [if error? try [
    append due-dates rejoin ["Booth " a ", " b newline]
    due-date: copy ""
    parse g [
        thru "[" copy due-date to "]"
    ]
    due-date: parse due-date " "
    if not empty? due-date [
        if error? try [
            either now/date >= (current-date: to-date first due-date) [
                append due-dates rejoin ["    DUE:  " (form current-date)]
                money-is-due: true
            ] [
                append due-dates rejoin ["    " (form current-date)]
                money-is-due: false
            ]
        ] [
            append due-dates "    (Invalid Date)"
        ]
        if error? try [
            current-money: to-money second due-date
            grand-total-monthly: grand-total-monthly + current-money
            if money-is-due = true [
                total-money: total-money + current-money
            ]
            append due-dates rejoin [
                newline "    " (form current-money) newline
            ]
        ] [
            append due-dates rejoin [
                newline "    (Invalid amount)" newline
            ]
        ]
    ]
    append due-dates "-------------------------------------------------^/"
] [alert rejoin ["ERROR: booth " a " " b " " c " " d " " e " " f " " g]] ]
append due-dates rejoin [
    newline newline "Total Due: " total-money
    newline newline "Total Monthly Rent: " grand-total-monthly
]
make-dir %./rent_reports/
change-dir %./rent_reports/
write report-filename: to-file rejoin [
    "rent_" now/date "_" (replace/all to-string now/time ":" "-") ".txt"
] due-dates
call/show rejoin ["notepad " to-local-file what-dir "\" report-filename]

Опять же, то, что началось с крошечной формы графического интерфейса для сохранения нескольких текстовых полей, превратилось в полнофункциональный набор приложений. Для создания всего вышеприведённого кода требуется только базовое понимание сохранения файлов, объединения и разделения текста, использования циклов foreach, тестирования на наличие ошибок и т.д. сценарии отчётов могут быть интегрированы обратно в главное окно графического интерфейса программы с помощью такого простого кода (внутри блока макета представления):

btn "Rent Due Report" [launch %rentdue.r]
btn "Total Rent Report" [launch %rentcollected.r]

10.3 Расширенные часы и автоматические отчёты о заработной плате

В том же бизнесе, что и выше, сначала использовалась специализированная машина для измерения времени с проверкой отпечатков пальцев для управления платёжными ведомостями сотрудников и отчётами о работе. Программное обеспечение, поставляемое с устройством для снятия отпечатков пальцев, было очень сложным для менеджеров, а создаваемые им отчёты были неудобными и неадекватными для нужд бизнеса. Для решения проблемы была создана следующая программа. Он просто добавляет функции к простому приложению Time Clock, показанному ранее в руководстве. Эта версия использует сценарий часов NIST для установки внутренних часов компьютера на точное правильное время (требуется подключение к Интернету). Чтобы сотрудники никогда не входили в систему от имени других, программа фотографирует человека, выполняющего каждый вход (процедура создания кода для захвата фотографий описана далее в этом тексте). Для дальнейшего повышения безопасности полная история аудита каждой отдельной записи, сделанной для входа, сохраняется на жёсткий диск, и каждый файл загружается на веб-сервер компании. Эта простая добавленная функция гарантирует, что время входа не может быть изменено кем-либо, у кого нет утверждённого доступа:

REBOL [title: "Time Clock"]
insert-event-func [
    either event/type = 'close [
        really: request {
            To restart this program you must also 
            restart the computer.  Really close?
        }
        if really = true [quit]
    ] [event]
]

; Nistclock.r Ладислава Мечира, чтобы убедиться, что часы компьютера 
; установлены правильно::

do decompress #{
789C6D544B6FDB300CBEFB57B005861D0ADB49BA6081D7B5971D765977688162
087C506D3AD1224B811E09BC61FF7D941C278A13388E2592FAF8FA44CB5B4C0D
EA1D6A53C032819A759664459EFB4FCA32C98DCD566A77A17ABFAAF2B2692ABB
CC199270B9CA945E5D18644C8974C732D3B52D5ACD2BD566F42665D2C7924A55
C024F1B66938078D93958FEFEF33921E9C41B06B6E82DC7225A1551A41351625
299804252B042EA1514E83C14AC9DADCFC4B2017AA62028ED0D0FEAE61DD9AA4
F4F04E5A2EFC02A2408E4BB88329A97813891E41A05CD9F513D8A896B08CCE4F
4B3A249505D45A6932D41D2CA3DC34B21AB6BCDA9C431C11E838FDB64C1BCC99
88635F9A0DDF42A5B65D48630E613FA30A6807B7F4041565078BA02298C8EFF4
73FEAC76F974315F50665651B92CAE50DF78B0C832EFCDC9C02F6E42B54E5A6A
5AA5B4C6D086D443E81D13D4BE62514C62558F22D51E52B872A2EFF649E111E8
3913637D4687432F351A276CE85FBFECBDDCC10831F19DBBE2191EBE42CD9B06
357AD2F418308A3C906280BFCCEA580D187B2DAE605378A1A7FD36D0DEA6A633
16DB8092EEB98C384F5AE8B5A1039ED66F5CD66A6FBE1084755AC2AB76415E31
E3AF01185755680C117E59315BAD4B7F072D1EF93F7238B05FD019A092387C82
8F1BD412C5FD0C96C3AA00A1589D0BFEAE1991F8C320CF6A21C8C308B480966D
2867E52C97C49B70AD82FA405EAB5D65495E96A1B63E111A42D416CF7838BABF
7D41FB12CEBDD2B9DB3E9322FC1395FC27FFA32426A765E04E021378781C07D5
C774744D8EF7BF90698A66ADB4770CFB1F4ADA752CF8C6BA9FCD1BE266248CB7
DFFD9C8941B8741663C94B1842E73642F0C36C3AC94B2A45EDAA9E7121A58E02
1CD6AD0F6ED8EC29261AADC3365A867BBBA698CE046D088A44F1653FA9FB5048
3DF1ECBC60254DC23D272E1D9A98FB29E5472FA3AC26997F66F3B97F7DE4BDE0
3E9BC2B2B8C2EE72B0F894CDAE58086F4183C5D12D9236BE64E7936064D128DD
422C0A792561609FB3E00AD661268F2650F91F6BB4707323070000
} 
make-dir img-dir: %./clock_photos/
unless exists? %employees [
    write %employees {"Nick Antonaccio" "(Add New...)"}
]
cur-employee: copy ""
avicap32.dll: load/library %avicap32.dll
user32.dll: load/library %user32.dll
find-window-by-class: make routine! [
    ClassName [string!] WindowName [integer!] return: [integer!]
] user32.dll "FindWindowA"
sendmessage: make routine! [
    hWnd [integer!] val1 [integer!] val2 [integer!] val3 [integer!]
    return: [integer!]
] user32.dll "SendMessageA"
sendmessage-file: make routine! [
    hWnd [integer!] val1 [integer!] val2 [integer!] val3 [string!]
    return: [integer!]
] user32.dll  "SendMessageA"
cap: make routine! [
    cap [string!] child-val1 [integer!] val2 [integer!] 
    val3 [integer!] width [integer!] height [integer!]
    handle [integer!] val4 [integer!] return: [integer!]
] avicap32.dll "capCreateCaptureWindowA"
log-it: func [inout] [
    if ((cur-employee = "") or (cur-employee = "(Add New...)")) [
        alert "You must select your name." return
    ]
    if set-system-time nist-corrected-time [nist-correction: 0:0]
    cur-time: now
    record: rejoin [
        newline {[} mold cur-employee 
        { "} mold cur-time {" "} inout { "]}
    ]
    either true = request/confirm rejoin [
        record " -- IS YOUR NAME AND THE TIME CORRECT?"
    ] [
        make-dir %./edit_history/
        write/append %time_sheet.txt ""
        write rejoin [
            %./edit_history/time_sheet--
             "_" now/date "_"
             replace/all form now/time ":" "-"
        ] read %time_sheet.txt
        write/append %time_sheet.txt record
        if error? try [
            write ftp://user:pass@site.com/public_html/time_sheet.txt
                read %time_sheet.txt
        ] [alert "Error uploading to web site (saved locally)."]
        alert rejoin [
            uppercase copy cur-employee ", YOU ARE " inout "."
        ]
    ] [
        alert "CANCELED"
        return
    ]
    time-filename: copy replace/all copy to-string cur-time "/" "_"
    time-filename: copy replace/all copy time-filename ":" "+"
    img-file: rejoin [
        img-dir 
        (replace/all copy cur-employee " " "_")
        "_"
        time-filename "_" 
        next find inout " "
        ".bmp"
    ]
    sendmessage cap-result 1085 0 0
    sendmessage-file cap-result 1049 0 img-file
    ; call to-rebol-file img-file
]
timeclock-report: does [
    timeclock-start-date: request-date
    timeclock-end-date: request-date
    totals: copy ""
    names: load %employees
    log: load %time_sheet.txt
    foreach name names [
        if name <> "(Add New...)" [
            times: copy reduce [name]
            foreach record log [
                if name = log-name: record/1 [
                    flag: none
                    date-time: parse record/2 "/"
                    log-date: to-date date-time/1
                    log-time: to-time first parse date-time/2 "-"
                    if (
                        (log-date >= timeclock-start-date) and 
                        (log-date <= timeclock-end-date)
                    ) [
                        previous-flag: flag
                        either record/3 = "CLOCKED IN " [
                            flag: true
                        ] [
                            flag: false
                        ]
                        either flag <> previous-flag [
                            append times log-date
                            append times log-time
                        ] [
                            alert rejoin [
                                "Duplicate successive IN/OUT entry: "
                                name ", " record/2
                            ] 
                        ]
                    ]
                ]
            ]
            append totals rejoin [name ":" newline]
            total-hours: 0
            foreach [in-date in-time out-date out-time] (at times 2) [
                append totals rejoin [
                    newline 
                    "    in: " in-date ", " in-time 
                    "  out: " out-date ", " out-time "    "
                ]
                if error? try [
                    total-hours: total-hours + (out-time - in-time)
                ] [
                    alert rejoin [
                        "Missing login or Missing logout: " name
                    ]
                ]
            ]
            append totals rejoin [
                newline newline
                "    TOTAL HOURS: " total-hours
                newline newline newline
            ]
        ]
    ]
    write filename: copy rejoin [
        %timeclock_report-- timeclock-start-date 
        "_to_" timeclock-end-date ".txt"
    ] totals
    call/show rejoin ["notepad " to-local-file filename]
]
view/new center-face layout/tight [
    image 320x240
    tl1: text-list 320x200 data sort load %employees [
        cur-employee: value
        if cur-employee = "(Add New...)" [
            write/append %employees mold trim request-text/title "Name:"
            tl1/data: sort load %employees show tl1
        ]
    ]
    key #"^~" [
        del-emp: copy to-string tl1/picked
        temp-emp: sort load %employees
        if true = request/confirm rejoin ["REMOVE " del-emp "?"] [
            new-list: head remove/part find temp-emp del-emp 1
            save %employees new-list
            tl1/data: sort load %employees show tl1
            alert rejoin [del-emp " removed."]
        ]
    ]
    across
    btn "Clock IN" [log-it "CLOCKED IN"]
    btn "Clock OUT" [log-it "CLOCKED OUT"]
    btn "Report" [timeclock-report]
    btn "EXIT" [
        sendmessage cap-result 1205 0 0
        sendmessage cap-result 1035 0 0
        free user32.dll   quit
    ]
]
hwnd: find-window-by-class "REBOLWind" 0
cap-result: cap "cap" 1342177280 0 0 320 240 hwnd 0
sendmessage cap-result 1034 0 0
sendmessage cap-result 1077 1 0
sendmessage cap-result 1075 1 0
sendmessage cap-result 1074 1 0
sendmessage cap-result 1076 1 0
do-events

Отчёты, напечатанные вышеуказанной программой, производятся в легко читаемом согласованном формате. Paychex.com используется для расчёта заработной платы для бизнеса, который использует приведённый выше скрипт. Несмотря на то, что формат отчёта чистый, ввод данных о заработной плате в веб-интерфейс Paychex.com требовал довольно много времени каждую неделю. Чтобы ускорить этот процесс, был создан следующий дополнительный скрипт. Эта программа обеспечивает чрезвычайно быстрое копирование и вставку непосредственно в пользовательский веб-интерфейс Paychex. Текстовые отчёты, созданные с помощью приведённого выше сценария, могут быть отправлены по электронной почте, скопированы в буфер обмена любой удалённой машины, на которой работает менеджер по заработной плате, а обработанный вывод может быть мгновенно введён на веб-сайт Paychex. Ввод платёжной ведомости для 30+ сотрудников занимает меньше минуты, используя этот скрипт:

REBOL [title: "Enter Time Clock Report into Paychex"]
dec-time: func [tm] [
    qq: (to-decimal second q: parse form round/to tm 00:00:60 ":") / 60 
    write clipboard:// form round/to ((to-decimal q/1) + qq) .01
]
data1: copy read clipboard://
data2: copy parse/all data1 "^/"
foreach line data2 [
     if line = (trim copy line) [print line]
     if find line "TOTAL HOURS:" [
         hours: to-time (last parse line " ")
         if hours <> none [
             dec-time hours
             ask "Paste Into Paychex, Then Press [ENTER]..."
         ]
     ]
]
halt

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

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

REBOL [title: "Payroll Audit History Report"]
start: ["John Smith" "18-Mar-2012/8:30:53-4:00" "CLOCKED IN "]
current: find/only (load %./time_sheet.txt) start
erased: copy []
collected: copy []
foreach file read %./edit_history/ [
    if error? try [
        current-period: find/only (load join %./edit_history/ file) start
        if current-period <> none [
           probe length? current-period
           difs: difference current current-period
           append collected difs
        ]
    ] [print file]
]
probe length? current
probe length? final: unique collected
ask "Done..."
editor difference current final
halt

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

11. Больше основ языка REBOL

Этот раздел охватывает множество тем, которые формируют более полное понимание основного языка REBOL.

11.1 Комментарии

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

REBOL []
; это комментарий
{
     Это многострочный комментарий.
     Комментарии ничего не делают в программе.
     Они просто напоминают программисту, что происходит в коде.
}
[[
     Это также многострочный комментарий.
     alert "Смотри ... Ничего".
]]
comment [
     С помощью функции "comment" можно пояснить, что следующий
     блок ничего не делает, но в этом нет необходимости.
]

11.2 Уточнения функций

У многих функций есть "доработки" или "уточнения" (параметры, разделённые "/"):

request-text/default "Text"
request-text/title  "The /title refinement sets this header text"
request-text/title/default  "Name:"  "John Smith"     ; 2 options together
request-text/title/offset "/offset repositions the requester" 10x100
request-pass/offset/title 10x100 "title" alert "Processing"  ; 2 functions
request-file/file %temp.txt          ; default file name
request-file/filter ["*.txt" "*.r"]  ; only show .txt and .r files
request-file/only  ; limit selection to a single file
request-file/save  ; save dialog (instead of open dialog)
request-file/save/file/filter %temp.txt ["*.txt" "*.r"]

Ввод "help (функция)" в консоли интерпретатора REBOL отображает все доступные уточнения для любой функции.

11.3 Пробелы и отступы

В отличие от других языков, REBOL не требует каких-либо окончаний строки между выражениями (функции, параметры и т.д.), и вы можете вставлять пустые пробелы (табуляции, пробелы, новые строки и т.д.) по желанию в код. Обратите внимание на использование отступов и комментариев в приведённом ниже коде. Также обратите внимание, что содержимое скобок распределено по нескольким строкам:

alert rejoin [
    "You chose: "               ; 1-я часть объединённых данных
    (request "Choose one:")     ; 2-я часть объединённых данных
]

Приведённый выше код работает точно так же, как:

alert rejoin ["You chose: " request "Choose one:"]

ОДНО УСЛОВИЕ: параметры для большинства функций должны начинаться в той же строке, что и слово функции. Следующий пример не будет работать должным образом, потому что открывающие скобки аргументов повторного объединения должны находиться в той же строке, что и функция повторного объединения:

alert rejoin                    ; Это НЕ работает.
[                               ; Поместите эту скобку на строку выше.
    "You chose: "
    (request "Choose one:")
]

Блоки часто содержат другие блоки. Такие составные блоки обычно имеют отступ с последовательными позициями табуляции. Начальные и конечные скобки обычно размещаются на одном уровне отступа. Это обычное явление для большинства языков программирования, поскольку упрощает чтение сложного кода за счёт визуальной группировки. Например, составной блок ниже:

big-block: [[may june july] [[1 2 3] [[yes no] [monday tuesday friday]]]]

можно записать следующим образом, чтобы более чётко показать начало и конец блоков:

big-block: [
    [may june july] 
    [ 
        [1 2 3] 
        [
            [yes no]
            [monday tuesday friday]
        ]
    ]
]

probe first big-block
probe second big-block
probe first second big-block
probe second second big-block
probe first second second big-block
probe second second second big-block

Отступы не требуются, но они действительно полезны.

11.4 Многострочные строки, кавычки и конкатенация

Строки текста могут быть заключены в кавычки или фигурные скобки:

print "Это строка текста."
print {
    Фигурные скобки используются для
    многострочные текстовые строки
    (вместо кавычек).
}

alert {Чтобы использовать "кавычки" в строке, поместите их в фигурные скобки.}
alert "Вы можете использовать {фигурные скобки} внутри кавычек."
alert "'Одиночные кавычки' могут заключаться в двойные кавычки ..."
alert {'... или внутри фигурных скобок'}
alert {"ЛЮБОЙ символ кавычки" {на самом деле может использоваться в} фигурных скобках}
alert "Во многих случаях"  alert {фигурные скобки и кавычки взаимозаменяемы.}

Вы можете напечатать возврат каретки, используя слово "newline" или символы "^/"

print rejoin ["This text if followed by a carriage return." newline]
print "This text if followed by a carriage return.^/"

Очистите экран консоли с помощью "newpage":

prin newpage

Функция "rejoin" СОЕДИНЯЕТ (объединяет) значения:

rejoin ["Hello " "World"]
rejoin [{Concatenate } {as } {many items } {as } {you } {want.}]
rejoin [request-date {  } request-color {  } now/time {  } $29.99]
alert rejoin ["You chose: " request "Choose one:"] ; CASCADE return values
join {"Join" only concatenates TWO items } {("rejoin" is more powerful).}
print rejoin ["This text is followed by a carriage return." newline]
print "This text is also followed by a carriage return.^/"
prin {'Prin' } prin {doesn't } prin {print } print {a carriage return.}

11.5 Подробнее о переменных

Символ "двоеточие" (:) присваивает значение словарной метке ("переменная").

x: 10
print x
x: x + 1    ; увеличить переменную на 1 (добавить 1 к текущему    
            ; значению x)
print x
y: "hello"  z: " world"

Вы можете использовать функцию "PROBE", чтобы показать RAW DATA, присвоенные переменной (PRINT форматирует хороший вывод). "Probe" полезен для отладки кода проблемы:

y: "hello"  z: " world"
print rejoin [y z]
probe rejoin [y z]   
print join y z

Функция "prin" печатает значения рядом друг с другом (без новой строки):

y: "hello"  z: " world"
prin y 
prin z

Переменные (словесные метки) ** НЕ ** ЧУВСТВИТЕЛЬНЫ К регистру:

person: "john"
print person   print PERSON   print PeRsOn

Вы можете каскадно присваивать значения переменных. Здесь все 3 переменные установлены в значение "yes":

value1: value2: value3: "yes "
print rejoin [value1 value2 value3]

Функция "ask" (спросить) получает текст от пользователя. Вы можете назначить этот ввод (возвращаемое значение функции запроса) непосредственно метке переменной:

name: ask "Enter your name:  "  
print rejoin ["Hello " name]

Вы можете сделать то же самое со значениями, возвращаемыми функциями запросчика:

filename: request-file/only
alert rejoin ["You chose " filename]
osfile: to-local-file filename  ; REBOL использует собственный 
                                ; многоплатформенный синтаксис.
to-rebol-file osfile  ; Преобразование из нотации файлов ОС обратно в REBOL
the-url: http://website.com/subfolder
split-path the-url  ; "split-path" разбивает любой файл или URL на 
                    ; 2 части

11.6 Типы данных

REBOL автоматически знает, как выполнять соответствующие вычисления времени, дат, IP-адресов, значений координат и других распространённых типов данных:

print 3:30am + 00:07:19             ; правильно увеличивает значения времени
print now                           ; распечатать текущую дату и время
print now + 0:0:30                  ; распечатать сколько времени 
                                    ; будет через 30 секунд
print now - 10                      ; печать время 10 дней назад
$29.99 * 5                          ; вычислять денежные значения
$29.99 / 7                          ; попробуйте это с десятичным значением
29.99 / 7
print 23x54 + 19x31                 ; легко добавлять пары координат
22x66 * 2                           ; и выполнить другие 
22x66 * 2x3                         ; операции с координатами
print 192.168.1.1 + 000.000.000.37  ; легко увеличивать IP-адреса
11.22.33.44 * 9                     ; обратите внимание, что 
                                    ; значение каждого IP-сегмента 
                                    ; ограничено 255
0.250.0 / 2                         ; цвета представлены в виде 
                                    ; значений кортежа с 3 сегментами
red / 2         ; так что это простой способ настроить значения цвета
view layout [image picture effect [flip]] ; применять эффекты к типам изображений
x: 12  y: 33  q: 18  p: 7
(as-pair x y) + (as-pair q p)   ; очень часто используется в 
                                ; графических приложениях, 
                                ; использующих координаты
remove form to-money 1 / 233
remove/part form to-time (1 / 233) 6

REBOL также изначально понимает, как использовать URL-адреса, адреса электронной почты, файлы/каталоги, денежные значения, кортежи, хэш-таблицы, звуки и другие общие значения ожидаемым образом, просто по способу форматирования данных. Вам не нужно объявлять, определять или иным образом подготавливать такие типы данных, как в других языках - просто используйте их.

Чтобы определить тип любого значения, используйте функцию "type?":

some-text:  "This is a string of text"  ; строки текста идут между
type? some-text                         ; "кавычками" или {фигурными
                                        ; скобками}

some-text:  {
    Это многострочная строка текста.
    Строки - это собственный тип данных, обозначенный
    кавычки или фигурные скобки, заключающие текст.
    REBOL имеет МНОГО других встроенных типов данных, все
    разграничены различными символами и текстовыми форматами.
    Функция "type" возвращает тип данных значения.
    Ниже приведены лишь несколько дополнительных типов данных: 
}                                          
type? some-text

an-integer: 3874904                 ; целочисленные значения просто 
                                    ; положены
type? an-integer                    ; целые/отрицательные целые числа

a-decimal: 7348.39                  ; распознаются десятичные числа
type? a-decimal                     ; по десятичной точке

web-site: http://musiclessonz.com   ; URL-адреса распознаются по
type? web-site                      ; http://, ftp://, и т.д.

email-address: user@website.com     ; значения электронной       
type? email-address                 ; почты находятся по формату
                                    ; user@somewebsite.domain

the-file: %/c/myfile.txt            ; файлам предшествует символ %
type? the-file                     

bill-amount: $343.56                ; денежным предшествует символ $
type? bill-amount                         

html-tag: <br>                      ; теги - это между символами <>
type? html-tag                            

binary-info:  #{ddeedd}             ; двоичные данные помещаются между
type? binary-info                   ; фигурных скобок и предшествующий
                                    ;символ решётки (фунта)l

image: load http://rebol.com/view/bay.jpg ; REBOL может даже 
type? image     ; автоматически распознавать тип данных наиболее  
                ; распространённых форматов изображений.

a-sound: load %/c/windows/media/tada.wav  ; и звуков тоже
a-sound/type

color: red
type? color

color-tuple: 123.54.212                    
type? color-tuple

a-character: #"z"                          
type? a-character

a-word: 'asdf                              
type? a-word

Типы данных могут быть специально "преобразованы" (созданы или назначены разным типам) с помощью функций "to-(тип)":

numbr: 4729                 ; Метка numbr представляет собой 
                            ; целое число 4729.
strng: to-string numbr      ; Метка 'strng теперь представляет собой 
                            ; фрагмент текста, состоящий из символов 
                            ; "4729". Попробуйте добавить strng + 
                            ; numbr, и вы получите сообщение об ошибке.

; В этом примере создаются и добавляются две пары координат. Пары 
; создаются из отдельных целочисленных значений с помощью функции 
; "to-pair":

x: 12  y: 33  q: 18  p: 7
pair1: to-pair rejoin [x "x" y]        ; 12x33
pair2: to-pair rejoin [q "x" p]        ; 18x7
print pair1 + pair2                    ; 12x33 + 18x7 = 30x40

; В этом примере создаётся значение времени и манипулируется его 
; значением с помощью функции "to-time":

hour: 3
minute: 45
second: 00
the-time: to-time rejoin [hour ":" minute ":" second]    ; 3:45am
later-time: the-time + 3:00:15
print rejoin ["3 hours and 15 seconds after 3:45 is " later-time]

; Это преобразует значения цвета (кортежи) REBOL в цвета HTML и наоборот:
to-binary request-color
to-tuple #{00CD00}

Попробуйте этот список примеров преобразования типов данных:

to-decimal 3874904        ; Теперь число содержит десятичную точку
to-string http://go.com   ; теперь URL-адрес веб-сайта заключён в 
                          ; кавычки
form web-site             ; "form" также преобразует различные 
                          ; значения в строку
form $29.99
alert form $29.99         ; функция alert ТРЕБУЕТ строковый параметр
alert $29.99              ; (это вызывает ошибку)
5 + 6                     ; вы можете выполнять математические 
                          ; операции с целыми числами
"5" + "6"                 ; (ошибка) вы не можете выполнять 
                          ; математические вычисления со строками
(to-integer "5") + (to-integer "6")     ; это устраняет 
                                        ; математическую проблему
to-pair [12 43]           ; создает пару координат
as-pair 12 43             ; лучший способ создать пару координат
to-binary 123.54.212      ; преобразовать значение цвета REBOL в 
                          ; шестнадцатеричное значение цвета
to-binary request-color   ; преобразовать цвет, выбранный 
                          ; пользователем, в шестнадцатеричный
to-tuple #{00CD00}        ; преобразовать шестнадцатеричное значение 
                          ; цвета в значение цвета REBOL
form to-tuple #{00CD00}   ; преобразовать шестнадцатеричное значение 
                          ; цвета в строку
write/binary %floorplan8.pdf debase read clipboard://  ; вложения 
                                                    ;электронной почты

REBOL имеет множество встроенных вспомогательных функций для работы с общими типами данных. Другой способ создать парные значения - использовать функцию "as-pair". Вы обычно будете видеть этот вид создания пар для построения графики в координатных точках на экране:

x: 12  y: 33  q: 18  p: 7
print (as-pair x y) + (as-pair q p)    ; намного проще!

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

11.7 Случайные значения

Вы можете создавать случайные значения любого типа:

random/seed now/time    ; всегда используйте эту строку, чтобы 
                        ; получить реальные случайные значения
random 50               ; случайное число от 0 до 50
random 50x100           ; Первое значение пары до 50, втрое до 100
random 222.222.222      ; каждый сегмент ограничен # от 0 до 222
random $500
random "asdfqwerty"     ; случайное сочетание заданных символов

11.8 Подробнее о чтении, написании, загрузке и сохранении в и из различных источников

Хотя в этом руководстве вы увидите примеры функций, которые читают и записывают данные в "локальные файлы", веб-сайты, системный "буфер обмена", электронную почту и другие источники данных. Если вы новичок в программировании, здесь будет полезно краткое объяснение этой темы и связанных с ней терминов.

Возможно, вам знакомо, что в MS Windows, когда вы щелкаете значок "Мой компьютер" (или "Компьютер") на рабочем столе, вы видите список жестких дисков, USB-накопителей, подключенных сетевых дисков и других устройств хранения, подключенных к компьютер:

Данные на постоянном жёстком диске вашего компьютера организованы в виде дерева папок (или "каталогов"), каждая из которых содержит отдельные файлы и дополнительные ветви вложенных папок. В Windows корневой каталог вашего основного жёсткого диска обычно называется "C:\". C - это буквенное имя, присвоенное диску, а "\" ("обратная косая черта") представляет корневую папку дерева каталогов. Интерпретатор REBOL по умолчанию устанавливается в папку C:\Program Files\rebol\view\

Когда вы выполняете какие-либо операции чтения, записи, сохранения, загрузки, редактирования или другие функции, которые считывают и записывают данные "на вашем компьютере", вы работаете с файлами на одном из "локальных" запоминающих устройств, подключённых к компьютеру (в отличие от в папку на веб-сервере, в учётную запись электронной почты и т.д.). При использовании функций чтения и записи расположение этих файлов по умолчанию при установке REBOL по умолчанию - "C:\Program Files\rebol\view\". На следующем изображении средства выбора файлов Windows показан список файлов, содержащихся в данный момент в этой папке на компьютере автора:

В REBOL символ процента ("%") используется для представления локальных файлов. Поскольку REBOL можно использовать во многих операционных системах и поскольку все эти операционные системы используют разный синтаксис для обозначения дисков, путей и т.д., REBOL использует универсальный формат: %/drive/path/path/.../file.ext . Например, "C:\ Program Files\rebol\view\" в Windows в коде REBOL будет представлен как "%/C/Program%20Files/rebol/view/". Обратите внимание, что Windows использует обратную косую черту (\), а REBOL использует косую черту (/) для обозначения папок. REBOL преобразует синтаксис "%" в соответствующий формат операционной системы, так что ваш код может быть написан один раз и использоваться в любой операционной системе без изменений.

Список файлов на изображении выше будет написан на REBOL, как показано ниже. Обратите внимание на косую черту в конце имен папок:

%data/
%desktop/ 
%local/ 
%public/ 
%temp-examples/ 
%console_email.r
%e 
%edit-prefs.r 
%r 
%read-email-header.r 
%rebol.exe 
%temp.txt 
%user.r 
%word-cats.r 
%word-defs.r 
%wordbrowser.r

Следующие 2 функции преобразуют формат файла REBOL в формат вашей операционной системы и наоборот. Это особенно полезно при работе с файлами, содержащими пробелы или другие символы:

print to-local-file %/C/Program%20Files/rebol/view/rebol.exe 
print to-rebol-file {C:\Program Files\rebol\view\rebol.exe}

Вы можете использовать функцию "change-dir" (или "cd") для смены папок:

change-dir %/c/    ; изменения в папке C:\ в Windows
cd %/c/            ; "cd" - это ярлык для "change-dir"

Чтобы переместить папку "вверх" из текущего каталога, используйте "%../". Например, следующий код перемещается из папки по умолчанию C:\Program Files\rebol\view\до C:\Program Files\rebol\, а затем в C: \ Program Files\:

cd %../
cd %../

Обратитесь к текущей папке с помощью "%./". Вы можете прочитать список файлов в текущей папке, например:

read %./
probe read %./
editor %./

Вы можете создать новую папку в текущей папке, используя функцию "make-dir". Вот некоторые дополнительные функции папки:

make-dir %./newfolder/
what-dir 
list-dir

Вы можете переименовывать, копировать и удалять файлы в папке, используя следующие функции:

rename %temp.txt %temp2.txt            ; изменяем имя файла
write %temp.txt read %temp2.txt        ; копирование файла
delete %temp2.txt

Функция "write" (запись) записывает данные в файл. Требуется два параметра - имя файла для записи и некоторые данные для записи:

write %name.txt "John Smith"

Функция "read" (чтения) считывает данные из файла:

read %name.txt

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

name: "John Smith"
write %name.txt name

Присвойте метки переменных данным, считываемым из файла:

loaded-name: read %name.txt
print loaded-name

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

editor %name.txt

Вы можете записывать данные на веб-сайт (или любой другой подключённый протокол), используя тот же синтаксис записи, который используется для записи в файл. Для записи в учётную запись "ftp" веб-сайта требуется имя пользователя и пароль:

write ftp://user:pass@website.com/folder/test.txt "Text written by REBOL"

Вы можете читать данные прямо с веб-сервера, учётной записи ftp, учётной записи электронной почты и т.д., используя тот же формат. Многие интернет-протоколы встроены прямо в интерпретатор REBOL. Они изначально понятны, и REBOL точно знает, как подключиться к ним без какой-либо подготовки со стороны программиста:

editor http://rebol.com                 ; Читает содержимое     
                                        ; документа по этому 
                                        ; URL-адресу.
editor pop://user:pass@website.com      ; Читает все электронные 
                                        ; письма в этом почтовом 
                                        ; ящике POP.
editor clipboard://                     ; Читает данные, которые 
                                        ; были скопированы/вставлены 
                                        ; в буфер обмена ОС.
print read dns://msn.com                ; Отображает информацию DNS 
                                        ; для этого адреса.
print read nntp://public.teranews.com   ; (Нажмите клавишу [ESC], 
                                        ; чтобы закрыть этот список 
                                        ; Usenet.)

ПРИМЕЧАНИЕ. Редактор читает И позволяет СОХРАНИТЬ РЕДАКТИРОВАНИЯ обратно на сервер:

editor ftp://user:pass@website.com/public_html/index.html

Передача данных между устройствами, подключёнными по любому поддерживаемому протоколу, проста - просто прочтите и напишите:

; считываем данные с веб-сайта и вставляем их в локальный буфер 
; обмена:

write clipboard:// (read http://rebol.com)   ; после этого 
        ;попробуйте вставить в свой любимый текстовый редактор
; прочитать страницу с одного веб-сайта и записать её на другой:

write ftp://user:pass@website2.com (read http://website1.com) 

; опять же, обратите внимание, что функция "write" принимает ДВА 
; параметра

Отправить электронное письмо так же просто, используя аналогичный синтаксис:

send user@website.com "Hello"
send user@website.com (read %file.txt)  ; отправляет электронное 
                                        ; письмо с file.txt в 
                                        ; качестве тела

Модификатор "/binary" используется для чтения или записи двоичных (нетекстовых) данных. Вы будете использовать "read/binary" (чтение/двоичный код) и "write/binary" (запись/двоичный код) для чтения и записи изображений, звуков, видео и других нетекстовых файлов:

write/binary %/c/bay.jpg read/binary http://rebol.com/view/bay.jpg

Для пояснения помните, что функция записи принимает два параметра. Первый параметр выше - "%/c/bay.jpg". Второй параметр - это двоичные данные, считанные с http://rebol.com/view/bay.jpg:

write/binary (%/c/bay.jpg) (read/binary http://rebol.com/view/bay.jpg)

Функции "load" (загрузка) и "save" (сохранение) также читают и записывают данные, но в процессе автоматически форматируют определённые типы данных для использования в REBOL. Попробуй это:

; назначьте слово "picture" (изображение) изображению, 
; "загруженному" (load) с заданного URL:
picture: load http://rebol.com/view/bay.jpg

; сохранить изображение в файл с заданным именем и автоматически 
; преобразовать его в формат .png;

save/png %/c/picture.png picture

; показать это в окне графического интерфейса:

view layout [image load %/c/picture.png]

"Load" (загрузить) и "save" (сохранить) используются для удобного управления определёнными типами данных в форматах, непосредственно используемых REBOL (блоки, изображения, звуки, библиотеки DLL, определённые собственные структуры данных и т.д. могут быть загружены и использованы немедленно). Чаще всего вы будете использовать "read" (чтение) и "write" (запись) для хранения и извлечения типичных типов данных, байт за байтом, на/с носителя данных, когда преобразование или форматирование не требуется.

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

11.9 Понимание возвращаемого значения и порядка оценки

В REBOL вы можете поместить сколько угодно функций в одну строку, и все они будут оцениваться строго слева направо. Функции группируются автоматически по их обязательным параметрам данных. Следующая строка содержит две функции предупреждений:

alert "First function" alert "Second function"

Rebol знает, что нужно искать один параметр после первой функции предупреждения, поэтому он использует следующий фрагмент данных в этой строке в качестве аргумента для этой функции. Затем интерпретатор встречает другую функцию предупреждения и использует следующий текст в качестве параметра данных.

Простые функции запроса не требуют каких-либо параметров, но, как и большинство функций, они ВОЗВРАЩАЮТ полезное значение. Попробуйте вставить эти функции прямо в консоль REBOL, чтобы увидеть их возвращаемые значения:

request-text
request-date 
request-color
request-file
request-dir
request-pass

В следующей строке первая функция с её уточнениями "request-pass/offset/title" (запрос-проход/смещение/заголовок) требует двух параметров, поэтому REBOL использует следующие два элемента в строке ("10x100" и "title") в качестве своих аргументов. После этого интерпретатор встречает другую функцию "alert" и использует следующий текст "Processing" в качестве аргумента:

request-pass/offset/title 10x100 "title" alert "Processing"

ВАЖНО: В REBOL возвращаемые значения (выходные данные) одной функции могут использоваться непосредственно как аргументы (входные данные) для других функций. Все просто оценивается слева направо. В строке ниже функция "alert" принимает следующий элемент в строке в качестве входного параметра, который в данном случае является не частью данных, а функцией, которая возвращает некоторые данные (объединённый текст, возвращаемый командой "rejoin" функция):

alert rejoin ["Hello " "there" "!"]

Другими словами, значение, возвращаемое функцией "rejoin" выше, передаётся (используется в качестве параметра) функции "alert". Скобки можно использовать для пояснения, какие выражения вычисляются и передаются в качестве параметров другим функциям. Строка в скобках ниже обрабатывается интерпретатором REBOL точно так же, как и строка выше - она просто позволяет вам более чётко видеть, какие данные функция "предупреждения" выводит на экран:

alert ( rejoin ["Hello " "there" "!"] )

Возможно, самая сложная часть начала работы с REBOL - это понять порядок, в котором оцениваются функции. Время от времени может показаться, что этот процесс срабатывает. В приведённом ниже примере функция "editor" (редактор) принимает следующий элемент в строке в качестве входного параметра и редактирует этот текст. Однако для того, чтобы функция редактора начала операцию редактирования, ей необходимо текстовое значение, возвращаемое функцией "request-text". Поэтому первое, что видит пользователь при запуске этой строки, - это запросчик текста. Это выглядит наоборот, по сравнению с тем, как это написано:

editor (request-text)

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

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

alert ( rejoin ( ["You chose: " ( request "Choose one:" ) ] ) )

Вышеупомянутая строка типична для синтаксиса обычного языка REBOL. Есть три функции: "alert", "rejoin" и "request". Для завершения первой функции оповещения ей необходимо возвращаемое значение от "rejoin", которое, в свою очередь, требует возвращаемого значения от функции "request". Поэтому первое, что видит пользователь, - это функция запроса. После того, как пользователь ответит на запрос, к выбранному ответу присоединится текст "You chose:", и присоединённый текст отображается как предупреждающее сообщение. Думайте об этом как о чтении "дисплея" (следующий текст, соединённый вместе ("вы выбрали" (ответ, выбранный пользователем))). Чтобы завершить строку, пользователь должен сначала ответить на вопрос.

Чтобы изучить REBOL, важно сначала запомнить и распознать многие встроенные функциональные слова REBOL, а также параметры, которые они принимают в качестве входных данных, и значения, которые они возвращают в качестве выходных данных. Когда вы привыкнете читать строки кода как функции, аргументы и возвращаемые значения слева направо, язык быстро приобретёт смысл.

Следует отметить, что в REBOL математические выражения вычисляются слева направо, как и все другие функции. Не существует "порядка старшинства", как в других языках (т.е. умножение не вычисляется автоматически перед сложением). Чтобы установить определённый порядок оценки, заключите функции в круглые скобки:

print   10  +  12  /  2   ; 22 / 2 = 11
print  (10  +  12) /  2   ; 22 / 2 = 11 (как без скобок)
print   10  + (12  /  2)  ; 10 + 6 = 16 (сперва деление, затем сложение)

Оценка REBOL слева направо проста и последовательна. Скобки можно использовать для пояснения последовательности кода, если возникнет путаница.

11.10 Подробнее об условных оценках

Вы уже видели условные операции "if" и "either". Математические операторы обычно используются для выполнения условных оценок: =, <, >, <> (равно, меньше, больше, не равно):

if now/time > 12:00 [alert "It's after noon."] 

either now/time > 8:00am [
    alert "It's time to get up!"
][
    alert "You can keep on sleeping."
]

11.10.1 Switch

Оценка "switch" (переключение) выбирает одну из множества функций для выполнения на основе нескольких оценок. Его синтаксис:

switch/default (main value) [

(value 1) [block to execute if value 1 = main value
(value 2) [block to execute if value 2 = main value]
(value 3) [block to execute if value 3 = main value]
; etc...

] [default block of code to execute if none of the values match]

Вы можете сравнить любое количество значений с основным значением и запустить блок кода для каждого совпадающего значения:

favorite-day:  request-text/title "What's your favorite day of the week?"

switch/default favorite-day [
    "Monday"    [alert "Monday is the worst!  The work week begins..."]
    "Tuesday"   [alert "Tuesdays and Thursdays are both ok, I guess..."]
    "Wednesday" [alert "The hump day - the week is halfway over!"]
    "Thursday"  [alert "Tuesdays and Thursdays are both ok, I guess..."]
    "Friday"    [alert "Yay!  TGIF!"]
    "Saturday"  [alert "Of course, the weekend!"]
    "Sunday"    [alert "Of course, the weekend!"]
] [alert "You didn't type in the name of a day!"]

11.10.2 Case

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

name: "john"
case [
    find name "a" [print {Your name contains the letter "a"}]
    find name "e" [print {Your name contains the letter "e"}]
    find name "i" [print {Your name contains the letter "i"}]
    find name "o" [print {Your name contains the letter "o"}]
    find name "u" [print {Your name contains the letter "u"}]
    true [print {Your name doesn't contain any vowels!}]
]

for i 1 100 1 [
    case [
        (0 = modulo i 3) and (0 = modulo i 5) [print "fizzbuzz"]
        0 = modulo i 3 [print "fizz"]
        0 = modulo i 5 [print "buzz"]
        true [print i]
    ]
]

По умолчанию оценка "case" автоматически завершается после того, как будет найдена истинная оценка (т.е. в приведённом выше примере имени, если имя содержит более одной гласной, будет напечатана только первая гласная). Чтобы проверить все возможные случаи перед завершением оценки, используйте уточнение "/all":

name: "brian" 
found: false
case/all [
    find name "a" [print {Your name contains the letter "a"} found: true]
    find name "e" [print {Your name contains the letter "e"} found: true]
    find name "i" [print {Your name contains the letter "i"} found: true]
    find name "o" [print {Your name contains the letter "o"} found: true]
    find name "u" [print {Your name contains the letter "u"} found: true]
    found = false [print {Your name doesn't contain any vowels!}]
]

11.10.3 Несколько условий: "and", "or", "all", "any"

Вы можете проверить, выполняется ли более одного условия, используя слова "and" (и), "or" (или), "all" (все) и "any" (любые):

; сначала установите некоторые начальные значения, чтобы все они 
; были истинными:
value1: value2: value3: true

; затем установите некоторые дополнительные значения как false:

value4: value5: value6: false

; Следующее выводит "оба условия истинны", потому что истинны и 
; первое условие, и второе условие:

either ( (value1 = true) and (value2 = true) ) [
    print "both true"
] [
    print "not both true"
]

; Следующее печатает "оба неверны", потому что второе условие ложно:

either ( (value1 = true) and (value4 = true) ) [
    print "both true"
] [
    print "not both true"
]

; Следующее печатает "либо одно ИЛИ другое истинно", потому что 
; первое условие истинно:

either ( (value1 = true) or (value4 = true) ) [
    print "either one OR the other is true"
] [
    print "neither is true"
]

; Следующее печатает "либо одно ИЛИ другое истинно", потому что 
; второе условие истинно:

either ( (value4 = true) or (value1 = true) ) [
    print "either one OR the other is true"
] [
    print "neither is true"
]

; Следующее печатает "либо одно ИЛИ другое истинно", потому что оба 
; условия истинны:

either ( (value1 = true) or (value4 = true) ) [
    print "either one OR the other is true"
] [
    print "neither is true"
]

; Следующие надписи "ни то, ни другое не соответствует действительности":

either ( (value4 = true) or (value5 = true) ) [
    print "either one OR the other is true"
] [
    print "neither is true"
]

Для сравнений, включающих больше элементов, вы можете использовать "any" (любой) и "all" (все):

; Обе следующие строки печатают "yes" (да), потому что ВСЕ сравнения 
; верны. "All" (Все) - это просто сокращение для множественных 
; оценок "and" (и).
if ((value1 = true) and (value2 = true) and (value3 = true)) [
    print "yes"
]

if all [value1 = true  value2 = true  value3 = true] [
     print "yes"
]

; Обе следующие строки печатают "yes" (да), потому что ЛЮБОЕ ОДНО из 
; сравнений верно. "Any" (Любой) - это просто сокращение для 
; множественных оценок "or" (или):

if ((value1 = true) or (value4 = true) or (value5 = true)) [
    print "yes"
]

if any [value1 = true  value4 = true  value5 = true] [
     print "yes"
]

11.11 Подробнее о циклах

11.11.1 Forever

Структуры "циклов" предоставляют программные способы для систематического повторения действий, управления потоком выполнения программы и автоматизации длительных операций по обработке данных. Вы уже видели структуру цикла "foreach". Функция "forever" (навсегда) создаёт простой повторяющийся цикл. Его синтаксис:

forever [блок действий для повторения]

В следующем коде используется бесконечный цикл для постоянной проверки времени. Он предупреждает пользователя, когда прошло 60 секунд. Обратите внимание на функцию "break", используемую для остановки цикла:

alarm-time: now/time + :00:60
forever [if now/time = alarm-time [alert "1 minute has passed" break]]

Вот более интерактивная версия, использующая некоторую информацию, предоставленную пользователем. Обратите внимание, как аргументы цикла forever, if и alert имеют отступ, чтобы уточнить группировку связанных параметров:

event-name: request-text/title "What do you want to be reminded of?"
seconds: to-integer request-text/title "Seconds to wait?"
alert rejoin [
    "It's now " now/time ", and you'll be alerted in " 
    seconds " seconds."
]
alarm-time: now/time + seconds
forever [
    if now/time = alarm-time [
        alert rejoin [
            "It's now "alarm-time ", and " seconds 
            " seconds have passed.  It's time for: " event-name
        ] 
        break
    ]
]

Вот бесконечный цикл, который отображает/обновляет текущее время в графическом интерфейсе:

view layout [
    timer: field
    button "Start" [
        forever [
            set-face timer now/time 
            wait 1
        ]
    ]
]

11.11.2 Loop

Функция "Loop" позволяет многократно оценивать блок кода указанное количество раз:

loop 50 [print "REBOL is great!"]

11.11.3 Repeat

Как и "Loop", функция "Repeat" позволяет многократно оценивать блок кода определённое количество раз. Кроме того, он позволяет вам указать переменную счётчика, которая автоматически увеличивается каждый раз в цикле:

repeat count 50 [print rejoin ["This is loop #: " count]]

Приведённый выше код делает то же самое, что и:

count: 0
loop 50 [
    count: count + 1
    print rejoin ["This is loop #: " count]
]

Другой способ написать это:

for i 1 50 1 [print rejoin ["This is loop #: " i]]

11.11.4 Forall и Forskip

"Forall" проходит по блоку, увеличивая отмеченный порядковый номер серии по мере прохождения:

some-names: ["John" "Bill" "Tom" "Mike"]

foreach name some-names [print index? some-names]  ; index ndex не меняется
forall some-names [print index? some-names]  ; index меняется

foreach name some-names [print name]
forall some-names [print first some-names] ; то же, что и строка выше

"Forskip" работает как forall, но пропускает блок, перескакивая периодическое количество элементов в каждом цикле:

some-names: ["John" "Bill" "Tom" "Mike"]
forskip some-names 2  [print first some-names]

11.11.5 While и Until

Функция "while" многократно оценивает блок кода, пока данное условие истинно. Цикл "while" имеет следующий формат:

while [условие] [
     блок функций, которые будут выполняться, пока условие истина
]

В этом примере считается до 5:

x: 1  ; начальное значение счётчика 
while [x <= 5] [
    alert to-string x 
    x: x + 1
]

На человеческом языке этот код гласит:

"x" изначально равно 1.
Пока x меньше или равно 5, отобразить значение x,
затем добавьте 1 к значению x и повторите.

Некоторые дополнительные примеры цикла "while":

while [not request "End the program now?"] [
    alert "Select YES to end the program."
] 
; "not" инвертирует значение данных, полученных от пользователя 
; (т. е. "да" становится "нет" и наоборот)
alert "Please select today's date" 
while [request-date <> now/date] [
    alert rejoin ["Please select TODAY's date.  It's " now/date]
]

while [request-pass <> ["username" "password"]] [
    alert "The username is 'username' and the password is 'password'"
]

Циклы "Until" аналогичны циклам "while". Они делают всё в заданном блоке несколько раз, пока последнее выражение в блоке не станет истинным:

x: 10
until [
    print rejoin ["Counting down: " x]
    x: x - 1
    x = 0
]

11.11.6 For

Функция "for" может перебирать серию элементов в блоке, как и "foreach", но с использованием счётного индекса. Синтаксис "for" выглядит следующим образом: "Назначьте (переменное слово) для ссылки на последовательность чисел, подсчитываемых по порядку, начните отсчёт с (начальное число), посчитайте до (конечное число), пропуская это (номер шага) [используйте метка переменной для ссылки на каждый последовательный номер в счётчике]:

REBOL []
for counter 1 10 1 [print counter] 
for counter 10 1 -1 [print counter] 
for counter 10 100 10 [print counter] 
for counter 1 5 .5 [print counter] 
halt

REBOL будет правильно увеличивать любой тип данных, который он понимает:

REBOL []
for timer 8:00 9:00 0:05 [print timer] 
for dimes $0.00 $1.00 $0.10 [print dimes] 
for date 1-dec-2005 25-jan-2006 8 [print date]
for alphabet #"a" #"z" 1 [prin alphabet]
halt

Вы можете выбрать проиндексированные элементы из списка, используя постепенно подсчитываемые числа в цикле FOR. Просто определите длину блока, используя "length?" функция:

REBOL []
months: system/locale/months
len: length? months
for i 1 len 1 [
    print rejoin [(pick months i) " is month number " i]
]
halt

Этот код делает то же самое, что и выше, используя один из альтернативных синтаксисов вместо "pick":

REBOL []
months: system/locale/months
len: length? months
for i 1 len 1 [
    print rejoin [months/:i " is month number " i]
]
halt

Как вы видели, вы можете выбрать последовательные элементы из списка, используя счётную арифметику (индекс выбора, индекс выбора + 1, индекс выбора + 2). В структуре "for", которая использует значение пропуска, эта концепция позволяет вам выбирать столбцы данных:

REBOL []
months: system/locale/months
len: length? months
for i 1 len 3 [
    print rejoin [
        "Months " i " to " (i + 2) " are:" newline newline
        (pick months i) newline
        (pick months (i + 1)) newline
        (pick months (i + 2)) newline 
    ]
]
halt

Вот пример, который использует условную оценку "if" (если) для печати только тех имён, которые содержат запрошенную строку текста. Это обеспечивает функциональный поиск:

REBOL []
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]
search-text: request-text/title/default "Search for:" "t"
for i 1 length? users 3 [
    if find/only (pick users i) search-text [
        print rejoin [
            (pick users i) newline
            (pick users (i + 1)) newline
            (pick users (i + 2)) newline
        ]

    ]
]
halt

Чтобы прояснить синтаксическую разницу между циклами "for" и "foreach", вот та же программа, что и выше, написанная с использованием "foreach":

REBOL []
users: [
    "John Smith" "123 Tomline Lane Forest Hills, NJ" "555-1234"
    "Paul Thompson" "234 Georgetown Pl. Peanut Grove, AL" "555-2345"
    "Jim Persee" "345 Pickles Pike Orange Grove, FL" "555-3456"
    "George Jones" "456 Topforge Court Mountain Creek, CO" ""
    "Tim Paulson" "" "555-5678"
]
search-text: request-text/title/default "Search for:" "t"
foreach [name address phone] users [
    if find/only name search-text [
        print rejoin [
            name newline
            address newline
            phone newline
        ]

    ]
]
halt

Как видите, функция цикла "for" полезна почти так же, как и функция "foreach". Это немного загадочно, но полезно для некоторых общих алгоритмов выбора полей, которые включают непоследовательные поля. Вы обнаружите, что обе функции используются во всех типах приложений, которые имеют дело с табличными данными и списками. По мере продвижения этого урока вы увидите множество вариантов использования каждой функции - внимательно следите за "for" и "foreach".

В приведенном ниже примере используется несколько циклов, чтобы предупредить пользователя о необходимости кормить кошку каждые 6 часов с 8:00 до 20:00. Он использует цикл for для увеличения времени выдачи предупреждений, цикл while для непрерывного сравнения увеличенного времени с текущим временем и цикл forever, чтобы делать одно и то же каждый день, непрерывно. Обратите внимание на отступ:

forever [
    for timer 8:00am 8:00pm 6:00 [
        while [now/time <= timer] [wait :00:01] 
        alert rejoin ["It's now " now/time ".  Time to feed the cat."]
    ]
]

11.12 Подробнее о том, почему и чем полезны блоки

ВАЖНО: В REBOL блоки могут содержать смешанные данные ЛЮБОГО типа (текстовые и двоичные элементы, встроенные списки элементов (другие блоки), переменные и т.д.):

some-items: ["item1" "item2" "item3" "item4"]
an-image: load http://rebol.com/view/bay.jpg
append some-items an-image

; "some-items" теперь содержит 4 текстовые строки и изображение!

; Вы можете сохранить весь этот блок данных, ВКЛЮЧАЯ данные 
; ДВОИЧНОГО ИЗОБРАЖЕНИЯ, на жёсткий диск как ПРОСТОЙ ТЕКСТОВЫЙ ФАЙЛ.

save/all %some-items.txt some-items

; чтобы загрузить его и использовать позже:

some-items: load %some-items.txt
view layout [image fifth some-items]

Найдите минутку, чтобы изучить приведённый выше пример. Блочная структура REBOL чрезвычайно проста в использовании по сравнению с другими языками и решениями для управления данными (намного проще, чем в большинстве систем баз данных). Это очень гибкий, простой и мощный способ хранения данных в коде! Тот факт, что блоки могут содержать все типы данных с использованием одной простой синтаксической структуры, является фундаментальной причиной, по которой их проще использовать, чем другие языки программирования и вычислительные инструменты. Вы можете сохранить/загрузить код блока на жёсткий диск в виде простого текстового файла, отправить его по электронной почте, отобразить в графическом интерфейсе, сжать и передать его на веб-сервер для загрузки другими пользователями, передать его непосредственно через сетевое соединение или даже преобразование его в XML, шифрование и сохранение его частей в защищённой многопользовательской базе данных для доступа других языков программирования и т.д..

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

11.12.1 Оценка переменных в блоках: составление, сокращение, подбор и т.д.

Вы часто обнаруживаете, что хотите ссылаться на элемент в блоке по его индексу (номеру позиции), как в предыдущем примере с "some-items":

view layout [image some-items/5]

Однако вы не всегда можете знать конкретный порядковый номер элемента данных, к которому вы хотите получить доступ. Например, когда вы вставляете элементы данных в блок, позиция индекса последнего элемента изменяется (увеличивается). Вы можете получить порядковый номер последнего элемента в блоке, просто определив количество элементов в блоке (номер позиции последнего элемента в блоке всегда совпадает с общим количеством элементов в блоке). В приведённом ниже примере этому порядковому номеру присвоено слово переменной "last-item":

last-item: length? some-items

Теперь вы можете использовать эту переменную, чтобы выбрать (pick) последний элемент:

view layout [image (pick some-items last-item)]

; В нашем предыдущем примере с 5 элементами в блоке строка выше 
; оценивается так же, как:

view layout [image (pick some-items 5)]

Вы можете ссылаться на другие элементы, добавляя и вычитая порядковые номера:

alert pick some-items (last-item - 4)

Есть несколько других способов сделать то же самое в REBOL. Функция "compose" позволяет вычислять и вставлять переменные в круглых скобках, как если бы они были явно введены в блок кода:

view layout compose [image some-items/(last-item)]

; Вышеупомянутая строка представляется интерпретатору так, как если 
; бы была набрана следующая строка:

view layout [image some-items/5]

Функция "compose" очень полезна, когда вы хотите ссылаться на данные в позициях переменных индексов в блоке. Функцию "reduce" можно также использовать для получения такого же типа оценки. Функциональные слова в блоке должны начинаться с символа галочки ('):

view layout reduce ['image some-items/(last-item)]

Другой способ явно использовать значения переменных - использовать формат ":" ниже. Этот код оценивает то же, что и предыдущие два примера:

view layout [image some-items/:last-item]

Думайте о формате двоеточия выше как о противоположности установке переменной. Как вы видели, символ двоеточия, помещённый после переменного слова, устанавливает значение слова, равное некоторому значению. Символ двоеточия, помещённый перед словом переменной, получает значение, присвоенное переменной, и вставляет это значение в код, как если бы оно было набрано явно.

Вы можете использовать указатель "index?" и функции поиска "find" для определения позиции (позиций) индекса любых данных, которые вы ищете в блоке:

index-num: index? (find some-items "item4")

Для выбора данных в определённой позиции переменной можно использовать любой из четырёх предыдущих форматов:

print pick some-items index-num
print compose [some-items/(index-num)]
print reduce [some-items/(index-num)]  
; в приведённом выше блоке не используются служебные слова, поэтому 
; галочки не требуются
print some-items/:index-num

Вот пример, который отображает данные переменного изображения, содержащиеся в блоке, с использованием цикла foreach. Функция compose используется для включения динамически изменяемых данных (представлений изображений), как если бы эти данные были введены непосредственно в код:

photo1: load http://rebol.com/view/bay.jpg
photo2: load http://rebol.com/view/demos/palms.jpg

; Интерпретатор REBOL видит следующую строку, как если бы весь код, 
; представляющий приведённые выше изображения, был набран 
; непосредственно в блоке:

photo-block: compose [(photo1) (photo2)]

foreach photo photo-block [view layout [image photo]]

Для получения дополнительных подробных сведений об использовании блоков и функций последовательностей см. http://www.rebol.com/docs/core23/rebolcore-6.html.

11.13 Строки в REBOL

В REBOL "string" (строка) - это просто последовательность символов. Если у вас есть опыт работы с другими языками программирования, это может быть одним из препятствий в изучении REBOL. Решение REBOL на самом деле очень мощное, простое в освоении и согласуется с тем, как другие операции работают на этом языке. Правильное управление строками просто требует хорошего понимания функций списков. Взгляните на следующие примеры, чтобы узнать, как выполнять несколько общих операций:

the-string: "abcdefghijklmnopqrstuvwxyz"

;Левая строка: (получить 7 символов строки с лева):

copy/part the-string 7

; Правая строка: (Получите 7 символов строки с право):

copy at tail the-string -7

; Средняя строка 1: (получить 7 символов из середины строки, начиная 
; с 12-го символа):

copy/part (at the-string 12) 7

; Средняя строка 2: (получить 7 символов из середины строки, начиная 
; с 7 символов от буквы "m"):
copy/part (find the-string "m") -7

; Средняя строка 3: (получить 7 символов из середины строки, 
; начиная с 12 символов от буквы "t"):

copy/part (skip (find the-string "t") -12) 7

; 3 разных способа получить только 7-й символ:

the-string/7 
pick the-string 7
seventh the-string

; Измените "cde" на "123"

replace the-string "cde" "123"

; Несколько способов изменить седьмой символ на "7"

change (at the-string 7) "7"
poke the-string 7 #"7"  ; символ фунта (решётка) относится к одному 
                        ; символу
poke the-string 7 (to-char "7") ; другой способ использования 
                                ; одиночных символов
print the-string

; Удалите 15 символов, начиная с 3-й позиции:

remove/part (at the-string 3) 15
print the-string

; Вставьте 15 символов, начиная с 3-й позиции:

insert (at the-string 3) "cdefghijklmnopq"
print the-string

; Вставьте 3 экземпляра "-+ " в начало строки:

insert/dup head the-string "-+ " 3
print the-string

; Замените каждый экземпляр "-+ " на " ":

replace/all the-string "-+ "  " "
print the-string

; Удалите пробелы из строки (введите "? trim", чтобы увидеть все 
; её уточнения!):

trim the-string
print the-string

; Получить каждый третий символ из строки:

extract the-string 3

; Получите значение ASCII для "c" (ASCII 99):

to-integer third the-string

; Получите символ для ASCII 99 ("c"):

to-char 99

; Преобразуйте указанное выше значение символа в строковое значение:

to-string to-char 99

; Преобразуйте любое значение в строку:

to-string now
to-string $2344.44
to-string to-char 99
to-string system/locale/months

; Еще лучший способ преобразовать значения в строки:

form now
form $2344.44
form to-char 99
form system/locale/months   ; конвертировать блоки в красиво 
                            ; оформленные строки

; Преобразование строк в блок символов:

the-block: copy []
foreach item the-string [append the-block item]
probe the-block

Функции серии REBOL очень разнообразны. Часто можно придумать несколько способов сделать то же самое:

; Удалите последнюю часть URL-адреса:

the-url: "http://website.com/path"
clear at the-url (index? find/last the-url "/")
print the-url

; Другой способ сделать это:

the-url: "http://website.com/path"
print copy/part the-url (length? the-url)-(length? find/last the-url "/")

(Конечно, REBOL имеет встроенную вспомогательную функцию для достижения вышеуказанной цели напрямую с помощью URL-адресов):

the-url: http://website.com/path
print first split-path the-url

Есть ряд дополнительных функций, которые можно использовать специально для работы со строковыми сериями. Запустите следующий сценарий для ознакомления:

string-funcs: [
    build-tag checksum clean-path compress debase decode-cgi decompress
    dehex detab dirize enbase entab import-email lowercase mold parse-xml
    reform rejoin remold split-path suffix? uppercase
]    
echo %string-help.txt  ; "echo" сохраняет вывод консоли в файл
foreach word string-funcs [
    print "___________________________________________________________^/"
    print rejoin ["word:  " uppercase to-string word]  print "" 
    do compose [help (to-word word)]
]
echo off
editor at read %string-help.txt 4

См. http://www.rebol.com/docs/dictionary.html и http://rebol.com/docs/core23/rebolcore-8.html для получения дополнительной информации о вышеуказанных функциях.

12. Более важные темы

12.1 Встроенная справка и онлайн-ресурсы

Функция "help" отображает необходимый синтаксис для любой функции REBOL:

help print

"?" является синонимом слова "help":

? print

Функция "what" перечисляет все встроенные слова:

what

Вместе эти два слова представляют собой встроенное справочное руководство для всего основного языка REBOL. Вот сценарий, который сохраняет всю вышеуказанную документацию в файл. Дайте ему несколько секунд на запуск:

echo %words.txt what echo off  ; "echo" сохраняет вывод консоли в файл
echo %help.txt
foreach line read/lines %words.txt [
    word: first to-block line
    print "___________________________________________________________^/"
    print rejoin ["word:  " uppercase to-string word]  print "" 
    do compose [help (to-word word)]
]
echo off
editor at read %help.txt 4

Вы можете использовать справку для поиска определённых слов и значений, если не можете вспомнить точное написание слова. Просто введите часть слова (нажатие клавиши табуляции также покажет список слов для автоматического завершения слова):

? to-         ; показывает список всех встроенных преобразований типов
? reques      ; показывает список встроенных функций запроса
? "load"      ; показывает все слова, содержащие символы "load"
? "?"         ; показывает все слова, содержащие символ "?"

Вот ещё несколько примеров способов поиска полезной информации с помощью справки:

? datatype! ; показывает список встроенных типов данных
? function! ; показывает список встроенных функций
? native!   ; показывает список собственных (компилировых Cи) функций
? char!     ; показывает список встроенных управляющих символов
? tuple!    ; показывает список встроенных цветов (кортежи RGB)
? .gif      ; показывает список встроенных изображений .gif

Вы можете просмотреть исходный код для встроенных "мезонинных" (неродных) функций с помощью функции "source". Огромный объем кода REBOL доступен прямо в интерпретаторе, а все мезонинные функции были созданы дизайнером языка Карлом Сассенратом. Изучение исходного кода мезонина - отличный способ узнать больше о расширенных шаблонах кода REBOL:

source help
source request-text
source view
source layout
source ctx-viewtop ;попробуйте: view layout [image load ctx-viewtop/13]

Скрипт "браузер слов" (word browser)- полезный инструмент для поиска, перекрёстных ссылок и изучения всех важных функций REBOL:

write %wordbrowser.r read http://re-bol.com/wordbrowser.r
do %wordbrowser.r

12.1.1 Системный объект REBOL и справка с виджетами GUI

"Help system" отображает содержимое системного объекта REBOL, который содержит множество важных настроек и значений. Вы можете исследовать каждый уровень системного объекта, используя обозначение пути, например:

? system/console/history        ; текущая история сеанса консоли
? system/options
? system/locale/months
? system/network/host-address

Вы можете найти информацию обо всех компонентах GUI REBOL в "system/view/VID":

? system/view/VID

Блок system/view/VID настолько важен, что REBOL имеет встроенный ярлык для ссылки на него:

? svv

Вы найдёте список виджетов GUI REBOL в "svv/vid-styles". Используйте функцию editor (редактора) REBOL для просмотра больших разделов системы, например:

editor svv/vid-styles

Вот сценарий, который аккуратно отображает все слова в приведённом выше блоке "svv/vid-styles":

foreach i svv/vid-styles [if (type? i) = word! [print i]]

Вот более краткий способ отображения указанных выше виджетов с помощью функции extract (извлекать):

probe extract svv/vid-styles 2

Этот скрипт позволяет просматривать структуру объектов каждого виджета:

view layout [
    text-list data (extract svv/vid-styles 2) [
        a/text: select svv/vid-styles value
        show a focus a
    ]
    a: area 500x250 
]

Слова макета GUI REBOL доступны в "svv/vid-words":

? svv/vid-words

Следующий скрипт отображает все изображения в блоке svv/image-stock:

b: copy [] 
foreach i svv/image-stock [if (type? i) = image! [append b i]]
v: copy [] foreach i b [append v reduce ['image i]]
view layout v

Изменяемые атрибуты ("facets"), доступные для всех виджетов графического интерфейса, перечислены в "svv/facet-words":

editor svv/facet-words

Вот сценарий, который аккуратно отображает все вышеупомянутые фасетные слова:

b: copy [] 
foreach i svv/facet-words [if (not function? :i) [append b to-string i]]
view layout [text-list data b]

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

foreach i (extract svv/vid-styles 2) [
    x: select svv/vid-styles i
    ; additional facets are held in a "words" block:
    if x/words [
        prin join i ": "
        foreach q x/words [
            if not (function? :q) [prin join q " "]
        ]
        print ""
    ]
]

Чтобы изучить функции, которые обрабатывают любые дополнительные фасеты для виджетов выше, введите путь к блоку "words" (слов) виджета, то есть:

svv/vid-styles/TEXT-LIST/words

Для получения дополнительной информации о system/view/VID см. http://www.mail-archive.com/rebol-bounce@rebol.com/msg01898.html и http://www.rebol.org/ml-display-message.r?m=rmlHJNC.

Важно отметить, что вы можете УСТАНОВИТЬ любое системное значение. Просто используйте двоеточие, например, при присвоении значений переменных:

system/user/email: user@website.com

Знакомство с системным объектом даёт много полезных инструментов.

12.1.2 Ресурсы Viewtop

Рабочий стол REBOL, который появляется по умолчанию при запуске интерпретатора view.exe, можно использовать в качестве шлюза в мир "сайтов", которые разработчики используют для обмена полезным кодом. Просмотр общедоступных сайтов - отличный способ более глубоко изучить язык. Весь код из архива rebol.org и многое другое доступно на сайтах. При вводе текста на консоли интерпретатора функция "desktop" (рабочий стол) вызывает рабочий стол REBOL (также называемый "Viewtop"):

desktop

Щёлкните папки "REBOL" или "Public", чтобы увидеть сотни интересных демонстраций и полезных примеров. Исходный код для каждого примера доступен, щёлкнув правой кнопкой мыши отдельные значки программ и выбрав "edit" (редактировать). Вам не нужен веб-браузер или какое-либо другое программное обеспечение для просмотра содержимого веб-сайтов - Viewtop и все его функции являются частью исполняемого файла REBOL. Вы можете изучать тома о языке REBOL, используя только ресурсы, встроенные непосредственно в интерпретатор 600k!

Для получения подробной, категоризированной и перекрёстной информации о встроенных функциях см. Сайт REBOL Dictionary, находящийся в папке рабочего стола REBOL REBOL-> Tools (HTML-версия также доступна на http://www.rebol.com/docs/dictionary.html).

12.1.3 Электронная документация, список рассылки и форум сообщества AltME

Если вы не можете найти ответы на свои вопросы по программированию REBOL с помощью встроенной справки и ресурсов, в первую очередь следует поискать http://rebol.com/docs.html. Онлайн-документация в Google также даёт быстрые результаты, поскольку слово "REBOL" встречается нечасто.

Чтобы задать вопрос непосредственно другим разработчикам REBOL, вы можете присоединиться к списку рассылки сообщества, отправив электронное письмо на адрес rebol-request@rebol.com со словом "subscribe" (подписаться) в строке темы. Используйте обычную почтовую программу или просто вставьте следующий код в интерпретатор REBOL (убедитесь, что настройки вашей учётной записи электронной почты правильно настроены в REBOL):

send rebol-request@rebol.com "subscribe"

Вы также можете задать вопросы многочисленным гуру и постоянным пользователям в AltME, программе обмена сообщениями, которая составляет самый активный форум пользователей REBOL во всем мире. Rebol.org поддерживает доступную для поиска историю нескольких сотен тысяч сообщений как из списка рассылки, так и из AltME, а также богатый архив сценариев. Сообщество пользователей REBOL дружелюбно, хорошо осведомлено и полезно, и вы, как правило, найдёте ответы практически на любой вопрос уже в архивах. В отличие от других сообществ программистов, REBOL не имеет популярного форума поддержки в Интернете. AltME - это основной способ взаимодействия разработчиков REBOL. Если вы хотите поговорить с другими, вы должны загрузить программу AltME и создать учётную запись пользователя (это быстро и легко). Просто следуйте инструкциям на сайте http://www.rebol.org/aga-join.r.

12.2 Сохранение и запуск скриптов REBOL

До сих пор в этом руководстве вы набирали или копировали/вставляли фрагменты кода непосредственно в интерпретатор REBOL. Когда вы начнёте работать с более длинными примерами и полными программами, вам нужно будет сохранить свои сценарии для последующего выполнения. Всякий раз, когда вы сохраняете программу REBOL в текстовый файл, код должен начинаться со следующего текста заголовка:

REBOL []

Этот заголовок сообщает интерпретатору REBOL, что файл содержит допустимую программу REBOL. При желании вы можете задокументировать любую информацию о программе в блоке заголовка. Переменная title в блоке заголовка отображается в строке заголовка окон программы GUI:

REBOL [
    title:  "My Program"
    author: "Nick Antonaccio"
    date:   29-sep-2009
]
view layout [text 400 center "Look at the title bar."]

Приведённый ниже код представляет собой программу просмотра видео с веб-камеры. Введите или скопируйте/вставьте полный исходный код ниже в текстовый редактор, такой как Блокнот Windows или встроенный текстовый редактор REBOL (введите "editor none" в командной строке REBOL). Сохраните текст как файл с именем "webcam.r" на диске C:\.

REBOL [title: "Webcam Viewer"]

; try http://www.webcam-index.com/USA/ for more webcam links.

temp-url: "http://209.165.153.2/axis-cgi/jpg/image.cgi"
while [true]  [
    webcam-url: to-url request-text/title/default "Web cam URL:" temp-url
    either attempt [webcam: load webcam-url] [
        break
    ] [
        either request [
            "That webcam is not currently available." "Try Again" "Quit"
        ] [
            temp-url: to-string webcam-url
        ] [
            quit
        ]
    ] 
] 
resize-screen: func [size] [
    webcam/size: to-pair size
    window/size: (to-pair size) + 40x72
    show window
]
window: layout [
    across 
    btn "Stop" [webcam/rate: none show webcam]
    btn "Start" [
        webcam/rate: 0 
        webcam/image: load webcam-url 
        show webcam
    ]
    rotary "320x240" "640x480" "160x120" [
        resize-screen to-pair value 
    ]
    btn "Exit" [quit] return
    webcam: image load webcam-url  320x240 
    with [
        rate: 0
        feel/engage: func [face action event][
            switch action [
                time [face/image: load webcam-url show face]
            ] 
        ] 
    ] 
]
view center-face window

После того, как вы сохранили программу webcam.r на C:\, вы можете запустить ее любым из следующих способов:

  1. Если вы уже установили REBOL на свой компьютер, просто дважды щёлкните сохранённый файл сценария ".r" (найдите значок файла C:\webcam.r в проводнике файлов (щёлкните Мой компьютер -> C: -> веб-камера. р)). По умолчанию во время начальной установки REBOL все файлы с расширением ".r" связаны с интерпретатором. Их можно щёлкнуть и запустить, как если бы они были исполняемыми программами, как файлы ".exe". Интерпретатор REBOL автоматически открывает и выполняет любой выбранный текстовый файл ".r". Это наиболее распространённый способ запуска сценариев REBOL, и он работает одинаково во всех основных графических операционных системах. Если вы хотите, чтобы другие люди могли запускать ваши сценарии, просто попросите их загрузить и установить крошечный интерпретатор REBOL - это займёт всего несколько секунд.
  2. Используйте встроенный редактор REBOL. Введите "editor %/c/webcam.r" в командной строке интерпретатора или введите "editor none" и скопируйте/вставьте скрипт в редактор. Нажатие F5 в редакторе автоматически сохранит и запустит скрипт. Это удобный способ работы со сценариями, который позволяет REBOL быть собственной простой, автономной IDE.
  3. Введите "do %/c/webcam.r" в интерпретатор REBOL.
  4. Скрипты можно запускать из командной строки. В Windows скопируйте rebol.exe и webcam.r в ту же папку (C:\), затем нажмите Пуск -> Выполнить и введите "C:\rebol.exe C:\webcam.r" (или откройте окно DOS и введите то же самое). Эти команды запустят интерпретатор REBOL и выполнят код webcam.r. Вы также можете создать текстовый файл с именем webcam.bat, содержащий текст "C:\rebol.exe C:\webcam.r". Щёлкните файл webcam.bat в Windows, и он запустит эти команды. В Unix вы также можете запускать сценарии в запланированное время с помощью Cron. Просто введите путь к скрипту.
  5. Используйте такую ​​программу, как XpackerX, для упаковки и распространения программы. XpackerX позволяет вам обернуть интерпретатор REBOL и программу webcam.r в один исполняемый файл, который имеет кликабельный значок, и автоматически запускает оба файла. Это позволяет вам создать исполняемую программу Windows в виде отдельного файла, которую можно распространять и запускать, как любое другое приложение. Просто щёлкните по нему и запустите ... (этот метод рассматривается в следующем разделе).
  6. Купите коммерческую версию REBOL "SDK", которая обеспечивает наиболее безопасный метод упаковки приложений REBOL.

ОЧЕНЬ ВАЖНО: чтобы отключить запросчик безопасности по умолчанию, который постоянно запрашивает разрешение на чтение/запись жёсткого диска, введите "secure none" в интерпретаторе REBOL, а затем запустите программу с "do {filename}". Выполнение "C:\rebol.exe -s {filename}" делает то же самое. "-S" запускает интерпретатор REBOL без включения каких-либо функций безопасности, заставляя его вести себя как типичная программа Windows.

12.3 "Компиляция" программ REBOL - распространение исполняемых .EXE файлов

Интерпретатор REBOL.exe крошечный и не требует установки для правильной работы. Упаковывая его, ваши сценарии REBOL и любые вспомогательные файлы данных в один исполняемый файл со значком по вашему выбору, XpackerX работает как компилятор REBOL, который производит обычные программы Windows, которые выглядят и действуют так же, как те созданные другими компилируемыми языками. Для этого вам необходимо создать текстовый файл в следующем формате (сохраните его как "template.xml"):

<?xml version="1.0"?>
<xpackerdefinition>
    <general>
        <!--shown in taskbar -->
        <appname>your_program_name</appname>
        <exepath>your_program_name.exe</exepath>
        <showextractioninfo>false</showextractioninfo>
        <!-- <iconpath>c:\icon.ico</iconpath> -->
    </general>
    <files>
        <file>
            <source>your_rebol_script.r</source>
            <destination>your_rebol_script.r</destination>
        </file>
        <file>
            <source>C:\Program Files\rebol\view\Rebol.exe</source>
            <destination>rebol.exe</destination>
        </file>
        <!--put any other data files here -->
    </files>
    <!-- $FINDEXE, $TMPRUN, $WINDIR, $PROGRAMDIR, $WINSYSDIR -->
    <onrun>$TMPRUN\rebol.exe -si $TMPRUN\your_rebol_script.r</onrun>
</xpackerdefinition>

Просто скачайте бесплатную программу XpackerX и измените вышеуказанный шаблон, чтобы он содержал имена файлов, которые вы дали своим скриптам и файлам, а также правильный путь к вашему интерпретатору REBOL. Запустите XpackerX, и он выдаст красиво упакованный EXE-файл, который не требует установки. Вашим пользователям не нужно устанавливать REBOL для запуска этого типа исполняемого файла. Для них он выглядит и работает так же, как и любая другая скомпилированная программа для Windows. На самом деле происходит то, что каждый раз, когда запускается ваш упакованный файл .exe, интерпретатор REBOL и ваши скрипты/файлы данных распаковываются во временную папку на вашем компьютере. Когда ваш скрипт будет запущен, временная папка будет удалена.

Большинство современных приложений сжатия (zip) имеют функцию "sfx", которая позволяет создавать пакеты .exe из файлов zip. Вы можете создать упакованный REBOL .exe так же, как XpackerX, используя практически любое приложение для упаковки sfx (есть десятки бесплатных приложений для архивирования/сжатия, которые могут это сделать - используйте то, с которым вы наиболее знакомы).

Программа "iexpress.exe", которая есть во всех версиях Windows (начиная с Windows XP), является популярным выбором для создания файлов SFX, поскольку не требует установки дополнительного программного обеспечения. Просто нажмите Пуск -> Выполнить или Пуск -> Поиск программ и файлов и введите "iexpress". Следуйте указаниям мастеров, чтобы назвать пакет, выбрать включённые файлы (интерпретатор REBOL, сценарии, изображения и т.д.) И другие параметры. Настройки любого .exe, который вы создаёте с помощью iexpress, будут сохранены в .sed-файле, который вы можете перезагрузить и изменить позже. Это позволяет быстро перекомпилировать обновлённые сценарии.

Здесь объясняется, как использовать средство создания установки NSIS для создания файлов REBOL .exe. Это полезно, если вы хотите настроить параметры реестра и внести другие изменения в компьютерную систему при установке программы.

Чтобы создать самораспаковывающийся исполняемый файл REBOL для Linux, сначала создайте файл .tgz, содержащий все файлы, которые вы хотите распространять (интерпретатор REBOL, ваши скрипты, любые внешние двоичные файлы и т.д.). Для целей этого примера назовите этот пакет "rebol_files.tgz". Затем создайте текстовый файл, содержащий следующий код, и сохраните его как "sh_commands":

#!/bin/sh
SKIP=`awk '/^__REBOL_ARCHIVE__/ { print NR + 1; exit 0; }' $0`
tail +$SKIP $0 | tar xz    
exit 0
__REBOL_ARCHIVE__

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

cat sh_commands rebol_files.tgz > rebol_program.sh

Вышеупомянутая строка создаст единственный исполняемый файл с именем "rebol_program.sh", который может распространяться и запускаться конечными пользователями. Пользователь должен будет установить права доступа к файлу для rebol_program.sh как исполняемый перед его запуском ("chmod + x rebol_program.sh") или выполнить его, используя синтаксис "sh rebol_program.sh".

12.4 Распространённые ошибки REBOL и как их исправить

Ниже перечислены решения множества распространённых ошибок, с которыми вы столкнётесь при первом эксперименте с REBOL:

1) "** Syntax Error: Script is missing a REBOL header" (Синтаксическая ошибка: в сценарии отсутствует заголовок REBOL) - всякий раз, когда вы "выполняете" (do) сценарий, сохранённый в виде файла, он должен содержать хотя бы минимально необходимый заголовок в верхней части кода. Просто включите следующий текст в начало скрипта:

REBOL []

2) "** Syntax Error: Missing ] at end-of-script" (Синтаксическая ошибка: отсутствует ] в конце скрипта) - вы получите эту ошибку, если не поставите закрывающую скобку в конце блока. Вы увидите аналогичную ошибку для незакрытых скобок и строк. Приведённый ниже код выдаст вам ошибку, поскольку в конце блока отсутствует символ "]":

fruits: ["apple" "orange" "pear" "grape"
print fruits

Вместо этого должно быть:

fruits: ["apple" "orange" "pear" "grape"]
print fruits

Отступы блоков помогают находить и устранять подобные ошибки.

3) "** Script Error: request expected str argument of type: string block object none" (Ошибка сценария: запрос ожидаемого аргумента str типа: объект строкового блока нет) - этот тип ошибки возникает, когда вы пытаетесь передать неверный тип значения функции. Приведённый ниже код выдаст вам ошибку, потому что REBOL автоматически интерпретирует переменную веб-сайта как URL-адрес, а для функции "alert" требуется строковое значение:

website: http://rebol.com
alert website

Приведённый ниже код решает проблему путём преобразования значения URL-адреса в строку перед передачей его в функцию предупреждения:

website: http://rebol.com
alert to-string website

Каждый раз, когда вы видите ошибку типа "expected _____ argument of type: ___ ____ ___ ..." ("ожидаемый _____ аргумент типа: ___ ____ ___ ...), вам необходимо преобразовать ваши данные в соответствующий тип, используя одну из функций "to-(type)". Введите "? to-" в интерпретаторе REBOL, чтобы получить список всех этих функций.

4) "** Script Error: word has no value" (Ошибка сценария: слово не имеет значения) - неправильное написание вызовет этот тип ошибки. Вы столкнётесь с этим каждый раз, когда попытаетесь использовать слово, которое не определено (либо изначально в интерпретаторе REBOL, либо вами в предыдущем коде):

wrod: "Hello world"
print word

5) Если ошибка возникает в блоке "view layout" и графический интерфейс перестаёт отвечать, введите "unview" в командной строке интерпретатора, и неработающий графический интерфейс будет закрыт. Чтобы перезапустить остановленный графический интерфейс, введите "do-events". Чтобы выйти из бесконечного цикла или иным образом остановить выполнение ошибочного кода, просто нажмите клавишу [Esc] на клавиатуре.

6) "** User Error: Server error: tcp 550 Access denied - Invalid HELO name (See RFC2821 4.1.1.1)"(Ошибка пользователя: ошибка сервера: tcp 550 Доступ запрещен - недопустимое имя HELO (см. RFC2821 4.1.1.1)) и "** User Error: Server error: tcp -ERR Login failed." (Ошибка пользователя: ошибка сервера: ошибка входа tcp -ERR), среди прочего, являются ошибками, которые возникают при попытке отправить и получить электронную почту. Чтобы исправить эти ошибки, необходимо указать информацию о вашем почтовом сервере в пользовательских настройках REBOL. Наиболее распространённый способ сделать это - отредактировать информацию о своём почтовом аккаунте в графическом окне Viewtop или с помощью функции "set-net" (http://www.rebol.com/docs/words/wset-net.html). Вы также можете настроить все вручную - вот как настроить все индивидуальные настройки:

system/schemes/default/host: your.smtp.address
system/schemes/default/user: username
system/schemes/default/pass: password
system/schemes/pop/host: your.pop.address
system/user/email: your.email@site.com

7) Вот причуда REBOL, которая не вызывает ошибки, но может вызвать запутанные результаты, особенно если вы знакомы с другими языками:

unexpected: [
    empty-variable: ""
    append empty-variable "*"
    print empty-variable
]

do unexpected
do unexpected
do unexpected

Строка:

empty-variable: ""

НЕ инициализирует повторно переменную в пустое состояние. Вместо этого каждый раз, когда блок запускается, "empty-variable" содержит предыдущее значение. Чтобы вернуть переменную в пустое значение, как и предполагалось, используйте слово "copy" следующим образом:

expected: [
    empty-variable: copy ""
    append empty-variable "*"
    print empty-variable
]

do expected
do expected
do expected

8) Загрузка/сохранение (Load/Save), чтение/запись (Read/Write), формирование (Mold), преобразование (Reform) и т.д. - ещё одна путаница, с которой вы можете столкнуться изначально с REBOL, связана с различными словами, которые читают, записывают и форматируют данные. Например, при сохранении данных в файл на жёстком диске вы можете использовать одно из слов "save" (сохранить) или "write" (записать). "Save" используется для хранения данных в формате, более удобном для REBOL. "Write" сохраняет данные в сырой, "не РЕБОЛИЗОВАННОЙ" форме. "Load" (загрузить) и "read" (прочитать) имеют схожие отношения. "Load" считывает данные таким образом, чтобы их можно было автоматически понять и использовать в коде REBOL. "Read" открывает данные точно в том формате, в котором они сохранены, байт за байтом. Как правило, данные, которые являются "сохранёнными" (save), также должны быть "загружены" (load), а данные, которые "записывают" (write), должны быть "прочитаны" (read). Для получения дополнительной информации см. следующие словарные статьи REBOL:

http://rebol.com/docs/words/wload.html

http://rebol.com/docs/words/wsave.html

http://rebol.com/docs/words/wread.html

http://rebol.com/docs/words/wwrite.html

Другие встроенные слова, такие как "mold" и "reform", помогают работать с текстом способами, которые либо более удобочитаемы, либо более читаемы интерпретатором REBOL. Для полезного объяснения см. http://www.rebol.net/cookbook/recipes/0015.html.

9) Порядок приоритета - выражения REBOL всегда оцениваются слева направо, независимо от задействованных операций. Если вы хотите, чтобы сначала вычислялись определённые математические операторы, их следует заключить в круглые скобки или поставить первыми в выражении. Например, интерпретатору REBOL:

2 + 4 * 6

тоже самое, что:

(2 + 4) * 6  ; левое выражение считается первым

== 6 * 6

== 36

Это противоречит другим знакомым правилам оценки. Например, во многих языках умножение обычно выполняется перед сложением. Итак, то же выражение:

2 + 4 * 6

тоже самое, что:

2 + (4 * 6)  ; умножение выполняется первым

== 2 + 24

== 26

Просто помните, что оценка всегда выполняется слева направо без исключения.

10) Вы можете столкнуться с проблемами при копировании/вставке скриптов интерактивной консоли непосредственно в интерпретатор REBOL, особенно когда код содержит такие функции, как "ask", которые требуют ответа от пользователя перед оценкой остальной части скрипта (каждая строка скрипта просто запускается по завершении операции вставки без какого-либо ответа от пользователя, оставляя необходимые переменные неназначенными). Чтобы исправить такие проблемы с интерактивностью при копировании/вставке кода консоли в интерпретатор, просто заключите весь скрипт в квадратные скобки, а затем "сделайте" (do) этот блок: do [... ваш полный код скрипта ...]. Это заставит загружать весь скрипт до того, как будет оценён какой-либо код. Если вы хотите запустить код несколько раз, просто назначьте ему метку слова, а затем запустите метку слова столько раз, сколько необходимо: do x: [... ваш полный код сценария ...] do x do x do x .... Это избавляет вас от необходимости вставлять код более одного раза. Другой эффективный вариант, особенно с большими скриптами - запустить код из буфера обмена, используя "do read clipboard://". Это работает намного быстрее, чем просмотр больших объемов текста, вставленных в консоль.

12.4.1 Ошибки захвата

Есть несколько простых способов уберечь вашу программу от сбоя при возникновении ошибки. Слова "error?" (ошибка?) и "try" (попытка) вместе обеспечивают способ проверки и обработки ожидаемых ошибок. Например, если нет подключения к Интернету, приведённый ниже код внезапно завершится с ошибкой:

html: read http://rebol.com

Приведённый ниже скорректированный код будет более изящно обрабатывать ошибку:

if error? try [html: read http://rebol.com] [
    alert "Unavailable."
]

Слово "attempt" (попытка) является альтернативой подпрограмме "error? try". В случае успеха он возвращает оценённое содержимое данного блока. В противном случае возвращается "none":

if not attempt [html: read http://rebol.com] [
    alert "Unavailable."
]

Чтобы уточнить, "error? try [block]" оценивается как true, если блок вызывает ошибку, а "try [block]" оценивается как false, если блок вызывает ошибку.

Полное объяснение кодов ошибок REBOL см. По адресу: http://www.rebol.com/docs/core23/rebolcore-17.html.

13. Создание веб-приложений с использованием REBOL CGI

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

В веб-приложениях CGI HTML-формы на веб-сайте действуют как пользовательский интерфейс (GUI) для сценариев, выполняемых на веб-сервере. Обычно пользователи вводят текст в поля, выбирают варианты из раскрывающихся списков, устанавливают флажки и в противном случае вводят данные в "виджеты" формы на веб-странице, а затем нажимают кнопку "Отправить". Отправленные данные передаются и обрабатываются сценарием, который вы сохранили по указанному URL-адресу (Интернет-адресу) на вашем веб-сервере. Выходные данные сценария затем отправляются обратно в браузер пользователя и отображаются на экране в виде динамически создаваемой веб-страницы. Программы такого рода CGI, работающие на веб-сайтах, являются одними из наиболее распространённых типов компьютерных приложений в современном использовании. PHP, Python, Java, PERL и ASP - популярные языки, используемые для решения аналогичных задач Интернет-программирования, но если вы знаете REBOL, вам не нужно их изучать. Интерфейс CGI REBOL делает программирование в Интернете очень простым.

Для создания программ REBOL CGI вам понадобится доступный веб-сервер. Веб-сервер - это компьютер, подключённый к Интернету, на котором постоянно выполняется программа, которая хранит и отправляет текст и данные веб-страницы по запросу из Интернет-браузера, запущенного на другом компьютере. Самым популярным приложением для веб-обслуживания является Apache. Большинство небольших веб-сайтов обычно запускаются на общих учётных записях хостинга веб-серверов, арендованных в центре обработки данных за несколько долларов в месяц (см. http://www.lunarpages.com - они подходят для REBOL). При настройке учётной записи веб-сервера вы можете зарегистрировать доступное доменное имя (например, www.yourwebsitename.com). Когда посетители веб-сайта вводят адрес вашего домена ".com" в своем браузере, они видят файлы, которые вы создали и сохранили в общедоступной файловой папке на вашем компьютере веб-сервера. Компании, занимающиеся веб-хостингом, обычно предоставляют набор программных веб-инструментов, которые позволяют загружать, просматривать, редактировать и управлять файлами, папками, электронной почтой и другими ресурсами на вашем сервере. Самый популярный из этих инструментов - cPanel. Независимо от того, какой язык вы используете для программирования веб-приложений, регистрация домена, покупка размещённого пространства и обучение управлению файлами и ресурсами с помощью таких инструментов, как cPanel, является необходимой отправной точкой. Lunarpages и HostGator - два рекомендуемых хоста для сценариев REBOL CGI. Полнофункциональный хостинг веб-сайтов с предварительно настроенным интерфейсом cPanel, неограниченным пространством и пропускной способностью, а также всеми инструментами, необходимыми для создания профессиональных веб-сайтов и приложений, доступен менее чем за 10 долларов в месяц.

Для запуска сценариев REBOL CGI на вашем веб-сервере должен быть установлен интерпретатор REBOL. Для этого загрузите с сайта rebol.com правильную версию интерпретатора REBOL для операционной системы, в которой работает ваш веб-сервер (чаще всего это Linux). Загрузите его в свой путь пользователя на своем веб-сервере и установите разрешения, позволяющие его запускать (обычно "755"). Спросите хозяина своего веб-сайта, если вы не понимаете, что это значит. http://rebol.com/docs/cgi1.html#section-2.2 содержит базовую информацию о том, как установить REBOL на ваш сервер. Если у вас нет онлайн-учётной записи веб-сервера, вы можете загрузить полнофункциональный бесплатный пакет веб-сервера Apache, который будет работать на вашем локальном ПК с Windows, с http://www.uniformserver.com.

13.1 Ускоренный курс HTML

Чтобы создать любое приложение CGI, вам нужно немного разбираться в HTML. HTML - это язык макета, используемый для форматирования текста и элементов графического интерфейса пользователя на всех веб-страницах. HTML - это не язык программирования - у него нет средств для обработки или манипулирования данными. Это просто формат разметки, который позволяет вам формировать внешний вид текста, изображений и других элементов на страницах, просматриваемых в браузере.

В HTML элементы на веб-странице заключаются между начальным и конечным "тегами":

<НАЧАЛЬНЫЙ ТЭГ> Какой-то элемент, который нужно добавить на веб-страницу </ КОНЕЧНЫЙ ТЭГ>

Есть теги, чтобы всячески влиять на макет. Например, чтобы выделить текст жирным шрифтом, заключите его в открывающие и закрывающие теги "strong":

<STRONG>некоторый жирный текст</STRONG>

Приведённый выше код отображается на веб-странице как некоторый жирный текст

Чтобы выделить текст курсивом, заключите его в теги <i> и </i>:

<i>некоторый курсивный текст</i>

Приведённый выше код отображается на веб-странице как некоторый курсивный текст

Чтобы создать таблицу с тремя строками данных, выполните следующие действия:

<TABLE border=1>
    <TR><TD>Первая строка</TD></TR>
    <TR><TD>Вторая строка</TD></TR>
    <TR><TD>Третья строка</TD></TR>
</TABLE>

Обратите внимание, что каждому

<открывающиму тегу>

в HTML-коде следует соответствующий

</закрывающий тег>

Некоторые теги окружают всю страницу, некоторые - части страницы, и они часто вложены друг в друга для создания более сложных дизайнов.

Ниже показан минимальный формат для создания веб-страницы. Обратите внимание, что заголовок вложен между тегами "head", а весь документ вложен в теги "HTML". Контент страницы, который видит пользователь, окружен тегами "body":

<HTML>
    <HEAD>
        <TITLE>Заголовок страницы</TITLE>
    </HEAD>
    <BODY>
        Основной и <i>форрматированый</i> HTML текст тут...
    </BODY>
</HTML>

Если вы сохраните приведённый выше код в текстовый файл с именем "yourpage.html", загрузите его на свой веб-сервер и перейдёте по адресу http://yourwebserver.com/yourpage.html, вы увидите в своём браузере страницу с заголовком "Заголовок страницы" с текстом "Основной и форрматированый HTML текст тут...". Так работают все веб-страницы - это руководство фактически представляет собой HTML-документ, хранящийся в учётной записи автора на веб-сервере. Нажмите "Просмотр" -> "Источник" в своём браузере, и вы увидите HTML-теги, которые использовались для форматирования этого документа.

13.1.1 HTML-формы и серверные сценарии - базовая модель CGI

Следующий пример HTML содержит тег "form" внутри стандартного макета заголовка и тела HTML. Внутри тегов формы находится тег поля ввода текста и тег кнопки отправки:

<HTML>
    <HEAD><TITLE>Data Entry Form</TITLE></HEAD>
    <BODY>
        <FORM ACTION="http://yourwebserver.com/your_rebol_script.cgi">
            <INPUT TYPE="TEXT" NAME="username" SIZE="25">
            <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
        </FORM>
    </BODY>
</HTML>

Формы могут содержать теги для различных типов ввода: многострочные текстовые области, выпадающие поля выбора, флажки и т.д. См. Http://www.w3schools.com/html/html_forms.asp для получения дополнительной информации о тегах форм.

Вы можете использовать данные, введенные в любую форму, указав адрес действия на URL-адрес, по которому расположен конкретный сценарий REBOL. Например, 'FORM ACTION = "http://yourwebserver.com/your_rebol_script.cgi" в приведенной выше форме может указывать на URL-адрес следующего сценария CGI, который сохраняется в виде текстового файла на вашем веб-сервере. Когда посетитель веб-сайта нажимает кнопку отправки в приведенной выше форме, данные отправляются в следующую программу, которая, в свою очередь, выполняет некоторую обработку и распечатывает выходные данные непосредственно в веб-браузере пользователя. ПРИМЕЧАНИЕ: Помните, что в REBOL фигурные скобки совпадают с кавычками. Фигурные скобки используются во всех следующих примерах, поскольку они допускают многострочный контент и помогают улучшить читаемость, четко показывая, где строки начинаются и заканчиваются:

#!/home/your_user_path/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}  
; строка выше такая же, как:  print "content-type: text/html^/"
submitted: decode-cgi system/options/cgi/query-string

print {<HTML><HEAD><TITLE>Page title</TITLE></HEAD><BODY>}
print rejoin [{Hello } second submitted {!}]
print {</BODY></HTML>}

Чтобы приведённый выше код действительно работал на вашем веб-сервере, рабочий интерпретатор REBOL должен быть установлен по пути, обозначенному "/home/your_user_path/rebol/rebol -cs".

Первые 4 строки приведённого выше сценария в основном представляют собой базовый код. Включите их в начало каждого сценария REBOL CGI. Обратите внимание на строку "decode-cgi" - это ключ к получению данных, отправленных HTML-формами. В приведённом выше коде декодированным данным присваивается имя переменной "submitted" (отправлено). Отправленными данными формы можно манипулировать как угодно, а затем вывод возвращается пользователю через функцию "print". Это важно понимать: все данные, "распечатываемые" программой REBOL CGI, появляются непосредственно в веб-браузере пользователя (то есть для веб-посетителя, который ввёл данные в HTML-форму). Печатные данные обычно размещаются в формате HTML, поэтому в браузере пользователя они выглядят как красиво оформленная веб-страница.

Любой нормальный код REBOL может быть включён в сценарий CGI - вы можете выполнять любой тип хранения, поиска, организации и манипулирования данными, которые могут происходить в любой другой программе REBOL. Интерфейс CGI просто позволяет вашему коду REBOL запускаться онлайн на вашем веб-сервере и вводить/выводить данные через веб-страницы, которые также хранятся на веб-сервере и доступны для любого браузера посетителя.

13.2 Стандартный CGI шаблон для запоминания

Большинство коротких программ CGI обычно распечатывают исходную HTML-форму для получения данных от пользователя. В исходной печатной форме адрес действия обычно указывает на тот же URL-адрес, что и сам сценарий. Сценарий проверяет отправленные данные, и если они пустые (т.е. данные не были отправлены), программа печатает исходную HTML-форму. В противном случае он манипулирует отправленными данными выбранным вами способом(ами), а затем выводит некоторые выходные данные в веб-браузер пользователя в виде новой HTML-страницы. Вот базовый пример этого процесса с использованием приведённого выше кода:

#!/home/your_user_path/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}
submitted: decode-cgi system/options/cgi/query-string

; Четыре строки выше - это стандартные заголовки REBOL CGI.
; В приведенной ниже строке печатаются стандартные теги HTML, head 
; и body в браузере посетителя:

print {<HTML><HEAD><TITLE>Page title</TITLE></HEAD><BODY>}

; Затем определите, были ли отправлены какие-либо данные. 
; Распечатайте начальную форму, если она пуста. В противном случае 
; обработайте и распечатайте некоторый HTML, используя отправленные 
; данные. Наконец, распечатайте стандартные закрывающие теги body и 
; html, которые были открыты выше:

either empty? submitted [
    print {
        <FORM ACTION="http://yourwebserver.com/this_rebol_script.cgi">
        <INPUT TYPE="TEXT" NAME="username" SIZE="25">
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
        </FORM>
        </BODY></HTML>
    }
] [ 
    print rejoin [{Hello } second submitted {!}]
    print {</BODY></HTML>}
]

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

14. Примеры приложений CGI

14.1 Почтовая форма

Вот почтовая программа REBOL CGI, которая печатает исходную форму, а затем отправляет электронное письмо на заданный адрес, содержащее данные, отправленные пользователем:

#!/home/youruserpath/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}
submitted: decode-cgi system/options/cgi/query-string

; для отправки электронной почты требуется следующая информация об 
; учётной записи:

set-net [from_address@website.com  smtp.website.com]

; напечатаем более сложный заголовок HTML:

print read %template_header.html

; если в скрипт были отправлены некоторые данные формы:

if not empty? submitted [
    sent-message: rejoin [
        newline {INFO SUBMITTED BY WEB FORM} newline newline
        {Time Stamp: } (now + 3:00) newline
        {Name: } submitted/2 newline 
        {Email: } submitted/4 newline
        {Message: } submitted/6 newline 
    ]

    send/subject to_address@website.com sent-message "FORM SUBMISSION"

    html: copy {}
    foreach [var value] submitted [
        repend html [<TR><TD> mold var </TD><TD> mold value </TD></TR>]
    ]
    print {<font size=5>Thank You!</font> <br><br>
        The following information has been sent: <BR><BR>}
    print rejoin [{Time Stamp: } now + 3:00]
    print {<BR><BR><table>}
    print html
    print {</table>}
    ; напечатайте более сложный нижний колонтитул HTML:
    print read %template_footer.html
    quit
]

; если данные формы не были отправлены, распечатаем исходную форму:

print {
    <CENTER><TABLE><TR><TD>
    <BR><strong>Please enter your info below:</strong><BR><BR>
    <FORM ACTION="http://yourwebserver.com/this_rebol_script.cgi">
    Name: <BR> <INPUT TYPE="TEXT" NAME="name"><BR><BR>
    Email: <BR> <INPUT TYPE="TEXT" NAME="email"><BR><BR>
    Message: <BR>
    <TEXTAREA cols=75 name=message rows=5></TEXTAREA> <BR><BR>
    <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
    </FORM>
    </TD></TR></TABLE></CENTER>
}
print read %template_footer.html

Файл template_header.html, используемый в приведённом выше примере, может включать стандартную требуемую схему HTML, а также любые теги форматирования и статическое содержимое, которое вам нужно, для представления красиво оформленной страницы. Базовый макет может включать что-то вроде следующего:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML><HEAD><TITLE>Page Title</TITLE>    
<META http-equiv=Content-Type content="text/html; 
    charset=windows-1252">
</HEAD>    
<BODY bgColor=#000000>
<TABLE align=center background="" border=0 
    cellPadding=20 cellSpacing=2 height="100%" width="85%">
<TR>
<TD background="" bgColor=white vAlign=top>

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

</TD>
</TR>
</TABLE>
<TABLE align=center background="" border=0 
    cellPadding=20 cellSpacing=2 width="95%">
<TR>
<TD background="" cellPadding=2 bgColor=#000000 height=5>
<P align=center><FONT color=white size=1>Copyright © 2009
    Yoursite.com.  All rights reserved.</FONT>
</P>
</TD>
</TR>
</TABLE>
</BODY>
</HTML>

14.2 Типичное приложение с раскрывающимся списком

В следующем примере показано, как автоматически создавать списки дней, месяцев, времени и данных, считанных из файла, с помощью динамических циклов (foreach, for и т.д.). Элементы можно выбрать из раскрывающихся списков в распечатанной HTML-форме:

#!/home/youruserpath/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}
submitted: decode-cgi system/options/cgi/query-string

print {<HTML><HEAD><TITLE>Dropdown Lists</TITLE></HEAD><BODY>}

if not empty? submitted [
    print rejoin [{NAME SELECTED: } submitted/2 {<BR><BR>}]
    selected: rejoin [
        {TIME/DATE SELECTED: }
        submitted/4 { } submitted/6 {, } submitted/8
    ]
    print selected
    quit
]

; Если данные не были отправлены, распечатаем исходную форму:

print {<FORM ACTION="http://yourwebserver.com/your_rebol_script.cgi">
   SELECT A NAME:  <BR> <BR>}
names: read/lines %users.txt
print {<select NAME="names">}
foreach name names [prin rejoin [{<option>} name]]
print {</option> </select> <br> <br>}

print { SELECT A DATE AND TIME: }
print rejoin [{(today's date is } now/date {)} <BR><BR>]

print {<select NAME="month">}
foreach m system/locale/months [prin rejoin [{<option>} m]]
print {</option> </select>}

print {<select NAME="date">} 
for daysinmonth 1 31 1 [prin rejoin [{<option>} daysinmonth]]
print {</option> </select>}

print {<select NAME="time">
for time 10:00am 12:30pm :30 [prin rejoin [{<option>} time]]
for time 1:00 10:00 :30 [prin rejoin [{<option>} time]]
print {</option> </select> <br> <br>}

print {<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit"></FORM>}

Файл "users.txt", использованный в приведённом выше примере, может выглядеть примерно так:

nick
john
jim
bob

14.3 Фотоальбом

Вот простая программа CGI, которая отображает все фотографии в текущей папке на веб-сайте, используя цикл foreach:

#! /home/path/public_html/rebol/rebol -cs
REBOL [title: "Photo Viewer"]
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>Photos</TITLE></HEAD><BODY>}
print read %template_header.html

folder: read %.
count: 0
foreach file folder [
    foreach ext [".jpg" ".gif" ".png" ".bmp"] [
        if find file ext [
            print [<BR> <CENTER>]
            print rejoin [{<img src="} file {">}]
            print [</CENTER>]
            count: count + 1
        ]
    ]
]
print {<BR>}
print rejoin [{Total Images: } count]
print read %template_footer.html

Обратите внимание, что в приведенном выше примере нет кода "submit: decode-cgi system/options/cgi/query-string". Это потому, что приведённый выше сценарий не использует никаких данных, отправленных из формы.

14.4 Простая интерактивная консоль веб-сайта REBOL

Вот простой, но мощный скрипт, который позволяет вам вводить код REBOL в текстовую область HTML и выполнять этот код непосредственно на вашем веб-сервере. Затем результаты кода отображаются в вашем браузере. По сути, он работает как удалённая консоль для интерпретатора REBOL на вашем сервере. Вы можете использовать его для запуска кода REBOL или для вызова программ оболочки прямо на вашем веб-сайте - очень мощно! НЕ запускайте это на своём веб-сервере, если вас вообще беспокоит безопасность !:

#! /home/path/public_html/rebol/rebol -cs
REBOL [Title: "CGI Remote Console"]
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>Console</TITLE></HEAD><BODY>}
submitted: decode-cgi system/options/cgi/query-string

; Если данные не были отправлены, распечатаем форму для запроса 
; имени пользователя и пароля:

if ((submitted/2 = none) or (submitted/4 = none)) [
    print {
        <STRONG>W A R N I N G  -  Private Server, Login Required:</STRONG>
        <BR><BR>
        <FORM ACTION="./console.cgi">
        Username: <INPUT TYPE=text SIZE="50" NAME="name"><BR><BR>
        Password: <INPUT TYPE=text SIZE="50" NAME="pass"><BR><BR>
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
        </FORM>
    }
    quit
]

; Если код был отправлен, распечатаем вывод

entry-form: [
    print {
        <CENTER><FORM METHOD="get" ACTION="./console.cgi">
        <INPUT TYPE=hidden NAME=submit_confirm VALUE="command-submitted">
        <TEXTAREA COLS="100" ROWS="10" NAME="contents"></TEXTAREA><BR><BR>
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
        </FORM></CENTER></BODY></HTML>
    }
]

if submitted/2 = "command-submitted" [
    write %commands.txt join "REBOL[]^/" submitted/4
    ; The "call" function requires REBOL version 2.76:
    call/output/error 
        {/home/path/public_html/rebol/rebol -qs commands.txt}
        %conso.txt %conse.txt
    do entry-form
    print rejoin [
        {<CENTER>Output: <BR><BR>}
        {<TABLE WIDTH=80% BORDER="1" CELLPADDING="10"><TR><TD><PRE>}
        read %conso.txt
        {</PRE></TD></TR></TABLE><BR><BR>}
        {Errors: <BR><BR>}
        read %conse.txt
        {</CENTER>}
    ]
    quit
]

; В противном случае проверьте отправленного пользователя/пароль,
; затем распечатаем форму для ввода кода:

username: submitted/2 password: submitted/4 
either (username = "user") and (password = "pass") [
    ; если пользователь/пароль в порядке, продолжаем
][
    print "Incorrect Username/Password." quit
]

do entry-form

Загрузите скрипт на свой сервер, переименуйте его в console.cgi, сделайте его исполняемым и измените путь к вашему интерпретатору REBOL (2 места в скрипте). Затем попробуйте запустить следующий пример кода:

print 352 + 836
? system/locale/months 
call "ls -al"

14.5 Посещаемость

Вот пример, который позволяет пользователям проверять посещаемость на различных еженедельных мероприятиях и добавлять/удалять свои имена для каждого из событий. Он хранит всю информацию о пользователе в плоском файле (простой текстовый файл) с именем "jams.db":

#! /home/path/public_html/rebol/rebol -cs
REBOL [title: "event.cgi"]
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>Event Sign-Up</TITLE></HEAD><BODY>}

jams: load %jam.db

a-line: [] loop 65 [append a-line "-"]
a-line: trim to-string a-line

print {
    <hr> <font size=5>" Sign up for an event:"</font> <hr><BR>
    <FORM ACTION="http://yourwebsite.com/cgi-bin/event.cgi">
    Student Name:
    <input type=text size="50" name="student"><BR><BR>
    ADD yourself to this event:             "
    <select NAME="add"><option>""<option>"all"
}
foreach jam jams [prin rejoin [{<option>} jam/1]]
print {
    </option> </select> <BR> <BR>
    REMOVE yourself from this event:
    <select NAME="remove"><option>""<option>"all"
}
foreach jam jams [prin rejoin [{<option>} jam/1]]
print {
    </option> </select> <BR> <BR>
    <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
    </FORM>
}

print-all: does [
    print [<br><hr><font size=5>]
    print " Currently scheduled events, and current attendance:"
    print [</font><br>]
    foreach jam jams [
        print rejoin [a-line {<BR>} jam/1 {BR} a-line {<BR>}]
        for person 2 (length? jam) 1 [
            print jam/:person
            print {<BR>}
        ]
        print {<BR>}
    ]
    print {</BODY></HTML>}
]

submitted: decode-cgi system/options/cgi/query-string

if submitted/2 <> none [
    if ((submitted/4 = "") and (submitted/6 = "")) [
        print {
            <strong> Please try again. You must choose an event.</strong>
        }
        print-all
        quit
    ]
    if ((submitted/4 <> "") and (submitted/6 <> "")) [
        print {
            <strong> Please try again. Choose add OR remove.</strong>
        }
        print-all
        quit
    ]
    if submitted/4 = "all" [
        foreach jam jams [append jam submitted/2]
        save %jam.db jams
        print {
            <strong> Your name has been added to every event:</strong>
        }
        print-all
        quit
    ]
    if submitted/6 = "all" [
        foreach jam jams [
            if find jam submitted/2 [
                remove-each name jam [name = submitted/2]
                save %jam.db jams
            ]
        ]
        print {
            <strong> Your name has been removed from all events:</strong>
        }
        print-all
        quit
    ]
    foreach jam jams [
        if (find jam submitted/4) [
            append jam submitted/2
            save %jam.db jams
            print {
                <strong> Your name has been added to the selected event:
                </strong>
            }
            print-all
            quit    
        ]
    ]
    found: false
    foreach jam jams [
        if (find jam submitted/6) [
            if (find jam submitted/2) [
                remove-each name jam [name = submitted/2]
                save %jam.db jams
                print {
                    <strong>
                    Your name has been removed from the selected event:
                    </strong>
                }
                print-all
                quit
                found: true
            ]
        ]
    ]
    if found <> true [
        print {
            <strong> That name is not found in the specified event!"
            </strong>
        }
        print-all
        quit
    ]
]

print-all

Вот образец файла данных "jam.db", использованного в приведённом выше примере:

["Sunday September 16, 4:00 pm"
    "Nick Antonaccio" "Ryan Gaughan" "Mark Carson"]
["Sunday September 23, 4:00 pm"
    "Nick Antonaccio" "Ryan Gaughan" "Mark Carson"]
["Sunday September 30, 4:00 pm"
    "Nick Antonaccio" "Ryan Gaughan" "Mark Carson"]

14.6 Доска объявлений

Вот простая программа для доски объявлений на веб-сайте:

#! /home/path/public_html/rebol/rebol -cs
REBOL [title: "Jam"]
print {content-type: text/html^/}
print read %template_header.html
; print {<HTML><HEAD><TITLE>Bulletin Board</TITLE></HEAD><BODY>}

bbs: load %bb.db

print {
    <center><table border=1 cellpadding=10 width=600><tr><td>
    <center><strong><font size=4>
    Please REFRESH this page to see new messages.
    </font></strong></center>
}

print-all: does [
    print {<br><hr><font size=5> Posted Messages: </font> <br><hr>}
    foreach bb (reverse bbs) [
        print rejoin [
            {<BR>Date/Time: } bb/2 {         }
            {"Name: } bb/1 {<BR><BR>} bb/3 {<BR><BR><HR>}
        ]
    ]
]

submitted: decode-cgi system/options/cgi/query-string

if submitted/2 <> none [
    entry: copy []
    append entry submitted/2
    append entry to-string (now + 3:00)
    append entry submitted/4
    append/only bbs entry
    save %bb.db bbs
    print {<BR><strong>Your message has been added: </strong><BR>}
]

print-all

print {
    <font size=5> Post A New Public Message:</font><hr>
    <FORM ACTION="http://website.com/bb/bb.cgi">
    <br> Your Name:  <br>
    <input type=text size="50" name="student"><BR><BR>
    Your Message: <br>
    <textarea name=message rows=5 cols=50></textarea><BR><BR>
    <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Post Message">
    </FORM>
    </td></tr></table></center>
}
print read %template_footer.html

Вот пример файла данных для указанной выше программы:

[
    [
        "Nick Antonaccio"
        "8-Nov-2006/4:55:59-8:00"
        {
            WELCOME TO OUR PUBLIC BULLETIN BOARD.
             Please keep the posts clean cut and on topic.
            Thanks and have fun!
        }
    ]
]

14.7 Пример GET и POST

Формат по умолчанию для данных REBOL CGI - "GET". Данные, отправленные методом GET в форме HTML, отображаются в строке URL-адреса браузера пользователя. Если вы не хотите, чтобы пользователи видели отображаемые данные, или если количество отправленных данных больше, чем может содержаться в строке URL-адреса браузера, следует использовать метод "POST". Для работы с методом POST действие в вашей HTML-форме должно быть:

<FORM METHOD="post" ACTION="./your_script.cgi">

Вы также должны использовать функцию "read-cgi" ниже, чтобы декодировать отправленные данные POST в вашем сценарии REBOL. В этом примере создаётся текстовый онлайн-редактор, защищённый паролем, с функцией автоматического резервного копирования:

#!/home/path/public_html/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>Edit Text Document</TITLE></HEAD><BODY>}

; submitted: decode-cgi system/options/cgi/query-string

; Мы не можем использовать обычную строку выше для декодирования, 
; потому что мы используем метод POST для отправки данных (поскольку 
; данные из текстового поля могут стать слишком большими для метода 
; GET). Вместо этого используйте следующую стандартную функцию для 
; обработки данных из метода POST:
read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]

submitted: decode-cgi read-cgi

; если document.txt был отредактирован и отправлен:

if submitted/2 = "save" [ 
    ; save newly edited document:
    write to-file rejoin ["./" submitted/6 "/document.txt"] submitted/4
    print ["Document Saved."]
    print rejoin [
        {<META HTTP-EQUIV="REFRESH" CONTENT="0; 
            URL=http://website.com/folder/}
        submitted/6 {">}
    ]
    quit
]

; если пользователь только открывает страницу (т.е. данные ещё не 
; отправлены), запросите имя пользователя и пароль:

if ((submitted/2 = none) or (submitted/4 = none)) [
    print {
        <strong>W A R N I N G  -  Private Server, Login Required:
        </strong><BR><BR>
        <FORM ACTION="./edit.cgi">
        Username: <input type=text size="50" name="name"><BR><BR>
        Password: <input type=text size="50" name="pass"><BR><BR>
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
        </FORM>
    }
    quit
]

; проверяем имя пользователя и пароль на соответствие тем, что в 
; userlist.txt, завершим программу, если они неверны:

userlist: load %userlist.txt
folder: submitted/2 
password: submitted/4
response: false
foreach user userlist [
    if ((first user) = folder) and (password = (second user)) [
        response: true
    ]
]
if response = false [print {Incorrect Username/Password.} quit]

; если пользователь/пароль в порядке, продолжаем ...

; резервное копирование (до внесения изменений):

cur-time: to-string replace/all to-string now/time {:} {-}
document_text: read to-file rejoin [{./} folder {/document.txt}]
write to-file rejoin [
    {./} folder {/} now/date {_} cur-time {.txt}] document_text

;обратите внимание на метод POST в HTML-форме:

prin {
    <strong>Be sure to SUBMIT when done:</strong><BR><BR>
    <FORM method="post" ACTION="./edit.cgi">
    <INPUT TYPE=hidden NAME=submit_confirm VALUE="save">
    <textarea cols="100" rows="15" name="contents">
}
;
; Следующая строка - это то, что мы хотим сделать, но это не 
; сработает для HTML-документов, содержащих <textarea> 
;
; распечатать документ_текст
;
; Следующая строка устраняет проблему: 
;
prin replace/all document_text {</textarea>} {&lt\/textarea&gt;}
print {</textarea><BR><BR>}
print rejoin [{<INPUT TYPE=hidden NAME=folder VALUE="} folder {">}]
print {<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">}
print {</FORM>}
print {</BODY></HTML>}

14.8 Система групповых заметок

Ранее в руководстве была представлена полнофункциональная система обмена заметками, позволяющая группам пользователей обмениваться сообщениями. Эта программа CGI позволяет пользователям вводить и отображать заметки, используя те же файлы данных, которые сохраняются настольной программой. Настольная версия приложения и эта веб-программа на 100% совместимы - сообщения, введённые с помощью настольного приложения, можно сразу просмотреть с помощью этого CGI, и наоборот.

Обратите внимание, что в четвёртой строке используется функция "read-cgi". В последних версиях REBOL эта функция является встроенной, поэтому её не нужно определять в коде (рекомендуется включить определение функции, если сценарий когда-либо используется на веб-сервере с более ранней версией версия REBOL):

#! ../rebol276 -cs
REBOL [title: "Group Notes"]
print {content-type: text/html^/}
submitted: decode-cgi read-cgi
url: ftp://user:pass@site.com/public_html/Notes
print {
    <center><table border=0 cellpadding=10 width=600><tr><td>
}
if submitted/2 <> none [
    if error? try [
        write/lines/append url rejoin [
            "^/^/" now " (" submitted/2 "):  " submitted/4
        ]
    ] [print "ERROR: Not Saved" quit]
]
print {<pre>}
if error? try [notes: copy read/lines url] [write url notes: ""]
display: copy {}
count: 0
remove/part notes 2
foreach note reverse notes [
    either note = "" [
        note: {<br>}
    ] [
        count: count + 1
        note: rejoin [count ") "note]
    ]
    append display note
]
print display
print {
    </td></tr></table>
    <FORM ACTION="http://re-bol.com/notes.cgi">
        <table border=0 cellPadding=0 cellSpacing=4 width=600>
        <tr><td width=10%>Name:</td>
        <td><input type=text size="65" name="username"></td></tr>
        <tr><td width=10%>Message:</td>
        <td><textarea name=message rows=5 cols=50></textarea></td></tr>
        <tr><td></td><td>
        <input type="submit" name="submit" value="Submit">
        </td></tr></table>
    </FORM>
    </td></tr></table></center>
}

14.9 Универсальный обработчик форм

Ниже приведён общий обработчик формы, который можно использовать для сохранения данных GET или POST в текстовый файл. Это полезная замена универсальным почтовым программам форм и делает данные намного более доступными позже для других скриптов:

#!/home/path/public_html/rebol/rebol -cs
REBOL []
print {content-type: text/html^/}

read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]
submitted: decode-cgi read-cgi

print {
    <HTML><HEAD><TITLE>Your Form Has Been Submitted</TITLE></HEAD>
    <BODY><CENTER><TABLE border=0 cellPadding=10 width="80%"><TR><TD>
}

entry: rejoin [{[^/ "Time Stamp:" } {"} form (now + 3:00) {"^/}]
foreach [title value] submitted [
    entry: rejoin [entry { } {"} mold title {" } mold value {^/}]
]
append entry {]^/}
write/append %submitted_forms.txt entry

html: copy ""
foreach [title value] submitted [
    repend html [
        <TR><TD width=20%> mold title </TD><TD> mold value </TD></TR>
    ]
]

print rejoin [
    {
         <FONT size=5>Thank You! The following information has been
         submitted: </FONT><BR><BR>Time Stamp:
    } 
    now + 3:00  {<BR><BR><TABLE border=1 cellPadding=10 width="100%">}
    html  {</TABLE><BR>}
    {  
        To correct any errors or to submit forms for additional people,
        please click the [BACK] button in your browser, make any 
        changes, and resubmit the form.  You'll hear from us shortly.
        Thank you!<BR><BR><CENTER><FONT size=1>
        Copyright © 2009 This Web Site.  All rights reserved.</FONT>
        </CENTER></TD></TR></CENTER></BODY></HTML>
    }
]

quit

Вот базовый пример формы, который может быть обработан приведённым выше скриптом. Вы можете добавить столько текста, текстовых полей и других элементов формы, сколько хотите, и сценарий сохранит все отправленные данные (ссылка на действие в форме ниже предполагает, что приведённый выше сценарий сохранен в текстовом файле с именем "form.cgi" ):

<FORM action="form.cgi">
    Name:<BR><INPUT type="TEXT" name="name"><BR><BR>
    Email:<BR><INPUT type="TEXT" name="email"><BR><BR>
    Message:<BR>
        <TEXTAREA cols="75" rows="5" name="message">
        </TEXTAREA><BR><BR>
    <INPUT type="SUBMIT" name="Submit" value="Submit">
</FORM>

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

REBOL [title: "CGI form submission viewer"]

sort-column: 4  ; even numered cols contain data (2nd col is time stamp)
signups: load http://yoursite.com/submitted_forms.txt
do create-list: [
    name-list: copy []
    foreach item signups [append name-list (pick item sort-column)]
]
view center-face layout [
    the-list: text-list 600x150 data name-list [
        foreach item signups [
            if (pick item sort-column) = value [
                dt: copy ""
                foreach [label data] item [
                    dt: rejoin [
                        dt newline label "  " data 
                    ]
                ]
                a/text: dt
                show a
            ]
        ]
    ]
    a: area 600x300 across
    btn "Sort by..." [
        sort-column: to-integer request-text/title/default {
            Data column to list:} "4"
        do create-list
        the-list/data: name-list
        show the-list
    ]
    tab text join (form length? signups) " entries."
]

Вот ещё один сценарий, который удаляет столбцы заголовка и сокращает данные формы до удобного формата. Возможности управления такими данными форм безграничны:

submissions: load http://yoursite.com/submitted_forms.txt
do create-list: [
    data: copy []
    foreach block submissions [append/only data (extract/index block 2 4)]
    datastring: copy {}
    foreach block data [
        datastring: join datastring "[^/"
        foreach item block [datastring: rejoin [datastring item newline]]
        datastring: join datastring "]^/^/"
    ]
    editor datastring
]

14.10 Загрузчик файлов

В следующем примере показано, как загружать файлы на ваш веб-сервер с помощью функции http://www.rebol.org/cgi-bin/cgiwrap/rebol/ml-display-thread.r?m=rmlKVSQ>decode-multipart-form-data Андреаса Болка:

#! /home/path/public_html/rebol/rebol -cs
REBOL [Title: "HTTP File Upload"]
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>File Upload</TITLE></HEAD>}
print {<BODY><br><br><center><table width=95% border=1>}
print {<tr><td width=100%><br><center>}

read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]

submitted: read-cgi

if submitted/2 = none [
    print {
        <FORM ACTION="./upload.cgi" 
        METHOD="post" ENCTYPE="multipart/form-data">
            <strong>Upload File:</strong><br><br> 
            <INPUT TYPE="file" NAME="photo"> <br><br>
            <INPUT TYPE="submit" NAME="Submit" VALUE="Upload">  
        </FORM>
        <br></center></td></tr></table></BODY></HTML>
    }
    quit
]

decode-multipart-form-data: func [
    p-content-type
    p-post-data
    /local list ct bd delim-beg delim-end non-cr non-lf non-crlf mime-part
] [
    list: copy []
    if not found? find p-content-type "multipart/form-data" [return list]
    ct: copy p-content-type
    bd: join "--" copy find/tail ct "boundary="
    delim-beg: join bd crlf
    delim-end: join crlf bd
    non-cr:     complement charset reduce [ cr ]
    non-lf:     complement charset reduce [ newline ]
    non-crlf:   [ non-cr | cr non-lf ]
    mime-part:  [
        ( ct-dispo: content: none ct-type: "text/plain" )
        delim-beg ; mime-part start delimiter
        "content-disposition: " copy ct-dispo any non-crlf crlf
        opt [ "content-type: " copy ct-type any non-crlf crlf ]
        crlf ; content delimiter
        copy content
        to delim-end crlf ; mime-part end delimiter
        ( handle-mime-part ct-dispo ct-type content )
    ]
    handle-mime-part: func [
        p-ct-dispo
        p-ct-type
        p-content
        /local tmp name value val-p
    ] [
        p-ct-dispo: parse p-ct-dispo {;="}
        name: to-set-word (select p-ct-dispo "name")
        either (none? tmp: select p-ct-dispo "filename")
               and (found? find p-ct-type "text/plain") [
            value: content
        ] [
            value: make object! [
                filename: copy tmp
                type: copy p-ct-type
                content: either none? p-content [none][copy p-content]
            ]
        ]
        either val-p: find list name
            [change/only next val-p compose [(first next val-p) (value)]]
            [append list compose [(to-set-word name) (value)]]
    ]
    use [ct-dispo ct-type content] [
        parse/all p-post-data [some mime-part "--" crlf]
    ]
    list
]

; После следующей строки "probe cgi-object" отобразит все части 
; отправленного составного объекта:

cgi-object: construct decode-multipart-form-data 
    system/options/cgi/content-type copy submitted

; Записываем файл на сервер, используя исходное имя файла, и уведомляем пользователя:

the-file: last split-path to-file copy cgi-object/photo/filename
write/binary the-file cgi-object/photo/content
print {
    <strong>UPLOAD COMPLETE</strong><br><br>
    <strong>Files currently in this folder:</strong><br><br>
}
folder: sort read %.
foreach file folder [
    print [rejoin [{<a href="./} file {">} file {</a><br>}]]
]
print {<br></td></tr></table></BODY></HTML>}

; В качестве альтернативы вы можете перейти на другую страницу, 
; когда закончите:
; 
; wait 3
; refresh-me: {
;     <head><title></title>
;     <META HTTP-EQUIV="REFRESH" CONTENT="0; URL=./index.cgi"></head>
; }
; print refresh-me

Этот вариант сценария загрузки позволяет вам выбрать каталог, в который будут загружены файлы:

#! /home/path/public_html/rebol/rebol -cs
REBOL [Title: "HTTP File Upload"]
print {content-type: text/html^/}
print {<HTML><HEAD><TITLE>File Upload</TITLE></HEAD>}
print {<BODY><br><br><center><table width=95% border=1>}
print {<tr><td width=100%><br><center>}

read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]

submitted: read-cgi

if submitted/2 = none [
    print {
        <FORM ACTION="./upload.cgi" 
        METHOD="post" ENCTYPE="multipart/form-data">
            <strong>Upload File:</strong><br><br> 
            <INPUT TYPE="file" NAME="photo"> <br><br>
            <INPUT TYPE="text" NAME="path" SIZE="35" 
                VALUE="/home/path/public_html/">
            <INPUT TYPE="submit" NAME="Submit" VALUE="Upload">  
        </FORM>
        <br></center></td></tr></table></BODY></HTML>
    }
    quit
]

decode-multipart-form-data: func [
    p-content-type
    p-post-data
    /local list ct bd delim-beg delim-end non-cr non-lf non-crlf mime-part
] [
    list: copy []
    if not found? find p-content-type "multipart/form-data" [return list]
    ct: copy p-content-type
    bd: join "--" copy find/tail ct "boundary="
    delim-beg: join bd crlf
    delim-end: join crlf bd
    non-cr:     complement charset reduce [ cr ]
    non-lf:     complement charset reduce [ newline ]
    non-crlf:   [ non-cr | cr non-lf ]
    mime-part:  [
        ( ct-dispo: content: none ct-type: "text/plain" )
        delim-beg ; mime-part start delimiter
        "content-disposition: " copy ct-dispo any non-crlf crlf
        opt [ "content-type: " copy ct-type any non-crlf crlf ]
        crlf ; content delimiter
        copy content
        to delim-end crlf ; mime-part end delimiter
        ( handle-mime-part ct-dispo ct-type content )
    ]
    handle-mime-part: func [
        p-ct-dispo
        p-ct-type
        p-content
        /local tmp name value val-p
    ] [
        p-ct-dispo: parse p-ct-dispo {;="}
        name: to-set-word (select p-ct-dispo "name")
        either (none? tmp: select p-ct-dispo "filename")
               and (found? find p-ct-type "text/plain") [
            value: content
        ] [
            value: make object! [
                filename: copy tmp
                type: copy p-ct-type
                content: either none? p-content [none][copy p-content]
            ]
        ]
        either val-p: find list name
            [change/only next val-p compose [(first next val-p) (value)]]
            [append list compose [(to-set-word name) (value)]]
    ]
    use [ct-dispo ct-type content] [
        parse/all p-post-data [some mime-part "--" crlf]
    ]
    list
]

cgi-object: construct decode-multipart-form-data 
    system/options/cgi/content-type copy submitted

the-file: last split-path to-file copy cgi-object/photo/filename
write/binary the-file cgi-object/photo/content
print {
    <strong>UPLOAD COMPLETE</strong><br><br>
    <strong>Files currently in this folder:</strong><br><br>
}
folder: sort read to-file cgi-object/path
current-folder: rejoin  at 
foreach file folder [
    print [rejoin [
        {<a href="http://site.com/"} (at cgi-object/path 28) file {">}
        ; convert path to URL
        file "</a><br>"
    ]]
]
print {<br></td></tr></table></BODY></HTML>}

14.11 Скачивальщик файлов

Вот сценарий, демонстрирующий, как принудительно загрузить файл в браузер пользователя:

#!/home/path/public_html/rebol/rebol -cs
REBOL []
submitted: decode-cgi system/options/cgi/query-string
root-path: "/home/path"

; if no data has been submitted, request file name:

if ((submitted/2 = none) or (submitted/4 = none)) [
    print "content-type: text/html^/"
    print [<STRONG>"W A R N I N G  -  "]
    print ["Private Server, Login Required:"</STRONG><BR><BR>]
    print [<FORM ACTION="./download.cgi">]
    print [" Username: " <INPUT TYPE=text SIZE="50" NAME="name"><BR><BR>]
    print [" Password: " <INPUT TYPE=text SIZE="50" NAME="pass"><BR><BR>]
    print [" File: "<BR><BR>]
    print [<INPUT TYPE=text SIZE="50" NAME="file" VALUE="/public_html/">]
    print [<BR><BR>]
    print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">]
    print [</FORM>]
    quit
]

; Проверяем user/pass, и завершаем, если неверные:

username: submitted/2 password: submitted/4 
either (username = "user") and (password = "pass) [
    ; if user/pass is ok, go on
][
    print "content-type: text/html^/"
    print "Incorrect Username/Password." quit
]

print rejoin [
    "Content-Type: application/x-unknown"
    newline
    "Content-Length: "
    (size? to-file join root-path submitted/6) 
    newline
    "Content-Disposition: attachment; filename=" 
    (second split-path to-file submitted/6)
    newline
]

data: read/binary to-file join root-path submitted/6
data-length: size? to-file join root-path submitted/6
write-io system/ports/output data data-length

14.12 Полноценное приложение для управления веб-сервером

Этот последний сценарий использует несколько предыдущих примеров и некоторый дополнительный код для формирования полноценного приложения для управления веб-сервером. Он позволяет отображать содержимое каталога, загружать, скачивать, редактировать и искать файлы, выполнять команды ОС (chmod, ls, mv, cp и т.д. - любые команды, доступные в операционной системе вашего веб-сервера) и напрямую запускать команды REBOL на вашем сервере. Отредактированные файлы перед сохранением автоматически копируются в папку "edit_history" на сервере. Для большинства веб-серверов настройка не требуется. Просто сохраните этот сценарий как "web-tool.cgi", загрузите его и интерпретатор REBOL в ту же папку, что и файл index.html вашего веб-сайта, установите права доступа (chmod) на 755, затем перейдите по адресу http://yourwebsite/web-tool.cgi.

ЭТОТ СЦЕНАРИЙ МОЖЕТ ПРЕДСТАВИТЬ СЕРЬЁЗНУЮ УГРОЗУ БЕЗОПАСНОСТИ ВАШЕМУ СЕРВЕРУ. Это потенциально может позволить любому получить контроль над вашим веб-сервером и всем, что он содержит. НЕ УСТАНАВЛИВАЙТЕ его на свой сервер, если вас вообще беспокоит безопасность или вы не знаете, как защитить свой сервер самостоятельно.

Первая строка этого сценария должна указывать на расположение интерпретатора REBOL на вашем веб-сервере, и вы должны использовать версию REBOL, которая поддерживает функцию "call" (рекомендуется версия 2.76). По умолчанию интерпретатор REBOL должен быть загружен по тому же пути, что и этот сценарий, эта папка должна быть общедоступной, и вы должны загрузить правильную версию REBOL для операционной системы, в которой работает ваш сервер. В ЭТОМ ПРИМЕРЕ ИНТЕРПРЕТАТОР REBOL БЫЛ ПЕРЕИМЕНОВАН на "REBOL276".

#! ./rebol276 -cs
REBOL [Title: "REBOL CGI Web Site Manager"]

;--------------------------------------------------------------------
; Загрузите этот скрипт по тому же пути, что и index.html на вашем 
; сервере, затем загрузите интерпретатор REBOL по указанному выше 
; пути (по умолчанию тот же путь, что и у скрипта). УСТАНОВИТЕ 
; РАЗРЕШЕНИЕ НА ИНТЕРПРЕТАТОР И СКРИПТ "755". Затем, чтобы запустить 
; программу, перейдите на www.yoursite.com/this-script.cgi.
;--------------------------------------------------------------------

; ВЫ МОЖЕТЕ РЕДАКТИРОВАТЬ ЭТИ ПЕРЕМЕННЫЕ, _ЕСЛИ_ НЕОБХОДИМО (измените 
; значения в кавычках):

; Имя пользователя, которое вы хотите использовать для входа:

    set-username:   "username"

; Пароль, который вы хотите использовать для входа:

    set-password:   "password"

;--------------------------------------------------------------------

; НЕ редактируйте эти переменные, если вы не уверенны, что знаете, 
; что делаете:

doc-path: to-string what-dir
script-subfolder: find/match what-dir doc-path
if script-subfolder = none [script-subfolder: ""]

;--------------------------------------------------------------------
; Получим отправленные данные:

selection: decode-cgi system/options/cgi/query-string

read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    the-data: data
    data
]
submitted: read-cgi
submitted-block: decode-cgi the-data

; --------------------------------------------------------------------
; Этот раздел должен быть первым, потому что он печатает другой 
; заголовок для push-загрузки (не "content-type: text/html^/"):

if selection/2 = "download-confirm" [
    print rejoin [
        "Content-Type: application/x-unknown"
        newline
        "Content-Length: "
        (size? to-file selection/4) 
        newline
        "Content-Disposition: attachment; filename=" 
        (second split-path to-file selection/4)
        newline
    ]

    data: read/binary to-file selection/4
    data-length: size? to-file selection/4
    write-io system/ports/output data data-length
    quit
]

;--------------------------------------------------------------------
; Печатаем обычные заголовки HTML для использования остальной частью 
; скрипта:

print "content-type: text/html^/"
print {<HTML><HEAD><TITLE>Web Site Manager</TITLE></HEAD><BODY>}

;--------------------------------------------------------------------
; Если был вызван поиск (по ссылке в главной форме):

if selection/2 = "confirm-search" [
    print rejoin [
        {<center><a href="./} 
        (second split-path system/options/script) {?name=} set-username
        {&pass=} set-password {">Back to Web Site Manager</a></center>}
    ]
    print {<center><table border="1" cellpadding="10" width=80%><tr><td>}
    print [<CENTER><TABLE><TR><TD>]
    print rejoin [
        {<FORM ACTION="./} (second split-path system/options/script) 
        {"> Text to search for: <BR> <INPUT TYPE="TEXT" SIZE="50"}
        {NAME="phrase"><BR><BR>Folder to search in: <BR>}
        {<INPUT TYPE="TEXT" SIZE="50" NAME="folder" VALUE="} what-dir
        {" ><BR><BR><INPUT TYPE=hidden NAME=perform-search }
        {VALUE="perform-search"><INPUT TYPE="SUBMIT" NAME="Submit" }
        {VALUE="Submit"></FORM></TD></TR></TABLE></CENTER>}
        {</td></tr></table></center></BODY></HTML>}
    ]
    quit
]

;--------------------------------------------------------------------
; Если был отправлен отредактированный текст файла:

if submitted-block/2 = "save" [

    ; Save newly edited document:
    write (to-file submitted-block/6) submitted-block/4
    print {<center><strong>Document Saved:</strong>
        <br><br><table border="1" width=80% cellpadding="10"><tr><td>}
    prin [<center><textarea cols="100" rows="15" name="contents">]
    prin replace/all read (
        to-file (replace/all submitted-block/6 "%2F" "/")
    ) "</textarea>" "<\/textarea>"
    print [</textarea></center>]
    print rejoin [
        {</td></tr></table><br><a href="./} 
        (second split-path system/options/script) {?name=} set-username
        {&pass=} set-password {">Back to Web Site Manager</a></center>}
        {</BODY></HTML>}
    ]
    quit
]

;--------------------------------------------------------------------
; Если была нажата ссылка для загрузки, распечатаем форму загрузки 
; файла:

if selection/2 = "upload-confirm" [
    print rejoin [
        {<center><a href="./} 
        (second split-path system/options/script) {?name=} set-username
        {&pass=} set-password {">Back to Web Site Manager</a></center>}
    ]
    print {<center><table border="1" cellpadding="10" width=80%><tr><td>}
    print {<center>}

    ; If just the link was clicked - no data submitted yet:

    if selection/4 = none [
        print rejoin [
            {<FORM ACTION="./} (second split-path system/options/script)
            {" METHOD="post" ENCTYPE="multipart/form-data">
                <strong>Upload File:</strong><br><br> 
                <INPUT TYPE=hidden NAME=upload-confirm 
                VALUE="upload-confirm">
                <INPUT TYPE="file" NAME="photo"> <br><br>
                Folder: <INPUT TYPE="text" NAME="path" SIZE="35" 
                    VALUE="} what-dir {"> 
                <INPUT TYPE="submit" NAME="Submit" VALUE="Upload">  
            </FORM>
            <br></center></td></tr></table></center></BODY></HTML>}
        ]
        quit
    ]
]

;--------------------------------------------------------------------
; Если данные для загрузки были отправлены:

if (submitted/2 = #"-") and (submitted/4 = #"-") [

    ; Эта функция Andreas Bolka:

    decode-multipart-form-data: func [
        p-content-type
        p-post-data
        /local list ct bd delim-beg delim-end non-cr
        non-lf non-crlf mime-part
    ] [
        list: copy []
        if not found? find p-content-type "multipart/form-data" [
            return list
        ]
        ct: copy p-content-type
        bd: join "--" copy find/tail ct "boundary="
        delim-beg: join bd crlf
        delim-end: join crlf bd
        non-cr:     complement charset reduce [ cr ]
        non-lf:     complement charset reduce [ newline ]
        non-crlf:   [ non-cr | cr non-lf ]
        mime-part:  [
            ( ct-dispo: content: none ct-type: "text/plain" )
            delim-beg ; mime-part start delimiter
            "content-disposition: " copy ct-dispo any non-crlf crlf
            opt [ "content-type: " copy ct-type any non-crlf crlf ]
            crlf ; content delimiter
            copy content
            to delim-end crlf ; mime-part end delimiter
            ( handle-mime-part ct-dispo ct-type content )
        ]
        handle-mime-part: func [
            p-ct-dispo
            p-ct-type
            p-content
            /local tmp name value val-p
        ] [
            p-ct-dispo: parse p-ct-dispo {;="}
            name: to-set-word (select p-ct-dispo "name")
            either (none? tmp: select p-ct-dispo "filename")
                   and (found? find p-ct-type "text/plain") [
                value: content
            ] [
                value: make object! [
                    filename: copy tmp
                    type: copy p-ct-type
                    content: either none? p-content [none][copy p-content]
                ]
            ]
            either val-p: find list name
                [
                    change/only next val-p compose [
                        (first next val-p) (value)
                    ]
                ]
                [append list compose [(to-set-word name) (value)]]
        ]
        use [ct-dispo ct-type content] [
            parse/all p-post-data [some mime-part "--" crlf]
        ]
        list
    ]

    ; После следующей строки "probe cgi-object" отобразит все части 
    ; отправленного составного объекта:

    cgi-object: construct decode-multipart-form-data 
        system/options/cgi/content-type copy submitted

    ; Запишем файл на сервер, используя исходное имя файла, и 
    ; уведомим пользователя:
the-file: last split-path to-file copy cgi-object/photo/filename
write/binary 
    to-file join cgi-object/path the-file 
    cgi-object/photo/content
print rejoin [
    {<center><a href="./} 
    (second split-path system/options/script) {?name=} set-username
    {&pass=} set-password {">Back to Web Site Manager</a></center>}
]
print {
    <center><table border="1" width=80% cellpadding="10"><tr><td>
    <strong>UPLOAD COMPLETE</strong><br><br></center>
    <strong>Files currently in this folder:</strong><br><br>
}
change-dir to-file cgi-object/path
folder: sort read what-dir
foreach file folder [
    print [
        rejoin [
            {<a href="./} (second split-path system/options/script)
            {?editor-confirm=editor-confirm&thefile=} 
            what-dir file {">(edit)</a>   }
            {<a href="./} (second split-path system/options/script)
            {?download-confirm=download-confirm&thefile=}
            what-dir file {">} "(download)</a>   " 
            {<a href="./} (find/match what-dir doc-path) file 
            {">} file {</a><br>}
        ]
    ]
]
print {</td></tr></table></center></BODY></HTML>}
quit
]

;--------------------------------------------------------------------
; Если данные не были отправлены, распечатайте форму для запроса 
; имени пользователя и пароля:

if ((selection/2 = none) or (selection/4 = none)) [
print rejoin [{
    <STRONG>W A R N I N G  -  Private Server, Login Required:</STRONG>
    <BR><BR>
    <FORM ACTION="./} (second split-path system/options/script) {">
    Username: <INPUT TYPE=text SIZE="50" NAME="name"><BR><BR>
    Password: <INPUT TYPE=text SIZE="50" NAME="pass"><BR><BR>
    <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
    </FORM></BODY></HTML>
}]
quit
]

;--------------------------------------------------------------------
; Если имя папки было отправлено, распечатайте список файлов:

if ((selection/2 = "command-submitted") and (
selection/4 = "call {^/^/^/^/}")
) [
print rejoin [
    {<center><a href="./} 
    (second split-path system/options/script) {?name=} set-username
    {&pass=} set-password {">Back to Web Site Manager</a></center>}
]
print {<center><table border="1" cellpadding="10" width=80%><tr><td>}
print {<strong>Files currently in this folder:</strong><br><br>}
change-dir to-file selection/6
folder: sort read what-dir
foreach file folder [
    print rejoin [
        {<a href="./} (second split-path system/options/script)
        {?editor-confirm=editor-confirm&thefile=}
        what-dir file {">} "(edit)</a>   " 
        {<a href="./} (second split-path system/options/script)
        {?download-confirm=download-confirm&thefile=}
        what-dir file {">} "(download)</a>   " 
        {<a href="./} (find/match what-dir doc-path) file {">} file
        {</a><br>}
    ]
]
print {</td></tr></table></center></BODY></HTML>}
quit
]

;--------------------------------------------------------------------
; Если редактор был вызван (через построенную ссылку):

if selection/2 = "editor-confirm" [

; резервное копирование (до внесения изменений):

cur-time: to-string replace/all to-string now/time ":" "-"
document_text: read to-file selection/4
if not exists? to-file rejoin [
    doc-path script-subfolder "edit_history/"
] [
    make-dir to-file rejoin [
        doc-path script-subfolder "edit_history/"
    ]
]
write to-file rejoin [
    doc-path script-subfolder "edit_history/" 
    to-string (second split-path to-file selection/4)
    "--" now/date "_" cur-time ".txt"
] document_text

; обратите внимание на метод POST в HTML-форме:

print rejoin [
    {<center><strong>Be sure to SUBMIT when done:</strong>}
    {<BR><BR><FORM method="post" ACTION="./} 
    (second split-path system/options/script) {">}
    {<INPUT TYPE=hidden NAME=submit_confirm VALUE="save">}
    {<textarea cols="100" rows="15" name="contents">}
    (replace/all document_text "</textarea>" "<\/textarea>")
    {</textarea><BR><BR><INPUT TYPE=hidden NAME=path VALUE="}
    selection/4
    {"><INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
    </FORM></center></BODY></HTML>}
]
quit
]

;--------------------------------------------------------------------
; Если критерии поиска были введены:

if selection/6 = "perform-search" [
phrase: selection/2
start-folder: to-file selection/4
change-dir start-folder
; found-list: ""

recurse: func [current-folder] [ 
    foreach item (read current-folder) [ 
        if not dir? item [  
            if error? try [
                if find (read to-file item) phrase [
                    print rejoin [
                        {<a href="./}
                        (second split-path system/options/script)
                        {?editor-confirm=editor-confirm&theitem=} 
                        what-dir item {">(edit)</a>   }
                        {<a href="./}
                        (second split-path system/options/script)
                        {?download-confirm=download-confirm&theitem=}
                        what-dir item {">(download)</a>   "}
                        phrase {" found in:  } 
                        {<a href="./} (find/match what-dir doc-path)
                        item {">} item {</a><BR>}
                    ]
                    ; found-list: rejoin [
                    ;     found-list newline what-dir item
                    ; ]
                ] 
            ] [print rejoin ["error reading " item]]
        ]
    ]
    foreach item (read current-folder) [ 
        if dir? item [
            change-dir item 
            recurse %.\
            change-dir %..\
        ] 
    ]
]

print rejoin [
    {<center><a href="./} 
    (second split-path system/options/script) {?name=} set-username
    {&pass=} set-password {">Back to Web Site Manager</a></center>}
]
print {<center><table border="1" cellpadding="10" width=80%><tr><td>}
print rejoin [
    {<strong>SEARCHING for "} phrase {" in } start-folder
    {</strong><BR><BR>}
]
recurse %.\
print {<BR><strong>DONE</strong><BR>}
print {</td></tr></table></center></BODY></HTML>}
; save %found.txt found-list
quit
]

;--------------------------------------------------------------------
; Это основная форма записи, используемая ниже:

entry-form: [
print rejoin [
    {<CENTER><strong>current path: </strong>} what-dir 
    {<FORM METHOD="get" ACTION="./} 
    (second split-path system/options/script) {">}{<INPUT TYPE=hidden}
    { NAME=submit_confirm VALUE="command-submitted">}
    {<TEXTAREA COLS="100" ROWS="10" NAME="contents">}
    {call {^/^/^/^/}</textarea><BR><BR>}
    {List Files: <INPUT TYPE=text SIZE="35" NAME="name" VALUE="} 
    what-dir {"><INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">}
    {          <A HREF="./} (second split-path system/options/script)
    {?upload-confirm=upload-confirm">upload</A>       } ; leave spaces
    {<A HREF="./} (second split-path system/options/script)
    {?confirm-search=confirm-search">search</A>} 
    {</FORM><BR></CENTER>}
]
]

;--------------------------------------------------------------------
; Если код был отправлен, распечатайте вывод вместе с формой ввода):

if ((selection/2 = "command-submitted") and (
selection/4 <> "call {^/^/^/^/}") and ((to-file selection/6) = what-dir))[
write %commands.txt join "REBOL[]^/" selection/4
; The "call" function requires REBOL version 2.76:
call/output/error 
    "./rebol276 -qs commands.txt" 
    %conso.txt %conse.txt
do entry-form
print rejoin [
    {<CENTER>Output: <BR><BR>}
    {<TABLE WIDTH=80% BORDER="1" CELLPADDING="10"><TR><TD><PRE>}
    read %conso.txt
    {</PRE></TD></TR></TABLE><BR><BR>}
    {Errors: <BR><BR>}
    read %conse.txt
    {</CENTER></BODY></HTML>}
]
quit
]
;--------------------------------------------------------------------
if ((selection/2 = "command-submitted") and (
selection/4 <> "call {^/^/^/^/}") and (
(to-file selection/6) <> what-dir)
) [
print rejoin [
    {<center><a href="./} 
    (second split-path system/options/script) {?name=} set-username
    {&pass=} set-password {">Back to Web Site Manager</a></center>}
]
print {
    <center><table border="1" cellpadding="10" width=80%><tr><td>
        <center>
    You must EITHER enter a command, OR enter a file path to list.<BR>
    Please go back and try again (refresh the page if needed).
        </center>
    </td></tr></center></BODY></HTML>
}
quit
]

;--------------------------------------------------------------------
; В противном случае проверьте отправленного пользователя/пароль, 
; затем распечатайте форму для ввода кода:

username: selection/2 password: selection/4 
either (username = set-username) and (password = set-password) [ 
; if user/pass is ok, go on
][
print "Incorrect Username/Password. </BODY></HTML>" quit
]

do entry-form
print {</BODY></HTML>}

14.13 Код CGI RebolForum.com

Вот код для CGI-приложения форума на http://rebolforum.com. Это фактический код, который запускает веб-сайт, на котором вы можете задать вопросы по этому руководству. Здесь он представлен здесь в сжатом формате, так что код хорошо помещается на этой веб-странице:

REBOL [title: "RebolForum.com"]  
editor decompress #{
789CDD5BEB72E3B8B1FEAFA7C0E1D638526D648177D22B698AD79DA99DCBC676
52495C4A8A926089198AD492D4787C5C7AF7D3006FA044D9F2665395B3DC1D4B
04D18D4677E3EB4683FAEE7F2E2F4729992791A46B68B8C87AD79EFDF903BA9B
F57ACB641384F11512D679BEBD1A15DDEE9374B7B95C249B51182FC9B7CBC52A
147ABD6D1AC6397A5A24714EE27C983F6EC915CAC9B77CB4CE37D13F46FB5E2F
7B08F3C51A658F594E36A3649B87499C8D801CF8FEB223593EDC907C9D2CD15D
0FC125FCFCF9E656286FE8053D87CB200FAED022D93E2241289AE6BBFB7B92D6
8D75F78775181174B74DB2300FBF92B72825C17218269500DB24CD3398C37697
737C90A8C9069E71C3D22BD86E49BCAC25E0FB2F2212A45C434D37EB357F851F
3D3A9566061D4A0015A48FC32C0745AE66BD592FDBCD37619E93E5155A9245B2
2443E8558BD0EB85F7A8EE3292D00409699655FA7A48C39C8C4AB1DFCCE797CB
79A59BF93CBB42511254EDAC31255F499A11FA90DD6779B8F8F2B84876717E85
306B02BB9300EC9727DB70413BA2BBAA09C6DA94ED7720D63D3846D1F6340EA7
37B7EF9D9FFE76B547772DA6DC0DFA1E89B359A1A9C28F400994F8EDB74D84A8
60A0A489205E62E1EDB4D6EF18A6DB3C94E021F76CB10EE29844D3AAA1799287
7944A6858FFBD495C7A3A2A9E9B224D9220D9969A6D76401FE8CB8FEE8964E34
1B8FF86E0D7114C65FA69DEB653C62CF58D73DFB5BAA426CE99729AE1FE44CC5
FD03350D069C678202864CF6D2F929E548EC7A9C926D142CC021A2A869464F17
7BF8176CB63FEC5F412450A25F7649FE2AAA3FB0A16031BE8A6A4CA9A2D78D34
A534AB431ACE5894F25F49181FACF13ED5E1681BA439EA8381BF9486E847245E
E5EBB7EC76808648021B28D8D406A8452EFC11FD9CC0A25EA2F92320E6F33CC0
8E6D627440D122181C60CA8939B5B5C13D3C69E8F3199C32FAF91C4E39C0F91C
BA9DE17CFA63C7E0C0A6D32500BE40FFD3565B0920FB969B1E4208EBC8E3C3BE
633ADD00C2484F83481374DFDE4F98FC0C779971D9B778B7999374B26FFB76ED
50002A030A24E08405A80C79241E946E12A48B35C4CCFB28584D62F2C023572D
E388E966DFE19B25ACD5A0553F80D850344E9188D1DD1C00EF0B1F2679F06F10
75548139D704DAE481F4975D9843C8ACB290F1BBDB8F1FA6E3779EE54EC7B7EF
6F3F786DC02F9A9896C1F0D14408A29CA471901301D1CC051AB6DB285C04D434
74ACEF2110C1236AE489D03608352CBA276429A0754AEE27C265CB442C268FA6
E31113A637B63FBB7F43F3959344493AF9CEC0F4BFE9D8F13EDD7AD7F0F8D6B2
3F78689EA44BB021460B12453F07CB25A40413A9B8BDD9060B768BD6245CADF3
8988F11BF4102EF3F5C450DF501ED73069B71E04F2A09CA0AF5614AEE209B808
98AC779F2439CD99C0096F4145234A326263C36729CC784465A592536502D12E
8E08CC957C0BB33C7B8BDE14D3A4E91D9F74B4DA9FC6F4633A5E43EA352D574E
B558C61FBD5B0BBDBBBDFD79E8FDE9CFEFFF3211AE3DFFDABB792720E73348F0
E97622E01FD09FAF3F4C38950A405F709B27CB4798E4824D52F8CE67177D4C1F
D05E74E43DF58AE34C69993CC43405A2292149ABA4A9713FEAEF24E39D906601
249D8E834E2BAF930D11A6EFE0EF7814C0D855EF79DA386DFF332463285F9332
998054394B003D024896402070AF7598A16DB02274CE034ACBFE6D53C2BB7A21
24CD0837F004ECF1DD534F374CC7515545B10DDBB5652C62DBF36D55C78E2129
92EAE8D8561C4F95444D77345B337DDD562CD5C4A2A7C8D8B3A59EECBAA665D8
AAA4AABA6839BA6B9A9AEFF9BA62F992242BD0C5B51DDD320D49C49AE96926DC
F9BAE15AAAAF1ABAAEF74C5FF434ACC8A624598E6360CB75645F543DD3B63543
9355C31225ECBAB6A299A2E462C7921CCBC1B2A79A8E645AA2DF93145DB31D5B
9754D956A1AFADFB926C2B12963CD3711DD7157D57925CC970254773B066A89A
07826098AD2A1958ED19A6EAA8A007C55145579135C7F30C184755A08F6963C3
756C0BD61A95C7760C0566EB4A9E684AA66B39B0849C9EEE69B62BAA926CF858
819EBEADB986E1CB862DBBBAE12B0A284DD5642ABA2B1A32C69AE56307882CD7
D24DDDEB29B66CA992836D4B940D4B935CDFD54D5FB2B0A7B8BE29A9A6853555
954D5FC1BA0C56901CAA3C037B86688335949E0F8259A203AA14E14307FEB2ED
E9D8D56D53D425072C815D0B8BBEEC2B3EC6A6A8688A272A922F3922CC071B3D
C515B1A849962F5A8A6A8B966A6153B2245FB75CD79341DBBE26DA8AAD2AB20F
36D0414596021D3CDB765445728D9EA5882228183B92070A36C13ABE221A8EA9
CA202768D0554D5B0413E8862D59AE07248EABF9BAAA0307DB36DC1EE805C404
F791B1E1AB1698DEF56547934D59D35D45F36559A1CA3545CB1035C570555BA5
082881FE0CCF56CC9EAA49BAAA582286F17D1154A483E72A3615C2F63C5083EB
AA58D50C57C38664788E6459A6A6F9BEED49962E6B720FFB60041F7AE920BFE1
4BBAEED89AEA3BAA6B599AAB2BB62BDBA20F42199AA14B606B59F6B18A4D5997
1DD0A6D2336040C3F034C77764DBD66DB02DB8BDE92AA266192E78B66D82BE6D
0C8BC852650FFCDE0287D07DD5716553D5704F96757062309669FA362841D544
C7F44077A04B4705B61E2C1355577C51C79E0F1EEEBB74B18AB2694B92EA39E0
CAB0181D5F164DE08C6520F1654D35C1180EAC304BF7B1AB890A95CCC1B04A7C
132B86A2A8A00498A761383D49048FA503D9D89264CBD5C1F6A012DB335C0F0C
66F836000416C16F6DDFA4B387ABC7C30BC4050A3B7C5311325AD1F61857836D
D806D3A7F1219F63E4B25515CB2EB61D8922970BCA906CC3C39E6C892A985F12
75D0B7674820B1255B8EE28387038A6892E99B3D7027C5B75C5F715DD905AF80
F91A8601EEAA508FC4BAE77B3238A62D7ABEA6C03297E91AD244BA8A5C07E0A1
674BB0D4240900D1912DC5C5BA288AB07814CB82412D579340D396EDBA580577
057015250B569668E89E02506A393D51310151557058780E2E8F154F150D0903
99EB99A2896D09A66588A20D24B60AFE0AA3586018C004589E188007C0D5B75D
ECB9966B2A008200AF9E659BE04D2E60922B027B51B7B12CC12AD21559014581
88922CE91A76003B0DDF81EEB02C4C0035AA0219D8808E5415445400443D5BF6
C07764C51141C31AB83B0038AC7ECD0428B37A00B6B01281B1678120A6A61BAA
E902B2C17AF13D400E0C7374240798820EA5DFCC5BB264972EC8FFBBE88B9EAA
0A19426419E649CA3B35AF85BA91E61955620432735D0AD53DAFB97631A7CCCD
CB8ACE8942CF5191671966B0357A2CB601570890B6B708B639A4D68B35597CB9
42CB04545E58A2652948D4C753D4677661DBAAB252C4F590F8A244E512599E26
F16AFA3E5E24694A1639728AE1D02DF9968F47E5E34ACFADAD8513D18D30B5D5
9D6D393FCDD07C07BB9C1881EA1FC165D03C4D1E3292A23C01777944C12A08E3
CBF6E6A432DF590EC38F7FB839E4CC519BA4D9B080A6FAFD46130A787573A70D
10B846BFEDF45CE7E71F6B8367B4FA29D814AEEBC5540337EB64172DD1A7CFB7
681380927F0FEA05BF4F6186B0E3CD607385E2E401F60D306B58A359F6001BA4
B2E84557E143BA0477BF0FD32CAF2AABB447D6AB6A6AF0FF26010DB10A0BED8D
FA057354ED8B692328BC5C48D5184038EB350356BE5FB57486C0E51276CC42B3
940EFC43100EEDAE75B61A65EB910FB42DC12DB3CD362279B930335A15FD3DB8
01FDCB03156BD8045FC87019A6E8CDE508601B10F8715494AD83AFE4B894C3F5
627EF4482BF6C23F0576B349E27C5DDF2D8347F6BD891B7C4909DC69D378A270
25D0BE032400C016B5F5595D40277461D62ECA9A4ADF624FF8858ED00F45F1BC
9842F560B881C801F1A85DD5E21DA48A437BF8863C64A3CF88DEBD8A4DF5B864
D6C9AB5BF09A12C42FBF3E374D85F68B01B78E3B31B5F641AFE87B245F613CE0
BA8C92387A44791046AC1ECE08EA95D52A6A4D512BC671F63F193F192600315F
34279B047C8896186A5BCE3A8F48DAE721AC7A5C0872577B4FD7F908BF829E39
2B692D34EAF220D50896629F12144AA1FA604C075C890D7185B8D933D2B39552
E60B55DB6190210FC55907B2964BB2ACD1E4449EF710843902EB5D61A9C5ED57
9579003E023AA8306D17F88EF1B6A98156984B4200B5141D006C6976FE40914B
921AA7A89F7235D0AB86BC5CE67727BA51E8E7B45E8AD2671EDE08240F98E424
DD04B406C9CB441B17579C413B8EDD5ABED1D2080D307D867EACFB482E3F95C1
DD620769183D99A50D2530B1704859B251672DBEA520ECA355C49D1DA9E059D6
FD3C198285C8AA6512A53C3D99D13F9CBB1C61F7D3AB727F7411CFB3ED0FDC71
5F37151BAC32E9D42ABE3CCF608F8AD370F4D42EBCD7569CEC1193BDA50DB040
FB5E69583F5DF065F63DEF49E84998FE5C313EBDA9A975930773D8CB74968DDB
55635C568A4DFC06A828E5723A5E1FB2BD879888B2F07FC9449DEE01DA5A3310
0774C347BBD479C59E73F9CA5FEFAAF040311FD18099E5C1663B63479B2D8EA8
B57FE0A354C5421843A4A5473FC2F3DD6897700A7D014ACFE83A2AFA8ECEEA5C
821F25A8BE9E310247353A9F6CCE08E6678D50F41DBDD4198089EEAAD15391B6
F12958D9452A572E1FCBE955C6CD3A4F407BF4DC48121D02D8837BB3A574FCF7
25FAEFFAE0CE835F4FFFF48FE173A337501B40406C29E92E88215BCBD7E9AE7E
E5452894C28E8A20FCD2D95729613362BF82ADA7926ACF086603EE098F24ED6E
B0D821C14957249FFC731E05CDC95FF59801D37E3618CCA808608C06A94FA026
6F56A0AFF2F4665D8BD36D73585D625EFBC8902DDBA7EA59FBA3DDB35EDB47B8
5021CBE12161BD793E88D1DCC6E985C93D8DFDCFD71F51F1C2D044A0931190E5
DCBEFFFCA985F5427BCF43AF71F1CA0F3BE75B879064C46CAE13BA734B49B64D
E20CB284AF41B43B687B052BEE24B666B53F88042205F9239674677F858E366B
87A3D137AB0A4B0A1A16CA5177193DC4A4F1B073C347AF8FD566A07B04CA3600
FC2E1856CB826E04272A2C84083E303DC02BBB9D1EE71684647B4B40E232F5A4
388B92FBA2D0566EE9D81B62570DA4EE9B9DF7530398274779512394DB33DAE0
E985C2803C259583335F23DA0BAE20146E5DB1AAEE4A46C52D3AE156E311756C
AAE325FD931EBE7CC042360AD8616E4ACF7FCF2D8A56FCE00F4D169A8CA27B71
BEA68C4B68E67258F7FFD525DB366814CC8780D2AD1225BD5ECA8CCFCA443A32
90EA8299B278C0B65C758C68E5B0C523C6976F3F7C7DB0BA5E1998F9EB8520CD
5FCF05EC17A47955F0E6AFDF432C6DCDE774E8E1AFA78354B94CE187312C1B54
C37C67B6FC9C917F65C8E6AFF3C3377FBD2694F3D7B112DB2DB34E8CE1B575C5
DEE76D567BB92BECDCA21F523EF5CB1DDC003C8AE3F15A206B25005DF5DBFF58
A5F6F2775BAAFDEFA9C89645D6222BBB42DD6509AE232A7A76D132D5BD45ED52
1CC785D1B428DAAFF87211ABAC105558D13A20AADE90BB3B10BCBE295E9C2E54
F3DF5B37A605D62B5EEA8E1232574446E7D1D2BA795749F988A2BBBA5CD753CB
17AD398AC141611906C99234AFE4CB0080813CA3E181A62D657995E2C612A654
16C1FAC5695414648CFFE0B008CB264C2BCDC5DB970D73BA8693EDEFBAC25CC7
F10A13AF4BD445ACE64B550081FD40A510D99B0AF4891CB52941CBFFB912F451
79B929431FD4989FA9209F2E3DD3938761B92DB22A925927C7BAD8CCD3702FBA
025DEF40E3AC7B9D9B37C98506C94BCDA515FC0B8A57EFB64F6E8DABDD42B51B
2A6EBB09B93D9D5AEFE90A0AFAE804D50BDB2F7EC072B7557C6F1551C5561155
3D5944ED812FB2779A0B94E71741B536E7736E7372B2CC5DEEEC0ACE9AFAA62E
453F57C22E4AD0DDEF7D9F7EE71B20EEDC22F41E88212C7172D69BC95A589075
DF1A8DFDC2018D903CE0E95A3432AD3FF3B92D7028B192FB5DC5D31F21A12B4F
128E47E0EAD0F5EE96AF44D76629BFD4A718ECAD8DAAF1E0D870C05EABE8F36A
280B53F47D8C221E235857255E0FEB9F48D1C4ABFE411444065A2868CC7ED0BD
750B7235732B28390DCE665D88339E7643CE0BEE35AD10B7DE0A1C88F204DBA4
AFA489464D5E4A55BCE7192E5941681BC413A9AC4794B0728EE336672FD7CEBB
F77FF15CF4D1BBB9B17EF46EDAD58ACA9EB317E7C789D3485097950E67598E5F
EF92B36727FA9AE99DAABBD433E986E4AA2AF36F20EED987404AB570297EBD08
D8D50B2D4D4594DEF2E3F2C825024F56C36433E73A811A4E54EBD48EFAE571F9
AB6318F25098821D52FF9A016145FD8B2CF233C7AB6AA71D03BDBA6E7ACE78E5
0B731DE3FDE6D5D597A26FA3B2A6A2DA1D7B5F55453DAD863AC64C5F5D5805EF
2C92D66EFEE542FB0DC2BF61D467A86D84E0029BC48DFC1C5E54262A70A386A2
F34FB4E9EF8CA6D73737C827F4558DF3090F7E0A03B938FD7C0D87F235DEE90D
FB7C9EB2ACC6658F71B0CDC890AC53F6A336F66BAAACF828C3F1E576BD7D2B0F
AFE98FAD0E6B7A3FA6C13AD8FC21AB7ECF75C6880F0F0F97EC875B9749BA1A05
AB60B84A93DD361B163349DF824F46CB492A3F6C0F47B3A2FCA377D6209B208C
CA5162928FD88FC2C398350FA330CB2FD343DE1F3FBC527A9E6F2ACFA36475CC
F45A46363C388B739E0671160539B95C25C92A22CC1C75E3DB753421F145164D
EED38BDD84936619AEC245B0C992FB9C91D4329139896C9BBAC6050901439477
B79F1CFBC7EC273BB2EE97F1FBC7BFBBAB8B2C98FCF52281C7D540178B7C021B
BE5D945FC007E4B213F102C2F3043B4EF6277DE53D58D6C516A06E322AF2F737
B2FFCB1BD965A3CDE76F246D1DC12D89E1DB220A6119C25DB22569401F6570E3
AB7F87AF6994B16E471A2BA47EA52DD8BC93CD661787F923FBC1DB215FA77A7A
16E7940C4BB6877C6E777992864144D994C87590F81E1DEBD08D496B83CCF6AF
FF07541284A7EE410000
}

14.14 Менеджер по работе с клиентами Etsy

Полное руководство по использованию REBOL с Etsy API доступно по адресу http://re-bol.com/etsy_api_tutorial.html. Эта программа CGI демонстрирует, как получить доступ ко многим функциям Etsy API:

#! ../rebol276 -cs
REBOL [Title: "Etsy"]
print {content-type: text/html^/^/}
print {<HTML><HEAD><TITLE>Etsy</TITLE></HEAD><BODY>}
read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]
submitted: decode-cgi submitted-bin: read-cgi
if ((submitted/2 = none) or (submitted/4 = none)) [
    print {
        <STRONG>W A R N I N G  -  Private Server:</STRONG><BR><BR>
        <FORM METHOD="post" ACTION="./etsy.cgi">
            Username: <input type=text size="50" name="name"><BR><BR>
            Password: <input type=text size="50" name="pass"><BR><BR>
            <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
        </FORM>
        </BODY></HTML>
    } 
    quit
]
myusername: "some-user-name"  mypassword: "some-password"
username: submitted/2   password: submitted/4 
either ((username = myusername) and (password = mypassword)) [][
    print "Incorrect Username/Password." 
    print {</BODY></HTML>} quit
]
do-etsy: does [
    do/args http://reb4.me/r/etsy.r context [
    Consumer-Key: #<my-key>
    Consumer-Secret: #<my-secret>
    User-Store: %etsyusers
    Scope: [listings_w listings_r listings_d]
    Sandbox: true
    ] 
    etsy/as "<my-user-name>"
]
if submitted/6 = "sale" [
    do-etsy
    coupon-code: submitted/8
    add-or-remove: submitted/10
    print "FOUND ITEMS:<br><br>"
    found: copy []
    x: get in (etsy/listings []) 'results
    for i 1 (length? x) 1 [  
        print copy rejoin [
            (get in x/:i 'title) <br>
        ] 
        append found (get in x/:i 'listing_id)
        append found (get in x/:i 'description)
        append found (get in x/:i 'title)
        append found (get in x/:i 'state)
    ]
    print "<br><br>REPLACED ITEMS:<br><br>"
    foreach [lstngid dscrptn titl state] found [
        either state <> "active" [
            print copy rejoin [
                titl { was NOT replaced (listing inactive)<br>}
            ]
        ][
            etsy/api-call/with put rejoin [
                %listings/ lstngid
            ] either add-or-remove = "add" [
                [
                    title: rejoin ["SALE-" titl]
                    description: rejoin [coupon-code dscrptn]
                ]
            ] [
                [
                    title: replace titl "SALE-" ""
                    description: replace dscrptn rejoin [
                        coupon-code] ""
                ]
            ]
            print copy rejoin [titl {<br>}]
        ]
    ]
    print "<br>Done<br><br>"
    quit
]
if submitted/6 = "replace" [
    do-etsy
    search-text: submitted/8
    replace-text: submitted/10
    print "FOUND ITEMS:<br><br>"
    found: copy []
    x: get in (etsy/listings []) 'results              
    for i 1 (length? x) 1 [  
        if find (get in x/:i 'description) search-text [ 
            print rejoin [
                ; {"} search-text {" found in:  } 
                (get in x/:i 'title) "<br>"
            ] 
            append found (get in x/:i 'listing_id)
            append found (get in x/:i 'description)
            append found (get in x/:i 'title)
            append found (get in x/:i 'state)
        ]
    ]
    print "<br><br>REPLACED ITEMS:<br><br>"
    foreach [lstngid dscrptn titl state] found [
        either state <> "active" [
            print copy rejoin [
                titl { was NOT replaced (listing inactive)<br>}
            ]
        ][
            etsy/api-call/with put rejoin [
                %listings/ lstngid
            ] [
                description: (
                    replace/all dscrptn search-text replace-text
                )
            ]
            print copy rejoin [titl {<br>}]
        ]
    ]
    print "<br>Done<br><br>"
    quit
]
if submitted/6 = "create-listing" [
    do-etsy
    itm: submitted/8
    desc: submitted/10
    prc:  to-decimal next find submitted/12 "$"
    ctgry: submitted/14
    print "Creating item...<br><br>"
    etsy/api-call/with post %/listings [
        quantity: 1 
        title: itm 
        description: desc 
        price: prc 
        category_id: ctgry 
        who_made: "i_did" 
        is_supply: "1" 
        when_made: "2010_2012" 
        shipping_template_id: "330"
    ]
    print rejoin ["CREATED: " itm ", " desc ", " prc]
    quit
]
if submitted/6 = "delete-listing" [
    do-etsy
    itm2del: submitted/8
    print "Deleting...<br><br>"
    etsy/api-call/with get rejoin [%listings/ itm2del] [
        method: "DELETE"
    ]
    print rejoin ["Item " itm2del " deleted."]
    quit
]
if submitted/6 = "view-raw" [
    do-etsy
    print {<pre>}
    probe copy get in (etsy/listings []) 'results
    print {</pre>}
    quit
]
if submitted/6 = "get-image2" [
    do-etsy
    photo-item-id: submitted/8
    photo-list: etsy/api-call/with get rejoin [
        %listings/ photo-item-id "/images"
    ] []
    either error? try [photo-id: first get in photo-list 'results] [
        print "No photo available for that item."
        return
    ][
        photo-info: etsy/api-call/with get the-code: rejoin [
            %listings/ photo-item-id "/images/ " photo-id
        ] []
    ]
    probe either [] = the-photo: (get in photo-info 'results) [
        "none"
    ] [
        the-photo
    ]
    quit
]
default-coupon-code: {
    ** SALE ** Enter the coupon code &quot;982u3445&quot; at 
    checkout to receive 10% off your order.&lt;br&gt;&lt;br&gt;
}
print rejoin [
    {<h2>Add or Remove Sale:</h2>
    <FORM METHOD="post" ACTION="./etsy.cgi">
        <INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
        <INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
        <INPUT TYPE=hidden NAME="subroutine" VALUE="sale">
        Coupon Code:<BR>
        <TEXTAREA COLS="50" ROWS="18" NAME="couponcode">}
            default-coupon-code 
        {</TEXTAREA><BR><BR>
        Add or Remove: <select NAME="addorremove">
            <option>add
            <option>remove
        </option></select><br><br>
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
    </FORM>
    <BR><HR>
    <h2>Replace In Description:</h2>
    <FORM METHOD="post" ACTION="./etsy.cgi">
        <INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
        <INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
        <INPUT TYPE=hidden NAME="subroutine" VALUE="replace">
        Search Text:<BR><BR>
        <input type=text size="35" name="searchtext">
        <BR><BR>
        Replace Text:<BR><BR>
        <input type=text size="35" name="replacetext">
        <BR><BR>
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
    </FORM>
    <BR><HR>
    <h2>Create Listing:</h2>
    <FORM METHOD="post" ACTION="./etsy.cgi">
        <INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
        <INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
        <INPUT TYPE=hidden NAME="subroutine" VALUE="create-listing">
        Title: <BR><BR>
        <input type=text size="35" name="title" value="Ring 100">
        <BR><BR>
        Description: <BR><BR>
        <input type=text size="35" name="description" 
            value="Ring 100.  A very pretty ring."><BR><BR>
        Price: <BR><BR>
        <input type=text size="35" name="Price" value="$19.99">
        <BR><BR>
        Category: <BR><BR>
        <input type=text size="35" name="Category" value="69150467">
        <BR><BR>
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
    </FORM> 
    <BR><HR>
    <h2>Delete Listing:</h2>
    <FORM METHOD="post" ACTION="./etsy.cgi">
        <INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
        <INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
        <INPUT TYPE=hidden NAME="subroutine" VALUE="delete-listing">
        Listing ID #: <BR><BR>
        <input type=text size="35" name="listing-id"><BR><BR>
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
    </FORM> 
    <BR><HR>
    <h2>View All Raw Listing Data:</h2>
    <FORM METHOD="post" ACTION="./etsy.cgi">
        <INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
        <INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
        <INPUT TYPE=hidden NAME="subroutine" VALUE="view-raw">
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
    </FORM> 
    <BR><HR>
    <h2>View Image:</h2>
    <FORM METHOD="post" ACTION="./etsy.cgi">
        <INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
        <INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
        <INPUT TYPE=hidden NAME="subroutine" VALUE="view-image">
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
    </FORM> 
    </BODY></HTML>}
]
quit

Для сравнения ниже представлена графическая версия программы, предоставляющей доступ к тем же функциям Etsy API:

REBOL [title: "Etsy"]
do/args http://reb4.me/r/etsy.r context [
    Consumer-Key: #<type_your_key_here>
    Consumer-Secret: #<typ_your_secret_here>
    User-Store: %etsyusers
    Scope: [listings_w listings_r listings_d]  ; edit permissions here
    Sandbox: false                      ; change to true when approved
]
coupon-text: {
    ** SALE ** Enter the coupon code &quot;893894&quot; at checkout to
    receive 10% off your order &lt;br&gt;&lt;br&gt;
}
replace-items: does [
    found-items/text: copy {}  show found-items
    replaced-items/text: copy {}  show replaced-items
    found: copy []
    x: get in (etsy/listings []) 'results              
    for i 1 (length? x) 1 [  
        if find (get in x/:i 'description) search-text/text [ 
            insert head found-items/text copy rejoin [
                ; {"} search-text/text {" found in:  } 
                (get in x/:i 'title) newline
            ] 
            show found-items
            append found (get in x/:i 'listing_id)
            append found (get in x/:i 'description)
            append found (get in x/:i 'title)
            append found (get in x/:i 'state)
        ]
    ]
    foreach [lstngid dscrptn titl state] found [
        either state <> "active" [
            insert head replaced-items/text copy rejoin [
                titl { was NOT replaced (listing inactive)^/}
            ]
            show replaced-items
        ][
            etsy/api-call/with put rejoin [
                %listings/ lstngid
            ] [
                description: (
                    replace/all dscrptn search-text/text replace-text/text
                )
            ]
            insert head replaced-items/text copy rejoin [titl {^/}]
            show replaced-items
        ]
    ]
    ; alert "Done"
]
sale: func [add-or-remove] [
    coupon-code: copy request-text/title/default"Coupon Text:" coupon-text
    found-items/text: copy {}  show found-items
    replaced-items/text: copy {}  show replaced-items
    found: copy []
    x: get in (etsy/listings []) 'results
    focus found-items           
    for i 1 (length? x) 1 [  
        insert head found-items/text copy rejoin [
            (get in x/:i 'title) newline
        ] 
        show found-items
        append found (get in x/:i 'listing_id)
        append found (get in x/:i 'description)
        append found (get in x/:i 'title)
        append found (get in x/:i 'state)
    ]
    foreach [lstngid dscrptn titl state] found [
        either state <> "active" [
            insert head replaced-items/text copy rejoin [
                titl { was NOT replaced (listing inactive)^/}
            ]
            show replaced-items
        ][
            etsy/api-call/with put rejoin [
                %listings/ lstngid
            ] either add-or-remove = true [
                [
                    title: rejoin ["SALE-" titl]
                    description: rejoin [coupon-code dscrptn]
                ]
            ] [
                [
                    title: replace titl "SALE-" ""
                    description: replace dscrptn rejoin [coupon-code] ""
                ]
            ]
            insert head replaced-items/text copy rejoin [titl {^/}]
            show replaced-items
        ]
    ]
    focus replaced-items
    ; alert "Done"
]
create-listing: does [
    itm: request-text/title/default "Title:" "Item 100"
    desc: request-text/title/default "Description:" "Ring #100"
    prc:  to-decimal next find (
            request-text/title/default "Price:" "$19.99"
            ) "$"
    if true = request "Would you like to see a listing of category IDs?" [
        categories: etsy/api-call get %taxonomy/categories
        cat-list: copy []
        foreach category categories/results [
            append cat-list reduce [
                category/long_name category/category_id
            ]
        ]
        chosen-category: request-list "Categories" cat-list
    ]
    if unset? chosen-category [chosen-category: "69150467"]
    ctgry: request-text/title/default "Category ID:" form chosen-category
    flash "Creating item..."
    etsy/api-call/with post %/listings [
        quantity: 1 
        title: itm 
        description: desc 
        price: prc 
        category_id: ctgry 
        who_made: "i_did" 
        is_supply: "1" 
        when_made: "2010_2012" 
        shipping_template_id: "330"
    ]
    unview
    alert rejoin ["CREATED: " itm ", " desc ", " prc]
]
delete-listing: does [
    itm2del: request-text/title "Listing ID #:"
    either true = request "Really Delete?" [
        flash "Deleting..."
        etsy/api-call/with get rejoin [%listings/ itm2del] [
            method: "DELETE"]
        unview
        alert rejoin ["Item " itm2del " deleted."]
    ] [
        return
    ]
]
get-image: does [
    found: copy []
    x: get in (etsy/listings []) 'results  
    for i 1 (length? x) 1 [  
        append found (get in x/:i 'title)
        append found (get in x/:i 'listing_id)
    ]
    photo-item-id: request-list "Select Item:" found
    photo-list: etsy/api-call/with get rejoin [
        %listings/ photo-item-id "/images"] []
    either error? try [photo-id: first get in photo-list 'results] [
        alert "No photo available for that item."
        return
    ][
        photo-info: etsy/api-call/with get the-code: rejoin [
            %listings/ photo-item-id "/images/ " photo-id
        ] []
    ]
    editor either [] = the-photo: (get in photo-info 'results) [
        "none"
    ] [
        the-photo
    ]
]
etsy/as "<your-username>"
view center-face gui: layout [
    across
    text 80 right "If" 
    cond1: drop-down 100 data [
        "Title" "Description" "Listing ID" "Any Field"
    ]
    cond2: drop-down 150 data [
        "REPLACE ALL" "Contains" "Does NOT Contain" "Equals"
    ]
    cond3: field 454 "ring" return
    text 80 right "Search Text:" search-text: field 720 "ring" [
        replace-text/text: copy search-text/text show replace-text
    ] return
    text 80 right "Replace Text:" replace-text: field 720 "ring" return
    text 805 "" return
    box black 805x2 return
    text 805 "" return
    text 400 "Found Items:" text 200 "Replaced Items:" return
    found-items: area  replaced-items: area  return
    btn "List Raw Data" [editor copy get in (etsy/listings []) 'results]
    btn "Create Listing" [create-listing]
    btn "Delete Listing" [delete-listing]
    btn "Add Sale" [sale true]
    btn "Remove Sale" [sale false]
    btn "View Image" [get-image]
    btn "Replace Description" [replace-items]
]

Не забудьте просмотреть следующие ссылки, чтобы получить больше информации о программировании REBOL CGI:

http://re-bol.com/cgi_tutorial.txt http://rebol.com/docs/cgi1.html http://rebol.com/docs/cgi2.html http://rebol.com/docs/cgi-bbs.html http://www.rebol.net/cookbook/recipes/0045.html

Чтобы создать веб-сайты с использованием PHP-подобной версии REBOL, которая работает на веб-сервере, полностью написанном на REBOL, см.:

http://cheyenne-server.org (двоичные файлы доступны для Windows, Mac и Linux). http://cheyenne-server.org/docs/rsp-api.html - документация по API "RSP" (серверные страницы REBOL).

14.15 Замечание о работе с веб-серверами

Чтобы выполнять любую работу с вашим веб-сервером, вам необходимо знать имя пользователя FTP вашей учётной записи, пароль, URL-адрес и общедоступную корневую папку. Убедитесь, что у вас есть эта информация, прежде чем пытаться выполнить какое-либо программирование CGI.

Наиболее распространённым интерфейсом для управления веб-сайтами популярных компаний является программное обеспечение под названием "cPanel". Инструкции по доступу и использованию cPanel доступны по всему Интернету и, скорее всего, будут отправлены вашей хостинговой компанией по электронной почте. Этот автор рекомендует lunarpages.com и hostgator.com, и известно, что эти две компании поддерживают программирование REBOL CGI.

Чтобы начать создание веб-приложения с REBOL, вам необходимо загрузить интерпретатор REBOL на свой веб-сервер. Вам нужно сделать это только один раз для каждого веб-сервера (а не для каждой программы). Если у вас нет доступа к cPanel или другому файловому менеджеру в сети, вы можете использовать REBOL для управления всей настройкой сервера, а также всеми операциями по редактированию скриптов и управления файлами. Чтобы загрузить REBOL на свой сервер, откройте локальную консоль интерпретатора REBOL и введите следующий код, но ЗАМЕНИТЕ имя пользователя, пароль, URL-адрес и общедоступную папку информацией вашей учётной записи. Это загрузит интерпретатор REBOL из Интернета и загрузит его на ваш собственный сервер:

; РЕДАКТИРУЙТЕ строку ниже, с учётом вашей информации
file: ftp://user:pass@site.com/public_html/rebol 
write/binary file (read/binary http://re-bol.com/rebol276)

Вышеупомянутый процесс передачи файлов должен занять всего несколько секунд, если вы используете широкополосное соединение. Как только это будет сделано, вам необходимо установить "разрешения на выполнение" для интерпретатора REBOL в вашей учётной записи хостинга. Для этого вы можете использовать следующий сценарий REBOL. Просто вставьте этот код в свой локальный интерпретатор REBOL и введите URL-адрес веб-сервера, имя пользователя, пароль, папку и т.д., когда будет предложено:

REBOL [title: "FTP CHMOD"]
website: request-text/title/default "Web Site:" "site.com"
username: request-text/title/default "User Name:" "user"
password: request-text/title/default "Password:" "pass"
folder: request-text/title/default "Folder:" "public_html"
file: request-text/title/default "File:" "rebol"
permission: request-text/title/default "Permission:" "755"
write %ftpargs.txt trim rejoin [{
    open } website {
    user } username { } password {
    cd } folder {
    literal chmod } permission { } file {
    quit
}]
call/show "ftp -n -s:ftpargs.txt"

Если у вас есть какие-либо проблемы с использованием указанной выше программы в вашей операционной системе, вы можете просто запустить FTP-программу из командной строки вашей ОС, используя приведённый ниже код, и вручную ввести информацию об учётной записи веб-хоста, как описано:

call/show {ftp}   ; запустите эту строку в консоли интерпретатора REBOL

; Введите следующую команду в приглашении FTP(замените своим веб-адресом):

open yourdomain.com     ; при появлении запроса введите своё имя
                        ; пользователя и пароль

; Введите следующие команды (замените имена папок и файлов):

cd public_html/
literal chmod 755 rebol
quit

Все вышеперечисленные шаги необходимо выполнить только при первой настройке учётной записи веб-хостинга. После того, как вы загрузили REBOL на свой сервер, вы можете приступить к созданию своей первой CGI-программы. Чтобы начать редактирование нового сценария веб-сайта, введите в локальную консоль интерпретатора REBOL следующее (опять же, не забудьте отредактировать имя пользователя, пароль, URL-адрес и информацию общедоступной папки в соответствии с настройками учётной записи вашего веб-сервера):

; РЕДАКТИРУЙТЕ строку ниже, с учётом вашей информации
editor ftp://user:pass@site.com/public_html/example.cgi

Введите следующий код в текстовый редактор REBOL и сохраните его (нажатие кнопки "Сохранить" в текстовом редакторе REBOL ДЕЙСТВИТЕЛЬНО СОХРАНЯЕТ/ЗАГРУЖАЕТ ИЗМЕНЕНИЯ НА ВАШ ВЕБ-СЕРВЕР):

#!./rebol -cs
REBOL []
print {content-type: text/html^/}

Запустите программу "FTP CHMOD", описанную выше, ещё раз, чтобы установить разрешения для этого нового "example.cgi" на 755 (вам необходимо установить разрешения на 755 для каждого сценария .cgi, который вы создаёте и загружаете на свой сервер, поэтому сохраните приведённый выше скрипт пригодится). Для ясности, в приведённом выше сценарии ftp измените "rebol" на "example.cgi". Теперь перейдите по адресу http://yourdomain.com/example.cgi, и вы увидите пустую страницу (замените yourdomain.com в URL-адресе на домен вашего фактического веб-сервера). При возникновении ошибок см. выделенные ниже инструкции о журналах ошибок в cPanel.

15. Организация эффективных структур данных и алгоритмов

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

15.1 Пример простого цикла

Приведённый ниже пример демонстрирует, как неэффективный дизайн может снизить производительность. Этот код выводит на консоль интерпретатора REBOL 30 строк текста, каждая из которых состоит из 75 знаков тире:

REBOL [title: "Badly Designed Line Printer"]
for i 1 30 1  [
    for i 1 75  1 [prin "-"]
    print ""
]
halt

Даже на очень быстром компьютере вы можете наблюдать, как мерцает экран, и видеть, как символы появляются в виде тире, напечатанных в цикле, 75 раз на каждую строку, через 30 строк. Эта операция печати внутренних символов повторяется в общей сложности 2250 раз (30 строк * 75 символов). Как оказалось, функция REBOL "loop" немного быстрее, чем функция "for", при создании простых структур цикла с подсчётом. Итак, вы можете увидеть небольшое улучшение производительности, используя следующий код:

REBOL [title: "Slightly Improved Line Printer"]
loop 30 [
    loop 75 [prin "-"]
    print ""
]
halt

Но в приведённом выше примере не рассматривается основная проблема с горлышком бутылки, которая замедляет отображение. Функция, выполнение которой действительно требует времени, - это функция prin. Компьютер НАМНОГО медленнее при выводе элементов на экран, чем при выполнении невидимых вычислений в оперативной памяти. Посмотрите, сколько времени занимает простое выполнение одного и того же набора циклов и увеличение счётчика (т.е. выполнение вычисления суммы) по сравнению с печатью во время каждого цикла. Этот пример выполняется мгновенно даже на очень медленных компьютерах:

REBOL [title: "Loops Without Printing"]
count: 0
loop 30 [
    loop 75 [count: count + 1]
]
print count
halt

Следовательно, гораздо более эффективным решением было бы создать строку символов один раз, сохранить её в метке переменной, а затем распечатать эту единственную сохранённую строку 30 раз:

REBOL [title: "Well Designed Line Printer"]
line: copy {} 
loop 75 [append line "-"]
loop 30 [print line]
halt

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

REBOL [title: "Printing Algorithm Timer"]

timer1: now/time/precise
for i 1 30 1  [
    for i 1 75  1 [prin "-"]
    print ""
]
elapsed1: now/time/precise - timer1
print newpage

timer2: now/time/precise
loop 30 [
    loop 75 [prin "-"]
    print ""
]
elapsed2: now/time/precise - timer2
print newpage

timer3: now/time/precise
count: 0
loop 30 [
    loop 75 [count: count + 1]
]
print count
elapsed3: now/time/precise - timer3
print newpage

timer4: now/time/precise
line: copy {} 
loop 75 [append line "-"]
loop 30 [print line]
elapsed4: now/time/precise - timer4
print newpage

print rejoin ["Printing 2250 characters, 'for':    " elapsed1]
print rejoin ["Printing 2250 characters, 'loop':   " elapsed2]
print rejoin ["Counting to 2250, printing result:  " elapsed3]
print rejoin ["Printing 30 preconstructed lines:   " elapsed4]

halt

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

15.2 Пример из реальной жизни: кассовая регистратор и система отчётов кассира

Пример ниже - это тривиальный пример POS, продемонстрированный ранее в руководстве. Он сохраняет данные для всех чеков в одном текстовом файле:

REBOL [title: "Minimal Cash Register - Inefficient"]
view gui: layout [
    style fld field 80
    across
    text "Cashier:"   cashier: fld 
    text "Item:"      item: fld 
    text "Price:"     price: fld [
        if error? try [to-money price/text] [alert "Price error" return]
        append a/text reduce [mold item/text "    " price/text newline]
        item/text: copy "" price/text: copy ""
        sum: 0
        foreach [item price] load a/text [sum: sum + to-money price]
        subtotal/text: form sum
        tax/text: form sum * .06
        total/text: form sum * 1.06 
        focus item
        show gui
    ]
    return
    a: area 600x300
    return
    text "Subtotal:"   subtotal: fld 
    text "Tax:"        tax: fld 
    text "Total:"      total: fld
    btn "Save" [
        items: replace/all (mold load a/text) newline " "
        write/append %sales.txt rejoin [
            items newline cashier/text newline now/date newline
        ]
        clear-fields gui
        a/text: copy ""             
        show gui             
    ]
]

Этот код сообщает общее количество всех товаров, проданных в любой выбранный день любым выбранным кассиром:

REBOL [title: "Cashier Report - Ineffecient"]
report-date: request-date
report-cashier: request-text/title/default "Cashier:" "Nick"
sales: read/lines %sales.txt
sum: $0
foreach [items cashier date] sales [
    if ((report-cashier = cashier) and (report-date = to-date date)) [
        foreach [item price] load items [
            sum: sum + to-money price
        ]
    ]
]
alert rejoin ["Total for " report-cashier " on " report-date ": " sum]

На первый взгляд кажется, что вся эта программа работает нормально, но что произойдёт после того, как в систему будет введён миллион торговых транзакций? В этом случае программа отчёта должна прочитать и просмотреть 3 миллиона строк данных, выполнить оценку даты и сравнения кассиров для каждой отдельной записи о продажах, а затем выполнить цикл суммирования только для совпадающих записей о продажах. Это ОЧЕНЬ ненужная обработка, а чтение данных с жёсткого диска происходит особенно медленно.

Мы могли бы просто запланировать время от времени копировать, вставлять и стирать старые транзакции из файла sales.txt в отдельный архивный файл. Это решило бы проблему (и иногда может быть полезной целью обслуживания), но на самом деле это не улучшает производительность. Немного изменив способ хранения, мы можем значительно повысить производительность. В приведённом ниже примере создаётся новая папка и каждая транзакция продаж записывается в отдельный файл. ИМЕНА ФАЙЛОВ сохранённых продаж содержат дату, время и имя кассира каждой транзакции (в этом примере был немного изменён только код кнопки "Сохранить"):

REBOL [title: "Minimal Cash Register - Efficient"]
view gui: layout [
    style fld field 80
    across
    text "Cashier:"   cashier: fld 
    text "Item:"      item: fld 
    text "Price:"     price: fld [
        if error? try [to-money price/text] [alert "Price error" return]
        append a/text reduce [mold item/text "    " price/text newline]
        item/text: copy "" price/text: copy ""
        sum: 0
        foreach [item price] load a/text [sum: sum + to-money price]
        subtotal/text: form sum
        tax/text: form sum * .06
        total/text: form sum * 1.06 
        focus item
        show gui
    ]
    return
    a: area 600x300
    return
    text "Subtotal:"   subtotal: fld 
    text "Tax:"        tax: fld 
    text "Total:"      total: fld
    btn "Save" [
        file: copy to-file rejoin [
            now/date
            "_" replace/all form now/time ":" "-"
            "_" cashier/text
        ]
        save rejoin [%./sales/ file ] (load a/text)
        clear-fields gui
        a/text: copy ""
        show gui             
    ]
]

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

REBOL [title: "Cashier Report - Efficient"]
report-date: request-date
report-cashier: request-text/title/default "Cashier:" "Nick"
sum: $0
files: copy []
foreach file read %./sales/ [
    parsed: parse file "_"
    if ((report-date = to-date parsed/1) and (report-cashier = parsed/3))[
        append files file
    ]
]
cd %./sales/
foreach file files [ 
    foreach [item price] load file [
        sum: sum + to-money price
    ]
]
cd %../
alert rejoin ["Total for " report-cashier " on " report-date ": " sum]

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

REBOL [title: "Automated Test Sales Data Generator"]
random/seed now/time/precise
loop 10000 [
    file: to-file rejoin [
        ((random 31-dec-0001) + 734868)   ; 734868 days between 2013/0001
        "_" replace/all form random 23:59:59 ":" "-"
        "_" random {abcd}
    ]
    items: reduce [random "asd fgh jkl" random $100]
    save rejoin [%./sales/ file] items
]

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

REBOL [title: "Cashier Report - Efficient"]
report-date: request-date
report-cashier: request-text/title/default "Cashier:" "abcd"
sum: $0
files: copy []
foreach file read %./sales/ [
    parsed: parse file "_"
    if  report-cashier = parsed/3 [
        append files file
    ]
]
cd %./sales/
foreach file files [ 
    foreach [item price] load file [
        sum: sum + to-money price
    ]
]
cd %../
alert rejoin ["Total for " report-cashier ": " sum]

Метод сохранения структурированных данных в отдельных текстовых файлах, продемонстрированный упрощёнными примерами POS выше, был протестирован автором в различных реализациях коммерческого программного обеспечения большого объёма. Скорость вычислений, продемонстрированная отчётами с использованием этого метода, превосходит возможности даже мощных систем баз данных. Другие преимущества, такие как переносимость между системами (может использоваться любая операционная система с файловой структурой), отсутствие необходимости в установке СУБД, простое резервное копирование и перенос данных в другие системы и носители и т.д., делают это чрезвычайно мощным способом решения серьёзных проблем потребности в управлении данными. Резервное копирование всех новых данных на флэш-накопитель так же просто, как выполнение синхронизации папок. В одном случае система отчётности веб-сайта была создана, чтобы соответствовать настольной системе отчётности POS, которая отслеживала десятки миллионов продаж товаров. Простой сценарий REBOL использовался для ежедневной загрузки новых файлов транзакций продаж на веб-сервер, и точно такой же код использовался для печати отчёта в виде выходных данных сценария CGI. Затем пользователи могли проверять продажи в Интернете с помощью браузеров на любом настольном ПК, iPhone, Android и т.д. При использовании этой простой концептуальной схемы сохранения данных в текстовые файлы не требовалось абсолютно никакой базы данных или машинно-зависимой архитектуры программного обеспечения. Ключ к успеху этого примера организации данных, их хранения и алгоритмического вычисления отчётов состоит в том, чтобы просто спроектировать систему для хранения потенциально искомых полей данных в именах файлов.

Умение избегать напрасных усилий и определять узкие места в вычислениях, особенно в циклах, управляющих большими наборами данных, имеет решающее значение для разработки быстрых приложений. Даже если вы используете мощные системы баз данных для хранения данных, всё равно важно учитывать, как вы читаете, ищите и иным образом управляете сохранёнными данными. Как правило, избегайте чтения, записи, передачи или выполнения вычислений для чего-либо, кроме минимально возможных наборов значений, и вы обнаружите, что производительность всегда можно улучшить.

В этом руководстве вы увидите такие эффективные дизайнерские решения, применяемые ко многим приложениям. Обратите особое внимание на то, как данные хранятся и сообщаются в текстовых файлах в примерах "Точка продаж RebGUI", "Простой POS" и "Простой принтер отчёта о продажах POS". Более точные методы, такие как способ автоматического перемещения сообщений в базу данных "Архив" в CGI-приложении "RebolForum", помогают значительно улучшить взаимодействие с пользователем за счёт прямого уменьшения количества данных, необходимых для представления начального отображения. Это приложение было протестировано в производственных условиях и мгновенно реагирует, даже если система содержит сотни тысяч сообщений.

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

16. Дополнительные темы

16.1 Объекты

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

"Прототипы" объектов определяют новый объект-контейнер. Чтобы создать исходный прототип объекта в REBOL, используйте следующий синтаксис:

label: make object! [object definition]

Определение объекта может содержать функции, значения и/или данные любого типа. Ниже приведён пустой объект учётной записи пользователя, содержащий 6 переменных, для которых установлено значение "none"):

account: make object! [
    first-name: last-name: address: phone: email-address: none
]

Приведённое выше определение учётной записи просто помещает 6 переменных в контейнер или контекст, называемый "учётная запись".

Вы можете ссылаться на данные и функции в объекте, используя нотацию уточнения ("/путь"):

object/word

В объекте учётной записи "account/phone" относится к данным номера телефона, содержащимся в учётной записи. Вы можете вносить изменения в элементы объекта следующим образом:

object/word: data

Например:

account/phone: "555-1234"
account/address: "4321 Street Place Cityville, USA 54321"

После создания объекта вы можете просмотреть все его содержимое с помощью функции "help" (справка):

help object
? object  

; "?" синоним команды "help"

Если вы ввели все примеры учётных записей в интерпретатор REBOL, то:

? account

отображает следующую информацию:

ACCOUNT is an object of value:
   first-name      none!     none
   last-name       none!     none
   address         string!   "4321 Street Place Cityville, USA 54321"
   phone           string!   "555-1234"
   email-address   none!     none

Вы можете получить список всех элементов в объекте, используя формат "first (метка объекта)":

first account

Вышеупомянутая строка возвращает [self first-name last-name address phone email-address]. Первым элементом в списке всегда является "self", и для большинства операций вы захотите удалить этот элемент. Для этого используйте формат "next first (метка объекта)":

next first account

Чтобы перебрать каждый элемент в объекте, вы можете использовать цикл foreach для указанных выше значений:

foreach item (next first account) [print item]

Чтобы получить значения, на которые ссылаются отдельные словесные метки в объектах, используйте "get in":

get in account 'first-name
get in account 'address

; обратите внимание на галочку

В следующем примере показано, как получить доступ и управлять каждым значением в объекте:

count: 0
foreach item (next first account) [
    count: count + 1
    print rejoin ["Item " count ":   " item]
    print rejoin ["Value:    " (get in account item) newline]
]

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

label: make исходный-объект [
    значения должны быть изменены по сравнению с исходным определением
]

Такое поведение копирования значений на основе предыдущих определений объекта (так называемое "наследование") является одной из основных причин того, что объекты полезны. Приведённый ниже код создаёт новый объект учётной записи с меткой "user1":

user1: make account [
    first-name: "John"
    last-name: "Smith"
    address: "1234 Street Place  Cityville, USA 12345"
    email-address: "john@hisdomain.com"
]

В этом случае переменная номера телефона сохраняет значение по умолчанию "none", установленное в исходном определении учётной записи.

Вы можете расширить любое существующее определение объекта новыми значениями:

label: make исходный-объект [новые значения для добавления]

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

user2: make account [
    first-name: "Bob"
    last-name: "Jones"
    address: "4321 Street Place Cityville, USA 54321"
    phone:  "555-1234"
    email-address: "bob@mysite.net"
    favorite-color: "blue"
]

"user2/favourite-color" теперь означает "blue".

Приведенный ниже код создает дубликат учетной записи user2 с измененными только именем и адресом электронной почты:

user2a: make user2 [
    first-name: "Paul"
    email-address: "paul@mysite.net"
]

"? user2a" предоставляет следующую информацию:

USER2A is an object of value:
   first-name      string!   "Paul"
   last-name       string!   "Jones"
   address         string!   "4321 Street Place Cityville, USA 54321"
   phone           string!   "555-1234"
   email-address   string!   "paul@mysite.net"
   favorite-color  string!   "blue"

Вы можете включать функции в определение вашего объекта:

complex-account: make object! [
    first-name: 
    last-name:
    address:
    phone:
    none
    email-address: does [
        return to-email rejoin [
            first-name "_" last-name "@website.com"
        ]
    ]
    display: does [
        print ""
        print rejoin ["Name:     " first-name " " last-name]
        print rejoin ["Address:  " address]
        print rejoin ["Phone:    " phone]
        print rejoin ["Email:    " email-address]
        print ""
    ]
]

Обратите внимание, что переменная "email-address" изначально присваивается результату функции (которая просто создаёт адрес электронной почты по умолчанию из переменных имени и фамилии объекта). Вы можете переопределить это определение, присвоив указанное значение адреса электронной почты. Как только вы это сделаете, функция адреса электронной почты больше не существует в этом конкретном объекте - она будет перезаписана указанным значением электронной почты.

Вот некоторые реализации указанного выше объекта. Обратите внимание на значение адреса электронной почты в каждом объекте:

user1: make complex-account []

user2: make complex-account [
    first-name: "John"
    last-name: "Smith"
    phone:  "555-4321"
]

user3: make complex-account [
    first-name: "Bob"
    last-name: "Jones"
    address: "4321 Street Place Cityville, USA 54321"
    phone:  "555-1234"
    email-address: "bob@mysite.net"
]

Чтобы распечатать все данные, содержащиеся в каждом объекте:

user1/display user2/display user3/display

Функция отображения распечатывает данные, содержащиеся в каждом объекте, и в каждом объекте одни и те же переменные относятся к разным значениям (первые два сообщения электронной почты создаются функцией адреса электронной почты, а третье назначается).

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

REBOL []

hidden-prize: random 15x15
character: make object! [
    position: 0x0
    move: does [
        direction: ask "Move up, down, left, or right:  "
        switch/default direction [
            "up" [position: position + -1x0]
            "down" [position: position + 1x0]
            "left" [position: position + 0x-1]
            "right" [position: position + 0x1]
        ] [print newline print "THAT'S NOT A DIRECTION!"]
        if position = hidden-prize [
            print newline
            print "You found the hidden prize.  YOU WIN!"
            print newline
            halt
        ]
        print rejoin [
            newline
            "You moved character " movement " " direction
            ".  Character " movement " is now " 
            hidden-prize - position
            " spaces away from the hidden prize.  "
            newline
        ]
    ]
]
character1: make character[]
character2: make character[position: 3x3]
character3: make character[position: 6x6]
character4: make character[position: 9x9]
character5: make character[position: 12x12]
loop 20 [
    prin "^(1B)[J"
    movement: ask "Which character do you want to move (1-5)?  "
    if find ["1" "2" "3" "4" "5"] movement [
        do rejoin ["character" movement "/move"]
        print rejoin [
            newline
            "The position of each character is now:  "
            newline newline
            "CHARACTER ONE:   " character1/position newline
            "CHARACTER TWO:   " character2/position newline
            "CHARACTER THREE: " character3/position newline
            "CHARACTER FOUR:  " character4/position newline
            "CHARACTER FIVE:  " character5/position
        ]
        ask "^/Press the [Enter] key to continue."
    ]
]

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

16.1.1 Управление пространством имён

В этом примере одни и те же слова определены дважды в одной программе:

var: 1234.56
bank: does [
    print ""
    print rejoin ["Your bank account balance is:  $" var]
    print ""
]

var: "Wabash"
bank: does [
    print ""
    print rejoin [
        "Your favorite place is on the bank of the:  " var]
    print ""
]

bank

После выполнения вышеуказанного кода невозможно получить доступ к балансу банковского счета, потому что слова "bank" и "var" были перезаписаны. В больших проектах кодирования несколько разработчиков могут непреднамеренно использовать одни и те же имена переменных для ссылки на разные фрагменты кода и/или данных, что может привести к случайному удалению или изменению значений. Этой потенциальной проблемы можно избежать, просто разделив приведённый выше код на отдельные объекты:

money: make object! [
    var: 1234.56
    bank: does [
        print ""
        print rejoin ["Your bank account balance is:  $" var]
        print ""
    ]
]

place: make object! [
    var: "Wabash"
    bank: does [
        print ""
        print rejoin [
            "Your favorite place is on the bank of the:  " var]
        print ""
    ]
]

Теперь вы можете получить доступ к словам "bank" и "var" в их соответствующих объектных контекстах:

money/bank
place/bank

money/var
place/var

Приведённые ниже объекты дополнительно используют функции и переменные, содержащиеся в указанных выше объектах. Поскольку новые объекты "deposit" (депозит) и "travel" (путешествие) созданы из объектов "money" (деньги) и "place" (место), они наследуют весь существующий код, содержащийся в указанных выше объектах:

deposit: make money [
    view layout [
        button "Deposit $10" [
            var: var + 10
            bank
        ]
    ]
]

travel: make place [
    view layout [
        new-favorite: field 300 trim {
            Type a new favorite river here, and press [Enter]} [
            var: value
            bank
        ]
    ]
]

Обучение использованию объектов важно, потому что большая часть REBOL построена с использованием объектных структур. Как вы видели ранее в разделе о встроенной справке, "system" (системный) объект REBOL содержит множество важных настроек интерпретатора. Чтобы получить доступ ко всем значениям в системном объекте, важно понимать нотацию объекта:

get in system/components/graphics 'date

То же самое верно для свойств виджета GUI и многих других функций REBOL.

Для получения дополнительной информации об объектах смотрите:

http://rebol.com/docs/core23/rebolcore-10.html
http://en.wikibooks.org/wiki/REBOL_Programming/Language_Features/Objects
http://en.wikipedia.org/wiki/Prototype-based_programming

16.2 Порты - точный доступ к файлам, электронной почте, сети и т.д.

"Порты" REBOL обеспечивают единый способ обработки множества типов ввода и вывода данных. Они обеспечивают доступ к различным источникам данных и позволяют согласованно управлять ими всеми с помощью стандартных функций серии REBOL. Вы можете открывать порты для ящиков электронной почты POP, каталогов FTP, локальных текстовых файлов, сетевых подключений TCP, буферов ввода с клавиатуры и т.д., а также использовать их для вывода данных, таких как звуки и взаимодействия с консолью. Как только порт открыт для источника данных, данные, содержащиеся в порту, можно рассматривать как последовательный список элементов, которые можно перемещать, упорядочивать, искать, сортировать и иным образом организовывать/манипулировать, всё с использованием последовательных функций, таких как описанные ранее в этом тексте (foreach, find, select, reverse, length?, head, next, back, last, tail, at, skip, extract, index?, insert, append, remove, change, poke, copy/part, clear, replace, join, intersect, difference, exclude, union, unique, empty?, write, sav и т.д.).

В некоторых случаях существуют другие собственные способы доступа к данным, содержащимся в данном порту, обычно с помощью функций "read" (чтения) и "write" (записи). В таких случаях доступ к порту просто обеспечивает более точный контроль данных (например, работа с электронной почтой и данными файлов). В других случаях порты предоставляют основной интерфейс для доступа к данным в данном источнике данных (например, сокеты TCP и другие сетевые протоколы).

Порты создаются с помощью функции "open", и при создании им обычно присваивается словесная метка:

my-files: open ftp://user:pass@site.com/public_html/

После открытия указанного выше порта файлы в каталоге FTP можно просматривать последовательно или по индексу, используя последовательные функции:

print first my-files
print length? my-files
print pick my-files ((length? my-files) - 7)  ; 7 элемент с конца

; и т.д. ...

Чтобы изменить отмеченную позицию индекса в порту, повторно назначьте метку порта новой позиции индекса:

my-files: head my-files
    print index? my-files
    print first my-files
my-files: next my-files
    print index? my-files
    print first my-files
my-files: at my-files 10
    print index? my-files
    print first my-files

Чтобы закрыть соединение с данными, содержащимися в порту, используйте функцию "close" (закрыть):

close my-files

Конечно, можно читать и писать прямо в/из папки в приведённых выше примерах, не открывая порт вручную:

print read ftp://user:pass@site.com/public_html/
write ftp://user:pass@site.com/public_html/temp.txt (read %temp.txt)

Разница между открытием порта для вышеуказанных файлов и простым их чтением/записью заключается в том, что соединение с портом предлагает более конкретный доступ и контроль над отдельными файлами. Функции "get-mode" и "set-mode" могут использоваться для установки различных свойств файлов:

my-file: open %temp.txt
set-modes port [
    world-read: true
    world-write: true
    world-execute: true
]
close my-file

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

print read pop://user:pass@site.com

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

print second read pop://user:pass@site.com

Гораздо лучшее решение - открыть порт для указанного выше источника данных:

my-email: open pop://user:pass@site.com

Как только указанный выше порт открыт, к каждому из отдельных электронных писем в данной учётной записи POP можно будет получить доступ отдельно, без необходимости загружать какие-либо другие электронные письма в учётной записи:

print second my-email  ; не требуется загрузка всех 10000 писем

И вы можете переключаться между сообщениями в аккаунте:

my-email: head my-email
    print first my-email   ; печатаем 1-е письмо в ящике
my-email: next my-email
    print first my-email   ; печатаем 2-е письмо в ящике
my-email: at my-email 4
    print first my-email   ; печатаем 5-е письмо в ящике
my-email: head my-email
    print first my-email   ; печатаем снова 1-е письмо в ящике

; и т.д....

Вы также можете удалить сообщения электронной почты из учётной записи:

my-email: head my-email
    remove my-email  ; удаляем первое письмо

На самом деле REBOL работает с большинством типов источников данных как с портами. Следующая строка:

write/append %temp.txt "1234"

тоже самое, что и:

temp: open %temp.txt
append temp "1234"
close temp

Порты REBOL - это объекты. Вы можете увидеть все свойства открытого порта, используя функцию "probe":

temp: open %temp.txt
probe temp
close temp

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

temp: open %temp.txt

print temp/date
print temp/path
print temp/size

close temp

State/inBuffer и state/outBuffer являются особенно важными значениями в любом порту. В этих элементах хранятся изменения данных, содержащихся в порту, до тех пор, пока порт не будет закрыт или обновлён. Внимательно посмотрите на этот пример:

; Сперва создадим файл:

write %temp.txt ""

; Этот файл пуст:

print read %temp.txt

; Открываем файл как порт:

temp: open %temp.txt

; Добавляем в него какой то текст:

append temp "1234"

; Отобразим текст для сохранения в файл:

print temp/state/inBuffer

; Добавленные изменения ещё НЕ были сохранены в файл, потому что 
; порт ещё не был закрыт или обновлён:

print read %temp.txt

; Для сохранения изменений в файле можно использовать либо 
; "update" (обновить), либо "close" (закрыть):

update temp

; Функция "update" заставила добавленные данные быть записанными в 
; файл, но ещё НЕ закрыла порт:

print read %temp.txt

; Мы все ещё можем перемещаться по содержимому порта и добавлять в 
; него дополнительные данные:

temp: head temp
insert temp "abcd"

; Отобразить текст для сохранения в файл

print temp/state/inBuffer

; Эти изменения ещё не сохранены в файле:

print read %temp.txt

; Закрытие порта сохранит изменения в файле:

close temp

; Вот сохранённые изменения:

print read %temp.txt

; И дополнительные изменения вносить уже нельзя:

append temp "1q2w3e4r"  ; (ошибка)

Порты можно открывать с различными настройками, чтобы правильно обрабатывать данные. "Help open" отображает следующий список:

/binary  - Точно сохраняет содержимое.
/string  - Переводит все окончания строк.
/direct  - Открывает порт без буферизации.
/seek    - Открывает порт в режиме поиска без буферизации.
/new     - Создаёт файл. (Файлу не обязательно существовать.)
/read    - Только чтение. Отключает операции записи
/write   - Только запись. Отключает операции чтения.
/no-wait - Возврат немедленно без ожидания, если нет данных.
/lines   - Обрабатывает данные как строки.
/with    - Задаёт альтернативное завершение строки. (Тип: char string)
/allow   - Задаёт атрибуты защиты при создании. (Тип: block)
/mode    - Блок вышеперечисленных уточнений. (Тип: block)
/custom  - Позволяет особые уточнения. (Тип: block)
/skip    - Пропускает количество байтов. (Тип: number)

Некоторые из этих вариантов будут продемонстрированы в примерах следующих приложений.

16.3 Консоль и приложения электронной почты CGI, использующие порты

Следующая программа электронной почты открывает порт для выбранной учётной записи электронной почты и позволяет пользователю перемещаться по сообщениям, читать, отправлять, удалять и отвечать на электронные письма. Он полностью запускается из командной строки - никаких компонентов графического интерфейса VID или графического представления не требуется. Вы можете сохранить информацию о конфигурации для любого количества учётных записей электронной почты в блоке "accounts" (учётные записи) и легко переключаться между ними в любой точке программы:

REBOL [Title: "Console Email"]

accounts: [
    ["pop.server" "smtp.server" "username" "password" you@site.com]
    ["pop.server2" "smtp.server2" "username" "password" you@site2.com]
    ["pop.server3" "smtp.server3" "username" "password" you@site3.com]
]

empty-lines: "^/"
loop 400 [append empty-lines "^/"]  ; # строк, необходимых для 
                                    ; очистки экрана 
cls: does [prin {^(1B)[J}]
a-line:{-----------------------------------------------------------------}

select-account: does [
    cls
    print a-line
    forall accounts [
        print rejoin ["^/" index? accounts ":  " last first accounts]
    ]
    print join "^/" a-line
    selected: ask "^/Select an account #:  "
    if selected = "" [selected: 1]
    t: pick accounts (to-integer selected)
    system/schemes/pop/host:  t/1
    system/schemes/default/host: t/2
    system/schemes/default/user: t/3 
    system/schemes/default/pass: t/4 
    system/user/email: t/5
]
send-email: func [/reply] [
    cls
    print rejoin [a-line "^/^/Send Email:^/^/" a-line] 
    either reply [
        print join "^/^/Reply-to:  " addr: form pretty/from
    ] [
        addr: ask "^/^/Recipient Email Address:  "
    ]
    either reply [
        print join "^/Subject:  " subject: join "re: " form pretty/subject
    ] [
        subject: ask "^/Email Subject:  "
    ]
    print {^/Body (when finished, type "end" on a seperate line):^/}
    print join a-line "^/"
    body: copy ""
    get-body: does [
        body-line: ask ""
        if body-line = "end" [return]
        body: rejoin [body "^/" body-line]
        get-body
    ]
    get-body
    if reply [
        rc: ask "^/Quote original email in your reply (Y/n)?  "
        if ((rc = "yes") or (rc = "y") or (rc = "")) [
            body: rejoin [
                body 
                "^/^/^/--- Quoting " form pretty/from ":^/"
                form pretty/content
            ]
        ]
    ]
    print rejoin ["^/" a-line "^/^/Sending..."]
    send/subject to-email addr body subject 
    cls 
    print "Sent^/" 
    wait 1
]
read-email: does [
    pretty: none
    cls
    print "One moment..."
    ; СЛЕДУЮЩАЯ СТРОКА ОТКРЫВАЕТ ПОРТ ДЛЯ ВЫБРАННОЙ УЧЁТНОЙ ЗАПИСИ 
    ; ЭЛЕКТРОННОЙ ПОЧТЫ:
    mail: open to-url join "pop://" system/user/email
    cls
    while [not tail? mail] [
        print "Reading...^/"
        pretty: import-email (copy first mail)
        either find pretty/subject "***SPAM***" [
            print join "Spam found in message #" length? mail
            mail: next mail
        ][
            print empty-lines
            cls
            prin rejoin [
                a-line
                {^/The following message is #} length? mail { from:  } 
                system/user/email {^/} a-line {^/^/}
                {FROM:     } pretty/from {^/}
                {DATE:     } pretty/date {^/}
                {SUBJECT:  } pretty/subject {^/^/} a-line
            ]
            confirm: ask "^/^/Read Entire Message (Y/n):  "
            if ((confirm = "y") or (confirm = "yes") or (confirm = "")) [
                print join {^/^/} pretty/content
            ]
            print rejoin [
                {^/} a-line {^/}
                {^/[ENTER]:  Go Forward  (next email)^/}
                {^/    "b":  Go Backward (previous email)^/}
                {^/    "r":  Reply to current email^/}
                {^/    "d":  Delete current email^/}
                {^/    "q":  Quit this mail box^/}
                {^/  Any #:  Skip forward or backward this # of messages}
                {^/^/} a-line {^/}
            ]
            switch/default mail-command: ask "Enter Command:  " [
                ""  [mail: next mail]
                "b" [mail: back mail]
                "r" [send-email/reply]
                "d" [
                    remove mail
                    cls 
                    print "Email deleted!^/" 
                    wait 1
                ]
                "q" [
                    close mail
                    cls
                    print"Mail box closed^/"
                    wait 1 
                    break
                ]
            ] [mail: skip mail to-integer mail-command]
            if (tail? mail) [mail: back mail]
        ]
    ]
]

; Начало программы:

select-account

forever [
    cls
    print a-line
    print rejoin [
        {^/"r":  Read Email^/}
        {^/"s":  Send Email^/}
        {^/"c":  Choose a different mail account^/}
        {^/"q":  Quit^/}
    ]
    print a-line
    response: ask "^/Select a menu choice:  "
    switch/default response [
        "r" [read-email]
        "s" [send-email]
        "c" [select-account]
        "q" [
            cls
            print "DONE!"
            wait .5 
            quit
        ]
    ] [read-email]
]

Вот CGI-версия программы электронной почты, которая работает на веб-сервере и работает в самых простых браузерах (это приложение было создано для работы в экспериментальном браузере, который поставлялся с первым устройством чтения книг Amazon Kindle):

#!../rebol276 -cs
REBOL [Title: "Kindle Email"]
print {content-type: text/html^/^/}
print {<HTML><HEAD><TITLE>Kindle Email</TITLE></HEAD><BODY>}
read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]
submitted: decode-cgi submitted-bin: read-cgi
if ((submitted/2 = none) or (submitted/4 = none)) [
    print {
        <STRONG>W A R N I N G  -  Private Server:</STRONG><BR><BR>
        <FORM METHOD="post" ACTION="./kindle_email.cgi">
            Username: <input type=text size="50" name="name"><BR><BR>
            Password: <input type=text size="50" name="pass"><BR><BR>
            <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
        </FORM>
        </BODY></HTML>
    } 
    quit
]
accounts: [
    ["pop.server1" "smtp.server1" "username1" "password1" you@site1.com]
    ["pop.server2" "smtp.server2" "username2" "password2" you@site2.com]
    ["pop.server3" "smtp.server3" "username3" "password3" you@site3.com]
]
myusername: "username"  mypassword: "password"
username: submitted/2   password: submitted/4 
either ((username = myusername) and (password = mypassword)) [][
    print "Incorrect Username/Password." 
    print {</BODY></HTML>} quit
]
if submitted/6 = "read" [
    account: pick accounts (to-integer submitted/8)
    mail-content: read [
        scheme: 'POP
        host: account/1
        port-id: 110
        user: account/3
        pass: account/4
    ]
    mail-count: length? mail-content
    for i 1 mail-count 1 [
        single-message: import-email (pick mail-content i)
        print rejoin [
            i {) &nbsp; <a href="./kindle_email.cgi?} 
            {u=} myusername {&p=} mypassword 
            {&subroutine=displaymessage&themessage=} 
            ; copy/part for URI length, at 3 is a serialization trick:
            at (mold compress (copy/part single-message/content 5000)) 3
            {">} single-message/subject
            {</a> &nbsp; <a href="./kindle_email.cgi?} 
            {u=} myusername {&p=} mypassword 
            {&subroutine=delete&theaccount=} submitted/8
            {&thesubject=} single-message/subject 
            {&thedate=} single-message/date
            {&thefrom=} single-message/from {">delete</a>
            <br> &nbsp; &nbsp; &nbsp; &nbsp; } single-message/from {<br>}
        ]
    ]
    quit
]
if submitted/6 = "displaymessage" [
    compressed-message: copy join "#{" submitted/8
    print "<pre>"  print decompress load compressed-message  print "<pre>"
    quit
]
if ((submitted/6 = "send") or (submitted/6 = "delete")) [
    my-account: pick accounts (to-integer submitted/8)
    system/schemes/pop/host:  my-account/1
    system/schemes/default/host: my-account/2
    system/schemes/default/user: my-account/3 
    system/schemes/default/pass: my-account/4 
    system/user/email: my-account/5
] 
if submitted/6 = "send" [
    print "Sending..."
    header: make system/standard/email [
        To: to-email submitted/10
        From: to-email my-account/5
        Subject: submitted/12
    ]
    send/header (to-email submitted/10) (trim submitted/14) header
    print "<strong>Sent</strong>"
]
if submitted/6 = "delete" [
    mail: open to-url join "pop://" system/user/email
    while [not tail? mail] [
        pretty: import-email (copy first mail)
        either all [
            pretty/subject = submitted/10
            form pretty/date = submitted/12
            form pretty/from = submitted/14
        ][
            remove mail  print "<strong>Deleted</strong>"  wait 1
        ][
            mail: next mail
        ]
    ]
]
print {<hr><h2>Read:</h2>}
for i 1 (length? accounts) 1 [
    print rejoin [
        i {) &nbsp; <a href="./kindle_email.cgi?}
        {u=} myusername {&p=} mypassword {&subroutine=read&accountname=}
        i {">} (first pick accounts i) {</a><br>}
    ]
]
print rejoin [
    {<BR><HR>
    <h2>Send:</h2>
    <FORM METHOD="post" ACTION="./kindle_email.cgi">
        <INPUT TYPE=hidden NAME="username" VALUE="} myusername {">
        <INPUT TYPE=hidden NAME="password" VALUE="} mypassword {">
        <INPUT TYPE=hidden NAME="subroutine" VALUE="send">
        From Account #: <select NAME="account">}
]
for i 1 (length? accounts) 1 [prin rejoin [{<option>} i]]
print {
        </option> </select><br><br>
        To: <BR><input type=text size="35" name="to"><BR><BR>
        Subject: <BR><input type=text size="35" name="subject"><BR><BR>
        <TEXTAREA COLS="50" ROWS="18" NAME="contents"></TEXTAREA><BR><BR>
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">
    </FORM>
    </BODY></HTML>
}
quit

16.4 Сетевые порты - передача данных и файлов с помощью HTTP

Одно из важных применений портов - это передача данных через сетевые соединения ("сокеты" TCP и UDP). При написании сетевого приложения вы должны выбрать конкретный номер порта, через который будут передаваться данные. Возможные порты варьируются от 0 до 65535, но многие из этих номеров зарезервированы для определённых типов приложений (программы электронной почты используют порт 110, веб-серверы по умолчанию используют порт 80 и т.д.). Чтобы избежать конфликта с другими установленными сетевыми приложениями, для небольших скриптов лучше выбрать номер порта от 49152 до 65535. Список зарезервированных номеров портов доступен здесь.

Сетевые приложения обычно состоят из двух или более отдельных программ, каждая из которых работает на разных компьютерах. Любому компьютеру, подключённому к сети или Интернету, назначается определённый "IP-адрес", записанный в формате xxx.xxx.xxx.xxx. Номера разные для каждого компьютера в сети, но большинство домашних компьютеров и компьютеров малого бизнеса обычно находятся в диапазоне IP-адресов "192.168.xxx.xxx". Вы можете получить IP-адрес своего локального компьютера с помощью следующего кода REBOL:

read join dns:// (read dns://)

"Серверные" программы открывают выбранный сетевой порт и ждут, пока одна или несколько "клиентских" программ откроют тот же порт, а затем отправят в него данные. Порт, открытый серверной программой, упоминается в клиентской программе путём объединения IP-адреса компьютера, на котором работает сервер, с выбранным номером порта, каждый из которых разделён двоеточием (т.е. 192.168.1.2:55555).

Следующий простой набор сценариев демонстрирует, как использовать порты REBOL для передачи одной строки текста от клиента к программе сервера. Этот пример предназначен для запуска на одном компьютере для демонстрации, поэтому слово "localhost" используется для обозначения IP-адреса сервера (это стандартное соглашение, используемое для обозначения собственного локального IP-адреса любого компьютера). Если вы хотите запустить это на двух отдельных компьютерах, подключённых через локальную сеть, вам необходимо получить IP-адрес серверной машины (используйте приведённый выше код) и замените слово "localhost" этим номером:

Вот программа СЕРВЕР. Обязательно запускайте его перед запуском клиента, иначе вы получите сообщение об ошибке:

; Откроем сетевой порт 55555 в линейном(строчном) режиме (в этом 
; режиме предполагается наличие полных строк текста, оканчивающихся ; символам новой строки):

server: open/lines tcp://:55555

; Ждём подключения к открытому выше порту:

wait server

; Присваиваем метку первому подключению к указанному выше порту:

connection: first server

; Получим данные, которые были отправлены клиентом в указанный выше 
; объект порта:

data: first connection

; Покажем полученные данные:

alert rejoin ["Text received: " data]

; Закрываем порт

close server

Вот КЛИЕНТ. Запустите его в отдельном экземпляре интерпретатора REBOL после запуска указанной выше программы:

; Откроем порт, созданный указанным выше сервером (замените 
; "localhost" на IP-адрес, если эти сценарии выполняются на разных 
; машинах):

server-port: open/lines tcp://localhost:55555

; Вставляем некоторый текст в порт:

insert server-port "Hello Mr. Watson."

; Закрываем порт:

close server-port

Обычно серверы постоянно ждут появления данных в порту и постоянно что-то делают с этими данными. Приведённые ниже сценарии расширяют приведённый выше пример бесконечными циклами для непрерывной отправки, получения и отображения сообщений, передаваемых от клиента(ов) на сервер. Этот тип цикла является основой для большинства одноранговых и клиент-серверных сетевых приложений. Введите "end" в клиентской программе ниже, чтобы закрыть и клиент, и сервер.

Вот программа-сервер (сначала запустите):

server: open/lines tcp://:55555             ; Открываем сетевой порт
print "Server started...^/"
connection: first wait server               ; Метка первого соединения
forever [
    data: first connection                  ; Получаем строку данных
    print rejoin ["Text received: " data]   ; Отображаем данные.
    if find data "end" [                    
        close server                        ; Завершаем сервер, если 
        print "Server Closed"               ; клиент прислал "end"
        halt
    ]
]

Вот клиентская программа. Запускать его только после того, как серверная программа была запущена, и в отдельном экземпляре интерпретатора REBOL (или на отдельном компьютере):

server-port: open/lines tcp://localhost:55555   ; Открываем порт на 
                                                ; сервере
forever [
    user-text: ask "Enter some text to send:  "
    insert server-port user-text                ; Передаём данные
    if user-text = "end" [
        close server-port                       ; Завершаем программу
        print "Client Closed"                   ; если пользователь 
        halt                                    ; ввёл "end".
    ]
]

Важно понимать, что серверы REBOL, подобные приведённому выше, могут независимо взаимодействовать с более чем одним одновременным клиентским подключением. Определение "connection" ждёт, пока не подключится новый клиент, и возвращает порт, представляющий это первое клиентское соединение. Когда это происходит, "connection" относится к порту, который используется для приёма данных, передаваемых уже подключённым клиентом. Если вы хотите добавить больше одновременных клиентских подключений во время цикла forever, просто определите другой "первый сервер ожидания". Попробуйте запустить указанный ниже сервер, а затем одновременно запустите два экземпляра указанного выше клиента:

server: open/lines tcp://:55555             ; Открываем сетевой порт
print "Now start TWO clients..."
connection1: first wait server              ; Метка первого соединения
connection2: first wait server              ; Метка второго соединения
forever [
    data1: first connection1                ; Получаем строку от 
                                            ; первого клиента
    data2: first connection2                ; Получаем строку от 
                                            ; второго клиента
    print rejoin ["Client1: " data1]
    print rejoin ["Client2: " data2]
    if find data1 "end" [                    
        close server                        ; Завершаем программу
        print "Server Closed"               ; если первый клиент 
        halt                                ; (data1) прислал "end".
    ]
]

Вот пример, демонстрирующий, как отправлять данные туда и обратно (в обоих направлениях) между клиентом и сервером. Опять же, запустите обе программы в отдельных экземплярах интерпретатора REBOL и обязательно сначала запустите сервер:

; Сервер:

print "Server started...^/"
port: first wait open/lines tcp://:55555
forever [
    user-text: ask "Enter some text to send:  "
    insert port user-text
    if user-text = "end" [close port  print "^/Server Closed^/"  halt]
    wait port
    print rejoin ["^/Client user typed:  " first port "^/"]
]


; Клиент:

port: open/lines tcp://localhost:55555
print "Client started...^/"
forever [
    user-text: ask "Enter some text to send:  "
    insert port user-text
    if user-text = "end" [close port  print "^/Client Closed^/"  halt]
    wait port
    print rejoin ["^/Server user typed:  " first port "^/"]
]

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

do [
    either find ask "Server or client?  " "s" [
        port: first wait open/lines tcp://:55555 ; сервер
    ] [
        port: open/lines tcp://localhost:55555   ; клиент
    ]
    forever [
        insert port ask "Send:  "
        print join "Received: " first wait port
    ] 
]

Следующий сценарий представляет собой полноценное сетевое приложение для обмена мгновенными сообщениями с графическим интерфейсом пользователя. В отличие от программы FTP Chat Room, представленной ранее, текст в этом приложении пересылается напрямую между двумя компьютерами через подключение к сетевому сокету (пользователи FTP Chat Room никогда не подключаются напрямую друг к другу - они просто подключаются к общедоступному стороннему FTP-серверу):

view layout [
    btn "Set client/server" [
        ip: request-text/title/default trim {
            Server IP (leave EMPTY to run as SERVER):
            } (to-string read join dns:// read dns://)
        either ip = "" [
            port: first wait open/lines tcp://:55555 z: true ; server
        ] [
            port: open/lines rejoin [tcp:// ip ":55555"] z: true
        ]
    ]
    r: area rate 4 feel [
        engage: func [f a e] [
            if a = 'time and value? 'z [
                if error? try [x: first wait port] [quit]
                r/text: rejoin [form x newline r/text] show r
            ]
        ]
    ]
    f: field "Type message here..."
    btn "Send" [insert port f/text] 
]

Вот ещё более компактная версия (вероятно, самая короткая программа обмена мгновенными сообщениями, которую вы когда-либо видели!):

view layout [ across
    q: btn "Serve"[focus g p: first wait open/lines tcp://:8 z: 1]text"OR"
    k: btn "Connect"[focus g p: open/lines rejoin[tcp:// i/text ":8"]z: 1]
    i: field form read join dns:// read dns://  return
    r: area rate 4 feel [engage: func [f a e][if a = 'time and value? 'z [
        if error? try [x: first wait p] [quit]
        r/text: rejoin [x "^/" r/text] show r
    ]]]  return
    g: field "Type message here [ENTER]" [insert p value  focus face]
]

А вот расширенная версия вышеупомянутого скрипта, который загружает выбранное вами имя пользователя, WAN/LAN IP и номера портов на FTP-сервер, чтобы этой информацией можно было поделиться с другими в Интернете (что позволяет им найти вас и связаться с вами). Подключение в качестве сервера загружает информацию о пользователе и запускает приложение в режиме сервера. Как только это будет сделано, другие могут нажать кнопку "Servers", чтобы получить и вручную ввести информацию о вашем подключении (IP-адрес и порт), чтобы подключиться в качестве клиента. Используя разные номера портов и имена пользователей, несколько пользователей могут подключаться к другим нескольким пользователям в любом месте в Интернете:

server-list: ftp://username:password@yoursite.com/public_html/im.txt ;edit
view layout [ across
    q: btn "Serve" [
        parse read http://guitarz.org/ip.cgi[thru<title>copy p to</title>]
        parse p [thru "Your IP Address is: " copy pp to end]
        write/append server-list rejoin [
            b/text " " pp " " read join dns:// read dns://"  " j/text "^/"
        ] 
        focus g p: first wait open/lines join tcp:// j/text z: 1
    ] text "OR"
    k: btn "Connect" [
        focus g p: open/lines rejoin [tcp:// i/text j/text] z: 1
    ]
    b: field 85 "Username"
    i: field 98 form read join dns:// read dns:// 
    j: field 48 ":8080" return
    r: area rate 4 feel [engage: func [f a e][if a = 'time and value? 'z [
        if error? try [x: first wait p] [quit]
        r/text: rejoin [x "^/" r/text] show r
    ]]]  return
    g: field "Type message here [ENTER]" [insert p value  focus face]
    tabs 181 tab btn "Servers" [print read server-list]
]

Если вы хотите запускать подобные сценарии между компьютерами, подключёнными к Интернету через широкополосные маршрутизаторы, вам, вероятно, потребуется научиться "перенаправлять" порты с вашего маршрутизатора на IP-адрес машины, на которой запущена ваша серверная программа. В большинстве ситуаций, когда маршрутизатор подключает локальную домашнюю/бизнес-сеть к Интернету, только устройство-маршрутизатор имеет IP-адрес, который виден в Интернете. Сами компьютеры имеют назначенные IP-адреса, которые доступны только в локальной сети. Переадресация портов позволяет отправлять данные, поступающие на IP-адрес маршрутизатора (IP-адрес, видимый в Интернете) через уникальный порт на конкретный компьютер в локальной сети. Полное обсуждение переадресации портов выходит за рамки этого руководства, но научиться делать это легко - просто введите "переадресация портов" в Google. Вам нужно будет узнать, как перенаправить порты на вашем маршрутизаторе конкретной марке и модели.

При любой конфигурации клиент-сервер только серверная машина должна иметь открытый IP-адрес или открытый порт маршрутизатора/брандмауэра. Клиентский компьютер может быть расположен за маршрутизатором или межсетевым экраном без перенаправления входящих портов.

Другой вариант, позволяющий сетевым приложениям работать через маршрутизаторы, - это программное обеспечение "VPN". Такие приложения, как hamachi, comodo и OpenVPN, позволяют подключать две отдельные сети LAN через Интернет и обрабатывать все машины так, как если бы они были подключены локально (подключаться к любому компьютеру в VPN, используя локальный IP-адрес, например 192.168.0.1). 1.xxx). Программное обеспечение VPN также обычно добавляет уровень безопасности к данным, передаваемым между подключенными машинами. Обратной стороной программного обеспечения VPN является то, что передача данных может быть медленнее, чем прямое соединение с использованием переадресации портов (данные проходят через сторонний сервер).

16.4.1 Одноранговый мессенджер

Следующий пример программы текстового чата содержит построчную документацию по различным полезным методам кодирования. Инструкции см. В справочной документации, включённой в код.

REBOL [Title: "Peer-to-Peer Instant Messenger"]

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

; Приведённый ниже код перехватывает кнопку закрытия (просто вариант 
; процедуры, использованной в предыдущем примере списка). Он 
; гарантирует, что все открытые порты будут закрыты, и отправляет 
; сообщение на удалённый компьютер о том, что соединение было 
; разорвано. Обратите внимание, что строки в сообщении об отключении 
; отправляются в обратном порядке. Когда они принимаются другим 
; компьютером, они распечатываются по очереди, каждая строка поверх 
; предыдущей, поэтому при просмотре с другой стороны они 
; отображаются правильно:


insert-event-func closedown: func [face event] [
    either event/type = 'close [
        if connected [
            insert port trim {
                *************************************************
                AND RECONNECT.
                YOU MUST RESTART THE APPLICATION
                TO CONTINUE WITH ANOTHER CHAT,
                THE REMOTE PARTY HAS DISCONNECTED.
                *************************************************
            }
            close port
            if mode/text = "Server Mode" [close listen]
        ]
        quit
    ] [event]
]

view/new center-face gui: layout [
    across
    at 5x2  ; этот код позиционирует следующие элементы в GUI

    ; Текст ниже отображается как пункт меню в верхнем левом углу 
    ; графического интерфейса. При щелчке по нему текст, 
    ; содержащийся в области "display", сохраняется в выбранный 
    ; пользователем файл:

     text bold "Save Chat" [
        filename: to-file request-file/title/file/save trim {
            Save file as:} "Save" %/c/chat.txt
        write filename display/text 
    ]

    ; Текст ниже - это ещё один вариант меню. При нажатии 
    ; отображается IP-адрес пользователя. Он использует 
    ; общедоступный веб-сервер для поиска внешнего адреса. Команда 
    ; "parse" используется для извлечения IP-адреса со страницы. 
    ; Синтаксический анализ рассматривается в отдельном специальном 
    ; разделе далее в руководстве.

    text bold "Lookup IP" [
        parse read http://guitarz.org/ip.cgi [
            thru <title> copy my-ip to </title>
        ]
        parse my-ip [
            thru "Your IP Address is: " copy stripped-ip to end
        ]
        alert to-string rejoin [
            "External: " trim/all stripped-ip "  "
            "Internal: " read join dns:// read dns://
        ]
    ]

    ; Текст ниже - это третий вариант меню. При нажатии отображается 
    ; текст справки.

     text bold "Help" [
        alert {
        Enter the IP address and port number in the fields
        provided.  If you will listen for others to call you, 
        use the rotary button to select "Server Mode" (you
        must have an exposed IP address and/or an open port
        to accept an incoming chat).  Select "Client Mode" if
        you will connect to another's chat server (you can do
        that even if you're behind an unconfigured firewall, 
        router, etc.).  Click "Connect" to begin the chat. 
        To test the application on one machine, open two
        instances of the chat application, leave the IP set
        to "localhost" on both.  Set one instance to run as 
        server, and the other as client, then click connect.
        You can edit the chat text directly in the display
        area, and you can save the text to a local file.
        }
    ]
    return

    ; Ниже представлены виджеты, используемые для ввода информации о 
    ; подключении. Обратите внимание на ярлыки, присвоенные каждому 
    ; элементу. Позже текст, содержащийся в этих виджетах, будет 
    ; называться <метка>/text. Также обратите внимание на блок 
    ; действий для поворотной кнопки. Каждый раз, когда по нему 
    ; щёлкают, он либо скрывает, либо показывает другие виджеты. В 
    ; режиме сервера IP-адрес подключения не требуется - приложение 
    ; просто ожидает подключения на заданном порту. Скрытие поля 
    ; IP-адреса избавляет пользователя от некоторой путаницы.
lab1: h3 "IP Address:"  IP: field "localhost" 102
lab2: h3 "Port:" portspec: field "9083" 50
mode: rotary 120 "Client Mode" "Server Mode" [
    either value = "Client Mode" [
        show lab1 show IP
    ][
        hide lab1 hide IP
    ]
]

; Ниже находится кнопка подключения и большой блок действий, 
; который выполняет большую часть работы. Когда кнопка нажата, 
; она сначала скрывается, чтобы у пользователя не возникало 
; соблазна снова открыть порт (это могло бы вызвать ошибку). 
; Затем открывается порт TCP/IP - тип (сервер/клиент) 
; определяется с помощью конструкции "either" (либо). Если 
; ошибка возникает в любой из операций открытия порта, ошибка 
; перехватывается, и пользователь получает предупреждение с 
; сообщением - это более изящно и информативно, чем позволить 
; программе аварийно завершить работу с ошибкой. Обратите 
; внимание, что IP-адрес и информация о порте собраны из полей 
; выше. Если выбран режим сервера (т.е. если кнопка "mode" 
; (режим) выше не отображает текст "Client Mode" (Режим 
; клиента)), то порты TCP открываются в режиме прослушивания - 
; ожидая подключения клиента. Если выбран режим клиента, 
; делается попытка открыть прямое соединение с выбранным 
; IP-адресом и портом.
cnnct: button red "Connect" [
    hide cnnct
    either mode/text = "Client Mode" [
        if error? try [
            port: open/direct/lines/no-wait to-url rejoin [
                "tcp://" IP/text ":" portspec/text]
        ][alert "Server is not responding." return]
    ][
        if error? try [
            listen: open/direct/lines/no-wait to-url rejoin [
                "tcp://:" portspec/text]
            wait listen
            port: first listen
        ][alert "Server is already running." return]
    ]

    ; После того, как порты были открыты, поле ввода текста 
    ; подсвечивается, а флаг соединения устанавливается в 
    ; значение true. Сосредоточение внимания на поле ввода 
    ; текста даёт пользователю хороший визуальный сигнал о том, 
    ; что соединение установлено, но это не обязательно.

    focus entry
    connected: true

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

    forever [
        wait port
        foreach msg any [copy port []] [
            display/text: rejoin [
                ">>>  "msg newline display/text]
        ]
        show display
    ]
]

; Ниже находятся область отображения и поля ввода текста. 
; Обратите внимание на ярлыки, присвоенные каждому. "Return" 
; просто помещает каждый виджет в новую строку в графическом 
; интерфейсе (потому что режим макета установлен как "across"
; (поперек) выше).

return  display: area "" 537x500
return  entry: field 428  ; числа - это размеры в пикселях

; Кнопка отправки ниже выполняет ещё одну важную работу. 
; Во-первых, он проверяет, установлено ли соединение (с помощью 
; флага, установленного выше). Если это так, он вставляет текст, 
; содержащийся в поле "entry" выше, в открытый порт TCP/IP, 
; чтобы его перехватил удалённый компьютер - если соединение 
; было установлено, программа на другом конце ожидает чтения 
; любого данные, вставленные в этот порт. После отправки данных 
; через сетевое соединение текст добавляется в локальную текущую 
; область отображения текста, и отображение обновляется:

button "Send Text" [
    if connected [
        insert port entry/text focus entry
        display/text: rejoin [
            "<<<  " entry/text newline display/text]
        show display
    ]
]
]

show gui do-events  ; они необходимы, потому что выше используется 
                ; уточнение "/new".

16.5 Передача двоичных файлов через сетевые сокеты TCP

Эти 2 сценария, основанные на http://www.rebol.net/cookbook/recipes/0058.html Карла Сассенрата (отредактированы и сокращены здесь), демонстрируют, как передавать двоичные файлы напрямую между любыми двумя компьютерами в сети (через сокет TCP соединение), используя порты. Отправка двоичных файлов отличается от отправки текста тем, что длина файла должна быть передана перед отправкой файла. Это необходимо сделать так, чтобы код приёма знал, когда был передан полный файл. Сценарий отправки ниже добавляет информацию о длине файла и имя файла к отправляемым данным. Получающий скрипт ищет эту информацию, затем получает указанное количество двоичных данных и по завершении сохраняет его в файл:

REBOL [Title: "Server/Receiver"]

p: ":8000"  ; порт #
print "receiving"

data: copy wait client: first port: wait open/binary/no-wait join tcp:// p
info: load to-string copy/part data start: find data #""
remove/part data next start
while [info/2 > length? data] [append data copy client]
write/binary (to-file join "transferred-" (second split-path info/1)) data

insert client "done" wait client close client close port print "Done" halt


REBOL [Title: "Client/Sender"]

ip: "localhost" p: ":8000"  ; IP адрес и порт #
print "sending"

data: read/binary file: to-file request-file
server: open/binary/no-wait rejoin [tcp:// ip p]
insert data append remold [file length? data] #""
insert server data

wait server close server print "Done" halt

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

; сервер/приёмник - запускается первым:

if error? try [port: first wait open/binary/no-wait tcp://:8] [quit]
mark: find file: copy wait port #""
length: to-integer to-string copy/part file mark
while [length > length? remove/part file next mark] [append file port]

view layout [image load file]


; клиент/отправитель - запускается после сервера (изменить IP-адрес при использовании на 2 ПК):

save/png %image.png to-image layout [box blue "I traveled through ports!"]

port: open/binary/no-wait tcp://127.0.0.1:8  ; adjust this IP address
insert file: read/binary %image.png join l: length? file #""
insert port file

Следующая программа представляет собой рацию типа push-to-talk для передачи голоса по IP. Это очень просто - он просто записывает звук с микрофона в файл .wav, а затем передаёт волновой файл на другой IP-адрес (где запущена та же программа) для воспроизведения. Отправитель и получатель открываются в отдельных процессах, и оба работают в бесконечных циклах, чтобы обеспечить непрерывную связь туда и обратно. В нынешнем виде это приложение предназначено только для MS Windows. Код, обрабатывающий запись звука, более подробно обсуждается в разделе этого руководства, посвящённом библиотекам DLL:

REBOL [Title: "Intercom (VOIP Messenger)"]

write %wt-receiver.r {
    REBOL []
    print join "Receiving at " read join dns:// read dns://
    if error? try[c: first t: wait open/binary/no-wait tcp://:8000][quit]
    s: open sound://
    forever [
        d: copy wait c
        if error? try [i: load to-string copy/part d start: find d #""] [
            print "^lclient closed" close t close c close s wait 1 quit
        ]
        remove/part d next start
        while [i/2 > length? d] [append d copy c]
        write/binary (to-file join "t-" (second split-path i/1))
            decompress to-binary d
        insert s load %t-r.wav wait s
    ]
}
launch %wt-receiver.r

lib: load/library %winmm.dll
mciExecute: make routine! [c [string!] return: [logic!]] lib "mciExecute"

if (ip: ask "Connect to IP (none = localhost):  ") = "" [ip: "localhost"]
if error? try [s: open/binary/no-wait rejoin [tcp:// ip ":8000"]] [quit]

mciExecute "open new type waveaudio alias buffer1 buffer 4"
forever [
    x: ask "^lPress [ENTER] to start sending sound (or 'q' to quit): "
    if find x "q" [close s  free lib  break]
    ; if (ask "^lPress [ENTER] to send sound ('q' to quit): ") = "q"[quit]
    mciExecute "record buffer1"
    ask "^l*** YOU ARE NOW RECORDING SOUND ***   Press [ENTER] to send:  "
    mciExecute join "save buffer1 " to-local-file %r.wav
    mciExecute "delete buffer1 from 0"
    data: compress to-string read/binary %r.wav
    insert data append remold [%r.wav length? data] #""
    insert s data
]

Вот более компактная версия вышеуказанного приложения с включённой функцией громкой связи (несколько обфусцированных версий этого скрипта можно найти в конце этого руководства - вероятно, самые компактные программы VOIP, которые вы найдёте где-либо):

REBOL [title: "VOIP"] do [write %ireceive.r {REBOL []
if error? try [port: first wait open/binary/no-wait tcp://:8] [quit]
wait 0  speakers: open sound://
forever [
    if error? try [mark: find wav: copy wait port #""] [quit]
    i: to-integer to-string copy/part wav mark
    while [i > length? remove/part wav next mark] [append wav port]
    insert speakers load to-binary decompress wav
]} launch %ireceive.r
lib: load/library %winmm.dll
mci: make routine! [c [string!] return: [logic!]] lib "mciExecute"
if (ip: ask "Connect to IP (none = localhost):  ") = "" [ip: "localhost"]
if error? try [port: open/binary/no-wait rejoin [tcp:// ip ":8"]] [quit]
mci "open new type waveaudio alias wav"
forever [
    mci "record wav"  wait 2  mci "save wav r"  mci "delete wav from 0"
    insert wav: compress to-string read/binary %r join l: length? wav #""
    if l > 4000 [insert port wav]  ; squelch (don't send) if too quiet
]]

16.6 Передача данных через сетевые порты UDP

UDP - это сетевой протокол многоадресной рассылки, используемый для передачи данных любой прослушивающей программе, подключённой через сетевой порт. Если программы TCP/IP требуют, чтобы серверная программа имела известный IP-адрес, к которому подключаются все клиентские программы, программы UDP просто транслируют сообщения на открытый сетевой порт. Любая программа, прослушивающая данные UDP на этом порту, может получать сообщения. Следующая программа является прекрасным примером функциональности UDP. Для обмена сообщениями нет необходимости подключаться к центральному серверу. Любое количество пользователей может подключаться к локальной сети и транслировать сообщения всем, кто запускает эту программу. Это даёт возможность функциональной офисной "текстовой внутренней связи":

REBOL [Title: "UDP Group Chat Program"]
net-in: open udp://:9905  ; This is UDP, so NO known IP addresses required
net-out: open/lines udp://255.255.255.255:9905
set-modes net-out [broadcast: on]
svv/vid-face/color: white
name: request-text/title "Your name:"
prev-message: ""
gui: view/new layout [
    a1: area wrap rejoin [name ", you are logged in."]  across
    f1: field
    btn "Save Chat" [write request-file/only/save/file %chat.txt a1/text]
    btn "?" [alert "Press [CTRL] + U to see who's online."]
    at 0x0 key #"^M" [
        if f1/text = "" [return]
        insert net-out rejoin [name {, } now/time {:    } f1/text]
    ]
    at 0x0 key #"^u" [
        insert net-out rejoin [name {, } now/time {:    Who's online?}]
    ]
]
forever [
    focus f1
    received: wait [net-in]
    if not viewed? gui [quit]
    if find (message: copy received) "Who's online" [
        insert net-out rejoin [name " is online."]
    ]
    if message <> prev-message [
        insert (at a1/text 1) message show a1
        attempt [
            insert s: open sound:// load %/c/windows/media/ding.wav
            wait s close s
        ]
    ]
    prev-message: copy message
]

Следующий набор программ используется в авторском бизнесе по урокам музыки, чтобы предупреждать учителей о том, что их ученики пришли на приём. Протокол UDP используется потому, что центральная "серверная" программа может уведомить любое количество учителей, и серверной программе не требуется знать IP-адреса компьютеров потенциальных преподавателей. Он просто передаёт сообщения всем, кто подключён и слушает:

REBOL [Title: "UDP Signin Client Alarm"]
if error? try [net-in: open udp://:9905] [
    alert {
        This program is already running.  If you want to start a new
        instance, please close the currently open program.  If you have
        any problems, close "rebol" in the task manager or just restart
        your computer.
    }
    quit
]
svv/vid-face/color: white
name: request-list "Your name:" [
    "alex" "brian" "chad" "chris" "david" "dorian" "doug" "emerald"
    "jarrod" "josh" "kevin" "kyle" "lindsey" "mark" "nick" "peter"
    "ryan_gaughan" "stef" "steve"
]
previous-signin: []
attempt [
    insert s: open sound:// load %/c/windows/media/ding.wav
    wait s close s
]
gui: view/new center-face layout [
    a1: area 600x400 wrap rejoin [name ", you are logged in."]
    across
    btn "Save History" [
        write request-file/only/save/file (to-file now/date) a1/text
    ]
    btn "Quit" [quit]
    ; at 0x0 key #"^M" [if a1/text = "" [return]]
]
arrive-sound: load %/c/windows/media/ding.wav
forever [
    received: wait [net-in]
    if not viewed? gui [quit]
    if find (message: copy received) name [
        teacher: first parsed: parse copy message none
        student: at (find/match copy message teacher) 2  ; erase newline
        insert (at a1/text 1) rejoin [
            now/time {, } now/date {:     }
            uppercase teacher ", your student has arrived:   " 
            uppercase student
        ] 
        show a1
        attempt [insert s: open sound:// arrive-sound wait s close s]
    ]
    wait 1
]

Это сервер, необходимый для запуска указанной выше программы:

REBOL [Title: "UDP Signin Server"]
net-out: open/lines udp://255.255.255.255:9905
set-modes net-out [broadcast: on]
svv/vid-face/color: white
previous-signin: []
write/append %last-signin.txt ""
gui: view/new center-face layout [
    a1: area wrap "Server started"  across
    btn "Quit" [quit]
    ; at 0x0 key #"^M" [if a1/text = "" [return]]
]
forever [attempt [
    if not viewed? gui [quit]
    if previous-signin <> current-signin: load %last-signin.txt [
        insert net-out rejoin [current-signin/1 " " current-signin/2]
        previous-signin: current-signin
        insert (at a1/text 1) rejoin [
            "Last signed in student:  " current-signin/2 newline
            "Last signed in teacher:  " current-signin/1 newline
        ] 
        show a1
        write/append %alarm_history.txt rejoin [
            now 
            {  student: } current-signin/2 
            {  teacher: } current-signin/1 newline
        ]
        attempt [
            insert s: open sound:// load %/c/windows/media/ding.wav
            wait s close s
        ]
    ]
    wait 1
]]
quit
{%last-signin.txt contents should be 2 strings  "nick" "john smith"}

Для получения дополнительной информации о портах см. http://www.rebol.com/docs/core23/rebolcore-14.html, http://stackoverflow.com/questions/1291127/rebol-smallest-http-server-in-the-world-why-first-wait-listen-port и http://www.rebol.net/docs/async-ports.html.

16.7 Parse (ответ REBOL на регулярные выражения)

Функция синтаксического анализа "parse" используется для импорта и преобразования организованных фрагментов внешних данных в формат блока, который REBOL распознает изначально. Он также предоставляет средства анализа, поиска, сравнения, извлечения и воздействия на организованную информацию в неформатированных текстовых данных (аналогично функции сопоставления с образцом, реализованной с помощью регулярных выражений на других языках).

Основной формат синтаксического анализа:

parse <data> <matching rules>

Parse имеет несколько режимов использования. В простейшем режиме текст просто разделяется по общим разделителям и преобразуется в блок REBOL. Для этого просто укажите "none" в качестве правила сопоставления. Распространёнными разделителями являются пробелы, запятые, табуляторы, точки с запятой и новые строки. Вот некоторые примеры:

text1: "apple orange pear"
parsed-block1: parse text1 none

text2: "apple,orange,pear"
parsed-block2: parse text2 none

text3: "apple        orange                    pear"
parsed-block3: parse text3 none

text4: "apple;orange;pear"
parsed-block4: parse text4 none

text5: "apple,orange pear"
parsed-block5: parse text5 none

text6: {"apple","orange","pear"}
parsed-block6: parse text6 none

text7: {
apple
orange  
pear
}
parsed-block7: parse text7 none

Чтобы разделить файлы на основе какого-либо символа, отличного от общих разделителей, вы, как правило, можете указать разделитель. Просто заключите разделитель в кавычки:

text: "apple*orange*pear"
parsed-block: parse text "*"

text: "apple&orange&pear"
parsed-block: parse text "&"

text: "apple    &    orange&pear"
parsed-block: parse text "&"

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

text: "apple&orange*pear"
parsed-block: parse text "&*"

text: "apple&orange*pear"
parsed-block: parse text "*&" ; порядок следования не имеет значения

Использование режима синтаксического анализа "разбиение" - отличный способ поместить отформатированные таблицы данных в ваши программы REBOL. Разделив нижеследующий текст символами возврата каретки, вы столкнётесь с небольшой проблемой:

text: {    First Name
           Last Name
           Street Address
           City, State, Zip}

parsed-block: parse text "^/" 

; ^/ Символ новой строки в REBOL

Пробелы включены в правило синтаксического анализа по умолчанию (синтаксический анализ автоматически разделяется на все пустое пространство), поэтому вы получаете более разбитый блок данных, чем предполагалось:

["First" "Name" "Last" "Name" "Street" "Address" "City,"    "State," "Zip"]

Вы можете использовать уточнение "/all", чтобы исключить пробелы из правила разделителя. Код ниже:

text: {    First Name
           Last Name
           Street Address
           City, State, Zip}

parsed-block: parse/all text "^/"

преобразует заданный текст в следующий блок:

[" First Name" "      Last Name" "      Street Address"    "      City, State, Zip"]

Теперь вы можете обрезать лишнее пространство на каждой из строк:

foreach item parsed-block [trim item]

и вы получите следующий блок parsed-block, как и предполагалось:

["First Name" "Last Name" "Street Address" "City, State, Zip"]

16.8 Использование синтаксического анализа для загрузки файлов электронных таблиц CSV и других структурированных данных

Parse обычно используется для преобразования данных электронной таблицы в блоки REBOL. В Excel, Open Office и других программах для работы с электронными таблицами вы можете экспортировать все столбцы данных на листе, сохранив их как текстовый файл в формате CSV ("Comma Separated Value" (запятыми разделённые значения)) .csv. Люди часто помещают в электронные таблицы различные фрагменты описательного текста, меток и заголовков столбцов, чтобы сделать их более удобочитаемыми:

;                         TITLE
                       DESCRIPTION

                         Header1 

Category1      data       data      data        Notes...
               data       data      data
               data       data      data
               data       data      data

                         Header2

Category2      data       data      data        Notes...
               data       data      data
               data       data      data
               data       data      data

                         Header3

Category3      data       data      data        Notes...
               data       data      data
               data       data      data
               data       data      data

Следующий код превращает экспортированные данные электронной таблицы CSV в удобный блок REBOL с данными заголовка группы, добавленными в каждую строку:

; Читаем и разбираем CSV файл:

filename: %filename.csv
data: copy []
lines: read/lines filename
foreach line lines [
    append/only data parse/all line ","
]

; Добавляем заголовки из разделов таблицы в каждую позицию:

info: copy ""
foreach line data [
    either find "Header" line/1 [
        info: line/1
    ][
        append line info
    ]
]

; Удаляем ненужные описательные строки заголовка:

remove-each line data [find "Header" line/1/1]
remove-each line data [
    (line/3 = "TITLE") or (line/3 = "DESCRIPTION")
]

16.9 Использование режима сопоставления с образцом в Parse для поиска данных

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

parse ["apple"] ["apple"]

parse ["apple" "orange"] ["apple" "orange"]

Обе строки выше оцениваются как истина, потому что они точно совпадают. ВАЖНО: По умолчанию, как только синтаксический анализ обнаруживает что-то, что не соответствует, все выражение оценивается как ложное, ДАЖЕ, если данное правило встречается в данных один или несколько раз. Например, неверно следующее:

parse ["apple" "orange"] ["apple"]

Но это просто поведение по умолчанию. Вы можете контролировать, как синтаксический анализатор реагирует на несоответствующие элементы. Добавление следующих слов к правилу вернёт истину, если данное правило соответствует данным указанным образом:

  1. "any" - правило соответствует данным ноль или более раз
  2. "some" - правило соответствует данным один или несколько раз
  3. "opt" - правило совпадает с данными ноль или один раз
  4. "one" - правило соответствует данным ровно один раз
  5. целое число - правило сопоставляет данные заданное количество раз
  6. два целых числа - правило сопоставляет данные несколько раз, включённых в диапазон между двумя целыми числами

Все следующие примеры верны:

parse ["apple" "orange"] [any string!]
parse ["apple" "orange"] [some string!]
parse ["apple" "orange"] [1 2 string!]

Вы можете создать правила, которые включают несколько вариантов соответствия - просто разделите варианты знаком "|" и заключите их в квадратные скобки. Верно следующее:

parse ["apple" "orange"] [any [string! | url! | number!]]

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

parse ["apple" "orange"] [any [string! 
    (alert "The block contains a string.") | url! | number!]]

Вы можете пропускать данные, игнорируя фрагменты, пока не дойдёте до заданного условия или не пропустите его. Слово "to" (до) игнорирует данные ДО тех пор, пока не будет найдено условие. Слово "thru" игнорирует данные до тех пор, пока НЕ ПРОШЛО условие. Верно следующее:

parse [234.1 $50 http://rebol.com "apple"] [thru string!]

Настоящая ценность сопоставления с образцом состоит в том, что вы можете организованно искать и извлекать данные из неформатированного текста. Слово "copy" (копия) используется для присвоения переменной совпадающим данным. Например, следующий код загружает необработанный HTML-код с домашней страницы REBOL, игнорирует все, кроме того, что находится между тегами заголовка HTML, и отображает этот текст:

parse read http://rebol.com [
    thru <title> copy parsed-text to </title> (alert parsed-text)
]

Следующий код расширяет приведённый выше пример, чтобы предоставить полезную функцию отображения внешнего IP-адреса локального компьютера. Он читает http://guitarz.org/ip.cgi, разбирает текст заголовка, а затем снова анализирует этот текст, чтобы вернуть только номер IP. Также отображается адрес локальной сети с использованием встроенного в REBOL протокола DNS:

parse read http://guitarz.org/ip.cgi [
    thru <title> copy my-ip to </title>
]
parse my-ip [
    thru "Your IP Address is: " copy stripped-ip to end
]
alert to-string rejoin [
    "External: " trim/all stripped-ip "  "
    "Internal: " read join dns:// read dns://
]

В следующем примере загружаются и анализируются текущие (действующие) курсы обмена доллара США с http://x-rates.com. Пользователь выбирает из списка валют для конвертации, затем выполняет и отображает конвертацию из долларов США в выбранную валюту. Первая половина скрипта - это простой пример графического интерфейса калькулятора, взятый из первой части руководства. Весь анализ происходит при нажатии кнопки "Convert" (Конвертировать):

REBOL [title: "Currency Rate Conversion Calculator"]
view center-face layout [
    origin 0  space 0x0  across
    f: field 200x40 font-size 20
    return
    style btn btn 50x50 [append f/text face/text  show f]
    btn "1"  btn "2"  btn "3"  btn " + "  return
    btn "4"  btn "5"  btn "6"  btn " - "  return
    btn "7"  btn "8"  btn "9"  btn " * "  return
    btn "0"  btn "."  btn " / "   btn "=" [
        attempt [f/text: form do f/text  show f]
    ] return
    btn 200x35 "Convert" [
        x: copy []
        html: read http://www.x-rates.com/table/?from=USD&amount=1.00
        html: find html "src='/themes/bootstrap/images/xrates_sm_tm.png'" 
        parse html [
            any [
                thru {from=USD} copy link to {</a>} (append x link)
            ] to end 
        ]
        rates: copy []
        foreach rate x [
            parse rate [thru {to=} copy c to {'>}]
            parse rate [thru {'>} copy v to end]
            if not error? try [to-integer v] [append rates reduce [c v]]
        ]  
        currency: request-list "Select Currency:" extract rates 2
        rate: to-decimal select rates currency
        attempt [alert rejoin [currency ": " (rate * to-decimal f/text)]]
    ]
]

Вот полезный пример, который удаляет все комментарии из данного сценария REBOL (любая часть строки, начинающаяся с точки с запятой ";"):

code: read to-file request-file

parse/all code [any [
    to #";" begin: thru newline ending: (
        remove/part begin ((index? ending) - (index? begin))) :begin
    ]
]

editor code

Дополнительные сведения о синтаксическом анализе см. по следующим ссылкам:

http://www.codeconscious.com/rebol/parse-tutorial.html http://www.rebol.com/docs/core23/rebolcore-15.html http://en.wikibooks.org/wiki/REBOL_Programming/Language_Features/Parse http://www.rebolforces.com/zine/rzine-1-06.html#sect4.

16.10 Реагирование на особые события в графическом интерфейсе - "Feel"

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

view layout [btn "Click me" [alert "Thank you for the click :)"]]

Но что, если вы хотите, чтобы ваш графический интерфейс реагировал на события, отличные от щелчка мыши непосредственно по виджету? Что, если, например, вы хотите, чтобы программа реагировала всякий раз, когда пользователь щёлкает в любом месте экрана графического интерфейса пользователя (например, в программе рисования), или если вы хотите, чтобы виджет что-то делал по прошествии определённого времени, или если вы хотите фиксировать щелчки по кнопке закрытия графического интерфейса, чтобы пользователь не мог случайно закрыть экран важных данных. Вот для чего используются объект "feel" и функция "insert-event-func".

Вот пример базового синтаксиса Feel:

view layout [
    text "Click, right-click, and drag the mouse over this text." feel [
        engage: func [face action event] [
            print action
            print event/offset
        ]
    ]
]

Приведённый выше код часто сокращается с помощью "f a e" для обозначения "face action event" ( лицо событие действие):

view layout [
    text "Mouse me." feel [
        engage: func [f a e] [
            print a
            print e/offset
        ]
    ]
]

Вы можете реагировать на определённые события следующим образом:

view layout [
    text "Mouse me." feel [
        engage: func [f a e] [
            if a = 'up [print "You just released the mouse."]
        ]
    ]
]

В этом примере показано, как объединить обнаружение мыши в полноэкранном режиме с обычными щелчками мыши по виджетам. Для этого невидимое поле того же размера, что и экран, с прикреплённым событием ощущения используется для полноэкранного обнаружения. Затем другие виджеты просто помещаются поверх него, начиная с начала координат окна:

print "Click anywhere in the window, then click the text."
view center-face layout [
    size 400x200
    box 400x200 feel [
        engage: func [f a e] [
            print a
            print e/offset
        ]
    ]
    origin 
    text "Click me" [print "Text clicked"] [print "Text right-clicked"]
    box blue [print "Box clicked"]
]

Вы также можете назначить события таймера любому виджету следующим образом:

view layout [
    text "This text has a timer event attached." rate 00:00:00.5 feel [
        engage: func [f a e] [
            if a = 'time [print "1/2 second has passed."]
        ]
    ]
]

Вот кнопка с прикреплённым событием времени (оценка "0" означает, что не нужно ждать вообще). Каждые 0 секунд, когда обнаруживается событие таймера, смещение (положение) кнопки обновляется. Это создаёт анимацию:

view layout/size [
    mover: btn rate 0 feel [
        engage: func [f a e] [
            if a = 'time [
                mover/offset: mover/offset + 5x5
                show mover
            ]
        ]
    ]
] 400x400

Вот небольшая игра-стрелялка, в которой используется событие таймера для автоматизации перемещения графического интерфейса пользователя по экрану, проверки коллизий и управления другими игровыми операциями:

REBOL [title: "VID Shooter"]
score: 0   speed: 20   fire: false
do game: [
    view center-face layout [
        size 600x440
        at 270x0 text join "Score: " score
        at 280x440 x: box 2x20 yellow
        at (as-pair 0 (random 300) + 30) y: btn 50x20 red "Enemy"
        at 280x420 z: btn 50x20 blue "Player"
        box 0x0 #"l" [z/offset: z/offset + 10x0 show z]
        box 0x0 #"k" [z/offset: z/offset + -10x0 show z]
        box 0x0 #" " [
            if fire = false [
                fire: true 
                x/offset: as-pair z/offset/1 440
            ]
        ]
        box 0x0 rate speed feel [
            engage: func [f a e] [
                if a = 'time [
                    y/offset: y/offset + 5x0
                    if y/offset/1 > 600 [
                        y/offset: as-pair -10 ((random 300) + 30)
                    ]
                    show y
                    if fire = true [x/offset: x/offset + 0x-20]
                    if x/offset/2 < 0 [
                        x/offset/2: 440 
                        fire: false
                    ]
                    show x
                    if within? x/offset y/offset 50x25 [
                        alert "Kablammmm!!!"
                        score: score + 1
                        speed: speed + 5
                        fire: false
                        unview
                        do game
                    ]
                ]
            ]
        ]
    ]
]

Обновляя смещение виджета при каждом щелчке по нему, вы можете включить операции перетаскивания:

view layout/size [
    text "Click and drag this text" feel [
        ; запоните f="face", a="action", e="event":
        engage: func [f a e] [
            ; сначала запишите координату, в которой первоначально 
            ; щёлкнули мышью:
            if a = 'down [initial-position: e/offset]
            ; если мышь перемещается, удерживая нажатой кнопку, 
            ; переместите позицию виджета, по которому щёлкнули 
            ; мышью, на ту же величину (разница между начальной 
            ; координатой щелчка, записанной выше, и новой текущей 
            ; координатой, определяемой всякий раз, когда происходит 
            ; событие перемещения мыши):
            if find [over away] a [
                f/offset: f/offset + (e/offset - initial-position)
            ]
            show f
        ]
    ]
] 600X440

Объекты Feel и функции событий могут быть включены прямо в определение стиля. Приведённое ниже определение позволяет легко создавать несколько виджетов графического интерфейса, которые можно перетаскивать по экрану. "movestyle" определяется как блок кода, который позже передаётся объекту "feel" виджета, и поэтому включён в общее определение стиля (здесь были добавлены функции удаления и добавления, чтобы разместить перемещённый виджет поверх других виджетов. в графическом интерфейсе (то есть, чтобы вывести перетаскиваемый виджет на передний план)). Вы можете добавить этот код "feel movestyle" в любой виджет GUI, чтобы сделать его перетаскиваемым:

movestyle: [
    engage: func [f a e] [
        if a = 'down [
            initial-position: e/offset
            remove find f/parent-face/pane f
            append f/parent-face/pane f
        ]
        if find [over away] a [
            f/offset: f/offset + (e/offset - initial-position)
        ]
        show f
    ]
]

view layout/size [
    style moveable-object box 20x20 feel movestyle
    ; "random 255.255.255" представляет разные случайные цвета для 
    ; каждой детали: 
    at random 600x400 moveable-object (random 255.255.255)
    at random 600x400 moveable-object (random 255.255.255)
    at random 600x400 moveable-object (random 255.255.255)
    at random 600x400 moveable-object (random 255.255.255)
    at random 600x400 moveable-object (random 255.255.255)
    text "This text and all the boxes are movable" feel movestyle
] 600x440

Функция "detect" (обнаружение) внутри блока "feel" (чувств) полезна для постоянной проверки событий. Следующая программа постоянно проверяет движения мыши, и если мышь когда-либо находится над кнопкой, кнопка перемещается в случайное положение. Этот метод может быть полезен, например, в видеоиграх, управляемых движением мыши:

view center-face layout [
    size 600x440
    at 270x209 b: btn "Click Me!" feel [
        detect: func [f e] [
            ; Следующая строка проверяет любое движение мыши:
            if e/type = 'move [
                ; Эта строка проверяет, находится ли положение мыши 
                ; в пределах координат кнопки (то есть касание 
                ; кнопки):
                if (within? e/offset b/offset 59x22) [
                    ; Если да, переместим кнопку в случайное 
                    ; положение:
                    b/offset: b/offset + ((random 50x50) - (random 50x50))
                    ; Проверим, не переместилась ли кнопка за 
                    ; пределы экрана
                    if not within? b/offset -59x-22 659x462 [
                        ; Если да, вернём в центр окна:
                        b/offset: 270x209
                    ]
                    ; Обновляем экран:
                    show b
                ]
            ]
            ; При использовании функции обнаружения всегда 
            ; возвращаем событие:
            e
        ]
    ]
]

Для обработки глобальных событий в графическом интерфейсе пользователя, таких как изменение размера и закрытие, полезно использовать команду "insert-event-func". В следующем примере проверяются события изменения размера:

insert-event-func [
    either event/type = 'resize [
        alert "I've been resized"
        none    ; вернём это значение, если мы не хотим больше 
                ; ничего делать с событием.
    ][
        event   ; вернём это значение, если указанное событие не 
                ; найдено
    ]
]

view/options layout [text "Resize this window."] [resize]

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

insert-event-func [
    either event/type = 'resize [
        stay-here/offset:
            stay-here/parent-face/size - stay-here/size - 20x20
        show stay-here
        none    ; вернём это значение, если мы не хотите больше 
                ; ничего делать с событием.
    ][
        event   ; вернём это значение, если указанное событие не 
                ; найдено
    ]
]

view/options layout [
    stay-here: text "Resize this window."
] [resize]

Чтобы удалить установленный обработчик событий, используйте remove-event-func. В следующем примере фиксируются три последовательных события закрытия, а затем удаляется обработчик событий, что позволяет закрыть графический интерфейс с 4-й попытки:

count: 1
evtfunc: insert-event-func [
    either event/type = 'close [
        if count = 3 [remove-event-func :evtfunc]
        count: count + 1
        none
    ][
        event
    ]
]

view layout [text "Try to close this window 4 times."]

Для получения дополнительной информации об обработке событий см. http://www.rebol.com/how-to/feel.html, http://www.codeconscious.com/rebol/view-notes.html и http://www. rebol.com/docs/view-system.html.

16.11 2D рисование, графика и анимация

С помощью диалекта "view layout" ("VID") REBOL вы можете легко создавать графические пользовательские интерфейсы, которые включают кнопки, поля, текстовые списки, изображения и другие виджеты графического интерфейса, но он не предназначен для обработки графики или анимации общего назначения. Для этой цели REBOL включает встроенный диалект "draw" (рисование). Различные функции рисования позволяют создавать линии, прямоугольники, круги, стрелки и практически любую другую форму. К рисункам можно легко применить узоры заливки, цветовые градиенты и всевозможные эффекты.

Реализация функций рисования обычно включает в себя создание графического пользовательского интерфейса "view layout" с прямоугольным виджетом, который используется в качестве экрана просмотра. Затем к определению блока добавляются функции "Effect" (Эффект) и "draw" (рисование), и в функцию рисования передаётся блок, который содержит больше функций, которые фактически выполняют рисование различных форм и других графических элементов в блоке. Каждая функция рисования принимает соответствующий набор аргументов для типа созданной фигуры (значения координат, значение размера и т.д.). Вот базовый пример формата рисования:

view layout [box 400x400 effect [draw [line 10x39 322x211]]]
;  "line" это функция рисования

Вот тот же самый пример с помятой и разбитой на несколько строк:

view layout [
    box 400x400 effect [
        draw [
            line 10x39 322x211
        ]
    ]
]

В блок рисования может быть включено любое количество элементов формы (функций):

view layout [
    box 400x400 black effect [
        draw [
            line 0x400 400x50
            circle 250x250 100
            box 100x20 300x380
            curve 50x50 300x50 50x300 300x300
            spline closed 3 20x20 200x70 150x200
            polygon 20x20 200x70 150x200 50x300
        ]
    ]
]

Цвет может быть добавлен к графике с помощью функции "pen" (перо). Фигуры могут быть заполнены цветом, изображениями и другими графическими элементами с помощью функции "fill-pen". Толщина нарисованных линий задаётся функцией "line-width":

view layout [
    box 400x400 black effect [
        draw [
            pen red
            line 0x400 400x50
            pen white
            box 100x20 300x380
            fill-pen green
            circle 250x250 100
            pen blue
            fill-pen orange
            line-width 5
            spline closed 3 20x20 200x70 150x200
            polygon 20x20 200x70 150x200 50x300
        ]
    ]
]

К элементам можно легко применить градиенты и другие эффекты:

view layout [
    box 400x220 effect [
        draw [
            fill-pen 200.100.90
            polygon 20x40 200x20 380x40 200x80
            fill-pen 200.130.110
            polygon 20x40 200x80 200x200 20x100
            fill-pen 100.80.50
            polygon 200x80 380x40 380x100 200x200
        ]
        gradmul 180.180.210 60.60.90
    ]
]

Нарисованные фигуры автоматически сглаживаются (линии сглаживаются), но эту функцию по умолчанию можно отключить:

view layout [
    box 400x400 black effect [
        draw [
            ; со сглаживанием по умолчанию:
            circle 150x150 100
            ; без сглаживания:
            anti-alias off
            circle 250x250 100
        ]
    ]
]

16.11.1 Анимация

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

  1. Присвойте текстовой метке блоку, в котором происходит рисование (слово "scrn" используется в следующих примерах).
  2. Создайте новый блок рисования, в котором изменяются характеристики графических элементов (положение, размер и т.д.).
  3. Назначьте новый блок "{yourlabel}/effect/draw" (то есть "scrn/label/draw: [изменённый блок рисования]" в данном случае).
  4. Отобразите изменения с помощью функции "show {yourlabel}" (т.е. в данном случае "show scrn").

Вот базовый пример, который перемещает круг в новое положение при нажатии кнопки:

view layout [
    scrn: box 400x400 black effect [draw [circle 200x200 20]]
    btn "Move" [
        scrn/effect/draw: [circle 200x300 20]  ; заменить блок выше
        show scrn
    ]
]

Переменным можно назначать позиции, размеры и/или другие характеристики элементов рисования, а циклы можно использовать для создания плавных анимаций путём постепенной настройки этих элементов:

pos: 200x50
view layout [
    scrn: box 400x400 black effect [draw [circle pos 20]]
    btn "Move Smoothly" [
        loop 50 [
            ; увеличить значение "y" координаты:
            pos/y: pos/y + 1
            scrn/effect/draw: copy [circle pos 20]
            show scrn
        ]
    ]
]

Координаты анимации (и другие свойства рисования) также могут храниться в блоках:

pos: 200x200
coords: [70x346 368x99 143x45 80x125 237x298 200x200]

view layout [
    scrn: box 400x400 black effect [draw [circle pos 20]]
    btn "Jump Around" [
        foreach coord coords [
            scrn/effect/draw: copy [circle coord 20]
            show scrn
            wait 1
        ]
    ]
]

Другие источники данных также могут служить для контроля движения. В следующем примере ввод данных пользователя перемещает круг по экрану. Обратите внимание на использование функции "feel" для обновления экрана каждую 10-ю долю секунды ("скорость 0:0:0,1"). Поскольку чувство используется для просмотра, ожидания и реакции на события окна, оно вам, вероятно, понадобится во многих ситуациях, когда используется анимация, например, в играх:

pos: 200x200
view layout [
    scrn: box 400x400 black rate 0:0:0.1 feel [
        engage: func [face action event] [
            if action = 'time [
                scrn/effect/draw: copy []
                append scrn/effect/draw [circle pos 20]
                show scrn
            ]    
        ] 
    ] effect [ draw [] ]
    across
    btn "Up" [pos/y: pos/y - 10]
    btn "Down" [pos/y: pos/y + 10]
    btn "Right" [pos/x: pos/x + 10]
    btn "Left" [pos/x: pos/x - 10]
]

Вот очень простая программа рисования, которая также использует функцию ощущения. Всякий раз, когда обнаруживается действие мыши вниз, координата события мыши ("event/offset") добавляется к блоку рисования (т.е. новая точка добавляется на экран при каждом щелчке мыши), а затем блок показывается:

view layout [
    scrn: box black 400x400 feel [
        engage: func [face action event] [
            if find [down over] action [
                append scrn/effect/draw event/offset
                show scrn
            ]
            if action = 'up [append scrn/effect/draw 'line]
        ]
    ] effect [draw [line]]
]

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

view layout [
    box 400x400 black effect [
        draw [
            image logo.gif 10x10 350x200 250x300 50x300
            ; "logo.gif" is built into the REBOL interpreter
        ]
    ]
]

Вот пример, в котором описанная выше техника масштабирования изображения сочетается с некоторой анимацией. ВАЖНО: В следующем примере вычисления координатной позиции происходят внутри рисовального блока. Всякий раз, когда такие оценки происходят внутри блока рисования (то есть, когда значения добавляются или вычитаются из положения переменной координаты, размера и т.д.), для оценки этих значений должна использоваться функция "reduce" (уменьшить) или "compose" (составить). Обратите внимание на галочку (') рядом с функцией "image" (изображение). Функциональные слова внутри "сокращённого" блока должны быть помечены этим символом для правильной оценки:

pos: 300x300
view layout [
    scrn: box pos black effect [
        draw [image logo.gif 0x0 300x0 300x300 0x300]
    ]
    btn "Animate" [
        for point 1 140 1 [
            scrn/effect/draw: copy reduce [
                'image logo.gif 
                (pos - 300x300)
                (1x1 + (as-pair 300 point))
                (pos - (as-pair 1 point))
                (pos - 300x0)
            ]
            show scrn
        ]
        for point 1 300 1 [
            scrn/effect/draw: copy reduce [
                'image logo.gif 
                (1x1 + (as-pair 1 point))
                (pos - 0x300)
                (pos - 0x0)
                (pos - (as-pair point 1))
            ]
            show scrn
        ]
        ; ниже не требуется никакого "сокращения", потому что в 
        ; блоке рисования не выполняются никакие вычисления - это 
        ; просто статические координаты:
        scrn/effect/draw: copy [
            image logo.gif 0x0 300x0 300x300 0x300
        ]
        show scrn
    ]
]

Вот ещё один пример блока рисования, который содержит оценённые вычисления и, следовательно, требует вычисления "reduce":

view layout [
    scrn: box 400x400 black effect [draw [line 0x0 400x400]]
    btn "Spin" [
        startpoint: 0x0
        endpoint: 400x400
        loop 400 [
            scrn/effect/draw: copy reduce [
                'line 
                startpoint: startpoint + 0x1
                endpoint: endpoint - 0x1
            ]
            show scrn
        ]
    ]
]

Маленькая полезная программа рисования на http://rebol.org/cgi-bin/cgiwrap/rebol/view-script.r?script=paintplus.r состоит всего из 238 строк кода. Взгляните на него, чтобы увидеть, насколько эффективен код отрисовки REBOL:

url: http://rebol.org/cgi-bin/cgiwrap/rebol/download-a-script.r?
script: "script-name=paintplus.r"
do rejoin [url script]
paint none []

Приведённая выше программа рисования - действительно полезный инструмент. Вы можете загружать изображения для рисования и сохранять изменения для последующей загрузки:

paint load %myimage.png load %edits.txt

Взгляните на сценарий 100 примеров рисования, чтобы увидеть ещё много полезного кода рисования.

Для получения дополнительной информации о встроенных формах, функциях и возможностях рисования см. http://www.rebol.com/docs/draw-ref.html, http://www.rebol.com/docs/draw.html, http://translate.google.com/translate?hl=en&sl=fr&u=http://www.rebolfrance.info/org/articles/login11/login11.htm (переведено Google), http://www. rebolforces.com/zine/rzine-1-05.html, (обновлённый код для этих двух руководств доступен по адресу http://mail.rebol.net/maillist/msgs/39100.html). Хорошее короткое руководство, демонстрирующее, как создавать многопользовательские сетевые игры с рисованной графикой, доступно на RebolFrance (переведено Google). Также обязательно посетите http://www.nwlink.com/~ecotope1/reb/easy-draw.r (интерактивная версия сайта доступна на рабочем столе REBOL - Документы - Easy Draw).

16.12 3D-графика с r3D

Механизм моделирования "r3D" Эндрю Хоадли полностью построен на встроенных в REBOL 2D функциях рисования. Он демонстрирует весьма мощный потенциал рисования. Это было продемонстрировано ранее в руководстве с помощью полезного сценария трёхмерного построения. В приведённых ниже примерах показано ещё кое-что, чего можно достичь с помощью r3D:

do http://www.rebol.net/demos/BF02D682713522AA/i-rebot.r
do http://www.rebol.net/demos/BF02D682713522AA/histogram.r
do http://www.rebol.net/demos/BF02D682713522AA/objective.r

Движёк r3D небольшой. Вот весь модуль в сжатом, встраиваемом формате (это всего лишь стандартный код REBOL, сжатый в более компактный формат). Чтобы включить 3D-графику в ваших программах REBOL, просто включите этот текст в свой код (вставьте его или "do" из файла). Если вы хотите прочитать и изучить чистый код REBOL, из которого состоит этот модуль, см. примеры выше (модуль r3D включён в эти примеры как обычный текстовый код):

do to-string decompress 64#{
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
Rf8G28Udt8glAAA=
}

Вот простой пример, демонстрирующий базовый синтаксис и использование r3D. Обязательно выполните приведённый выше код перед запуском этого примера:

Transx:  Transy:  Transz: 300.0          ; Для начала установим  
Lookatx:  Lookaty:  Lookatz: 100.0       ; некоторые положения
                                         ; камеры.
o update: does [            ; В этой функции "update" все определяется
    world: copy []           
    append world reduce [   ; Добавим свои 3D-объекты внутрь этого 
                            ; "append".
        reduce [cube-model (r3d-scale 100.0 150.0 125.0) red]
    ]                        ; Красный куб 'cube' 100x150x125 добавлен
    camera: r3d-position-object   
        reduce [Transx Transy Transz]
        reduce [Lookatx Lookaty Lookatz]
        [0.0 0.0 1.0] 
    RenderTriangles: render world camera r3d-perspective 250.0 400x360
    probe RenderTriangles   ; Эта строка демонстрирует, что 
]                           ; происходит под капотом. Вы можете это 
                            ; отключить, закоментировав эту строку.

view layout [
    scrn: box 400x360 black effect [draw RenderTriangles] ; базовое 
                                                          ; рисование
    across return
    slider 60x16 [Transx: (value * 600 - 300.0) update show scrn]
    slider 60x16 [Transy: (value * 600 - 300.0) update show scrn]
    slider 60x16 [Transz: (value * 600) update show scrn]
    slider 60x16 [Lookatx: (value * 400 - 200.0) update show scrn]
    slider 60x16 [Lookaty: (value * 400 - 200.0) update show scrn]
    slider 60x16 [Lookatz: (value * 200 ) update show scrn]
]

R3D работает путём рендеринга 3D-изображений в собственные функции рисования REBOL 2D, которые содержатся в блоке "RenderTriangles" выше. R3D предоставляет базовые структуры фигур и простой языковой интерфейс для создания и просмотра этих изображений в приложении REBOL. Он автоматически регулирует освещение и другие характеристики изображений, когда они рассматриваются с разных точек зрения. Чтобы увидеть, как рендеринг изображений преобразуется в простые функции рисования REBOL, просмотрите вывод строки "probe RenderTriangles" в интерпретаторе REBOL, когда вы регулируете ползунки выше. Он отображает список команд рисования, используемых для создания каждого изображения в движущемся трёхмерном мире.

В приведённом выше примере виджеты ползунков используются для настройки значений в анимации. Эти значения можно так же легко контролировать с помощью циклов или других форм ввода данных. В приведённом ниже примере значения регулируются нажатием клавиш, назначенным пустым текстовым виджетам (используйте клавиши "asdfghqwerty" для перемещения куба):

Transx:  Transy:  Transz: 2.0
Lookatx:  Lookaty:  Lookatz: 1.0 
do update: does [
    world: copy []
    append world reduce [ 
        reduce [cube-model (r3d-scale 100.0 150.0 125.0) red]
    ]
    Rendered: render world 
        r3d-position-object   
        reduce [Transx Transy Transz]
        reduce [Lookatx Lookaty Lookatz]
        [0.0 0.0 1.0]
        r3d-perspective 360.0 400x360
]
view layout [
    across
    text "" #"a" [Transx: (Transx + 10) update show scrn]
    text "" #"s" [Transx: (Transx - 10) update show scrn]
    text "" #"d" [Transy: (Transy + 10) update show scrn]
    text "" #"f" [Transy: (Transy - 10) update show scrn]
    text "" #"g" [Transz: (Transz + 10) update show scrn]
    text "" #"h" [Transz: (Transz - 10) update show scrn]
    text "" #"q" [Lookatx: (Lookatx + 10) update show scrn]
    text "" #"w" [Lookatx: (Lookatx - 10) update show scrn]
    text "" #"e" [Lookaty: (Lookaty + 10) update show scrn]
    text "" #"r" [Lookaty: (Lookaty - 10) update show scrn]
    text "" #"t" [Lookatz: (Lookatz + 10) update show scrn]
    text "" #"y" [Lookatz: (Lookatz - 10) update show scrn]
    at 20x20
    scrn: box 400x360 black effect [draw Rendered] 
]

Модуль r3D может работать с моделями, сохранёнными в собственном формате .R3d и в формате "OFF" (установленном программой GeomView по адресу http://www.geom.uiuc.edu/projects/visualization/. См. http://local.wasp.uwa.edu.au/~pbourke/dataformats/oogl/#OFF для описания формата файла OFF). Ряд объектов-примеров OFF доступны по адресу http://www.mpi-sb.mpg.de/~kettner/proj/obj3d/.

Чтобы понять, как создавать/импортировать и манипулировать более сложными трёхмерными фигурами, изучите способ конструирования объектов внутри функции "update" в каждом из трёх примеров Эндрю. Вот упрощённый вариант примера Эндрю objective.r, который загружает модели .off с жёсткого диска. Обязательно выполните приведённый выше код модуля r3D перед запуском этого примера, а затем попробуйте загрузить и загрузить некоторые из примеров файлов .off с указанного выше веб-сайта:

RenderTriangles: []
view layout [
    scrn: box 400x360 black effect [draw RenderTriangles]
    across return
    slider 60x16 [Transx: (value * 600 - 300.0) update show scrn]
    slider 60x16 [Transy: (value * 600 - 300.0) update show scrn]
    slider 60x16 [Transz: (value * 600) update show scrn]
    slider 60x16 [Lookatx: (value * 400 - 200.0) update show scrn]
    slider 60x16 [Lookaty: (value * 400 - 200.0) update show scrn]
    slider 60x16 [Lookatz: (value * 200 ) update show scrn]
    return btn "Load Model" [
        model: r3d-load-OFF load to-file request-file
        modelsize: 1.0
        if model/3 [modelsize: model/3]
        if modelsize < 1.0 [ modelsize: 1.0 ]
        defaultScale: 200.0 / modelsize
        objectScaleX: objectScaleY: objectScaleZ: defaultscale
        objectRotateX: objectRotateY: objectRotateZ: 0.0
        objectTranslateX: objectTranslateY: objectTranslateZ: 0.0    
        Transx:  Transy:  Transz: 300.0
        Lookatx:  Lookaty:  Lookatz: 200.0
        modelWorld: r3d-compose-m4 reduce [
            r3d-scale objectScaleX objectScaleY objectScaleZ
            r3d-translate 
                objectTranslateX objectTranslateY objectTranslateZ
            r3d-rotatex objectRotateX
            r3d-rotatey objectRotateY 
            r3d-rotatez objectRotateZ
        ]
        r3d-object: reduce [model modelWorld red]
        do update: does [
            world: copy []    
            append world reduce [r3d-object]
            camera: r3d-position-object   
                reduce [Transx Transy Transz]
                reduce [Lookatx Lookaty Lookatz]
                [0.0 0.0 1.0] 
            RenderTriangles: 
                render world camera r3d-perspective 250.0 400x360
        ]
        update show scrn
    ]
]

Как и большинство решений REBOL, r3D - это гениально простой, компактный и мощный дизайн, не требующий каких-либо внешних инструментов. Это чистый REBOL, и это действительно потрясающе!

16.13 Несколько 3D-скриптов с использованием Raw REBOL Draw Dialect

Следующий короткий сценарий представляет собой сжатую версию "ebuc-cube" Грегори Печерета (с http://www.rebol.net/demos/download.html). Он демонстрирует некоторые простые 3D-техники с использованием только встроенных функций рисования REBOL (сторонняя библиотека не требуется). Его относительно легко понять, манипулировать и использовать для создания вашей собственной базовой трёхмерной графики:

z: 10 h: z * 12 j: negate h c: as-pair z * 5 z * 5 l: z * 4 w: z * 20
img: to-image layout [box effect [draw [pen logo.gif circle c l]]]
q: make object! [x: 0 y: 0 z: 0] 
cube: [[h h j] [h h h] [h j j] [h j h] [j h j] [j h h] [j j j] [j j h]]
view center-face layout [
    f: box 400x400 rate 0 feel [engage: func [f a e] [
        b: copy []   q/x: q/x + 5   q/y: q/y + 8   q/z: q/z + 3
        repeat n 8 [
            p: reduce pick cube n  ; point
            zx: (p/1 * cosine q/z) - (p/2 * sine q/z) - p/1
            zy: (p/1 * sine q/z) + (p/2 * cosine q/z) - p/2
            yx: (p/1 + zx * cosine q/y) - (p/3 * sine q/y) - p/1 - zx
            yz: (p/1 + zx * sine q/y) + (p/3 * cosine q/y) - p/3
            xy: (p/2 + zy * cosine q/x) - (p/3 + yz * sine q/x) - p/2 - zy
            append b as-pair (p/1 + yx + zx + w) (p/2 + zy + xy + w)
        ]
        f/effect: [draw [
            fill-pen 255.0.0.100  polygon b/6 b/2 b/4 b/8
            image img b/6 b/5 b/1 b/2 
            fill-pen 255.159.215.100  polygon b/2 b/1 b/3 b/4 
            fill-pen 54.232.255.100 polygon b/1 b/5 b/7 b/3 
            fill-pen 0.0.255.100 polygon b/5 b/6 b/8 b/7 
            fill-pen 248.255.54.100 polygon b/8 b/4 b/3 b/7
        ]]
        show f
    ]]
]

Вот версия, которая меняет форму и перемещает трёхмерный куб внутрь, наружу и вокруг экрана:

g: 12 i: 5 h: i * g j: negate h w: 0 v2: v1: 1  ; sizes/positions
img: to-image layout [box 200.200.200.50 center logo.gif]
q: make object! [x: 0 y: 0 z: 0]
cube: [[h h j] [h h h] [h j j] [h j h] [j h j] [j h h] [j j j] [j j h]]
view center-face layout/tight [
    f: box 500x450 rate 0 feel [engage: func [f a e] [
        b: copy []  q/x: q/x + 3 q/y: q/y + 3 ; q/z: q/z + 3  ; spinning
        repeat n 8 [
            if w > 500 [v1: 0]   ; w: xy pos        v1: xy direction
            if w < 0 [v1: 1]
            either v1 = 1 [w: w + 1] [w: w - 1]
            if j > (g * i * 2) [v2: 0]  ; j: z pos (size) v2: z direction
            if j < g [v2: 1]
            either v2 = 1 [h: h - 1] [h: h + 1]  j: negate h
            p: reduce pick cube n  ; point
            zx: p/1 * cosine q/z - (p/2 * sine q/z) - p/1
            zy: p/1 * sine q/z + (p/2 * cosine q/z) - p/2
            yx: (p/1 + zx * cosine q/y) - (p/3 * sine q/y) - p/1 - zx
            yz: (p/1 + zx * sine q/y) + (p/3 * cosine q/y) - p/3
            xy: (p/2 + zy * cosine q/x) - (p/3 + yz * sine q/x) - p/2 - zy
            append b as-pair (p/1 + yx + zx + w) (p/2 + zy + xy + w)
        ]
        f/effect: [draw [
            image img b/6 b/5 b/1 b/2 
            fill-pen 255.0.0.50      polygon b/6 b/2 b/4 b/8
            fill-pen 255.159.215.50  polygon b/2 b/1 b/3 b/4 
            fill-pen 54.232.255.50   polygon b/1 b/5 b/7 b/3 
            fill-pen 0.0.255.50      polygon b/5 b/6 b/8 b/7 
            fill-pen 248.255.54.50   polygon b/8 b/4 b/3 b/7
        ]]
        show f
    ]]
]

А вот небольшая трёхмерная игра со звуком, основанная на приведённом выше коде:

REBOL [title: "Little 3D Game"]

beep-sound: load to-binary decompress 64#{
eJwBUQKu/VJJRkZJAgAAV0FWRWZtdCAQAAAAAQABABErAAARKwAAAQAIAGRhdGEl
AgAA0d3f1cGadFQ+T2Z9jn1lSjM8T2uNsM/j7Midc05PWGh4eXVrXE5DQEZumsTn
4M2yk3hiVU9fcX+GcFU8KkNmj7rR3+HYroJbPUpfdoqAbldBP0ZWbpW62OvRrohk
WlleaHB2dW9bRzo1WYWy3OHbyrKObVNCVGp/jXpgRC48Vnievtfm6MCUaUVLWW1/
fXNkUkdCRlN7ps3r3cSkgm1fWFhmdH2AaVA6LElwnMja4dzNpHtXPUxje45/aVA5
PUtif6TG3uvMpHtXU1lkcnd2cGVURT0+ZJC84+HUvaGCZ1NIWm6AinVaQCtAX4Wu
yt3k37aJYEBKXXOHf3FdSEJET2KJsdPr1reUcGJbW2FsdXl2YUs5MFF7qdPe3tO+
mHNUP1Bnfo59ZEkyPFFukbTR5OvGm3BMTVlpent1aVpMQ0FJcZ3I6uHMsJB2YlZR
YXJ/hW5UOypEaJK90+Dg1qyBWjxKYHeLgG1WPz9HWXKYvNnr0KyFYVhZX2pydnVu
Wkc7N1yHtN3h2sivjGxTRFZrgI15X0MtPVh7osHZ5ua+kmdES1tvgn5zY1BGQ0hW
fqjO69vBoX9rXllaaHV9fmhPOi1Lcp/K2+DayaF4Vj1NY3uNfmhONjxLZIKnyODr
yqJ4VFFYZHN3dm5iUUM9QGaTv+Th0rqdf2VTSltvgIl0WT4rQGCIssze5N60iF8/
Sl10h39vW0ZBRFFljLPU69W1kG1gWlxiYHkWb1ECAAA=
}
alert {
   Try to click the bouncing REBOLs as many times as possible in
   30 seconds.  The speed increases with each click!
}
do game: [
   speaker: open sound://
   g: 12 i: 5 h: i * g j: negate h  x: y: z: w: sc: 0  v2: v1: 1  o: now
   img1: to-image layout [backcolor brown box red center logo.gif]
   img2: to-image layout [backcolor aqua box yellow center logo.gif]
   img3: to-image layout [backcolor green box tan center logo.gif]
   cube: [[h h j][h h h][h j j][h j h][j h j][j h h][j j j][j j h]]
   view center-face layout/tight [
      f: box white 550x550 rate 15 feel [engage: func [f a e] [
         if a = 'time [
            b: copy []  x: x + 3  y: y + 3  ; z: z + 3
            repeat n 8 [
               if w > 500 [v1: 0]   if w < 50 [v1: 1]
               either v1 = 1 [w: w + 1] [w: w - 1]
               if j > (g * i * 1.4) [v2: 0]   if j < 1 [v2: 1]
               either v2 = 1 [h: h - 1] [h: h + 1]  j: negate h
               p: reduce pick cube n 
               zx: p/1 * cosine z - (p/2 * sine z) - p/1
               zy: p/1 * sine z + (p/2 * cosine z) - p/2
               yx: (p/1 + zx * cosine y) - (p/3 * sine y) - p/1 - zx
               yz: (p/1 + zx * sine y) + (p/3 * cosine y) - p/3
               xy: (p/2 + zy * cosine x) - (p/3 + yz * sine x) - p/2 - zy
               append b as-pair (p/1 + yx + zx + w) (p/2 + zy + xy + w)
            ]
            f/effect: [draw [
               image img1 b/6 b/2 b/4 b/8
               image img2 b/6 b/5 b/1 b/2
               image img3 b/1 b/5 b/7 b/3 
            ]]
            show f
            if now/time - o/time > :00:20 [
               close speaker
               either true = request [
                  join "Time's Up! Final Score: " sc "Again" "Quit"
               ] [do game] [quit]
            ]
         ]
         if a = 'down [
            xblock: copy [] yblock: copy []
            repeat n 8 [
                append xblock first pick b n
                append yblock second pick b n
            ]
            if all [
                e/offset/1 >= first minimum-of xblock
                e/offset/1 <= first maximum-of xblock
                e/offset/2 >= first minimum-of yblock
                e/offset/2 <= first maximum-of yblock
            ][
               insert speaker beep-sound wait speaker
               sc: sc + 1
               t1/text: join "Score: " sc 
               show t1
               if (modulo sc 3) = 0 [f/rate: f/rate + 1]
               show f
            ]
         ]
      ]]
      at 200x0 t1: text brown "Click the bouncing REBOLs!"
   ]
]

16.14 Таблицы спрайтов

"Листы спрайтов" - это большие изображения, состоящие из множества изображений меньшего размера, каждое из которых предназначено для использования независимо. Вот пример, демонстрирующий, как обрезать и использовать отдельные изображения из листов спрайтов (в данном случае случайные изображения из листа смайлов):

REBOL [title: "Random Smileys 'Sprite Sheet'"] code: [
rows: 4   cols: 3   x: 64  y: 64   random/seed now 
update-pic: does [
    z: as-pair (random rows) * x - x (random cols) * y - y
    img/effect: [crop z]  show img
]
view layout [
   backdrop white
   img: image 61x61 pic #"^M" [update-pic]
]]
pic: load to-binary decompress 64#{
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
AJECvNrcU4oqWZJr/9l+BsVcgFoAAA==
}
do code

16.15 Многозадачность

"Потоки" - это особенность современных операционных систем, которая позволяет нескольким частям кода работать одновременно, не дожидаясь завершения других. Без потоков отдельные части кода должны оцениваться в последовательном порядке. К сожалению, REBOL не реализует формальный механизм многопоточности на уровне ОС, но содержит встроенную поддержку асинхронного сетевого порта и активности служб. См. http://www.rebol.net/docs/async-ports.html, http://www.rebol.net/rebservices/services-start.html и < a href="http://www.rebol.net/rebservices/quick-start.html">http://www.rebol.net/rebservices/quick-start.html для получения дополнительной информации.

Следующий метод предоставляет альтернативный способ оценки других типов кода в многозадачном режиме:

  1. Присвойте значение 0 элементу графического интерфейса в блоке 'view layout'.
  2. Присвойте этому элементу "feel" обнаружения и поместите действия, которые вы хотите выполнять одновременно, в блок, который оценивается каждый раз, когда происходит событие времени 'time.
  3. Остановите и начните оценку одновременно активных частей кода, назначив скорость "none" или 0 соответственно соответствующему элементу GUI.

Ниже приведён пример программы просмотра веб-камеры, которая создаёт видеопоток путём многократной загрузки и отображения изображений с заданного URL-адреса веб-камеры. Чтобы создать движущийся видеоэффект, процесс загрузки каждого изображения должен выполняться без остановки (то есть в каком-то бесконечном цикле "forever"). Но для того, чтобы пользователь мог управлять остановкой/запуском видеопотока (например, нажав кнопку), интерпретатор должен иметь возможность проверять пользовательские события, происходящие вне цикла навсегда. Запуская повторную загрузку с использованием описанной выше техники, программа может продолжать реагировать на другие события, непрерывно зацикливая код загрузки:

webcam-url: http://209.165.153.2/axis-cgi/jpg/image.cgi
view layout [
    btn "Start Video" [
        webcam/rate: 0 
        webcam/image: load webcam-url 
        show webcam
    ]
    btn "Stop Video" [webcam/rate: none show webcam]
    return 
    webcam: image load webcam-url 320x240 rate 0 feel [
        engage: func [face action event][
            if action = 'time [
                face/image: load webcam-url show face
            ] 
        ] 
    ]
]

Вот пример, в котором два обновления видео с веб-камеры обрабатываются как отдельные процессы. Оба могут быть остановлены и запущены по мере необходимости:

webcam-url: http://209.165.153.2/axis-cgi/jpg/image.cgi
view layout [
    across 
    btn "Start Camera 1" [
        webcam/rate: 0 
        webcam/image: load webcam-url 
        show webcam
    ]
    btn "Stop Camera 1" [webcam/rate: none show webcam]
    btn "Start Camera 2" [
        webcam2/rate: 0 
        webcam2/image: load webcam-url 
        show webcam2
    ]
    btn "Stop Camera 2" [webcam2/rate: none show webcam2]
    return 
    webcam: image load webcam-url 320x240 rate 0 feel [
        engage: func [face action event][
            if action = 'time [
                face/image: load webcam-url show face
            ] 
        ] 
    ]
    webcam2: image load webcam-url 320x240 rate 0 feel [
        engage: func [face action event][
            if action = 'time [
                face/image: load webcam-url show face
            ] 
        ] 
    ] 
]

К сожалению, этот метод не является асинхронным. Каждый фрагмент кода события фактически выполняется последовательно, в чередующемся шаблоне, а не одновременно. Хотя во многих случаях эффект схож (даже неотличим), оценка кода не выполняется одновременно. Например, в следующем примере в средство просмотра веб-камеры добавляется отображение времени. Вы увидите, что часы обновляются не каждую секунду. Это потому, что код загрузки изображения и код часов работают поочерёдно. Загрузка изображения должна быть завершена до того, как можно будет оценить действие часов. Попробуйте остановить видео, чтобы увидеть разницу:

webcam-url: http://209.165.153.2/axis-cgi/jpg/image.cgi
view layout [
    btn "Start Video" [
        webcam/rate: 0 
        webcam/image: load webcam-url 
        show webcam
    ]
    btn "Stop Video" [webcam/rate: none show webcam]
    return 
    webcam: image load webcam-url 320x240 rate 0 feel [
        engage: func [face action event][
            if action = 'time [
                face/image: load webcam-url show face
            ] 
        ] 
    ]
    clock: field to-string now/time/precise rate 0 feel [
        engage: func [face action event][
            if action = 'time [
                face/text: to-string now/time/precise show face
            ] 
        ] 
    ]
]

Одно из решений для достижения действительно асинхронной активности - просто записать код для одного процесса в отдельный файл и запустить его в отдельном процессе интерпретатора REBOL с помощью функции "launch" (запуск):

write %async.r {
    REBOL []
    view layout [
        clock: field to-string now/time/precise rate 0 feel [
            engage: func [face action event][
                if action = 'time [
                    face/text: to-string now/time/precise show face
                ] 
            ] 
        ]
    ]
}

launch %async.r
; REBOL НЕ будет ждать завершения оценки кода в async.r перед тем, 
; как продолжить:

webcam-url: http://209.165.153.2/axis-cgi/jpg/image.cgi
view layout [
    btn "Start Video" [
        webcam/rate: 0 
        webcam/image: load webcam-url 
        show webcam
    ]
    btn "Stop Video" [webcam/rate: none show webcam]
    return 
    webcam: image load webcam-url 320x240 rate 0 feel [
        engage: func [face action event][
            if action = 'time [
                face/image: load webcam-url show face
            ]
        ]
    ]
]

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

16.16 Использование DLL и файлов общего кода в REBOL

"Dll" в Windows, "So" файлы в Linux и "Dylib" на Mac - это библиотеки функций, которые могут использоваться разными языками программирования. Библиотеки общего кода используются для расширения возможностей языка с помощью новых функций. Они позволяют достигать целей, которые невозможно (или которые иным образом сложны) с использованием встроенных в язык собственных функций. В таких файлах содержится большая часть исполняемого кода и все потенциальные возможности большинства операционных систем. Также доступны сторонние библиотеки кода для облегчения работы над сложными задачами, такими как программирование мультимедиа, программирование трёхмерных игр, специализированное управление оборудованием и т.д.. Чтобы использовать библиотеки DLL и файлы общего кода в REBOL, вам необходимо загрузить версию 2.76 или более позднюю. интерпретатор REBOL (rebview.exe). Если вы используете какую-либо из бета-версий с http://www.rebol.net/builds/, используйте rebview.exe или rebcmdview.exe для запуска примеров в этом разделе.

Используя формат ниже, вы можете получить доступ и использовать функции, содержащиеся в большинстве DLL, как если бы они были собственными функциями REBOL:

lib: load/library %TheNameOfYour.DLL

; "TheFunctionNameInsideTheDll" загружается из Dll и преобразуется в 
; новую функцию REBOL с именем "your-rebol-function-name":

your-rebol-function-name: make routine! [
    return-value: [data-type!]
    first-parameter [data-type!] 
    another-parameter [data-type!] 
    more-parameters [and-their-data-types!]
    ...
] lib "TheFunctionNameInsideTheDll"

; Когда используется новая функция REBOL, она фактически запускает 
; функцию внутри Dll:

your-rebol-function-name parameter1 parameter2 ...

free lib

Первая строка открывает доступ к функциям, содержащимся в указанной Dll. Следующие строки преобразуют функцию, содержащуюся в Dll, в формат, который можно использовать в REBOL. Чтобы выполнить преобразование, функция REBOL помечена и определена (например, "your-rebol-function-name" выше), и должен быть предоставлен блок, содержащий метки и типы используемых параметров, а также значения, возвращаемые функцией ("[ return: [integer!]]" и " первый параметр [тип-данных!] другой-параметр [тип-данных!] дополнительные-параметры [и-их-типы-данных!]" выше). Имя функции, указанное в Dll, также должно быть указано сразу после блока параметров ("TheFunctionNameInsideTheDll" выше). Предпоследняя строка фактически выполняет новую функцию REBOL с использованием любых подходящих параметров, которые вы выберете. Когда вы закончите использовать функции из Dll, последняя строка используется для освобождения Dll, чтобы она закрывалась операционной системой.

Вот некоторые примеры:

REBOL []

; "kernel32.dll" iэто стандартная dll во всех версиях Windows:

lib: load/library %kernel32.dll

; Функция звукового сигнала "beep" содержится в библиотеке 
; kernel32.dll. Мы создадим новую функцию REBOL под названием 
; "play-sound", которая фактически выполняет функцию "beep" в 
; kernel32.dll. Функция "beep" принимает два целочисленных 
; параметра (значения высоты тона "pitch" и длительности "duration") 
; и возвращает целочисленное значение:
play-sound: make routine! [
    return: [integer!] pitch [integer!] duration [integer!]
] lib "Beep"

; (Beep возвращает нулевое значение, если функция не завершается 
; успешно. В противном случае возвращается ненулевое число).

; Теперь мы можем использовать функцию "play-sound" КАК ЕСЛИ ЭТО 
; ИСХОДНАЯ ФУНКЦИЯ REBOL:

for hertz 37 3987 100 [
    print rejoin ["The pitch is now " hertz " hertz."]
    play-sound hertz 500
]

free lib
halt

В следующем примере показано, как записывать звуки (с подключённым к компьютеру микрофоном) с помощью Windows MCI API. По завершении записанный звук воспроизводится через собственный звуковой порт REBOL:

; В библиотеку "winmm.dll" включены различные функции mci. Мы 
; создадим новую функцию REBOL под названием "mciExecute", которая 
; позволит нам запускать функции MCI в winmm.dll. Эта 
; функция-функция принимает один строковый параметр (текстовую 
; строку, записанную в синтаксисе функции MCI) и возвращает 
; логическое значение (истина, если функция выполнена успешно, 
; ложь, если она не выполняется):

lib: load/library %winmm.dll
mciExecute: make routine! [ 
    command [string!]
    return: [logic!] 
] lib "mciExecute"

; Получим от пользователя имя файла, которое будет использоваться 
; для сохранения записанного звука:

file: to-local-file to-file request-file/save/title/file "Save as:" {
    } %rebol-recording.wav

; Откроем буфер MCI и начнём запись:

mciExecute "open new type waveaudio alias buffer1 buffer 6"
mciExecute "record buffer1"

ask "RECORDING STARTED (press [ENTER] when done)...^/"

; Остановим запись и сохраним звук в выбранном выше WAV файле:

mciExecute "stop buffer1"
mciExecute join "save buffer1 " file

; Закрываем DLL:

free lib

print "Recording complete.  Here's how it sounds:^/"

; Воспроизведём записанный звук:

insert port: open sound:// load to-rebol-file file wait port close port
print "DONE.^/"

halt

В следующем примере показано, как воспроизводить видеофайлы AVI, снова используя Windows API "mciExecute" из winmm.dll. Демонстрационное видео загружается из Интернета и воспроизводится два раза - один раз с настройками по умолчанию, а второй раз в заданном месте на экране с удвоенной исходной записанной скоростью. Видеокодек в демонстрационном видео - MS-CRAM (Microsoft Video 1), а аудиоформат - PCM. Для получения дополнительной информации о командах mciExecute, Google "multimedia command strings" и см. http://msdn.microsoft.com/en-us/library/dd743572(VS.85).aspx:

; Открываем winmm.dll и определяем функцию "mciExecute" в REBOL:

lib: load/library %winmm.dll
mciExecute: make routine! [c [string!] return: [logic!]] lib "mciExecute"

; Скачиваем демонстрационный видео файл:

if not exists? %test.avi [
    flash "Downloading test video..."
    write/binary %test.avi read/binary http://re-bol.com/test.avi
    unview
]
video: to-local-file %test.avi

; Запускаем функцию "mciExecute" с параметрами необходимыми для 
; воспроизведения видео:

mciExecute rejoin ["OPEN " video " TYPE AVIVIDEO ALIAS thevideo"]
mciExecute "PLAY thevideo WAIT"
mciExecute "CLOSE thevideo"

mciExecute rejoin ["OPEN " video " TYPE AVIVIDEO ALIAS thevideo"]
mciExecute "PUT thevideo WINDOW AT 200 200 0 0"  ; at 200x200
mciExecute "SET thevideo SPEED 2000"  ; play twice a fast
mciExecute "PLAY thevideo WAIT"
mciExecute "CLOSE thevideo"

; Закрываем библиотеку и высвобождаем память:

free lib
quit

В следующем примере используется "dictionary.dll" из http://www.reelmedia.org/pureproject/archive411/dll/Dictionary.zip для проверки орфографии текста, введённого в командной строке REBOL. В dll есть две функции, которые необходимы для проверки орфографии - "Dictionary_Load" и "Dictionary_Check":

REBOL []

check-me: ask "Enter a word to be spell-checked:  "

lib: load/library %Dictionary.dll

; Создаются две новые функции REBOL, которые фактически запускают 
; функции Dictionary_Load и Dictionary_Check в DLL:

load-dic: make routine! [
    a [string!] 
    return: [none]
] lib "Dictionary_Load"

check-word: make routine! [
    a [string!]
    b [integer!]
    return: [integer!]
] lib "Dictionary_Check"

; Эта строка запускает функцию Dictionary_Load из библиотеки DLL.:

load-dic ""

; Эта строка запускает функцию Dictionary_Check в DLL для любого 
; текста, введённого в переменную "check-me" выше:

response: check-word check-me 0

; Функция Dictionary_Check возвращает 0, если ошибок нет:

either response = 0 [
    print "No spelling errors found." 
] [
    print "That word is not in the dictionary."
]

free lib
halt

В следующем примере воспроизводится звуковой файл в формате mp3 с использованием библиотеки DLL по адресу http://musiclessonz.com/mp3.dll. Конечно, эта Dll может быть сжата и встроена в код, чтобы исключить необходимость загрузки файла:

REBOL []

write/binary %mp3.dll read/binary http://musiclessonz.com/mp3.dll
lib: load/library %mp3.dll

; функция "playfile" загружается из Dll и преобразуется в новую 
; функцию REBOL "play-mp3":

play-mp3: make routine! [a [string!] return: [none]] lib "playfile"

; Затем у пользователя запрашивается имя mp3-файла, который 
; воспроизводится функцией "playfile" в Dll:

file: to-local-file to-string request-file
play-mp3 file

print "Done playing, Press [Esc] to quit this program: "
free lib

В следующем примере для перемещения мыши по экрану используется функция "AU3_MouseMove" из Dll-версии AutoIt. AutoIt содержит широкий спектр функций для программного нажатия кнопок, ввода текста, выбора элементов меню, выбора элементов из списков, управления мышью и т. Д. В любом существующем окне программы, как если бы эти действия были выполнены пользователем, щёлкнув и набрав экран. Изучение других функций языка AutoIt может быть очень полезным при настройке и автоматизации существующих приложений Windows:

REBOL []

if not exists? %AutoItDLL.dll [
    write/binary %AutoItDLL.dll
    read/binary http://musiclessonz.com/rebol_tutorial/AutoItDLL.dll
]

lib: load/library %AutoItDLL.dll
move-mouse: make routine! [
    return: [integer!] x [integer!] y [integer!] z [integer!]
] lib "AUTOIT_MouseMove"

print "Press the [Enter] key to see your mouse move around the screen."
print "It will move to the top corner, and then down diagonally to"
ask "position 200x200:  " 

for position 0 200 5 [
    move-mouse position position 10 
    ; "10" относится к скорости движения мыши
]

free lib
print "^/Done.^/"
halt

В этом примере используются функции Dll из собственного Windows API, чтобы исключить текст REBOL - по умолчанию в верхней части окна графического интерфейса:

REBOL []

; Сначала загрузите необходимые dll:

user32.dll: load/library %user32.dll

; Затем определите функции Windows API, которые вам понадобятся.:

get-focus: make routine! [return: [int]] user32.dll "GetFocus"

set-caption: make routine! [
    hwnd [int] 
    a [string!]  
    return: [int]
] user32.dll "SetWindowTextA"

; Затем создайте свой графический интерфейс - обязательно 
; используйте 'view/new', чтобы он не отображался сразу (запустите 
; графический интерфейс позже с 'do-events', после того, как вы 
; изменили строку заголовка ниже):

view/new center-face layout [
    backcolor white
    text bold "Notice that there's no 'Rebol - ' in the title bar above."
    text "New title text:" 
    across
    f: field "Tada!"
    btn "Change Title" [
        ; Эти функции изменяют текст в строке заголовка:
        hwnd: get-focus
        set-caption hwnd f/text
    ]
    btn "Exit" [
        ; Не забудьте закрыть dll, когда закончите:
        free user32.dll
        quit
    ]
]

; После того, как вы создали свой графический интерфейс, запустите 
; функции Dll, чтобы заменить текст по умолчанию в строке заголовка:

hwnd: get-focus
set-caption hwnd "My Title"

; Finally, start your GUI:

do-events

Вот немного более универсальная версия вышеупомянутого скрипта. Вы можете добавить его в начало любого существующего скрипта графического интерфейса, и он удалит текст "REBOL -" по умолчанию из всех строк заголовка графического интерфейса, включая предупреждения и запросчики:

title-text: "My Program"
if system/version/4 = 3 [
    user32.dll: load/library %user32.dll
    get-tb-focus: make routine! [return: [int]] user32.dll "GetFocus"
    set-caption: make routine! [
        hwnd [int] 
        a [string!]  
        return: [int]
    ] user32.dll "SetWindowTextA"
    show-old: :show
    show: func [face] [
        show-old [face]
        hwnd: get-tb-focus
        set-caption hwnd title-text
    ]
]

Следующее приложение демонстрирует, как использовать Windows API для просмотра видео с локальной веб-камеры, сохранения снимков в формате BMP и изменения заголовка окна графического интерфейса REBOL:

REBOL []

; Сначала откроем файлы Dll, которые содержат функции Windows API, 
; которые мы хотим использовать (для просмотра видео с веб-камеры и 
; для изменения заголовков окон):

avicap32.dll: load/library %avicap32.dll
user32.dll: load/library %user32.dll

; Создайте прототипы функций REBOL, необходимые для изменения 
; заголовков окон: (Эти функции находятся в user32.dll, встроенной 
; в Windows.)

get-focus: make routine! [return: [int]] user32.dll "GetFocus"
set-caption: make routine! [
    hwnd [int] a [string!]  return: [int]
] user32.dll "SetWindowTextA"

; Создайте прототипы функции REBOL, необходимые для просмотра 
; веб-камеры: (также встроен в Windows)
find-window-by-class: make routine! [
    ClassName [string!] WindowName [integer!] return: [integer!]
] user32.dll "FindWindowA"
sendmessage: make routine! [
    hWnd [integer!] val1 [integer!] val2 [integer!] val3 [integer!]
    return: [integer!]
] user32.dll "SendMessageA"
sendmessage-file: make routine! [
    hWnd [integer!] val1 [integer!] val2 [integer!] val3 [string!]
    return: [integer!]
] user32.dll  "SendMessageA"
cap: make routine! [
    cap [string!] child-val1 [integer!] val2 [integer!] val3 [integer!]
    width [integer!] height [integer!] handle [integer!] 
    val4 [integer!] return: [integer!]
] avicap32.dll "capCreateCaptureWindowA"

; Создаём окно GUI REBOL:

view/new center-face layout/tight [
    image 320x240
    across
    btn "Take Snapshot" [
        ; Запускаем функции dll, которые делают снимок:
        sendmessage cap-result 1085 0 0
        sendmessage-file cap-result 1049 0 "scrshot.bmp"
    ]
    btn "Exit" [
        ; Запускаем dll-функции, останавливающие видео:
        sendmessage cap-result 1205 0 0
        sendmessage cap-result 1035 0 0
        free user32.dll
        quit
    ]
]

; Запустите функции Dll, которые сбрасывают заголовок окна REBOL GUI:
; (удаляет "REBOL -" в строке заголовка)
hwnd-set-title: get-focus
set-caption hwnd-set-title "Web Camera"

; Запустите функции Dll, которые показывают видео:

hwnd: find-window-by-class "REBOLWind" 0
cap-result: cap "cap" 1342177280 0 0 320 240 hwnd 0
sendmessage cap-result 1034 0 0
sendmessage cap-result 1077 1 0
sendmessage cap-result 1075 1 0
sendmessage cap-result 1074 1 0
sendmessage cap-result 1076 1 0

; запускаем GUI:

do-events

Дополнительные сведения о библиотеках DLL и Windows API см. В следующих разделах:

http://rebol.com/docs/library.html http://en.wikipedia.org/wiki/Dynamic_Link_Library http://www.math.grin.edu/~shirema1/docs/DLLsinREBOL.html http://www.allapi.net/downloads/apiguide/agsetup.exe http://www.activevb.de/rubriken/apiviewer/index-apiviewereng.html http://msdn.microsoft.com/library/

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

16.17 Приложение с несколькими сетевыми камерами безопасности, использующее DLL веб-камеры Windows

Это приложение использует несколько подпрограмм DLL из предыдущего раздела, чтобы продемонстрировать, как создать приложение для наблюдения за камерой безопасности. Это приложение основано на общей способности IP-камер загружать поток изображений на FTP-сервер. Это приложение просто предоставляет интерфейс для мониторинга, хранения и просмотра кадров с 24 камер, чтобы эффективно сохранять, просматривать и искать каждый видеопоток. Видео с локальной веб-камеры можно использовать в качестве источника для тестирования программы.

REBOL [title: "Camera Manager"]
svv/vid-face/color: white
tt: "Camera Manager"
do set-title: {user32.dll: load/library %user32.dll
gf: make routine![return:[int]]user32.dll"GetFocus"
sc: make routine![hw[int]a[string!]return:[int]]user32.dll"SetWindowTextA"
so: :show show: func[face][so[face]hw: gf sc hw tt]}
make-dir %./history/
write %view1cam.r rejoin [{
    REBOL []
    cam: first parse (first system/options/args) "."
    tt: uppercase form cam } set-title {
    blank-image: to-image layout/tight [box black 640x480]
    file: to-file first system/options/args
    gui: view/new center-face layout [
        across
        i1: image (blank-image)
        return
        btn "Stop" [flag: false]
        btn "Start" [flag: true]
        box 0x0 []
    ]
    flag: true
    forever [
        wait .15  if not viewed? gui [quit]  show gui  if flag = true [
            attempt [i1/image: (load file) show i1]
        ]
    ]    
}]
write %viewhist.r rejoin [{
    REBOL []
    svv/vid-face/color: 230.230.230
    change-dir %./history/
    cam: to-file first system/options/args
    tt: uppercase form cam } set-title {
    files: read %./
    if cam <> %all [
        remove-each file files [
            cam-in-file: to-file (first parse (last parse file "_") ".")
            cam <> cam-in-file
        ]
    ]
    last-file: length? files
    counter: 0
    blank-image: to-image layout/tight [box black 640x480]
    update-screen: does [
        attempt [
            i1/image: (load cf: pick files counter)  show i1
            t1/text: rejoin [
                "Current file:  " form cf {  (} counter {/} last-file {)}
            ]
            show t1
        ]
    ]
    gui: view center-face layout [
        across
        i1: image (blank-image)
        return
        btn "Reverse" feel [engage: func [f a e] [
            if find [down over away] a [
                counter: counter - 1
                if counter < 1 [counter: 1]
                update-screen
            ]
        ]]
        btn "Forward" feel [engage: func [f a e] [
            if find [down over away] a [
                counter: counter + 1
                if counter > last-file [counter: last-file]
                update-screen
            ]
        ]]
        btn "Select" [
            selected-file: to-file request-list "Select An Image:" files
            if selected-file = %none [return]
            counter: index? find files selected-file
            update-screen
        ]
        btn "Save" [
            save/png request-file/only/save/file (pick files counter)
                i1/image
        ]
        t1: text bold 370 {
            Select image, or scroll forward/back (arrow keys work)
        }
        key keycode [left] [
            counter: counter - 1
            if counter < 1 [counter: 1]
            update-screen
        ]
        key keycode [right] [
            counter: counter + 1
            if counter > last-file [counter: last-file]
            update-screen
        ]
        key keycode [up] [
            counter: counter - 20
            if counter < 1 [counter: 1]
            update-screen
        ]
        key keycode [down] [
            counter: counter + 20
            if counter > last-file [counter: last-file]
            update-screen
        ]
    ]
}]
write %localcam.r {
    REBOL [title: "Local Camera Test Streamer"]
    avicap32.dll: load/library %avicap32.dll
    user32.dll: load/library %user32.dll
    find-window-by-class: make routine! [
        ClassName [string!] WindowName [integer!] return: [integer!]
    ] user32.dll "FindWindowA"
    sendmessage: make routine! [
        hWnd [integer!] val1 [integer!] val2 [integer!] val3 [integer!]
        return: [integer!]
    ] user32.dll "SendMessageA"
    sendmessage-file: make routine! [
        hWnd [integer!] val1 [integer!] val2 [integer!] val3 [string!]
        return: [integer!]
    ] user32.dll  "SendMessageA"
    cap: make routine! [
        cap [string!] child-val1 [integer!] val2 [integer!] 
        val3 [integer!] width [integer!] height [integer!]
        handle [integer!] val4 [integer!] return: [integer!]
    ] avicap32.dll "capCreateCaptureWindowA"
    gui: view/new center-face layout/tight [
        at -10x-10 b1: box 0x0 
        pic1: image 320x240
    ]
    hwnd: find-window-by-class "REBOLWind" 0
    cap-result: cap "cap" 1342177280 0 0 320 240 hwnd 0
    sendmessage cap-result 1034 0 0
    sendmessage cap-result 1077 1 0
    sendmessage cap-result 1075 1 0
    sendmessage cap-result 1074 1 0
    sendmessage cap-result 1076 1 0
    counter: 1  flag: true
    forever [
        wait .05
        if not viewed? gui [
            sendmessage cap-result 1205 0 0
            sendmessage cap-result 1035 0 0
            free user32.dll
            quit
        ]  
        show b1
        if flag = true [
            sendmessage cap-result 1085 0 0
            filename: rejoin ["cam" counter ".png"]
            sendmessage-file cap-result 1049 0 filename
            counter: counter + 1
            if counter > 24 [counter: 1]
        ]
    ]
}
blank-image: to-image layout/tight [box black 160x120]
gui: view/new center-face layout/tight [
    across
    btn "Stop" [flag: false]
    btn "Start" [flag: true]
    btn "Test With Local Camera" [launch %localcam.r]
    text "View History: "
    drop-down data [
        "cam1" "cam2" "cam3" "cam4" "cam5" "cam6" "cam7" "cam8" "cam9"
        "cam10" "cam11" "cam12" "cam13" "cam14" "cam15" "cam16" "cam17"
        "cam18" "cam19" "cam20" "cam21" "cam22" "cam23" "cam24" "all"
    ] [call/show join "rebol -s %viewhist.r " value]
    t1: text blue "Updating Camera: 000"
    return
    i1: image (blank-image) [call/show "rebol -s %view1cam.r cam1.png"]
    i2: image (blank-image) [call/show "rebol -s %view1cam.r cam2.png"]
    i3: image (blank-image) [call/show "rebol -s %view1cam.r cam3.png"]
    i4: image (blank-image) [call/show "rebol -s %view1cam.r cam4.png"]
    i5: image (blank-image) [call/show "rebol -s %view1cam.r cam5.png"]
    i6: image (blank-image) [call/show "rebol -s %view1cam.r cam6.png"]
    return
    i7: image (blank-image) [call/show "rebol -s %view1cam.r cam7.png"]
    i8: image (blank-image) [call/show "rebol -s %view1cam.r cam8.png"]
    i9: image (blank-image) [call/show "rebol -s %view1cam.r cam9.png"]
    i10: image (blank-image) [call/show "rebol -s %view1cam.r cam10.png"]
    i11: image (blank-image) [call/show "rebol -s %view1cam.r cam11.png"]
    i12: image (blank-image) [call/show "rebol -s %view1cam.r cam12.png"]
    return
    i13: image (blank-image) [call/show "rebol -s %view1cam.r cam13.png"]
    i14: image (blank-image) [call/show "rebol -s %view1cam.r cam14.png"]
    i15: image (blank-image) [call/show "rebol -s %view1cam.r cam15.png"]
    i16: image (blank-image) [call/show "rebol -s %view1cam.r cam16.png"]
    i17: image (blank-image) [call/show "rebol -s %view1cam.r cam17.png"]
    i18: image (blank-image) [call/show "rebol -s %view1cam.r cam18.png"]
    return
    i19: image (blank-image) [call/show "rebol -s %view1cam.r cam19.png"]
    i20: image (blank-image) [call/show "rebol -s %view1cam.r cam20.png"]
    i21: image (blank-image) [call/show "rebol -s %view1cam.r cam21.png"]
    i22: image (blank-image) [call/show "rebol -s %view1cam.r cam22.png"]
    i23: image (blank-image) [call/show "rebol -s %view1cam.r cam23.png"]
    i24: image (blank-image) [call/show "rebol -s %view1cam.r cam24.png"]
]
previous-files: read %./
remove-each file previous-files [%.png = suffix? file]
flag: true
forever [
    wait .05  show i1  if flag = true [
        current-files: read %./
        new-files: exclude current-files previous-files
        foreach file new-files [
            if not viewed? gui [quit]  show i1  if flag = true [
                do process-image: [
                    loaded-file: load file
                    if error? try [
                        rename file to-file rejoin [
                            %./history/ 
                            now/year "-" now/month "-" now/day 
                            "_" replace/all form now/time ":" "-"
                            "_" file
                        ]
                    ] [
                        ; print "error deleting"
                        do process-image
                    ]
                ]
                camnum: replace replace form copy file "cam" "" ".png" ""
                t1/text: join "Updating Camera: " camnum  show t1
                do rejoin [
                    "i" camnum "/image: " loaded-file " show i" camnum
                ]
                wait .05
            ]
        ]
    ]
]

16.18 REBOL как плагин для браузера

Интерпретаторы REBOL существуют не только для огромного количества операционных систем, но также как плагины для нескольких популярных браузеров (Internet Explorer и многие варианты Mozilla, включая Opera). Это означает, что вы можете встроить интерпретатор REBOL прямо на веб-страницу и иметь полные, сложные программы REBOL, запускаемые прямо на страницах вашего веб-сайта (аналогично Flash и Java и полезно, как код Javascript). Это обеспечивает хорошую альтернативу программированию CGI для любого типа приложения, которое должным образом запускается в автономном интерпретаторе view.exe (например, для игр, мультимедийных приложений и приложений с богатой графикой/графическим интерфейсом всех типов). Поскольку плагин браузера запускает типичный код REBOL так же, как загружаемый интерпретатор view.exe, вы можете запускать код прямо на своих веб-страницах, не внося никаких изменений.

Чтобы использовать плагин на веб-странице, просто включите необходимый объектный код на свою страницу, как в следующем примере. Обязательно измените URL-адрес сценария, который вы хотите запустить, размер окна, которое вы хотите создать, и другие параметры по мере необходимости. Вы можете загрузить файл rebolb7.cab, загрузить его на свой сайт и запустить оттуда, если хотите (имейте в виду, что существует несколько разных версий: "http://www.rebol.com/plugin/rebolb5.cab#Version=0,5,0,0", "http://www.rebol.com/plugin/rebolb6.cab#Version=0,6,0,0", и "http://www.rebol.com/plugin/rebolb7.cab#Version=1,0,0,0" - используйте версию 7, если у вас нет особых причин не делать этого). Если вы используете свою собственную копию, обязательно измените URL-адрес CAB-файла в следующем примере:

<HTML><HEAD><TITLE>REBOL Plugin Test</TITLE></HEAD><BODY><CENTER>

<OBJECT ID="REBOL" CLASSID="CLSID:9DDFB297-9ED8-421d-B2AC-372A0F36E6C5" 
    CODEBASE="http://www.rebol.com/plugin/rebolb7.cab#Version=1,0,0,0"
    WIDTH="500" HEIGHT="400">
    <PARAM NAME="LaunchURL" VALUE="http://yoursite.com/test_plugin.r">
    <PARAM NAME="BgColor" VALUE="#FFFFFF">
    <PARAM NAME="Version" VALUE="#FFFFFF">
    <PARAM NAME="BgColor" VALUE="#FFFFFF">
</OBJECT>

</CENTER></BODY></HTML>

Вот пример сценария, который можно запустить по ссылке "http://yoursite.com/test_plugin.r", указанной в приведённом выше коде. Вы можете заменить его любым допустимым кодом REBOL и запустить его прямо в браузере:

REBOL []
view layout [
    size 500x400 
    btn "Click me" [alert "Plugin is working!"]
]

Вы можете увидеть приведённый выше пример на http://re-bol.com/test_plugin.html. Вышеупомянутый сценарий необходимо запустить в Internet Explorer в MS Windows. Чтобы использовать плагин в браузерах на основе Mozilla (Firefox, Opera и т.д.), на вашу HTML-страницу необходимо вставить следующий код:

<HTML><HEAD><TITLE>REBOL Plugin Test</TITLE></HEAD><BODY><CENTER>

<OBJECT ID="REBOL_IE" CLASSID="CLSID:9DDFB297-9ED8-421d-B2AC-372A0F36E6C5"
    CODEBASE="http://re-bol.com/rebolb7.cab#Version=1,0,0,0"
    WIDTH="500" HEIGHT="400" BORDER="1" ALT="REBOL/Plugin">
    <PARAM NAME="bgcolor" value="#ffffff">
    <PARAM NAME="version" value="1.2.0">
    <PARAM NAME="LaunchURL" value="http://re-bol.com/test_plugin.r">
    <embed name="REBOL_Moz" type="application/x-rebol-plugin-v1"
        WIDTH="500" HEIGHT="400" BORDER="1" ALT="REBOL/Plugin"
        bgcolor="#ffffff"
        version="1.2.0"
        LaunchURL="http://re-bol.com/test_plugin.r"
    >
    </embed>
</OBJECT>

<script language="javascript" type="text/javascript">
var plugin_name = "REBOL/Plugin for Mozilla";
function install_rebol_plugin_mozilla() {
    if (navigator.userAgent.toLowerCase().indexOf("msie") == -1) {
        if (InstallTrigger) {
           xpi={'REBOL/Plugin for Mozilla':'http://re-bol.com/mozb2.xpi'};
           InstallTrigger.install(xpi, installation_complete);
        }
    }
}
function installation_complete(url, status) {
    if (status == 0) location.reload();
}
function is_mozilla_plugin_installed() {
    if (window.navigator.plugins) {
        for (var i = 0; i < window.navigator.plugins.length; i++) {
            if (window.navigator.plugins[i].name == plugin_name)
            return true;
        }
    }
    return false;
}
if (!is_mozilla_plugin_installed()) install_rebol_plugin_mozilla();
</script>

</CENTER></BODY></HTML>

Вы можете увидеть работающий выше скрипт на http://re-bol.com/test_plugin_mozilla.html.

Для получения дополнительной информации о подключаемых модулях REBOL см. http://www.rebol.com/plugin/install.html и http://home.comcast.net/~rebolore/plugin-guide.pdf.

16.19 Использование баз данных

Базы данных управляют всеми сложными деталями поиска, сортировки и других операций с большими объёмами данных быстро и безопасно в многопользовательской среде. MySQL - это бесплатная система баз данных с открытым исходным кодом, используемая во многих веб-сайтах и ​​программных проектах. ODBC - это общий интерфейс, который позволяет программистам подключаться ко многим другим типам баз данных. Самые последние выпуски REBOL, наряду со всеми коммерческими версиями, имеют встроенный собственный доступ к MySQL, ODBC и другим форматам баз данных. Вы также можете загрузить бесплатный модуль протокола MySQL, который работает в каждой бесплатной версии REBOL, с http://softinnov.org/rebol/mysql.shtml. На этом сайте также доступен бесплатный модуль для системы баз данных postgre.

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

Большинство учётных записей веб-хостинга поставляются с уже установленным MySQL. Смотрите инструкции для учётной записи вашего веб-хоста, чтобы узнать, как получить к ней доступ (вам нужен веб-адрес, имя базы данных, имя пользователя и пароль). Чтобы получить бесплатный простой в установке пакет веб-сервера для Windows, который включает MySQL, перейдите по адресу http://www.uniformserver.com. Эта программа позволяет вам легко установить веб-сервер с предварительно настроенным MySQL на вашем локальном компьютере. Это полезно, если у вас нет доступа к веб-серверу в Интернете или если вы хотите создавать многопользовательские приложения, использующие MySQL в локальной сети.

Чтобы использовать модуль REBOL MySQL, указанный выше, распакуйте сжатый файл "rip", доступный по ссылке выше. Этот шаг необходимо выполнить только при первом использовании пакета на данном компьютере:

do mysql-107.rip

Каждый раз, когда вы обращаетесь к базе данных MySQL, вам нужно "do" модуль, который был распакован на шаге выше:

do %mysql-r107/mysql-protocol.r

; На момент написания этой статьи 1.07 была самой последней версией
; модуля mysql. Обновите числа в двух предыдущих строках кода, чтобы 
; отразить текущий номер версии, которую вы загрузили.

Затем введите информацию о базе данных (расположение, имя пользователя и пароль), как в инструкциях на http://softinnov.org/rebol/mysql-usage.html:

db: open mysql://username:password@yourwebsite.com/yourdatabasename

В MySQL и других базах данных данные хранятся в "таблицах". Таблицы состоят из столбцов со связанной информацией. Например, таблица "Контакты" может содержать столбцы с именем, адресом, телефоном и днем рождения. Каждую запись в базе данных можно рассматривать как строку, содержащую информацию в каждом из этих полей столбца:

name             address                 phone       birthday
----             -------                 --------    --------
John Smith       123 Toleen Lane         555-1234    1972-02-01
Paul Thompson    234 Georgetown Place    555-2345    1972-02-01
Jim Persee       345 Portman Pike        555-3456    1929-07-02
George Jones     456 Topforge Court                  1989-12-23
Tim Paulson                              555-5678    2001-05-16

Операторы "SQL" позволяют работать с данными, хранящимися в таблице. Некоторые операторы SQL используются для создания, уничтожения и заполнения столбцов данными:

CREATE TABLE table_name          ; создание новой таблицы
DROP TABLE table_name            ; удаление таблицы
INSERT INTO table_name VALUES (value1, value2,....)
INSERT INTO Contacts 
    VALUES ('Billy Powell', '5 Binlow Dr.', '555-6789', '1968-04-19')
INSERT INTO Contacts (name, phone) 
    VALUES ('Robert Ingram', '555-7890')

Оператор SELECT используется для извлечения информации из столбцов данной таблицы:

SELECT column_name(s) FROM table_name
SELECT * FROM Contacts
SELECT name,address FROM Contacts
SELECT DISTINCT birthday FROM Contacts  ; возвращает не повторяющиеся 
                                        ; записи
; Для выполнения поиска используйте WHERE. Заключите поисковый текст в
; одинарные кавычки и используйте следующие операторы:
;  =, <>, >, <, >=, <=, BETWEEN, LIKE  (используйте "%" для 
; подстановочных знаков)
SELECT * FROM Contacts WHERE name='John Smith'
SELECT * FROM Contacts WHERE name LIKE 'J%'
SELECT * FROM Contacts WHERE birthday LIKE '%72%' OR phone LIKE '%34'
SELECT * FROM Contacts
    WHERE birthday NOT BETWEEN '1900-01-01' AND '2010-01-01'
; IN позволяет указать список данных для сопоставления в столбце:
SELECT * FROM Contacts WHERE phone IN ('555-1234','555-2345')
SELECT * FROM Contacts ORDER BY name  ; sort results alphabetically
SELECT name, birthday FROM Contacts ORDER BY birthday, name DESC

Другие операторы SQL:

UPDATE Contacts SET address = '643 Pine Valley Rd.' 
    WHERE name = 'Robert Ingram'    ; изменить или добавить к 
                                    ; существующим данным
DELETE FROM Contacts WHERE name = 'John Smith'
DELETE * FROM Contacts
ALTER TABLE  - изменить структуру столбцов таблицы
CREATE INDEX - создать поисковый индекс
DROP INDEX   - удалить поисковый индекс

Чтобы интегрировать операторы SQL в код REBOL, заключите их следующим образом:

insert db {SQL command}

Чтобы получить набор результатов, созданный вставленной командой, используйте:

copy db

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

first db

Когда вы закончите использовать базу данных, закройте соединение:

close db

Вот полный пример, который открывает соединение с базой данных, создаёт новую таблицу "Контакты", вставляет данные в таблицу, вносит некоторые изменения в таблицу, а затем извлекает и печатает все содержимое таблицы и закрывает соединение:

REBOL []

do %mysql-protocol.r 
db: open mysql://root:root@localhost/Contacts
; insert db {drop table Contacts}   ; стереть старую таблицу, если  
                                    ; она существует
insert db {create table Contacts (
    name            varchar(100),
    address         text,
    phone           varchar(12),
    birthday        date 
)} 
insert db {INSERT into Contacts VALUES 
    ('John Doe', '1 Street Lane', '555-9876', '1967-10-10'),
    ('John Smith', '123 Toleen Lane', '555-1234', '1972-02-01'),
    ('Paul Thompson', '234 Georgetown Pl.', '555-2345', '1972-02-01'),
    ('Jim Persee', '345 Portman Pike', '555-3456', '1929-07-02'),
    ('George Jones', '456 Topforge Court', '', '1989-12-23'),
    ('Tim Paulson', '', '555-5678', '2001-05-16')
}
insert db "DELETE from Contacts WHERE birthday = '1967-10-10'"
insert db "SELECT * from Contacts"
results: copy db
probe results
close db
halt

Вот более короткий формат кода, который можно использовать для быстрой и простой работы с таблицами базы данных:

read join mysql://user:pass@host/DB? "SELECT * from DB"

Например:

foreach row read rejoin [mysql://root:root@localhost/Contacts? 
    "SELECT * from Contacts"] [print row]

Вот пример графического интерфейса:

results: read rejoin [
    mysql://root:root@localhost/Contacts? "SELECT * from Contacts"]
view layout [
    text-list 100x400 data results [
        string: rejoin [
            "NAME:      " value/1 newline
            "ADDRESS:   " value/2 newline
            "PHONE:     " value/3 newline
            "BIRTHDAY:  " value/4
        ]
        view/new layout [
            area string
        ] 
    ] 
]

Для более подробного объяснения того, как настроить MYSQL, как использовать синтаксис языка SQL, и другие связанные темы, см. http://musiclessonz.com/rebol_tutorial.html#section-27.

16.19.1 SQLite

Вот пример, демонстрирующий, как использовать популярную СУБД SQLite, используя sqlite.r от Эшли Траттер:

REBOL [title: "SQLITE Example"]
unless exists? %sqlite3.dll [
    write/binary %sqlite3.dll read/binary http://re-bol.com/sqlite3.dll
]
unless exists? %sqlite.r [
    write %sqlite.r read http://re-bol.com/sqlite.r
]
do %sqlite.r
db: connect/create %contacts.db
SQL "create table contacts (name, address, phone, birthday)"
SQL {insert into contacts values 
    ('"John Doe"', '"1 Street Lane"', '"555-9876"', '"1967-10-10"')
}
data: [
    "John Smith" "123 Toleen Lane" "555-1234" "1972-02-01"
    "Paul Thompson" "234 Georgetown Pl." "555-2345" "1972-02-01"
    "Jim Persee" "345 Portman Pike" "555-3456" "1929-07-02"
    "George Jones" "456 Topforge Court" "" "1989-12-23"
    "Tim Paulson" "" "555-5678" "2001-05-16"
]
SQL "begin"
foreach [name address phone bd] data [
    SQL reduce [
        "insert into contacts values (?, ?, ?, ?)" name address phone bd
    ]
]
SQL "commit"
SQL ["DELETE from Contacts WHERE birthday = ?" "1967-10-10"]
results: SQL "select * from contacts"
probe results
disconnect db
halt

Чтобы использовать SQLite в REBOL, см. http://www.dobeash.com/sqlite.html.

16.19.2 Другие СУБД

Чтобы использовать ODBC в REBOL, см. http://www.rebol.com/docs/database.html.

Информацию о полезной системе баз данных SQL с открытым исходным кодом, полностью созданной на собственном языке REBOL, см. По адресу http://www.dobeash.com/rebdb.html.

Следующие дополнительные системы управления базами данных ("СУБД") также доступны для REBOL:

http://www.tretbase.com/forum/index.php http://www.fys.ku.dk/~niclasen/nicomdb/ http://www.rebol.net/cookbook/recipes/0012.html http://www.cs.unm.edu/~whip/ http://www.garret.ru/dybase.html http://www.rebol.org/view-script.r?script=sql-protocol.r http://www.rebol.org/view-script.r?script=db.r http://www.rebol.org/view-script.r?script=couchdb3.r

Не забудьте поискать на rebol.org дополнительную информацию и код, связанный с базами данных.

16.20 Меню

Одна странность диалекта GUI Rebol заключается в том, что он не включает в себя собственный способ создания стандартных меню. Пользователи обычно нажимают кнопки или текстовые варианты непосредственно в графическом интерфейсе REBOL, чтобы сделать выбор. Функция "request-list" (запрос-список) и виджет "choice" (выбор) графического интерфейса пользователя - это короткие и простые заменители, которые обеспечивают функциональность, подобную меню.

В приведённом ниже примере показано, как стилизовать виджет выбора для создания нормально выглядящих меню. Воспользуйтесь командой "editor decompress #{сжатый код}", чтобы изучить, как это работает. В противном случае просто следуйте формату меню в примере, заполните свой собственный код графического интерфейса и вставьте сжатый фрагмент в свой собственный скрипт:

REBOL [title: "Choice Button Menu Example"]
menu: [[
    "File" []
    "_________________________^/" []
    "File Option 1" [alert "File Option 1"]
    "File Option 2" [alert "File Option 2"]
    "File Option 3" [alert "File Option 3"]
    "_________________________^/" []
    "About" [do-face b1 1]
][
    "Edit" []
    "_________________________^/" []
    "Edit Option 1" [alert "Edit Option 1"]
    "Edit Option 2" [alert "Edit Option 2"]
    "Edit Option 3" [alert "Edit Option 3"]
][
    "Preferences" []
    "_________________________^/" []
    "Preferences Option 1" [alert "Preferences Option 1"]
    "Preferences Option 2" [alert "Preferences Option 2"]
    "_________________________^/" []
    "Preferences Option 3" [alert "Preferences Option 3"]
]]
gui: [
    a1: area wrap with [colors: [254.254.254 248.248.248]]
    b1: btn "About" [alert "These menus are just a choice button widgets"]
]
do decompress #{
789C5553DBAE9B30107CE72B567929E911C12149A5F2D20F41A87260093E3536
B24D427A947FEFDA26093542D87B19CFEC2E9D36C89B1E0618504D502540AB5B
8C0252EEC875D842352989D64227544BF6CD6FBF36500965D138E89107B3CF86
4D5D2775D248534237A906AA8E3758C74FEE7076647F6E6DBE07B0BDBE05539D
781659A3A5A6E4E270DA154746EF8962AED7FC2ADA2C643E034E87DDF22616DD
34968B006C2F5882D20ABD18451756D6DDE5CB667BDEEADBF314D02CC5B01D3D
44DE438CDCF0D2EB6BD1E71F6756BF230DB6538350BDD942B127B205912D4E5E
FD197D11331F5082E4773D39B84C2259D78BCE5069232E4241C1E623AB133E8E
A8164FA07146A96F71A76760F39E854350030375ACE9B5202AFB9F6C2E1848EC
1CAC58DD84EB219426A42D97B19905B85560CA6D36726160453CB7E22FE63371
DB866CDE184D23B046013B523FC29644CFD9A8A93A2C3138224D8E203EEAE2FA
5FEBE11A54094E6767A99B3F54C84F4D50D1E3D717A53DE04926000261BFDC41
73CB1D8794A6C7F026CACD1F74D91714DB15945FAD26F512FF8FBA7239F9B64B
1386EE95503FC236F6DF6BA7114BD31FF0FD25A313C63A1805310F8AC4163E68
0A637916F591F34704889DF3FFCCA0E09BBD5B8743ACE5BBD1834AEA7FEAF212
1C87030000
}
view center-face layout gui

В приведённом ниже примере демонстрируется полезный виджет меню от Cyphre (показанный ранее в разделе презентации этого руководства), который упрощает добавление красивых меню в ваши графические интерфейсы. Этот пример также включает виджет Cyphre "Панель вкладок", который является отличным способом максимально увеличить площадь экрана в программах с большим графическим интерфейсом пользователя. Показанный здесь код является предпочтительным для автора способом добавления меню в программы REBOL.

REBOL [title: "Cyphre's Menu and Tab Panel Example"]

; Menu Widget:

do load decompress #{
789CB51B6B73DAB8F67B7E85BA3B770237430CE4D12EDD6EC6109A92266D499A
B629E3CE1810C6606C6A9BE074BBFFFD9E23C9B66CCB84A477C384C8B2747474
5E3A0FE5EF5118D516D45DB5C8C29C53E20D6774143E23839D6F357B615A3468
119F8E5723CABABED55EE260329C5B646D875342C71665FD8E678EC9F1E1EF7F
DB9FDAEFAFD6F5B76796A7C3CFBBEB9B69F7C68256BB87CFD71DFD161F3EFFF0
8EBE6047FB6CDCFE78D3D5F58BB3F7930FE1D5CE7C0DBD9DF6ECFAF5F93B787D
7CDED775AB77A9EB1F5C0D5EE8C7F07AFC11BEDE2F11ECB103E35F37F7BE7CFF
EADE20C09DE6DCE9F63F5D1DBA67172F34ED85A6FFB8D1FB7A27B27A33FFD38B
BDABAFE7DF17E1B43B5B4DA7B06AD7BABE3A773AEFDAD7FE7BC07074D5DDB1E7
776BC4D25B2EDB00BBFBEE1656F7ACC08AACAF67572BBDD9A6BDC6E7B6ADEB6F
BAD6FCC5E91B5B3F8F2EBFF6FBD142BFA6D6E71D58EDEAF8C7CDF2EB25ECA0D3
D1DFBEE95E754EEFDF74FA7AB7FBEEA67376DFD1BF9FDDF6FBFA69FBEEF6D68F
AE7A17BADEEB0DDB9DE69DFEFEEDCE17EB6BEFB6F7767D7EEE0125EF69AF1FE8
EDB61E5993FAE5349AF5828B4B86E17A31E97FBC9E863F7AFACDFBFACCBA38EF
996DA0FBCE6D70F5E58D7DE67EE94CFBFDD7A3D3C6C58B8B2BFDB2DB7D7D7C7A
AD03B5FBB7FAC5DC72BF1EDE6B07EDBECE5873F3E9FDD5DBA3CE6DAFF7EA1F89
D9A3291DCD413CFCF9969CD6D9578F73FAB47BFAFCF8E2DFE1F44D3DE6F4F97C
4FDFEBAE7AB0F41A609DBE99CED7FDB6A5B7EDFECAB4AE0F7B20017A74DBB7EC
B707CF3F009C7EFFEDF4F38ED75D74901A97D7AF7FD43B9FDAC8ACB3CBCE9ED5
EDB65FB70F6F75ABDFEF358EEE74EB3540EEEA7BF3E0CB878F079ED6EE3292ED
E46966C06F10DE3BF60FAA2DCC20A4BED01BAE6313131489290EEFFD561B798E
E7B788EBB954F44C3C3714DA88CD64643276E898A379D2E94D26018509F5A89E
F4055373ECAD33500DF117155640C7A6043D009C5BA41135F2EB359E37F71BC7
2FF61B4707DF6A39706BCF1F838948A18CCDD0240397AE356CC18E6D3F08894B
A39098BE15A42D2399623ACBA92981F856C3E9ACB7305F1A04066A42827BA0F1
421BAE6C678C2B52F22769366AE7A65B6BD6EB8719A839C8CDA323522324E9C9
8C34A4A70C62793C8C1C41BC498617410838B5083029E5376C8A711824A236C9
304960164F9E50EAC492004D69333EE5DC9AACDC11190019E069CF90374B41C8
40F896A6ED9F90898603C84034B4A895B4C81E9F0C2D79CFF1D078603C4C1E13
4CBD35991428912388EDDA612B2BC42D62BAF764C0C48B6D3F850A2C75BD90F0
57032180CD83E3FDE6C1C17EB3719C8E5C9A2E6037F2960029ED659445B5497B
3C9F9A2350389B0C0DC2C553DA84ED06D40F4968DA0E419082DCE688E6642704
D6B7889DE9E3CA243130A337857E3835F93BE9503DFC63BF513FDE6FFCF13C47
098EBB1B1666ACA73688393302C5D1B1B8A0DD301DDB725B64D7A193303B7269
FAA618894D80E99BCB13344F4E003E806F5B364C6C46F5EC342E1087D1214883
45C35AC84408D7B271C5CCD8D82C813E64FAB9551812FCC9625F2AEC02E01DF5
5381378967E44730E9311D986A82D4C2C628500F39A9312DCCF571F55300E1B6
5A0315A321CC4184156354C0346F19DA9E1B2858BF6152B85872ABA10629E858
59DBAE202AE873153850A9470DF25FA19F553065F5E870DB353927033AF2DC31
0940EE4FD4AB732563D695D9AB7887822E894CEE7285CDC2803EB28BD231D1D8
41B6CB0D7D7610EB33B6C55B9C29654CE1164931B130B6B8E244136ABBB4C1DB
9968A9B292E2527C47B91992B62AA6E4ACA51A0DEA5AA6456531A70A090DC083
008B661674842998B776952FF2241547139A5BC50B0500159756CB92B5401555
3AA81E0C16C99BE715945963DB1DD3088413FEAA5E8324536722AC066F0F5468
E2671BAD8E1905F82C574B0D740EC8A9996BF35E2956ADD2D342FE240AFECBBA
2C84447DEAC89FB2132823289B7549880C3B53F2C63D4356738426A1852702B0
2B4B27D079F6B67476A981798C7979BC71119C2E5A41C4A7743CC8346C50B89C
77365D935D90135CA9A63CB232847CE078933F78368E1C2FA0357A077B41FF9A
3D96EF84CF015FC61ED393C47C2003180483B75FF1270D1F1E82B5F4BD110D82
9AB70A1128C74405F90138F8B3721D00C59578B080839C84F68212148D3B5471
D198D37BC2AC5730F23DC7A93936F84302E5F07E097230454C986A3E843EFEB0
990F8C7B900CDEB236A6211D852DE0E153362F1C71C11B92CE7E5004721018F9
64B128210DF723C980A3B8C51AC6D698288542DAD2368BFD0243CADF95BD3118
D56ADC8E174EDFF8B336EDB0FCDC509E9ACC53564E508129F6E57BB2CFF293DC
16A74888274623AAC3895129F1C6C12456AB85D0EC5BEDE543364F0C38AED7A3
669A504093A5F1A093B0539687E5B17DE6FD6C621232C47F172B27B485E5636D
49DA26B643333665E8CC334A0516973957D04F1A393165ECE087184B49719460
A4927859B40C96AA81AF8470311AEC7510FAECAF961E7C136F38C3497C804FC3
95EFB22DB3C050E107A003844A9E49E70C672067312A22E152879F887D8B7E1E
69C638705459582D7248299ECC0B49CEEFF8EC640F041E44EF24CE56404BF42D
CD702A3A97A2C55D23DE74BC91E9B0E65D92E0F55760C4590BED329F0381E842
38D7E355DC0A5643DE630727AC319ACE79C35E58ECEFD0F44547B8607FC1F2B3
BF3402591CF377AE07E6920F172C8D43C9241309188340C95CC1E76C5200D8C0
3616B305741942DC847004B605C76BE0C1793440C0102F7B2EA960536BE0C304
1CB49F04BD4614C2219066FE8C543818EC010055783FB79706093D42DD315F98
E076EC30E16192DB40DA275282BB0CB2181B29E15BE4203A48C99BA65114C1F0
D843E72476C2B82E9818976C769D779937B28B001F31CBC41457925254052DBF
860E07F92884A84C3D2E982AF7BCE03C6F4CC0629E2A3172D4013700F52A753B
8DE4D478446E67DBAC4E2E7768A4CA9718F0ACDDFFEDF237C277AE2529B08CD0
81AEC7B2561766881D29A9962BD4834AF95DAE278951455A0C7641D1CDA14349
0535BE45427F45AB06A3D380E5F976E1B4421B205E8136EDD6E21E768E560D31
1CB50C8C0419B0D2D33318892704FE45FC9FC5C376C180900A5A91643536335C
A0D5B45DEB191FF73BEB46B75274C7F3C144910A33300900AEFB0916C21211BE
6134093F992D90314DED81412AD2A1C3625926B43CAB08946F908A8864719DAA
34384E59A1809C24CBB22E84CF90348C814853E26C8E471604235AE674147C67
843C89896A89A3125A598783BD0A3D5EF293B5466887E553EA92547738DC1683
4B2713748F4129C08B4A8A08E09B3051355E368E23396F9BF568004D9040ED1E
629383FC0EA4CC2CB7EC04F896D169F2127A588D0A9F732E15FAC580557C1CC9
9B15B86379536BBC64E2CEAB214390217349FE634D2216E2EF2F5D2B373BDEEE
C85B2CC117073F3B0AC1E813166B8283432A65468347A8CDA323A3CA13DCE835
958E96B2E3D5BCB7581EDE97E606D4E97289090119A439DB20DCCE218599A886
1B3807E4D7F27975C2EB8A63EAD80BB057BE9A710058BD6DA91EB5EDEE13574B
AA6E091396E6A9C18F06A7B774DF19F73B9F9FE1C6281F156448554A146EB213
B2E06306B6A047B19F5724C2450E4975E2DDF73CF42A73DA90398B44A089669C
0C061299659283319226955720BC7C098208F649B3B5E653EA12982B83238599
1730331008E51444442251B31819C11191A90E31A9111A1D47EA92C92C0820D3
796D4CE9920CC6BEB9164715BA4360E2175695D4F741C3F7EB464E81F2A0C462
780A2AC2EF0C6C0CF184B16A9680572C80BE65410FCAB51A04741C0903B14C82
03C1D6C79666088F20C88C848B25FE7AF815E0976D902C150B68C3A0B2F89A1F
6A35FA7D653A2719416AC822AACC673C2ABFFFA40C7F31CA57E6FCE532154FBE
A36A663CDB3809FA0A5E025BD4099AF2792D3EAD300742928A0A12F4CFD2AA73
EE4D5C3F75B96CB08045890FC12F803303ADCB5BD9FCCE2B0E78F8E1F484235A
257FBE2249975B65647193DD6FCCAC7307AE74048421C9415D0107074C638DF9
C2E2C89D79109E0C7621F0406292DD4CEE1A3D6003F1C97B5CD98F4F594ED30C
057166A52319756AA5D4512D414A324A55456745D127EC4CD6A1243CBC050AB3
9D95E4FF4AD3829BC98E32A3F1B43F999448221B97C4C9998D0D54BD6011D5DD
6A188AF594F42A2D5DA1D92AABA115B44E594C2D8CDA504A2D8C55153F846F28
46F3334B7889E8472A0AADEA19B19F5A26CF42602A251B65374614EF44910B5E
436CC7E3F624F89732B0E08F6385AB4AFE22F50D69676081642294B44410F994
60414C3DB55993463063A95C406B8AE23BDBF231D90CC756C249A8B261726C60
6D7ECBA9049B8D3B4DB8868800B2B68CB91DF3ED2FF2105F1E5824DE285BA526
AF524B567900403AA7C5FD8274261279E3EC8DE51625F03DEE76D41E02BCF12D
1792ADF6C7252E4563C3E027D432CAB7AF7205124A3C412CCB5040B5E47BE492
C4BF379A3C38DB376A7A2AFD4F117C99E319A1378781E7AC42CAE4E0D1A4FE05
8A72992B5FF297CE8F47B24B5C87C983528EDED2C9D85CB7FA3F5E6099C49536
F022CA2E9EB03B26E25E49DEE530480A41EDBCB15B1D83CDA3B62FD32519371E
3DE7E6C57724A452D79DE9AC681C28452C32F5E154264A874DE5A2257EADCAB3
D9E0EA32D7B6D4DB85D80FFDDDE28D8D2D02CE38B0F35CE75E89166C92DF81D6
3077A118C0D3CF6C448953C4CA354A09CEE5A6B39F48DC39DDDD63E96783D1BB
126189870B2344A31313738CD047067B301206D57082610CA04FE970E3E72789
5AB9B21082C0B250E90CAC16A9B550949054CAA85415520119C92F14D7DAD574
5CC48A9248E12F887D2E1988B9F6C7E602C14AF9E1681516135F49EA2BAE0FA6
9FF20CE813EFB666925752112549812B2EDC6E4838F2DC5C116F6EEB9B8AD414
922E5FBC493FE2F0B96FF174791EF55C82262DC6AA39086B15B0CE7172B3FD79
98ABA6EFC3E153CE52F67E6BA696D239CEDD0DF87AF5FDE7F00B1FE27BEC5CF8
A3C0E64285E25FA474194DE3320FC6DFAEA1222DB7A18CBE3CA9A04CCCB14A13
D697E25889D79AF68863825F961FAD28A8F0407412B00B0149448A28629A9B9D
1808B25E128C4A858A5D9E5E4DF5261B84A605906DEEB6F0DB9095B20CAEB44C
55B67D80774148784D32D7C9CA5DB93E5E8FCC750A4615FA45ED30D39B629231
F0F11E25C3CEBBC41D92F412C30EBBE4C13204995B1EE9058FF45E867C3323BD
1921DFCDE0D1C84E725067FE312577BBE0A1A23262A03552BB943AED99FFA7B0
852B5B6902F7D81278B735E3D8DAB21F2B7BEF7238920BA3EEF3C3EFE597B082
ACA8316A33B194FC0F4EDCEB9C891B73123B8BC7D62C1358A4898F38807E8040
79F1CE5F8A5253AAD26477BA18373653EE49BB44B5891369E93EE37F084A7659
93B0D222BC8C7CA8DC1378B5CCE66E41B81A99A54D01B8042696F69F80E28B52
24C4202067732BD6487628BE646727D7B20587528E714B85025B2DAAB6505CE9
F69674D5077ACCF13873A72BBDFD1697FB714A72A52D003F4943CBA2C141A2A1
B30D5E5D6C99F9D04301C9244363202209FC77431B040804940CD96F930C307B
8BB782C12732E21B663C8F5D82914872B36B38325AB011E325D6BFBD0989FF97
78E79FFF0120649CE6573C0000
}

; Tab Panel Widget: 

do load decompress #{
789CBD586973E2B816FDCEAF50577FE86428C7989824506F2695349DADB37496
4E4FA09C2A63CBE0C6D8C4368DC9BCF9EFEF5E495E65B2CCAB9AA4005BBAD2DD
8E8EAEF45714AF3CF799AA33338A6948868D47253647CADCF4A9D7238E6951B2
74E309EBE05D6648CD1E999953CABB79CFA312384E44E31E6925ED9668A2F698
0A517CCC441F1577666297179836890365E4FA66B82236B582D93CA4514476F4
8F7F35E8D973D8FF76F8DC69FEB8D56767E387ABBED63D39383B884FC79713EB
E4B8D38F2EEF9ED4A3969BCCFBBF76AE7E9E2E4757ABB079D3D28FADC99F8DDB
6F0FA77BB3FBF1D3D199F7703C0906B3BEDEBCE99EFE6A1F8EBFF6EFC70F27EE
FDF2E2707C39681F5EDF256A337A1ADCEB874FB7CF897DF2E5E2A9D171EFBA4D
4775FC78767D6D1E4D6EA6D6F5D765D4D793E3D9E0AC3B4816E777E7CFA74F17
9FA7AD9B2FC9F27C104C8F0E7FBA17DD4B6B7BFAE3B6D1FC71393BD8BB9AF447
30CF73D3695D7C3B595EDEDE3C58BB5F4EA3CF5DB77564FF79D78CC0BC643038
39F8BAFB5DBB3EBF98B9FDD1FDFC9BAD37BE3E07CBF3D3D5C21B3C75CD8BE6F7
B3B63538BE9C3E9CF8FABDF7BDD554E943AB75A21FDF6996EA6DDF5F7DFEDE3A
38D8D54E0F9AD303F8FBBDF17716F40812DD237AA293ACC90ABC20EC113FF0A9
6833C4AF4311003CCDF058C85DA3F4F3A884D40ECD258065E15B64E8109350A3
200E30000001B61C750ED8F1630561A38E82D0A6E17E4910B4AA0814953A0EB5
004B08872002E0D024A6BECD6CDF7054748428446B6D12E600BE6E94A767ED91
AA6D1A85F98D3A6525EF8B1110225553C6A169BBA00780AE9176A7BDC53F3A69
EBFA565BDFDE6A6FEFBDC72CA31478235D67754B8C27B0038ABB6996023F4E25
E1B1104D9159ADDDDA129F0A0E34AD92F1F5AB5546493A0998F266DC507FCC16
7D012744F502CBF448E43965C844403AD60444CA19B383A55F690259CFE955C0
55789685D594C55464B91E89A8071926A227620F4E602D229E06D7B769B24F1C
F84D65945190B0C1A016463B151DAE039E0D51D6B46237F071587956E335ABD4
BA900BD949B064B3A15CA9CFA805B1F1D2425D9300173B7F279F98AE72BCDF11
6D474DF78499EBE3982012DFCDCA0C76E0C495A12200E868BA684A1282544A61
0593DF902D09409C50D4558FB4DB521777013B5B521F2EB9D4D091675A531908
68433DD5F129EAE84EB5299D9321268B0CE7D4271BC5286C82739EA7D4B4839F
B820BB408C2DD88535A30AB35A3E0B8305C44AEBEC6D693B5DF874C876755CF9
DD581FBF8C97EAE227C7568A1F0063A71AE6F706B166C914FDAEF39720A7831D
8CD2351DD8927F8023E1977F5E89C98B4BAF48ED9C373323855B3D12870BDE52
A01EA4A31EB1CDD864CF0864781D99D31E31C33058463D22A257D9C83826C0D9
8F436C3320313B5B6D0DF6A64E879BB104BD28202C441D64E8D3A5CAB5396E18
C5C4876D17348DA3FC2975C90F84E97C54E686637A00AA5C927FBBBECB20375F
91D94ABCA5AA33C7E4FD2E2F2AF524470F9205EE3C850D2DA6F348CC3FCCC33F
33936C9BCA85EB2386FF7C3B28CF62E4560A0B916786D2BE6B14048B2572FA0A
254C757B2D0DC92320DE7213380ACA66C1A2F08398E4CC372CC0464BA5B84340
FB0B0C6A668AD050988BA79F238B29C2868A2E16373214E15B4EDC98A6120BE4
11C80AF9657A0B1223563C73455C3B210EB32E9ECD59260AD49136959323529B
271BE60087F28255184367F378B52FEC2EAC3DD854C016A9192012CCA8C41DA6
0F21057C71BBE1812D8B0F063E7237A23874FDF107D6802E0D47B05F4E3F90FF
9260F413F8049FC4986C32EEF2279610836C485C042EE0907D3E217CF5C8984F
2FF335C8328D4561F80A166BE4A151CD0F62552A65F3A1AF741FECE3B891444A
0C04E1AF1158F8E8279F41EAAE752255CA025DA733CB0278B05E234ABC4D2182
841D13EA94E5D8038AE0894B9B306A061AC10F194DA2ED25DBDB6FD109168680
1AD3F5186F67CB0E57417D6E11DAD80B4A5E9B2EAF61320E2A170B355E426F8F
540B2B116DDCAF7161B5B014635C8F4D0AC37C8187418F5AA0BAF41578888843
04AB343A6CB1F4C806FE6CCA9E3E2AC5FE9A6E991BDF1F711622B613088F7051
41DDE999515C0EE127B45D9A8E510F9258136F24E6A61B6629D4B228BD3C2791
AA844DE08768EACEDF502CA4A501E7119C2D63CD824CAA34DFDBCA19CE02C1FA
36D83CBF112D81A3729328DA0E948505695828305C4DC81F8443A202A4628899
9B69D9C139282DEEE0A83B86A8BC8848D30A8328AA348A81DB499B4473445C95
B1983EA2ED26DA2ED910956E7B937814F2149A31253B35D067DB51E54099FE3B
FC52EB8DA74511232493093581B2F30305475A9D0AAC364B5250DC626D5B697D
6524AF72CAEF2F42B36A9A844501323CD265B23522AF2FBDEAFB9A24416E27FF
284BFF384788D37D5EABBE3751B5E156FEFF704B50A8B3EEDF499451A60385E8
AD44694B65025FE26B2E205E3EDBD4DFFD08C190724EE2B70FC4618CD324A5D3
A438D463C8F7457FF5CA2E3D6AA64F901026A816CB06E9A28FAB6682EBC89785
5B722F33BE7A7512A45727B083BFE9EA846354A14F0BD34322C16A1B7FE45A75
E2DA9438B51E48875B474D2959EAB20332C26B104715672EDCB91CB0AC7A6355
719D252271E33581023FC4A583949BF42AA2974AD4F5BF78DFCA6A222CCA3476
67E4517F1C4F8AB42BEE71B212B5519C3FBB421348CB2F8AA1469124D3E9544D
45B4AAFC7EB2A24BED6185F6895D3C56EC6681E0D19722F1E2222A0808FD42AE
DCDCCE9B5352AD9F242DF8D39BF1F202AF8C29121C96803E1D2343D7788D3775
6AB266B86033AC6DD8D628F1617D8E44D4AA550C141EFF917995973065A7A088
D1BB50C4D45CE2554CABB36AED18F94E614D8C15A87813FC7A43452753A4D130
FEFE1F50628086731B0000
}

insert-event-func [ 
    either event/type = 'resize [
        mn/size/1: system/view/screen-face/pane/1/size/1 
        my-tabs/size: system/view/screen-face/pane/1/size - 15x30
        show [mn my-tabs]  none
    ] [event]
]

view/options center-face layout  [
    across space 0x0 origin 0x0
    mn: menu with [ 
        size: 470x20 
        data: compose/deep [
            " File " [
                "Open" # "Ctrl+O" [request-file]
                "Save" # "Ctrl+S" [request-file/save]
                bar
                "Exit" [quit]
            ]
            " Options " [
                "Preferences" sub [
                    "Colors" [alert form request-color]
                    "Settings" [request-text/title "Enter new setting:"]
                ]
                "About" [alert "Menu Widget by Cyphre"]
            ]
        ]
    ]
    below
    at 10x25 my-tabs: tab-panel data [
        "Fields"   [
            h1 "Tab Panel by Cyphre" field field area area btn "Ok"
         ]
        "Data List"  [
            t1: text-list 400x430 data system/locale/months [alert value]
        ]
    ]
] [resize]

Для полноразмерных меню со всеми навороченными функциями, анимированными значками, подходящим внешним видом для различных операционных систем и всеми возможными вариантами отображения был создан модуль, который легко предоставляет такую возможность: http://www.rebol.org/library/scripts/menu-system.r. Вот минимальный пример, демонстрирующий его использование:

do-thru http://www.rebol.org/library/scripts/menu-system.r
menu-data: [edit: item "Menu" menu [item "Item1" item "Item2"]]
simple-style: [item style action [alert item/body/text]]

view center-face layout/size [
    at 2x2 menu-bar menu menu-data menu-style simple-style
] 400x500

Вот типичный пример, демонстрирующий базовый синтаксис для общих макетов меню:

REBOL []

; Вы можете скачать скрипт menu-system.r на свой жёсткий диск:

if not exists? %menu-system.r [write %menu-system.r (
        read http://www.rebol.org/library/scripts/menu-system.r)]

; Если вы упаковываете свою программу в файл .exe, обязательно 
; включите в свой пакет сценарий menu-system.r:
do %menu-system.r

; Вот как создать макет меню:
; Блок "menu-data" содержит всё меню верхнего уровня.
; Пункты каждого из этих меню разделены на отдельные блоки "menu".
; Подменю - это просто элементы с собственными дополнительными 
; блоками "menu".
; Используйте "---" для разделительных линий:
menu-data: [
    file: item "File" menu [item "Open" item "Save" item "Quit"]
    edit: item "Edit" menu [ 
        item "Item 1"
        item "Item 2" <ctrl-q>
        ---
        item "Submenu..." menu [
            item "Submenu Item 1" 
            item "Submenu Item 2"
            item "Submenu Item 3" menu [
                item "Sub-Submenu Item 1"
                item "Sub-Submenu Item 2"
            ]
        ]
        ---
        item "Item 3"       
    ]
    icons: item "Icons" menu [
        item "Icon Item 1" icons [help.gif stop.gif]
        item "Icon Item 2" icons [info.gif exclamation.gif]
    ]
]

; Каждый пункт меню теперь может запускать любой код, который вы 
; хотите. Просто используйте приведённую ниже структуру "switch":
basic-style: [item style action [
    switch item/body/text [
        ; поместите любой код, который хотите, в каждый блок:
        case "Open" [
            the-file: request-file
            alert rejoin ["You opened: " the-file]
        ] 
        case "Save" [
            the-file: request-file/save
            alert rejoin ["You saved to: " the-file]
        ]
        case "Quit" [
            if (request/confirm "Really Quit?") = true [quit]
        ]   
        case "Item 1" [alert "Item 1 selected"]
        case "Item 2" [alert "Item 2 selected"]
        case "Item 3" [alert "Item 3 selected"]
        case "Submenu Item 1" [alert "Submenu Item 1 selected"]
        case "Submenu Item 2" [alert "Submenu Item 2 selected"]
        case "Submenu Item 3" [alert "Submenu Item 3 selected"]
        case "Sub-Submenu Item 1" [alert "Sub-Submenu Item 1 selected"]
        case "Sub-Submenu Item 2" [alert "Sub-Submenu Item 2 selected"]
        case "Icon Item 1" [alert "Icon Item 1 selected"]
        case "Icon Item 2" [alert "Icon Item 2 selected"]
    ]
]]

; Следующие строки необходимо добавить, чтобы устранить 
; потенциальную проблему с закрытием:
evt-close: func [face event] [either event/type = 'close [quit] [event]]
insert-event-func :evt-close

; Теперь поместите меню в свой интерфейс, как показано ниже:

view center-face layout [
    size 400x500
    ; use this stock code:
    at 2x2 menu-bar menu menu-data menu-style basic-style
]

Демонстрация на http://www.rebol.org/library/scripts/menu-system-demo.r демонстрирует многие дополнительные возможности модуля.

Ниже приведён промежуточный пример с объяснением наиболее важных функций. Он также включает в себя некоторый стандартный код для отображения меню в стандартном стиле MS Windows (внешний вид зависит от ОС). Модуль меню был сжат и встроен непосредственно в скрипт с использованием "compress read http://www.rebol.org/library/scripts/menu-system.r" (так что модуль не нужно загружать или включать отдельным файлом):

REBOL [Title: "Menu Example"]

; The following line imports the compressed menu module.  

do decompress #{
789CED7DED761B3792E8EFEBA740343F24AD4D5352EC24C3331E1F59A213258E
E548B233191EDE735A64536A9B6433ECA64566BD8F71DFF7A2AAF0D9F8E82645
27D9DD602672B31B28140A8542A150285C745F9CBF62BD078CA78BECE6B62C3A
8CFD27FE847492CF567378CDF606FBECE8E0E0293BB99D6745992553D69D16E9
58653D1E8F19662DD83C2DD2F9C774F8F881FA7A910E79A97976BD28B37CCA92
E9902D8A94655356E48BF920C537D7D93499AFD8289F4F8A47EC2E2B6F593EC7
7FF345C914AC493ECC46D92001488F58324FD92C9D4FB2B24C876C36CF3F6643
FE50DE2625FF937268E3717E974D6FD8209F0E3328546858507A92961D8DE9FF
AB225BB07C24B11CE4439E7F5194BC8D65C2B1871A92EBFC237C12A45290789A
E66536481FF16C59C1C61C28C032F08056DB48F29A07E3249BA4F3C7319478D5
06B5244ABCF5C30547D38315DB165A8C5A6D821BE683C5249D9689ECDB36EFB6
9C679AB34952A6F32C1917BA67B05F01BAD922ABB1AFD30C0B43A6693249013B
78D68DB9CDC7439E619AEB4C9C266561B5923788E0E7F38223B262D729301D6F
5ACED2E990BF4D81BF386293BC4C19118FC3E09033CEBD16AC11CF44E42AF251
79076C23F9B298A503E0470E2003769D03274E89278B02DBA6205D7D7776C92E
CF5F5EFD7C7CD165FCF9CDC5F9BBB3D3EE297BF10BFFD86527E76F7EB938FBF6
BB2BF6DDF9ABD3EEC5253B7E7DCADFBEBEBA387BF1F6EAFCE252C1DA39BEE410
7630C3F1EB5F58F75F6F2EBA9797ECFC829DFDF8E6D51907CA6BB9387E7D75D6
BD7CC4CE5E9FBC7A7B7AF6FADB478C0362AFCFAF74035F9DFD7876C5F35F9D3F
422CDCF2ECFC25FBB17B71F21DFF79FCE2ECD5D9D52F58F1CBB3ABD750E94B5E
AB1605ECCDF1C5D5D9C9DB57C717ECCDDB8B37E7975D060D3E3DBB3C79757CF6
63F7F431C787E3C0BAEFBAAFAFD8E577C7AF5E55DAAFA09DFFFCBA7B01AD32A9
C05E7439D6C72F5E75B16A68FFE9D945F7E40A1AAA9F4E386D39C2AF1E697097
6FBA2767F0A6FBAF2E6FE6F1C52F8F04F0CBEE4F6F796EFE919D1EFF78FC2D6F
F55E53AAF17E3C797BD1FD115AC34975F9F6C5E5D5D9D5DBAB2EFBF6FCFC143B
E5B27BF1EEEC840315E9F4F8EA18ABE6653919D57B4E86F38B5F000A341049FD
88FDFC5D97BFBF00AA21198EA17D979C1C275766360E8D53C7C0513780BDEE7E
FBEAECDBEEEB932E643B07703F9F5D76F779CF9C5D42060E1BE8F8F331AFFC2D
36037AE42DEF3BDD172F6D167E841DC8CE5EB2E3D3771CD8A92CC5BBFCF24CF0
09D2E3E43B41D4C73BAC2E6175FFF5A0FFE0C1A05CB6B86459F059094673BA2C
F95CF5C0CA57DC26C3FCAE954D929BB483934A0FE69F92E19BBE98DB20E1EB0E
2B5645994EDAF90C655D9BC468EB3A29B44C0B67E9B0AF9EA86CA2CE719E0CF9
EBBFFDE783ECDD8BF38BBB831FBEBDC98F797A7DF9F6B6FBF6863FBD809FC73F
9D1CFF02FF8EBE69FFFD161E5E4CBE7F7571F0D371FBEEB47DFCE6E1CD838F49
7A051F4EFEF5E2ECE77FFDC89F0AF8FDAA7BD73D9ECCEEB0F48B2F2FBEBF7AFB
F6BBC39F7E7A7BF2DB2FDFAEDE24C9F827FEE1ECF5F70F2EBA2FDFA6AFE7C383
5FBBE777EF8FDFBE181E9F9E9E7D7F7AF64BF2EF9FDFBCFF7E74F9EFC5717E74
FD53B6BAFEE1877FBF9AFCFAF6B78B41F7F0EADFE70FDF7ED91D3D78F3F4EFA7
776F57DF9DDCFDF0EDAF372FF2C1CB7CF1F0EED5CDEDEDACFCE1DDE9FBF7DFFE
38FB707EFDD5ABF4F8CDCDC9AFBF7DDF3DBC7C77F9E4E3FBEF6F2E0E8BE3F70F
7EFEF7F7B7C7CB9BF7DF1497072F3ECE9E941F3EBE990C96CBB47BD25EFDB67A
F26A79F8F1F8F8E555FBFCB7BF1F5FBE6FBFBC3DCDAEFFF5DD3552A83B7EF9E0
EAC3E5E2A7C9C9C983FF6AD41FD8AB7697E0AF3EF1C92029789F8F16D301EB7D
4CC68B14DEA405E78B820BF0019F8956B3F479FB2E9F0F59C7CC40C581E55A45
F65BFA5C02D9B948CBC51C2649364A06E96E817918E481D9E46079C0391CBEB0
5E7EFD3E1D945FF4597B9C0F9231E5E1F82EC625AF5F8F2A78DFC1326D7854EF
D59B0E3B3CE069897F6B878F7F3C8911C0EBEE60852D441BAAF0D567E141C504
55154024758536AFCF5F775B45324AE92B02E323349F978345B9432F590FFFF9
82CFDED3F48BBE394293E98AF512AE48521603481F08DB7750981665321DA4AD
7C2451D0D3A3ECA7AB8BB75D968DF8945F66E58A65A8CFCDD35F17D99C4FF25C
AB290AB6078893CA729715E9FEE31D054714939D29B016E57AC0375F08BC8C76
40134CF58151E9E7029AF5691799E519BB01993595F5ED025F5AF946F9623A7C
CE461957CE9050950202A3BEC04D9575A8E6658DA6090B4CD3BBD61014E0B13B
2AA66C31CDA65C911CF36E1B72A52C9D30CAFB78C7ECEB49F2219503FB6396DE
F13F43F8AF4563675DBC3061C33B04FA9A8FB90F5F704EE2150864FB9BC04C87
20E307F9389F77583A1AF16E94FFF2AA801B9AC1B1D96134A2B908071B676DBB
A7D3742C5A8143127E57F8091267DF7972277B809A28204B89038DEF7B8A4282
6F1DD135ED19D768A765CB120756E691CC299A1E000AC9CAD861B36CF0A15A18
B9E21FFFC47FDB30C1B7930157D38953ABFFD9F37D90B6D8D135A4530F1C1F44
8E6887E8A41F3905AAC4E2ED063D3F9B3EA7EF6D415FBE286AD1E3736A0CB604
C59D4B18DD4A408733799195FA253EF9CB60765E2BACBA7C991025EB6DDF18F6
86FC3619C5CABF73CABFE0A8850A1AE866350969A1273E41ADDE2CC9E65F68DC
883B2D44B8202F531A66BA093D374B47FD44BEEA1DB1C33ED5FACCCB4D95C254
CD4355F8A02F7A0EDFB30A8FD9AC00F9841CA8BEE66B47BEDE2BB3B468231D31
5BD1EE2058174AC695D9B6D01E235020DB63CC56034A0EB5FB81BACE87ABF688
ABD91D16C30AB23D866C11088A4E7108ED1A3A213C2E9A920618413617C287B4
D2243F049E2DD0240940F77C14405D8B009CD5A030387F83683A725E57014036
7F619B858385EB1A8279EAF9CEC81681658289C00A83A9CAEC695EB274322B57
CF231C43C32124AF91F72466837C32CB8BB43D4CD35960E603111B991449A1DD
4EDA4B8A1648547660A00A734F7BC55A6CAF48C19467EAE7B5441034DD676D76
B41FD509F79A82F242E93B6FFB81E90B92F543B02D6ABE9E91E915405C094C06
1FFC6A9FCB8638DB4237B626C9FC8398CB08CEBAA9022F99CFF3BB4DE11913B9
42CE525CF4744BC07BE56236AEACAC1452505AB1B5BB4EB1F3B9030D12AC8F43
3AA55E9069306C779E0CB33C32381A0C2F93045C9F48A76C0F5BBBCF1744E371
CB7C31C8E68371CABE5E7EC3BE74190E49CA7CAC1869C0E036057D617B0D8866
311B33CEA669EB2E1B96B7EC887E0C92192B7E5D80091A7FBFCF613380AB82E3
2850C8CBBEE434F96A7978C80E8F964FB59EE5FBCF5E2F6A36DE9CF5B07813DE
2B16D72ED3DD5B02477966968F5737F9342EFAA4D8355A835277C9A52EFBC679
BD0269CA3F1DD648D41858DE5521B04F3E07D887B5F2FFFE44A859DCE1F24872
590C9535D2CE3B002AD69D7B435CF364E56EC16EF38F7C714533165FFDD84C6D
2C442AF345712BE5F9E6A962A0B15EE19ACFBF60EBC227D912C043B4840F1A34
959BB62B488126B5A7E9DD3D90F7266504CA26937498719A8E57B473067866D3
9BFBAF2FFF4A0D53DFB163A0C0DD661552566F0B1E28ED7A31EE482130B509F3
A769369CE5B3166E617BA78E7ECFB51A89DD651800FED902B815C7120E12A891
CFC0D3A11CF26BB5BB5F5305FEBA6715768BA382659C261FD32D08575B26BD02
A81EE9BA98A27C4D87C2EE027E0583319FC65D99E5015BD130EE87677F7BAC6F
2149BB15B2BBE403E9FED854F5AEEF9F418C69D1370FDCC022CE51F90DF36E51
D5B502EAF906AA39171AF968A40C6B6A45105BE6DA393BAC9C2FFC16655F8A82
2D3A86810F467C7B964CC3B047F93C4D06B7ACC8AEC7E0C182206A146F29256B
113689292BD805C45A5E3B6D35C922C2025E5F9DA2EACD3C5FCCF466910024FA
123F468105A4910737AB1347C9B8A8EF456469B3B8BBD8673ECD4FB572FDB597
C36E6A0E89AC5F0DBD4EFEAA0C38AFE685032F19A08F131ADE6D761543729E73
0CF89CC47B89D73E2FC1D348F09E8D4D608216153C63BB1C024D794ABAD81364
3F2AC05DEA8EC876B2F6BA7DED75B24BF048E7892D0E25E6FC03C26596E0AC0E
963FDCA9296EB351793F8D073AD3943A882CBCD4CDE6AFBD8D835CE62C00BF9B
12C69D1B54817A3A6423BFB54F30D6BDE831CC8D49A80F1E88C36865196D2BB1
DD221D8F02EBA5980110F8BFA2CD78F6B1F1C7753207F8E606A0FA6D307DB198
C1C24A7D521F404F11DBB98E30B0D0B037D3B04DB4CBE5B57908AB256E37693E
22DB7A6817D46AA0264868CB524A1E2CDB501930048B516F4CC6422E216377B9
9A3A6D95790B57A935659AEF78421AE58345D19EE62D3504FC23CC1D073652D6
66604C67F9DC88621D23C28E234592095C3B6A752893E0A845C729AE0AD6ED19
43AA33C343AA54DA9CA2BCB513703DB65B1BD7ED36E982462DF66B1B1EB902E9
F7B1BAD880EB4D26751A4388369BA4ED2FE97FFF353B4963EFD25D73143672AF
EAC5B1CF1EB2BD83E521FB0F9129FB2DDD6FD4D0BA65BEF41BF9AC1845ECF790
C06B0B80783C1985CB56C23B852FD6154EE8B1156DB7E9C5051AE130990FDBE4
C165CF43E49965BB66A90553DF193BB019DCDCB72A9EA09A6DC2DB76FA98D076
FCB6F003C74EDEC7E8F5209C4CD4EAE8CF4001AF47DD9F03354C8A587F342281
A495EDED908D5466AD2DDF370D9332D9668772C1B855FE206B1A78447518380C
74D88794FF11BB9BEB57046628F043C7A3571B4EC7CEEC414892BCD4EEB6AC87
361EC2722D932835D702B5A1499512116E7BF0B00B62F01C0AC9FE5A1F035A48
B2E04479BDC8C6436BAA549F775EC037E530F98895F94D8AAA091E6B03854CE9
85300D1686FEE6B167EF24A8D2C0971DE38023CCBF958C70CC0DDFC303429AA7
4536044BFA545781C718C8295D3AAAEFC8021C2B9E7F5A66A32C9DEB221C046F
204DC9827E6611F83CCFF0E00595118674AB556DA21569287CA9EF1AB295AED3
61D63A09DFD3040858DBEF499000062E400DCB303ED8C54D3989E348BAF0BBEB
7461B0E818C04D43869B5F398057F24B0767772743F884BA55086380AF88E1B5
C9CBF55C35897228647C3994B365C7AE36ECC6A9FD297D45BC6E89A603580053
E921D9A9345FBEF7345F7A41FA8AF8D1B0BD238D6E190AE73BDF6E0B328BE1FC
EAB568B9E4D2C508C770310B57CC66B8A57A8B39FEA8AA94A047B0945B974113
6F29CB55D42A5A35B80597E1243087191CF07517D04A66920013D9B42F80CA48
87100CC9B743C784313F9745E23B40F1CB22E5F7EF5F7DB88B0F01DB66571447
96890061A3C02114AC0F5575CCAE99B4DCC3AF8F1E1F7EF5CDE3C3A75F8668D8
2C5167EAF30DC8D8F73401D0211476B83C723A58A02F5611A6AE1EB0DA2A23AD
CB30B0FCDD1D27AB7C51B66843A4CA27AFF0235959CC89D39E0FFD3317BEAD9C
2BA0E37C3859EA4E12B37FCFE4BA2215C06C84F389CFC805D4EEED0ACE619FD8
EE7582FFB45AAD3EDB8BD0D94A519356C71E51F694594DF67E6807B666784B2B
AF6BCC685404952825340BA5D2A24AECAED3CD640E828A94F196719DCC3E3162
11EAB47C56C2AF16A9327BC17A31B7702614C737CB9C5129FCE947D9EFE3063D
BB4B26BE608590CC2EC2ECD2B2429C4A68586A8B2F6DA1DF109978DF4905ABA6
FF20ADDB8790FC8484BEDC2DC6C8BBCD49290A0031C5E35FE4C4E4AFC7FB32BC
C50B498F2F88BC31BD898D2C485AC5219B1296DD801774BD65D2AC525075A84E
3E9C25B29B56DFDB15FB911A0FA1057C725EF56BB0F3EF746E4E9ADE2EA8F030
7FC0BF45DF40489E93FE24E4D927705BC6DFB3A4BC857F3743D95A55C0F97859
5F140A24C2834E1E47A4AB990865D6C320090DCB8876F71A66276AB0DE306F5A
81D4011B648F7F15478D10DEF318A11B1037DE4DE4FEDE000CA4BD08ACFDF8D7
3F1CFE7DBA234A4221C22379EAC5927970731340FB7EA92D04011E1103498040
FCA2408A002912EE270AAC65FB5AA2609371BD81F8587F6CAF293C7E2751103F
70584DF16EBA9F2830607986AAF9F50F87BF4D51609130200ACC4E6235B131B4
6A141007E691C8C34DF46761173484C02E2EC43E9165D718F3750A95F0FCA3CD
788245A09E894125D81F0C88EB5155DA34054137558276A5D9B44163FFB0B62A
2BAB1A807B08693F0C26D26234791ACD6DD8BC8AA93268A51436A2E0F79AAE52
751986CA908D52EE5285BEB3CDD9028C99F72112194B8336D91891B0EA358824
ECB221936C8C4858D5E64402746B26830DB9CD26E6E7E7B8F08C700F6EB01BF1
F93922DC08BF9CC0A67136F8CC5DD86820C7C7F11FDD818D86597C94DDABFBC2
BAFB2E9AE17FD7C94B458EAA99C58C2D20DFC64F8060D81E42C09FEE6504EA89
53479FC481813ABA24B3993C45D1A62867BB9345B948C6F5A76DCCA2782484CA
3714ECE6C9927C345AB3D1587FD41E0769371FB11E98BBE9F88EB5EA1B67C202
DE788CE843427526A938DE950EABAD7C379F42A15D38EE850F2BAE437FC2D35F
EC5383D2A31114C27346F000BE579F1A1C3BEAAF4316D1917229B97D298EE66A
08526C1B191B682BE4E930CAE66A05BE7122270C7099127B6DE423415BA7AAB2
C0618C4AFA4B63D72D361B3ACC8AE47A8C9C9A4EE1A9914A2AF88F803C63124A
C45864A6863B03A1E334FA496D268A09ED41ED10C20CC9F0FDA2103BB7EE16FF
317E65D24B496CF062DC120A273E17BEC6655ECA48A860EF5E4CD221BB5E395B
BE543EB6B9AB1998A2F5E03B30A56342A51813A8569828088F6A8F8481119210
063D010C7A0218F4C461D003C2A0471521B5C199CA5052F57734021D8D4147A3
D05138740C24ECD091F2C42B524610D0420D43F1D1213B2014D208C88300FB72
118FA7BA22D9EC435DAEDB4E8BE27E70DCAC4FA05208A4D5E373773FCE063772
0E1A1AC76C7DD39241C6C3AF96875FB9390CEA4E92A5F19B1E45FCDBDAE05966
40BDC3469DEDA262F42EA2A2198EA9905E1D2F7341F620D789B3126ADF8EF5BE
5A3E610F8D58C6F88DCB551553370226ECD6A45A5F73A87A9D3864CD48A99313
99A59AAAB155ADC2EE17F78D1E77D8475A16E0A36017412BB96BE9A138FF2408
1E72658DB2ADF382661431D28C00701D37AB44133E928AC15B924D1693160642
A68D6D2F91D48068AFCC3A0C88EEFCE32121695F0203333610B4D6DB32418B20
B63121EDC12080923829B4A2AE35280AA18C0CA4318491CB46D4921810270E92
871791C6514CCC5E4020767B2231F3C2F2537A16F5D8BDBCB6EB6B234F8E90BC
D6624E341A73DB51D015F8EDF3454500EB32347CAB2835A7BB3938CDDE7CC844
70C6CA1468026810EF18492163487399E2D168DA4B43A7682FA1359236F84BB5
157F491AE10F2D8DF0A7F60AFC932818C66831085191459AEACB8E4915DE20E2
9ECFA78468C9B1F40B38359E971DB357C2A303321A1DE69DA45446B3335D1E32
23B679B10B9141B54C907B5955F054D37406BB0027BD411A7F6B8DB2162CC1BF
32D49C9F0246610B9860F750614B8E43610B182F6C9077CD89B9D9D8AF889575
457804850AEFC7917DEA10B5716A82747C263065ADEE0A6DB837BAE3CB268557
6661359756253ACDA851FA9B12A663CB1F3545D8AA50DF95C6F2CC112DB84DF8
F843BB3C46EE5348E84205E11A899AF38EBA59045F3AD72B28BF54AF3BBCB08D
0A8BAAD13D5666F2A69499D18952A28A97ED14E321AF976C4DED126FE782771D
89E8E1D1C1F2F09B3EC37C151A19F4305D40232708ACD6ABEFE2ADF76C157DAB
9EAE62D1F355025EF08415F39DB1A242B15356CC77CE4A15734E5A550C1E9C80
557B83E1826C7ACC228ECA61B6C27456E76258D70FD3FC5A925C9CD0984BC682
6F22B6EBCA642F2AC327684E42A57DED8E53CE49E4719BA2953E9F1355DBF0D4
07C00A82385A5039C1E58FA6AEFD275622964AAFF79471A6EA7DC39EF6112E18
D4A0520BC326F1BB03516363D162DDCC91E8B12A602A22240431A8F1D89C7620
386CB0548B7DC94B0582B4064B3DDCA8941743CF590F56E12A8013BA89E3FF40
060AB6431C871C062F9E07E2E8205673349DA29B3FE5C5B7C6AD25F6B3E627E0
AFC750DA137E0FABFE5CC90C2B62C614C34AAD306262A801A69EEB478C202A9C
C5CBCC7B26850E27C568E22D42A153B8D8C9C7543F8268DADDEE3C867A493218
A4EAB455E8829F36E5124D51906E52C2B683873FE869304E93B978C6CBBEC4F3
9F2862C0DA49F3C6321DDC271CA7D5073B976959C02564181D106605DC6B28D8
5EF6387D8C752D4AFE136F3115ABACDD42DEAC8741382D789E73C8BAAE2B3561
E6C0CE2C9FDA1170C073132E7E01FF4D9A2C335ECD4D3A87F9F20D7C848262A9
974C5777B7299F2C304E92B857B59CA7691525445D4CBE045BCFA83B27F051B4
72E83650E3E7B97F06D1D0BC5E352BDADE9D8405FCD5E7FEE157657F8A17C204
E83E370882871A087BF8EB292448D5B890D3146923A192B827098F9E7C66DC3C
BE10E27596F315EBF1CA647FD15917DF1C093371E5804DB59C2D4C5C18428530
8EDAA8F35B6E71EB97405C5CF3719B262AC610142FDAF314C35011059D7A8739
D9213AE6602093431DCA728755C8B036709C7A49F501D0A818D54B043C6FE40F
3A755C96C9E03655D10680B23866D2255CC13CBD41B255224F6100CAAA7A5B49
7AF0426ECFB865FE91ABF4DDEAF885C7215EAC6B45C5AA8EDECA5D7A7EDC7E04
BCF2B98E42C4A7FD39EFDD95D8D04E87C640C66EC7FD4771D59CB8F519DF8B6B
832B446A5FA760C8623B67487A11D44BBC448D9C2EF485B5D6027B3887C3CAF0
2C258F1BD72B1941F8B10A4C7A69839C71799112D01DFC11868967D01C98D43E
71D79B6A1FDB1BA6A364312ECD4064EDC4BE10194E363B4B979F61B170BD6277
B7D900FB749E8E00EB5C459AC093DA31D909CB2CD55B05FB98CDC139C710A6CE
ACA2C40E8D4E0C96698E268AA28ADF604462F8E43EE58B08BEEA11C040F043C1
339EF9B622CB1079D74E459C6217C7106795E2C031AAA1F202486223F8DBC7B6
55841F7E70EB14ECA5EA9CE2DD9D9E3A81A39C3A89CDE0AFB74EFC60D7D916DC
14B20EF62C49AB0F4BC668D777FB4A38136CD64F84BC0B1952B58FAE133E5369
7A19B486AFEB92DB20DB56A8166A49EC36243FC96484936774ADAD12B85E8C22
F7F820116A5DD74C1B9FBC85541BC6372B2AAC9AC1C235DE1618BB0131EF80B0
AB5C91CA5FC53D88AC0EF29F20F6DCE0DA180A75B4D41801418216478AF2F8A2
E738C4097E09D1BBA6878528EFC8320DF0ECC84252A00B3D4CBC65E1B33090FE
EADC353BB7492857E18758150A6CE775FB78A7B9CCA94EAF162550584742F39B
F21E52A030E608A3209CD090732D6279364D89EF02B79EA615CD1A43AE2A65CA
7458CB4A9F82DD64552C181F407FEEA57183056DDD7AB14E07ABD8CF3C2B2D9D
462A30785DF8050F1CB05A95C68CADE1E879121B77E4B4420C23273E8D55ADB1
C1832169C0E6845DE663141D2255C4251999D15137E38BDFCF5A62868373AC25
A8F88B85AF752F538869221C62C56EF38C452474F13F97D06410B23677344A40
E80E7B07CDBA2F9D2DBB4288CE348CC2A4FE9F26FC22E4135828027A68E7D28F
26F520FD7A83A41C54EC6FF730D5DCCF50F359CC34B4655C6FA4F9CB38F3A730
CE8406825F5D263BA7A32CC7D7E0B689860608762FB48219024AEC3A783471D1
875E48F40D60358124BAD30B09BF21520D20C9F5BA1F5221B3D541F21DB278A0
7FBA920678E23A99DB3B65C6DE19EF072D91E8C212E3ABFE666BB855326D96DC
D9610FC4043EE5D3F16ABF66B6A8C2136D35833A4A9906AF6B670A264DD1FD10
9B0B50B2FB906052F0CB6A24284F8A4DA79FC738FF97C4FF4BE263FA5398E371
67138EDE0536CACA5B383685220AF707BF901400162FE1220FB87A9AA9BD3B31
E4387D8AE92E9F7126B3713A819B55866C95965FEC0444A74CBFCF0C4642627B
F398056F0BB399056F0B739A0D6FE399AD492C605F52B39EE91554B910069C3A
F0335C8FD24AEE12D3FA5C9D0E71856FCC83E6955C95C50EDE469AF88471C5BD
41DCD0D20357A8F86C83B68CB1BCDB4BBEABF81CE1D75051E997E62F4A5F8365
653C677F59339AB3674A33E7B66C48ABC4E796F71F3A1C55B7FB21BA7D367D2E
BC9104A9CCCB6A9449CE3EBCE19F5465C52D58CA35A8DDBF15A13D98CCB6300D
C61D6BC1CB3D8D43D2E1EFD5BA087DB36C3B527943A51112DC8F645FF716A64D
40ECD5212C7CCF827778A1E9AD87CBEEC58CA13285BE67784E635CB6F00B3CC0
4597E840D61A67702745A32BCEFC1472A9E47F835105FCC00D6F166A8185BC81
281DC86B84ACB6CDDB174046ADF53A3BDDFF18CC1CD9D0DA9C4A067719E7BB11
93ED2E17D8CE09DDE72C82BBEFC17A008619BAFDEF8A58DEBB4AF12CF66BEE80
B7857245CDD912CE12A6BCBE6F0B2970371B72015DAEA81FD19B213464550E83
7914A2768E6DE0DD34393E6AE13BDD02DB8A8B29DE9928DDEF7FFF24D7996A1B
DFF6949DA66BA266C20B1222D2C99E7D0A488B29FACECB2EEFE98E37F08DEDD9
6523333BEBD957555627ABE83C8477316E5B60BC03A0855EA13C0275906EE8C6
350E2C5F9040359242A48A1657BF928DA4EACD8392943482C54DADD1C9769C26
1FD76524234520CFF96C20BCC42B7B7B565EC15ACE461B36A794B765EA4B369B
2B25D834753EE915FC12BD88753E12915C78AF55BAC4B8ACBE7A9DA0B8ACB863
92F9BE8C563BC23419A8B7E810DE66294A31A1283867BA00C35DB13AA47589D7
F91F56911FFD07FCE49ADD1AE8D7F334F9D026631CB3C7B6499B8E230322C224
D63CF430F6B58F5C8F85ADC266065F1BD591E2CA66B1B7DDD1A3B8160590DF82
ECAD756D4EE4D60C4254785A021FB37C51F81B23393B5D96F384E5A07956DB67
FDC07CC2A051D50C881F9342699631D915AEC2A2A7765B43462A527204AFFABC
20F691515747F508A2F124FCE375FB412C5D8FE50A458872206BC7D39B0694A9
184861281667504F3556A121D9FD28DC2BF1AFC7090FDF6F27795607A67CC3BE
7372DCDD66E3D08226BC04B661C36CA7492A877AD3358FDD057E2CB1796E2744
D6E7C6D465D81BF02D8EE2107E7E3948EDB4FB3CD4326F8AC946187A41A982E3
F2F7912856C7F5D7171CEBCA0A2BFD2538FED70B0E7346FB33CA0D1CA75B901B
516B1EC9A6B5958BBB7932633B3FC3DFA4944A6399CFF0566E1F5F193AA150A5
632B9550CF7828197605DF40DF88E227D33D353DB7373CE442FAF642DA572F24
44EF63D815ECB6D68CF079D9203CB36F830FFEBB747D659A0C31C51A5D6F77F9
E036991BFD6E957299006E20A7FD55BC54180AB33D4EB3D7E7AFBBFBA6C30FA4
0ABB60E61EFCB557DEDAA9BA6AAA249F6C4198680F89AC0DBAD7E51C116B04E4
016D15C323B9FA56A0B82EE0A1B9CCEF696EF7496DCC2A6CD82832BD4598D19F
BF342F4DF373E1AF8B64FC9C2D3851E6B87F83BDA67FDA203CD787540D553239
63E33EC24AB07EC87C019F3758E4C7A686DF67C52F950CA76157176FBB58235C
4D3A1EAF644E5728DB56C66A7B41AB8C6C59627BAB3184D4EF7FB003B75FEB52
03B856AC222674B4C1389BC957D5E4A51DD8905B60A0B6776D4D527870E16DEA
DB20201ECC5A201E1A6DB4DB40BBE2F44ED7720B7B9E6B23FACF67AC252A42B7
091FD06DA0FE8F676CCFE900910FE08B57FB7D97F0A2664FD5B658371637D416
0EE0798825110D2308A5F34E354293250249461473DFF1572E811B0C938E8FC5
1B40D27B1E5670A02A7F3BBC19ECDF2091A9DB6A892C49E310A6E5F47EA43190
ABB6316A08346ECCD6B8C5ED992AA02061BC3CE3725F0C1E66A38841DE0D0020
CC1A4CA146FD7A74BC2F43B8E220D066DDEEDA11E3C2F46386191A51B01127EA
F1850E187E3A5238A62059637E2EB863FA4C0008DB011AD3A728D399FF1247BD
3050A2226CDB909C80F9F584249409CFA746AB0C850656BE8D56B7D66CB542B5
61AB71C6F4B75A11B0766DE5AB0A9295CBDCE2E65A418515D54FDA4B0DADC6E0
5BA1DC6DD9DEF54AF02D7ABC82FBF084AF1652AEFD26937C312DF982089C2C93
F9AA6E7166AB8D2AA7DC9CB71139CDC17F165B0495F225C2F42635EF48F0B8FE
0ED371993090220CBA38B4E6C36C143918F21AC2078B194C12B665CA7B3B7C2A
FB01D7CDB00E12E816B705D7FD325515D74DCCC46B0DD3CA00A98E0DD190CF3A
C22181575B83A18FE4FD4790BCF58EB955FA362D20E6A0DAEC2D66CE629B19F9
D712385BEDBFCD6415A406FDE70B26452AA4D7DD874BB0B05B9F95DC997692CC
5A1FD255DCE23401B3CC9075DF755F5FB57FE8FE127790161EA3F88F362EF14A
426206EB27174B339768367D80D86C5CB67AE4089646112342D6802F276EA9F1
7F6FF3494A7FD011155FE3135899F87F7D7454451F4F3BF5AB187AD4158B9076
F44624892F7A23D9CC50C0D3550FFEE6CA388EFE0BFFC4E68C6184F3DEE6A094
64E44DD1D362CBC4EB32EABC88847ED99D630CDF672C7E27A13499119A51BF46
A78584379C0999B795B7574DBC18F412AA38472AABAD7A5F0BA3ADE9A67DA0C8
6B3B68D093E92EE1F97BE1701EEBD8A1FFB6C3A4771B519A0528B0F7B79DFFFB
E38E9173BFA6536AACEB9084AD573B92C6B343AAF61BF39B79CDD4E05E240B2A
0C3381FE2E1FE161E76755C1567A222D06C98C8257A89EF076C4EE381D954C67
DC0F76D92E04AF2866C920555DA6D94EBB83FBF1D945A966E2D3ABF22CB2AB67
F3025204B0949D8D019BAE5131C008731D8CC58E286D6D4500A3445F07B0D85F
AB05ACE78B75003720054C3F6B612CF7976A3A4F1FEB0A323D4663174E96B65F
A56E81D887222450C07BD1432881FB4ED7DDFF6C7266EC816C3C9C8A368E88C1
AB96081C2DF39A07A2E193FAE01E988193BF7466C67F6298144C3C1AA74FDC30
ABA4473B90A7CD2CC38875B44240081886E8AC726087AD9225229D9D7C55F162
7D6C68535054D17ECCFC5530ABB8D170972BFC830FAD326FE1DCE06417AEFFB5
6622B92CB7286B7B5275C478519F6DF7CD8E9485213DD847C9B5D3EFAA3FFB17
D95225C63989CB5FFE7FD2DB50A8E1633FA07A43F27F71CE11FC795574F3849C
C52E145ADE7AE5B055E58A0FB82503BA2A72F7065DBD01996CAF8B688C73FFAD
1B58444464EBB9027F344E6E78AEDE284BC7C30AA224199947F64152D124521D
2AC2FC6E23436757E92C29DD3E8BB70617E635D1FC199DC43B7261E2D882AD1F
683FECB027EC0F4B36FF92B78245EF7B5D76B6E564D3EE36E163A383F7ABF03E
A65D1BB2CD098B1CEE3FFC6131EF29E29F0C6EB81D98B2CDEA8E08160A546A32
AB773F0212DE20D911505BF8CBAFF08BB3D73D3EB1A4785A71769B4C1663F6E5
5113D3BBE8A1A64807D1AD698DC43298C1EE91A06A35D61166482639C4146245
5DDA115E56095BB186155D9E35EBB5359BA39A458322D62C82680FFF70DB20AD
BBA08444E3727DFAF6ABFFB91A32DDEE63CE4CEAB3BE10890C52182651C47701
34AA57292B2DC4BAB108CAF242C6DDE04C7899552E2EAA46B6D3B72599971611
F6955D09CC651CAC37541C6A99487212B6EF696ACB09C832CEE0179AB498BEE7
C8A21F15366632156F5C4C68EA74AE68BDF1DCAE14767622E93A89C066E46321
87443240D217A79B55411514C229485F8205C1D4904D6F3A6E41F12558E3D0C0
D4AE714898060AD2D6B0AF207C89145431339C8274C99448EEC208FCCCC8EE5A
2190F12558505D04EF1494D7D8070AD2FD53BE1AE94BB046E37AE80EBBDFB4AD
E1A966DC1F9E71E7F256F04378706517D2CA2494FAE22794BCEA8B686C16945F
021446B83C7FE2AF11BE846B54E59C1AE14B888B7CA3457DC197A125EE3D0488
77E6F4C88BDECD3C196620CA0E97877092A3843BEAC71F7D07563C52E36879D4
4446980B1E70CB1C26F3611B4FD0230D7A74992E0726A77F4241A933BBD7102D
2A80912D55BE7AD2448460439B888BE051E08A74902A088E091F518F9E3E7DFC
D513FEFF3E0BE61184F7ECDEBA52C5AA50900B9FE14F00822B5E22F9D6101B91
A1D6A0365B0884590547754FDE5F7CB43CE0D37176C37B8A4CEC1FE5AF016D35
487249425D8F213634FDE5B45FE19FBE5A4E44C8660B9B3F2182B6500B238852
AA97CFB39B8CE3F1940FB74932C7E76C3A4405077C3BC00AFE1CEA8B90434BC2
CF5DDB268284695162D017F91886E1E1DF8FE03F63B830C5EC56E6DD0CE50E33
7FF4E3766B6B3828DF03A1A6DB6B77081A4CC110BD41838DD8D6F8790F9AB98F
EA7D49D1348B72354ED1CE0662C4D0A9D9304BC0B0F638A490EF9C52065AE240
D41D84553C624959CEB36BB8EFED11AE0F08C1425C2D5A0975B5F3421EE7B0A2
2F1776D4C8D0A282A3714C66277C0DCD13D52032462D89B04DB3DC88938999F8
E84853B39DF8DB68E8B1994F85074F448078D1ECC7DE9B5331278515D75ABDEA
3EC39827AE4B67B16BE48DEBD12D6A884868F44C77FE86927F21A4977BD42271
65AC89A1B9A0A92C525C9ECD042FB09EB8A5020122F97E7F33587555A703D955
DAE189626714095C1AABF5025F21A14F790BD192CC251E1F1F9C5569AC6DA5DD
90FCA67CBA8765577077EEBDDF13F3D10A09788C7859440EFDC4CAC56C0C8BFA
BDA095C24B2BBC7849018B5A46A826AEEAC8CAE3861442082E418966F57F712F
A9FDC4FFDB954B59A3FD2AFAED2746411BD7A080D27EF156455E9880AE83114E
54063ABB38BB7C22130BFF578AAEBDF0C6A774A2A4239B0487C03C13741644EC
C81991CFB4112B966F1C09EB94EFD3DA6DEEA16BC39083E23F7771F0F47D1C39
CAC64802CCB156BF88C1FAB938931063BD719E0C9BE4A706484E363E6C87A985
6127C24492A2E3AC6CC967C558DBE02BBF362A933F34A9B09F115C71B736EBED
E1EFFD90E3804B02BB5A24081DF06B280B8B46A36F2B83AFD998B3567742015D
5FAC0893D49FB16D4ABB763A7D9D16E2F2CA685E7D73AA78182BCDAA70B3BF47
C8EF836B2C10BD70954D6AFD6EC545D4FD1B4DEBB570A3B19AF51B2D9681C146
23D8F51B0D284504F8069C1024CC16B8C16D846AC8FABD1744740B3DE847D41D
83887ADC8F774B5DA0074E7CDCFCC11DA0593DCEE95B247F632DF18F50120DC3
8C97205A49F4835A4716883D1A4D0AB12BDB5C004A83EE3A6228169B0996771D
EB0D5F0CA4D3E84D66C2CE439124D416A8BDEC34EE488C5D8BE8B12E599F1E58
996209331977232A4FA6637C57A87B29B85681260CEBD2167A198DE2E0ACDAD7
5BB6D7AEC05D9A88D6683A570818722BADA51766A0FBE8F06898CF6077C9BF78
2E0DB8CBA64338A3E31CE0B309472628610EDA66DA793347D3E0755A30BA168D
F32BA7E6D4B857651D7889BAEDE0FEB8D961FF772ED2A29C670315AF08BE6A42
4E922599C8CCCB16E028880670CE9B055D00E74304DD2DAE9D4CD2619694E978
55DB66153B5C41B7050859DBE8DE61CB9487264DC453381509B35ED57E677CED
B027CB2D38C04902C9D07E925E4409A475DFE275856A277C476A4548C9B3F41D
7D9A58FC3E5C1EB0FFD020DB4BF6904F298747216CC11FBA85C790C99485C7F5
EE91AAF0F0E0DD3D92473ACBBD1DD8B8D83C697872BBA2480739C403E772EA79
456C3585A74F4F2E3B951ADA4B9F74956DC157C48B66FB3CDD6E9530223798AC
60BD0958CF5CD632C34899014FEE951EB23D4157986090279F1B04D8DF00DE91
60F050CBDAABFDCA54F2D01CE6D5E3A4FF5423B65D55B68DC106B1203ABE8821
1DB64A0BB754252E8659C7FD53AB86A87513696B1322FAC0F8C9EABB76C7A4A2
43C1695E43C02A4FEAAA98B9A3571D01F17838460D0139ABC31375EA29B6EFC0
E52FBCFDA3672F376C0B53AE6D3A3252A78A8D37A64F9F3970CD3026EB403562
8CF43D1B2CA6B03266210DC91E706106F2A88EA59836C385D64EC9763539E792
866DC0847657AF6492156C060F6620F82BFB54F83C09A560DD64B799BC2C69B3
BF5979AF280922A9C057674E99BCA1152A0B9D502C5C940A89113B451709C5D5
71AB33A03C14E1C04C02EB284A5283681FCA2F6D76E40A6882C727097680CF9E
0C91C5B0A929F42BC3548F4F63C4FAEE141B593D21F8E49FA6DACA7FAFC7ECA6
4621478BAFE94BA3E94BBB5B44BD2D0F13DB048903B65BD132DB18A06BBF4299
954599558532EB4A2CB3BF6D38266EBED3539F031B0392CB2BF0CE02EECED59E
6945B5E6899BDD9DDAAD498897A98BA8D64C070B6A028E12618F19A93E1B63CE
582FF2596D96CF16B3B6200A5E77888B1DB1D65D2BB937FCA8AAE46D427C15DD
13F10BE4B15CDB76D2C8CEE44B644F29577044AE3D498AD20A6D8E8BC86BB8C6
134F47603CDB4A7050FBC09BF92974620E00DA39A55BBFE39B48A7049CD70D0E
AEA2A38CA7289D3C810398A34DC8E524E4929B74ABD72CED5C828B94B830B5C4
9B3DC78BB4607BE9321D804B181BE4C354DEA5BA5B488F64BC9CCDE104E7DAE1
3E732F1AE60A1799A4F08867462629B86ED007116BD7AE5D27F053A03674B1B2
0FD57AA25E4172A277F47B0177D68E3C3C4BB645BAAC065A277C41A0991DF8C7
292DA6A274322B57CF290C72E0D25C48C39C5D83575DC7A435DB05C6F1E6C7C1
EB8FD91B084C8CBE938BEB36D8AA343658DF5A7670AF7FE5E1D7DF3C3EFCE6E0
F1E1DF0FAB1195D5360FF9C94E93091F4B3B57C96D3E497698B0211D4A0F70F2
8395F210C36290DD25EC0F6B9C9D330F5219A750552CEAEAF004060571E21000
A52BA51E1787F2FC10753F522F99DF14FAC9EFA3DE122E750041488886102A87
ABA770B19A8B25ED1A5070644FA8996D0B1F41C55075F2C0F1A05CD2ED8DA855
CB200DEEF0924790DDFCE2627B9FCA8DAE24E4D5097DD2D7FE8AFC97F05774CA
61B00BDA46923302DEEA5DE840DFD28A4BB116F063283A919EA582A353380402
4A7E18C10914894BB871F9C231127E3C7BF84E9E9FC7EFA21FFA61E34BF043C4
594665513B6F1A0972B8896FA242125D82F79CAB28ED420AC0D13DF2076EA444
504916267535093540C590879F7541D8AB8976C5744F372A84D78F33DC15246A
C5B785CD244F19C8FADADE1350BE241DF2EBDCF11B2B6CDA6FFF89F2DB4711AA
1CF61B818AFBC9C9E44A0CDE781C8D4EBC82CD2A31195111976672397DE1BC08
F7C5BBF1E0AB69838106297C0F86998CF156CEB3E94D6CABDB4C883F283EB19D
6E33C56DB5B8F98EB3DF1A5BEF0E3E345D0AEA8B56A1CC6A0E82AB2A1D669E93
683E9C94AA639E076E52D093E2D48AF34BF0E33A11C782CA5CCBB76B6326EB22
85C08D0926C0B6DC111C32E0276144C6D28651392A44E5048DE050F2A8A73649
96BA907F0622EA516EC1546185FB26DC2F16E5CC1A8C30A7724FD3C87B9F0E73
B369425B3B8074699A26B533E7D990268B7199C9D857F0EC212D7830CBB522AE
A9AFC71F425DC03B6F96E151B40FEC30D24D38ADD00847C768526979A986446A
12016C031343DCB4005EE8D2DB056C073EE3026AEBBEF53FAD510E0E964787D5
7BF0F4EAC67714F0ABD869BF3E0BAD85CCF38336B50CEF2BE7F81B1C28350381
C88551706D66F8B619AA829AE8D5FC7F74B47C5A59D5E9A86B95806B32C9B867
06EB71D59ACD72AE56E34237C8832440A6640292AEDB38053D2734E9554C90E8
6C1D6269A31CC1FDC73FC10E968C31FE859F6F0392D746907AFC1E5842791755
FDB639BEEE38033382116E2A604DB043D6AA9EA25078B1BE2AF810E2D30B640E
37120D3C8D028C8A6B8E1001DD56D65B4CC710A0AC057352BF7A41395E6F1E97
1503AE04F011418312AC77F882EAA1906562E9175F0EC9DAC2734E1840EEBF98
D94C14D9B925884028D6A35CAF568A88D1B79C844446BC2D8CF52A2F3A54650D
2921E9827C79D5D9127521D5531852E47C58B88AB8533324C98332D21D445D6E
C0BA9084E89CA66DF2388DA38F16DB5A98B020C2CBBCD261469C6F50940CB552
4036A9F4DE8A68035152D14D70A65DD3DE6658DAEE6368A3597CAB86366578C2
6A7C8627F810303C25D3C16D2E8309852684901989E20F218488BD498C725A39
3E67142F84168582B3E9155F124C9810A4683EDAD9091DC0AA2A2E14AA62958E
C77C983E6418D302C35A043097E1B4508AB761F5C776AFCBD880AA865A3B7C82
BE7DAC073EAB078FF9FF409D1DB7F4CF593E5EDDE453F674F935579BF89F6F96
87315F76A1277DBD3CFC3A2CABAD2580B1FDCB5AECE860D972AFD753D069588A
6D675A449B0B58010BF82508433AADC7C58E2F44AA838DF457960218EE606C89
0D4381A3E1FCC0F64C6710FA00FE457B075CABB4E8B0CF0971E889FF22536399
125D064012EB8107FDFF0F1217899678240100
}

; Note that there are two menus in the following 
; block.  The "file" menu is indented and spread 
; across several lines.  The edit menu is all on
; one line.  Notice that you can place action blocks
; after each menu item, to be performed whenever the
; menu item is selected - as with the [print "You
; chose Item 1"] block below:

menu-data: [
    file: item "File" 
        menu [
            new:    item "Item 1" [print "You chose Item 1"]
            open:   item "Item 2" ; icons [1.png 2.png]
            ---
            recent: item "Look In Here..."
                menu [
                    item "WIN A PRIZE!" 
                    item "Try door number two"
                ]
            ---
            exit:   item <Ctrl-Q> "Exit"   
          ]
    edit: item "Edit" menu [item "copy" item "paste"]
] 

; Most of the style definition below is totally optional.
; It's designed to look like a native Microsoft menu.  The
; example at
; http://www.rebol.org/library/scripts/menu-system-demo.r
; contains many more examples of menu styles and options.
; The only part that's required in the example below is 
; the action block in the "item style" section.  Everything
; else serves only to adjust the cosmetic appearance of the
; menu:

winxp-menu: layout-menu/style copy menu-data xp-style: [
    menu style edge [size: 1x1 color: 178.180.191 effect: none]
        color white
        spacing 2x2 
        effect none
    item style 
        font [name: "Tahoma" size: 11 colors: reduce [
            black black silver silver]]
        colors [none 187.183.199] 
        effects none
        edge [size: 1x1 colors: reduce [none 178.180.191] 
            effects: []]    
        action [

            ; Change the lines below to fit your needs.
            ; You can use the action block of each item
            ; in the switch structure to run your own 
            ; functions.  "item/body/text" refers to the
            ; selected menu item.  This does the exact same
            ; thing as including a code block for each item
            ; in the menu definition above (i.e., you can
            ; put the [quit] block after the "exit" item 
            ; above, and it will perform the same way - 
            ; just like the "[print "You chose Item 1"]" 
            ; block after the "new" item above).

            switch/default item/body/text [
                "exit" [quit]
                "WIN A PRIZE!" [alert "You win!"]
                "Try door number two" [alert "Bad choice :("]
            ] [print item/body/text]  ; default thing to do
        ]
]

; The following function traps the GUI close event.  This
; must be included whenever the menu module is used, or a 
; portion of the application will continue to run after being
; shut down. 

evt-close: func [face event] [
    either event/type = 'close [quit] [event]
]
insert-event-func :evt-close

; And finally, here's the user interface:

window: layout [

    size  400x500

    ; The line below shows the winxp style menu:

    at 2x2 app-menu: menu-bar menu menu-data menu-style xp-style

    ; THE LINE BELOW SHOWS THE SAME MENU, WHENEVER THE BUTTON 
    ; IS CLICKED:

    at 150x200 btn "Menu Button" [
        show-menu/offset window winxp-menu
        0x1 * face/size + face/offset - 1x0
    ]
]

view center-face window

Популярный инструмент REBOL GUI под названием RebGUI (описанный в следующем разделе этого руководства) также имеет простую возможность для создания базовых меню, которые могут быть полезны.

16.21 Создание многоколоночных текстовых списков графического интерфейса пользователя (таблиц данных) с нуля

Встроенный виджет GUI "text-list" (текст-список) REBOL очень прост в использовании, но он может отображать только один столбец данных:

view layout [text-list data (system/locale/months)]

REBOL имеет встроенный виджет "list" (список) для отображения "сетки данных" с несколькими столбцами, но он немного сложнее в использовании, чем виджет текстового списка. Ранее в этом тексте модуль listview Хенрика Микаэля Кристенсена был представлен как простое решение для создания отображения сетки данных с несколькими столбцами. Он работает хорошо, но требует включения стороннего модуля. Немного знаний и практики, вы обнаружите, что встроенный виджет списка REBOL может быть очень мощным и простым в использовании. В простейшей форме виджет собственного списка принимает параметр размера и 2 дополнительных параметра блока:

list (размер) [блок макета виджета] data [блок данных для отображения]

Параметр "(размер)" представляет собой пару XxY, указывающую размер в пикселях всего виджета списка. "[блок макета виджета]" - это макет стандартных виджетов VID, используемых для отображения каждой строки данных в сетке. Элементы графического интерфейса пользователя в этом блоке дублируются для отображения каждой последовательной строки данных в сетке. Блок макета графического интерфейса пользователя обычно содержит слово "across" (поскольку эти виджеты используются для отображения строк данных) и обычно включает параметры размера для каждого виджета. Блок "данных" состоит из строк информации, отображаемых в сетке. Каждая строка данных содержится в отдельном внутреннем блоке:

view layout [
    list 220x100 [across text 100 text 100] data [
        ["row 1, column 1" "row 1, column 2"]
        ["row 2, column 1" "row 2, column 2"]
        ["row 3, column 1" "row 3, column 2"]
        ["row 4, column 1" "row 4, column 2"]
    ]
]

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

view layout [
    list 200x100 [across space 0 text red 100 text blue 100] data [
        ["row 1, column 1" "row 1, column 2"]
        ["row 2, column 1" "row 2, column 2"]
        ["row 3, column 1" "row 3, column 2"]
        ["row 4, column 1" "row 4, column 2"]
    ]
]

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

view layout [
    list 304x100 [across space 0 button 150 button 150] data [
        ["row 1, column 1" "row 1, column 2"]
        ["row 2, column 1" "row 2, column 2"]
        ["row 3, column 1" "row 3, column 2"]
        ["row 4, column 1" "row 4, column 2"]
    ]
]

ВАЖНО: Вы можете заставить виджеты в списке выполнять действия, как и в любом другом коде "view layout":

view layout [
    list 304x100 [
        across space 0 
        button 150 [alert face/text]  ; При нажатии отображается 
        button 150 [alert face/text]  ; текст, содержащийся на кнопке
    ] data [
        ["row 1, column 1" "row 1, column 2"]
        ["row 2, column 1" "row 2, column 2"]
        ["row 3, column 1" "row 3, column 2"]
        ["row 4, column 1" "row 4, column 2"]
    ]
]

Это означает, что создавать ячейки, редактируемые пользователем, очень просто - просто переназначьте текст для лица, по которому щёлкнули мышью, а затем обновите отображение:

view layout [
    list 304x92 [
        across space 0 
        btn 150 [face/text: request-text/default face/text show face]
        btn 150 [face/text: request-text/default face/text show face]
    ] data [
        ["row 1, column 1" "row 1, column 2"]
        ["row 2, column 1" "row 2, column 2"]
        ["row 3, column 1" "row 3, column 2"]
        ["row 4, column 1" "row 4, column 2"]
    ]
]

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

view gui: layout [
    list 304x84 [
        across space 0 
        text 150 [
            face/text: request-text/default face/text
            show gui focus face unfocus face
        ]
        text 150 [
            face/text: request-text/default face/text 
            show gui focus face unfocus face
        ]
    ] data [
        ["row 1, column 1" "row 1, column 2"]
        ["row 2, column 1" "row 2, column 2"]
        ["row 3, column 1" "row 3, column 2"]
        ["row 4, column 1" "row 4, column 2"]
    ]
]

Обратите внимание, что количество строк, содержащихся в блоке данных, не влияет на количество отображаемых строк. В списке всегда отображается столько строк, сколько соответствует общему размеру виджета в пикселях (мы рассмотрим эту проблему позже ...):

view layout [
    list 304x100 [across space 0 button 150 button 150] data [
        ["row 1, column 1" "row 1, column 2"]
    ]
]

view layout [
    list 304x100 [across space 0 button 150 button 150] data [
        ["row 1, column 1" "row 1, column 2"]
        ["row 2, column 1" "row 2, column 2"]
        ["row 3, column 1" "row 3, column 2"]
        ["row 4, column 1" "row 4, column 2"]
        ["row 5, column 1" "row 5, column 2"]
        ["row 6, column 1" "row 6, column 2"]
        ["row 7, column 1" "row 7, column 2"]
    ]
]

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

insert-event-func [
    either event/type = 'resize [
        li/size: gui/size - 40x40
        t1/size: t2/size: as-pair (round (li/size/1 / 2)) 19
        show li unview view gui
        none
    ][event]
]
view/options gui: layout [
    li: list 220x110 [across t1: text 100 t2: text 100] data [
        ["row 1, column 1" "row 1, column 2"]
        ["row 2, column 1" "row 2, column 2"]
        ["row 3, column 1" "row 3, column 2"]
        ["row 4, column 1" "row 4, column 2"]
    ]
] [resize]

Вот один из способов растянуть и/или сжать все ячейки, чтобы они поместились в список с изменяемым размером:

gui-size: 220x110  li-size: 100x19
gui-block: [
    li: list li-size [
        across 
        text first (li-size / 2)  ; (1/2 the width of the list widget)
        text first (li-size / 2)
    ] data [
        ["row 1, column 1" "row 1, column 2"]
        ["row 2, column 1" "row 2, column 2"]
        ["row 3, column 1" "row 3, column 2"]
        ["row 4, column 1" "row 4, column 2"]
    ]
]
insert-event-func [
    either event/type = 'resize [
        li-size: gui/size - 40x40
        unview
        view/options gui: layout gui-block [resize]
        none
    ][event]
]
view/options gui: layout gui-block [resize]

Конечно, вы можете назначить метку любому правильно отформатированному блоку данных и отобразить его позже в виджете списка:

x: [
    ["row 1, column 1" "row 1, column 2"]
    ["row 2, column 1" "row 2, column 2"]
    ["row 3, column 1" "row 3, column 2"]
    ["row 4, column 1" "row 4, column 2"]
]

view layout [list 220x100 [across text 100 text 100] data x]

Это позволяет очень легко создавать и отображать списки из нескольких столбцов:

x: copy []
for i 1 12 1 [
    some-info: copy []
    append some-info (pick system/locale/months i)
    append some-info (pick system/locale/days i)
    append/only x some-info
]

view layout [list 220x240 [across text 100 text 100] data x]

Вот версия сценария выше с изменяемым размером, в котором пользовательское редактирование разрешено только для первого столбца:

x: copy []
for i 1 12 1 [
    some-info: copy []
    append some-info (pick system/locale/months i)
    append some-info (pick system/locale/days i)
    append/only x some-info
]
gui-size: 220x110  li-size: 100x19
gui-block: [
    li: list li-size [
        across 
        text first (li-size / 2) [
            face/text: request-text/default face/text  ; enable user edit
            show face focus face unfocus face
        ]
        text first (li-size / 2) 
    ] data x
]
insert-event-func [
    either event/type = 'resize [
        li-size: gui/size - 40x40
        unview
        view/options gui: layout gui-block [resize]
        none
    ][event]
]
view/options gui: layout gui-block [resize]

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

editor second second get in (list-widget-label) 'subfunc

Например:

view layout [
    the-list: list 304x100 [
        across space 0 
        info 150 [face/text: request-text/default face/text show face]
        info 150 [face/text: request-text/default face/text show face]
    ] data [
        ["row 1, column 1" "row 1, column 2"]
        ["row 2, column 1" "row 2, column 2"]
        ["row 3, column 1" "row 3, column 2"]
        ["row 4, column 1" "row 4, column 2"]
    ]
    btn "Display Current Data" [
        editor second second get in the-list 'subfunc
    ]
]

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

x: copy [
    ["row 1, column 1" "row 1, column 2"]
    ["row 2, column 1" "row 2, column 2"]
    ["row 3, column 1" "row 3, column 2"]
    ["row 4, column 1" "row 4, column 2"]
]
do qq: [view layout [
    the-list: list 304x100 [
        across space 0 
        info 150 [face/text: request-text/default face/text show face]
        info 150 [face/text: request-text/default face/text show face]
    ] data x
    across
    btn "Save" [
        save to-file request-file/save
            second second get in the-list 'subfunc
        show the-list
    ]
    btn "Load" [
        x: copy load to-file request-file
        unview do qq
    ]
]]

16.21.1 "Supply" функция

Для обеспечения более гибкого отображения списков блок "данных" можно заменить функцией "supply". "Supply" работает так же, как цикл "for", который выполняет итерацию по каждой строке виджетов в отображаемом списке графического интерфейса. Функция "supply" автоматически создаёт 2 новые переменные, которые автоматически увеличиваются каждый раз по строкам в списке:

  1. "count": текущая СТРОКА в списке
  2. "index": текущая КОЛОНКА в текущей строке

Вы можете использовать переменные "count" и "index" для выбора последовательных значений из блока данных с помощью функции "pick" (точно так же, как в цикле "for"). Обычно это используется для установки свойства "/text" каждого виджета в каждой строке.

В следующем примере каждая строка в списке содержит один текстовый виджет. Функция снабжения проходит через каждую строку и устанавливает текстовое свойство лица виджета как один элемент из блока "x" (список месяцев). Цикл автоматически увеличивает переменную "count" для отображения каждого месяца.

x: copy system/locale/months

view layout [ 
    list 200x300 [text 200] supply [
        face/text: pick x count
    ]
]

В этом примере просматривается список файлов, прочитанных из текущего каталога:

x: read %.

view layout [
    list 200x400 [text 200] supply [face/text: pick x count]
]

Вы можете использовать переменную "count" для изменения свойств каждой грани виджета. В этом примере изменяется свойство цвета альтернативных строк (один цвет назначается чётным счётным строкам, другой - нечётным строкам):

x: read %.

view layout [
    list 200x400 [text 200] supply [
       either even? count [face/color: white][face/color: tan]
       face/text: pick x count
    ]
]

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

x: read %.

view layout [
    list 200x400 [
        text 200 [editor to-file face/text]
    ] supply [
        face/text: pick x count
    ]
]

Вы можете использовать переменную "count" в функции снабжения для создания списков с несколькими столбцами из 2 или более отдельных блоков данных (сетки с несколькими столбцами - это весь смысл обучения использованию виджета списка):

x: copy system/locale/months
y: copy system/locale/days

view layout [
    list 250x400 [across t1: text 50 t2: text 100 t3: text 100] supply [
        t1/text: count
        t2/text: pick x count
        t3/text: pick y count
    ]
]

В следующем примере используются переменные "count" и "index" для циклического прохождения блока с двумя столбцами данных. Понимание этого формата является основой для всех самых сложных макетов списков, которые вам понадобятся. Обратите особое внимание на первую строку в блоке "supply". После того, как все данные из блока "x" пройдены по циклу, если в отображении списка появятся другие строки, значение индекса выйдет за пределы длины блока данных и вызовет ошибку. Чтобы избежать этого, вы просто проверяете, является ли выбранное значение "none", и применяете значение "none" к face/text, а затем выходите из цикла:

x: copy []
for i 1 12 1 [
    append/only x reduce [
        pick system/locale/months i
        pick system/locale/days i 
    ]
]

view layout [
    list 400x400 [across text 200 text 200] supply [
        if none? q: pick x count [face/text: none exit]
        face/text: pick q index
    ]
]

Чтобы прояснить вышеуказанный формат, вот тот же пример с добавленной третьей строкой:

x: copy []
for i 1 12 1 [
    append/only x reduce [
        i
        pick system/locale/months i
        pick system/locale/days i 
    ]
]

view layout [
    list 250x300 [
        across 
        text 50 
        text 100
        text 100
    ] supply [
        if none? q: pick x count [face/text: none exit]
        face/text: pick q index
    ]
]

Вот пример из предыдущего примера чтения каталога, но с двумя отображаемыми столбцами данных (имя файла и размер). Щелчок по именам файлов по-прежнему вызывает редактор:

y: read %.
x: copy []
foreach i y [append/only x reduce [i (size? to-file i)]]

view layout [
    list 300x400 [
        across
        text 200 [editor to-file face/text]
        text 100
    ] supply [
        if none? q: pick x count [face/text: none exit]
        face/text: pick q index
    ]
]

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

x: copy []  for i 1 397 1 [append x i]

slider-pos: 0
view layout [
    across
    the-list: list 240x400 [text 200] supply [
        count: count + slider-pos
        face/text: pick x count
    ]
    slider 16x400 [
        slider-pos: (length? x) * value
        show the-list
    ]
]

Вот метод ползунка выше, применённый к предыдущему примеру чтения каталога:

x: read %.

slider-pos: 0
view layout [
    across
    the-list: list 300x400 [
        text 200 [editor to-file face/text]
    ] supply [
        count: count + slider-pos
        face/text: pick x count
    ]
    slider 16x400 [
        slider-pos: (length? x) * value
        show the-list
    ]
]

Вот 2-х колоночная версия скрипта чтения каталога с прикреплённым слайдером. Имейте в виду, что нажатие на любое имя файла по-прежнему читает и редактирует этот файл:

y: read %.
x: copy []
foreach i y [append/only x reduce [i (size? to-file i)]]

slider-pos: 0
view layout [
    across
    the-list: list 300x400 [
        across
        text 200 [editor to-file face/text]
        text 100
    ] supply [
        count: count + slider-pos
        if none? q: pick x count [face/text: none exit]
        face/text: pick q index
    ]
    slider 16x400 [
        slider-pos: (length? x) * value
        show the-list
    ]
]

Вот ещё одно уточнение приведённого выше сценария с добавлением третьего столбца. Внешний вид этого дисплея изменяется путём добавления строки между каждой строкой (линия рисуется с помощью виджета-коробки) и путём изменения цвета и шрифта текста:

y: read %.
c: 0
x: copy []
foreach i y [append/only x reduce [(c: c + 1) i (size? to-file i)]]

slider-pos: 0
view layout [
    across space 0
    the-list: list 400x400 [
        across 0 space 0x0
        text 50 purple
        text 250 bold [editor read to-file face/text]
        text 100 red italic
        return box green 400x1
    ] supply [
        count: count + slider-pos
        if none? q: pick x count [face/text: none exit]
        face/text: pick q index
    ]
    slider 16x400 [
        slider-pos: (length? x) * value
        show the-list
    ]
]

В этом примере Карла Сассенрата демонстрируется базовый четырёхколоночный дисплей:

REBOL [] 

db: [
    [ "000" "Ian Fleming" "ian" 31-Dec-2003 ]
    [ "007" "James Bond" "jb" 1-Jan-2004 ]
    [ "001" "M" "m" 2-Jan-2004 ]
    [ "ABC" "Miss Moneypenny" "missm" 3-Jan-2004]
    [ "008" "Pierce Brosnan" "pb" 4-Jan-2004 ]
    [ "009" "George Lazenby" "gl" 5-Jan-2004 ]
    [ "010" "Roger Moore" "rm" 6-Jan-2004 ]
]
sld-cnt: 0
view lst1: layout [across space 0x0 
    style text text [alert form face/user-data] 
    list 406x100 [
        across space 0x0 text 36 text 100 text 120 text 150
    ] supply [
        face/text: none face/user-data: none
        count: count + sld-cnt
        record: pick db count
        if not record [exit]
        n: pick [1 2 3 4] index
        face/text: pick record n
        face/user-data: record
    ]
    scl1: scroller 16x100 [
        value: to-integer value * length? db
        if value <> sld-cnt [sld-cnt: value show lst1]
    ]
]

В следующем примере показано, как разрешить пользователям добавлять и удалять данные из отображения списка. Обратите внимание, что после корректировки содержимого исходного блока данных и последующего показа списка ("show") отображаемая сетка автоматически обновляется новыми данными:

x: copy []
for i 1 10 1 [
    append/only x reduce [form random 1000 form random 1000]
]

slider-pos: 0
view layout [
    across
    the-list: list 220x240 [across text 100 text 100] supply [
        count: count + slider-pos
        if none? q: pick x count [face/text: none exit]
        face/text: pick q index
    ]
    slider 16x240 [
        slider-pos: (length? x) * value
        show the-list
    ]
    return
    btn "Remove" [remove head x  show the-list
    ]
    btn "Add" [
        insert/only head x reduce [form random 1000 form random 1000]
        show the-list
    ]
]

Чтобы сохранить отредактированное пользователем содержимое списка графического интерфейса пользователя, созданного с помощью функции "Supply", вам необходимо использовать следующий код "set-it" при итерации по функции Supply с "count" и "index" ("second second get in (list-widget-label) 'subfunc" трюк работает только для списков, созданных с помощью функции "data"):

x: copy [
    ["row 1, column 1" "row 1, column 2"]
    ["row 2, column 1" "row 2, column 2"]
    ["row 3, column 1" "row 3, column 2"]
    ["row 4, column 1" "row 4, column 2"]
]
do qq: [view gui: layout [
    the-list: list 304x100 [
        across space 0 
        info 150 [face/text: request-text/default face/text show gui]
        info 150 [face/text: request-text/default face/text show gui]
    ] supply [
        either count > length? x [face/text: "" face/image: none] [
            the-list/set-it face x index count
        ]
    ]
    across
    btn "Save" [
        save to-file request-file/save x
    ]
    btn "Load" [
        x: copy load to-file request-file
        unview do qq
    ]
]]

В этом примере столбцы можно отсортировать, щёлкнув заголовки. Отдельные значения в любой позиции столбца/строки могут редактироваться пользователем (просто щёлкните текущее значение). Целые строки могут быть добавлены, удалены или перемещены в/из выбранных пользователем позиций. Блок данных может быть сохранен или загружен в/из файла(ов). Прокрутка может выполняться с помощью мыши, клавиш со стрелками или клавиш перехода вверх/вниз. Также демонстрируются несколько концепций изменения размера:

REBOL [title: "List Widget Example"]

x: copy []   random/seed now/time   ; generate 5000 rows of random data:
repeat i 5000 [
    append/only x reduce [random "asdfqwertyiop" form random 1000 form i]
]  y: copy x
Alert help-txt: {Be sure to try the following features:  1) Resize the GUI
    window to see the list automatically adjust to fit  2) Click column
    headers to sort by field  3) Use the arrow keys and page-up/page-down
    keys to scroll  4) Use the Insert, Delete and "M" keys to add, remove
    and move rows (by default, at the currently highlighted row)  5) Click
    the small "r" header button in the top right corner to reset the list
    back to its original values  6) Click any individual data cell to edit
    the selected value.}
sort-column: func [field] [
    either sort-order: not sort-order [
        sort/compare x func [a b] [(at a field) > (at b field)]
    ] [
        sort/compare x func [a b] [(at a field) < (at b field)]
    ]  
    show li
]
key-scroll: func [scroll-amount] [
    s-pos: s-pos + scroll-amount
    if s-pos > (length? x) [s-pos: length? x]
    if s-pos < 0 [s-pos: 0]
    sl/data: s-pos / (length? x)  
    show li  show sl
]
resize-grid: func [percentage] [
    gui-size: system/view/screen-face/pane/1/size ; - 10x0
    list-size/1: list-size/1 * percentage
    list-size/2: gui-size/2 - 95
    t-size: round (list-size/1 / 3)
    sl-size: as-pair 16 list-size/2
    unview/only gui view/options center-face layout gui-block [resize]
]
resize-fit: does [
    gui-size: system/view/screen-face/pane/1/size
    resize-grid (gui-size/1 / list-size/1 - .1)
]
insert-event-func [either event/type = 'resize [resize-fit none] [event]]
gui-size: system/view/screen-face/size - 0x50
list-size: gui-size - 60x95
sl-size: as-pair 16 list-size/2
t-size: round (list-size/1 / 3)
s-pos: 0  sort-order: true  ovr-cnt: none  svv/vid-face/color: white
view/options center-face gui: layout gui-block: [
    size gui-size  across
    btn "Smaller" [resize-grid .75]
    btn "Bigger" [resize-grid 1.3333]
    btn "Fit" [resize-fit]
    btn #"^~" "Remove" [attempt [
        indx: to-integer request-text/title/default "Row to remove:" 
            form to-integer ovr-cnt
        if indx = 0 [return]
        if true <> request rejoin ["Remove: " pick x indx "?"] [return]
        remove (at x indx)  show li
    ]]
    insert-btn: btn "Add" [attempt [
        indx: to-integer request-text/title/default "Add values at row #:"
            form to-integer ovr-cnt
        if indx = 0 [return]
        new-values: reduce [
            request-text request-text (form ((length? x) + 1))
        ]
        insert/only (at x indx) new-values  show li
    ]]
    btn #"m" "Move" [
        old-indx: to-integer request-text/title/default "Move from row #:"
            form to-integer ovr-cnt
        new-indx: to-integer request-text/title "Move to row #:"
        if ((new-indx = 0) or (old-indx = 0)) [return]
        if true <> request rejoin ["Move: " pick x old-indx "?"] [return]
        move/to (at x old-indx) new-indx  show li
    ]
    btn "Save" [save to-file request-file/save x]
    btn "Load" [y: copy x: copy load request-file/only  show li]
    btn "Read Me" [alert help-txt]
    btn "View Data" [editor x]
    return  space 0x0
    style header button as-pair t-size 20 black white bold
    header "Random Text" [sort-column 1]
    header "Random Number" [sort-column 2] 
    header "Unique Key" [sort-column 3]
    button black "r" 17x20 [if true = request "Reset?"[x: copy y show li]]
    return
    li: list list-size [
        style cell text t-size feel [
            over: func [f o] [
                if (o and (ovr-cnt <> f/data)) [ovr-cnt: f/data show li]
            ]
            engage: func [f a e] [
                if a = 'up [
                    f/text: request-text/default f/text show li
                ]
            ]
        ]             
        across  space 0x0
        col1: cell blue
        col2: cell
        col3: cell red
    ] supply [
        either even? count [face/color: white] [face/color: 240.240.255]
        count: count + s-pos
        if none? q: pick x count [face/text: copy "" exit]
        if ovr-cnt = count [face/color: 200.200.255]
        face/data: count
        face/text: pick q index
    ]
    sl: scroller sl-size [s-pos: (length? x) * value  show li]
    key keycode [up] [key-scroll -1]
    key keycode [down] [key-scroll 1]
    key keycode [page-up] [key-scroll -20]
    key keycode [page-down] [key-scroll 20]
    key keycode [insert] [do-face insert-btn 1]
] [resize]

Обязательно посетите http://www.rebol.org/view-script.r?script=list-supply-how-to.r, http://www.rebol.org/view-script.r?script=vid-usage.r, http://www.rebol.org/view-script.r?script=list-scroll-demo.r и http://www.pat665.free.fr/gtk/rebol-view.html#sect19 для получения дополнительной информации о списках.

16.21.2 Создание самодельных многоколоночных сеток данных

Как оказалось, на самом деле может быть проще и универсальнее развернуть свои собственные сетки данных с использованием собственных компонентов VID, чем использовать виджет "list". Следующие ниже примеры основаны на концепции http://www.rebol.org/view-script.r?script=presenting-text-in-columns.r. В каждом примере цикл forskip используется для построения визуальной сетки виджетов GUI. Цикл вставляет отдельные текстовые элементы из блока данных на лицо каждого виджета. Для больших списков этот пример выполняется медленно, но он может быть полезен для создания небольших дисплеев.

В первом примере создаётся случайный блок из двух столбцов данных с меткой "x". Затем цикл forskip используется для сборки блока макета из виджетов полей, при этом каждая строка полей содержит 2 последовательных текстовых элемента из блока данных. Затем этот блок графического интерфейса отображается на панели внутри виджета "field", который сам отображается внутри макета главного окна. Добавлен виджет скроллера для прокрутки видимой части макета сетки. Это достигается путём регулировки смещения панели, которая содержит всю компоновку виджетов поля. ВАЖНО: обратите внимание, что каждая ячейка в этой сетке доступна для редактирования пользователем (просто потому, что каждая ячейка отображается с использованием стандартного виджета VID "field"). Также обратите внимание, что данные преобразуются в строку с помощью функции "form", потому что поля могут отображать только текст.

x: copy [] for i 1 179 1 [append x reduce [i random "abcd"]]

grid: copy [across space 0]  ; the GUI block containing the grid of fields
forskip x 2 [append grid compose [field (form x/1)field (form x/2)return]]
view center-face layout [across
    g: box 400x200 with [pane: layout/tight grid pane/offset: 0x0]
    scroller [g/pane/offset/y: g/size/y - g/pane/size/y * value show g]
]

В следующем примере показано, как взять два столбца данных (блоков) и объединить их в один блок, который можно отобразить с помощью макета выше. Во-первых, размер самого длинного блока определяется с помощью функции "max", и запускается цикл for для добавления последовательных элементов из каждого из исходных блоков группами по 2 в целевой блок. Если в каком-либо столбце заканчиваются данные, к остальной части целевого блока добавляются пустые строки в качестве заполнителей столбца.

x: copy [] 
block1: copy system/locale/months  block2: copy system/locale/days
for i 1 (max length? block1 length? block2) 1 [
    append x either g: pick block1 i [g] [""]
    append x either g: pick block2 i [g] [""]
]

grid: copy [across space 0]
forskip x 2 [append grid compose [field (form x/1)field (form x/2)return]]
view center-face layout [across
    g: box 400x200 with [pane: layout/tight grid pane/offset: 0x0]
    scroller [g/pane/offset/y: g/size/y - g/pane/size/y * value show g]
]

В следующем примере показано, как изменить внешний вид макета сетки и как получить блок, содержащий все данные, отображаемые в сетке, включая изменения, внесённые пользователем. Чтобы наглядно разделить данные строк, каждой строке в сетке назначается чередующийся цвет. Это обрабатывается с помощью функции "remainder" (остаток) для проверки чётных пронумерованных строк. Для каждых 4 частей текста в блоке данных (каждые 2 отображаемых строки) устанавливается белый цвет. В противном случае - пшеничный. Самая важная часть этого примера - строка, которая собирает все данные, содержащиеся в каждой грани отображаемой сетки, и строит блок ("q") для их хранения.

x: copy [] for i 1 179 1 [append x reduce [i random "abcd"]]

grid: copy [origin 0x0 across space 0x0]
forskip x 2 [
    color: either (remainder ((index? x) - 1) 4) = 0 [white][wheat]
    append grid compose [
        field 180 (form x/1) (color) edge none
        field 180 (form x/2) (color) edge none return
    ]
]
view center-face layout [
    across space 0  
    g: box 360x200 with [pane: layout grid pane/offset: 0x0]
    scroller[g/pane/offset/y: g/size/y - g/pane/size/y * value / 2 show g]
    return box 1x10 return  ; just a spacer
    btn "Get Data Block (INCLUDING USER EDITS)" [
        q: copy [] foreach face g/pane/pane [append q face/text] editor q
    ]
]

В следующем примере демонстрируется ряд функций, которые действительно делают сетку гибкой и полезной для ввода, редактирования и хранения столбцов данных. Во-первых, внешний вид корректируется путём изменения краёв каждого стиля поля. Чтобы включить все новые функции, создаётся функция "update" для запуска строки кода из предыдущего примера, которая создаёт блок данных "q" из текста, отображаемого в каждой ячейке сетки. В каждом случае данные собираются и сохраняются в переменной "q", и желаемая операция выполняется с этим блоком (добавление и удаление строк или данных, извлечение вертикальных столбцов данных, сохранение и загрузка данных в/из файлов на жёсткий диск и т.д.). После того, как блок данных был изменён операцией, весь макет не просматривается и перестраивается с использованием новых данных (то есть данные "q" переназначаются начальному блоку "x"). Код запускается повторно, помечая весь сценарий как "qq" и используя функцию "do" для его повторной оценки. Последняя кнопка демонстрирует, как собирать и отображать историю изменений, внесённых пользователем.

x: copy [] for i 1 179 1 [append x reduce [i random "abcd"]]

update: does [q: copy [] foreach face g/pane/pane [append q face/text]]
do qq: [grid: copy [across space 0]
forskip x 2 [append grid compose [
    field (form x/1) 40 edge none 
    field (form x/2) 260 edge [size: 1x1] return
]]
view center-face gui: layout [across space 0
    g: box 300x290 with [pane: layout/tight grid pane/offset: 0x0]
    slider 16x290 [
        g/pane/offset/y: g/size/y - g/pane/size/y * value show g
    ]
    return btn "Add" [
        row: (to-integer request-text/title "Insert at Row #:") * 2 - 1
        update insert at q row ["" ""] x: copy q unview do qq
    ]
    btn "Remove" [
        row: (to-integer request-text/title "Row # to delete:") * 2 - 1
        update remove/part (at q row) 2 x: copy q unview do qq
    ]
    btn "Col 1" [update editor extract q 2]
    btn "Col 2" [update editor extract/index q 2 2]
    btn "Save" [update save to-file request-file/save q]
    btn "Load" [x: load to-file request-file do qq]
    btn "History" [
        m: copy "ITEMS YOU'VE EDITED:^/^/" update for i 1 (length? q) 1 [
            if (to-string pick x i) <> (to-string pick q i) [
                append m rejoin [pick x i " " pick q i newline]
            ]
        ] editor m 
    ]
]]

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

x: copy [] for i 1 99 1 [append x reduce [i random 99x99 random "abcd"]]

grid: copy [origin 0x0 across space 0x0]
forskip x 3 [
    append grid compose [
        b: box 520x26 either (remainder((index? x)- 1)6)= 0 [white][beige]
        origin b/offset
        text bold 180 (form x/1)
        text 120 center blue (form x/2) [alert face/text]
        text 180 right purple (form x/3) [face/text: request-text] return
        box 520x1 green return
    ]
]
view center-face layout [
    across space 0  
    g: box 520x290 with [pane: layout grid pane/offset: 0x0]
    scroller 16x290 [
        g/pane/offset/y: g/size/y - g/pane/size/y * value / 2 show g
    ]
    return box 1x10 return  ; just a spacer
    btn "Get Data Block" [
        q: copy [] 
        foreach face g/pane/pane [
            if face/style = 'text [append q face/text]
        ]
        editor q
    ]
]

Эти примеры полезны для списков, содержащих ~ 1000 или меньше строк данных. Для дисплеев с сеткой большего размера следует использовать одну из других опций просмотра списка REBOL.

16.22 RebGUI

Диалект VID REBOL ("view layout []") - одна из самых привлекательных особенностей языка. Возможность создавать окна графического интерфейса пользователя в нескольких операционных системах всего с одной строкой кода практична для создания самых разных приложений. "RebGUI" - это сторонний набор инструментов графического интерфейса, созданный на REBOL/View, который копирует многие из основных компонентов в VID, и обновляет/добавляет к концепции многие дополнительные функции:

  1. Современный вид.
  2. Множество мощных и полезных новых виджетов и встроенных функций: таблицы изменяемого размера (сетки данных) с автоматической сортировкой столбцов, деревья, меню, панели вкладок и прокрутки, групповые поля, панели инструментов, электронные таблицы, круговые диаграммы и виджеты чата, новые запросчики, встроенные функции отмены/повтора, проверки орфографии и перевода (со многими языковыми словарями) для текстовых виджетов и т.д.
  3. Простой и элегантный синтаксис (похожий на VID).
  4. Полная документация и демонстрационный код для всех виджетов.
  5. Супер простая нотация для автоматического выравнивания и компоновки виджетов в окнах с изменённым размером.
  6. Файл конфигурации для простого управления пользовательскими настройками для глобальных размеров пользовательского интерфейса, цветов, поведения и эффектов всех виджетов. Также предоставляется встроенный собственный запросчик для настройки всех этих параметров.
  7. Автоматическая обработка событий закрытия окна.
  8. Назначаемые пользователем действия функциональных клавиш.
  9. Простое автоматическое управление несколькими языками пользователя.
  10. Хорошо продуманная структура объектов для доступа к каждому виджету, функции и возможности (и содержащая всю необходимую справочную информацию, встроенная).
  11. Вся система сжимается до чуть более 30 КБ.

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

RebGUI доступен по адресу http://www.dobeash.com/download.html, а несколько руководств доступны по адресу http://www.dobeash.com/rebgui.html. Зеркало необходимых файлов в версии 117 доступно по адресу http://musiclessonz.com/rebol_tutorial/rebgui.zip. Вы также можете загрузить RebGUI прямо в REBOL, используя встроенный Viewtop. Чтобы открыть Viewtop, введите "desktop" в интерпретатор REBOL, затем щёлкните REBOL -> Demos -> RebGUI. Будет загружен основной включаемый файл "rebgui.r" вместе со справочной программой "RebDoc.r", демонстрационной программой "tour.r" и некоторыми вспомогательными графическими изображениями. Загруженный пакет автоматически запустит "tour.r", который демонстрирует многие функции RebGUI. Обязательно нажмите кнопку "RebDOC", чтобы просмотреть всю документацию, необходимую для использования RebGUI.

Всё, что вам нужно для использования RebGUI, - это %rebgui.r. Скопируйте его в доступную папку и включите строку "do %rebgui.r" (с её путём, если необходимо), после чего вы сможете использовать все встроенные виджеты и функции в RebGUI. Быстрый и грязный способ сделать это в Windows - запустить функцию "request-file" (запрос-файл) в REBOL, затем щёлкнуть Public -> www.Dobeash.com -> RebGUI, щёлкнуть правой кнопкой мыши rebgui.r и вставить его в папку вашего выбор. Вы также можете использовать следующий скрипт, чтобы скопировать его в любую папку:

write to-file request-file/file/title/save %rebgui.r "Save As:" {
    } read view-root/public/www.dobeash.com/RebGUI/rebgui.r

Если вы собираетесь использовать RebGUI регулярно, рекомендуется скопировать его прямо в ваш основной установочный каталог REBOL (папка по умолчанию - c:\program files\rebol\view).

Чтобы создать свой первый интерфейс RebGUI, после запуска демонстрации RebGUI попробуйте следующий код:

do view-root/public/www.dobeash.com/RebGUI/rebgui.r
display "Test" [button "Click Me" [alert "Clicked"]]
do-events

Обратите внимание, что "view layout" был заменён на "display". Эта функция всегда требует текста заголовка. Также обратите внимание, что "do-events" должны быть включены после кода RebGUI, чтобы активировать графический интерфейс.

После того, как вы включили %rebgui.r, вы можете попробовать любые встроенные виджеты и функции:

display "" [area] do-events ; "area" это виджет

Обратите внимание, что виджет "area" выше имеет встроенные функции отмены/повтора с использованием [CTRL]-Z и [CTRL]-Y (собственный "view layout [area]" REBOL не имеет возможности отмены/повтора). Встроенная проверка орфографии также может быть активирована с помощью [CTRL]-S! Чтобы использовать проверку орфографии, вам необходимо загрузить словарь с http://www.dobeash.com/RebGUI/dictionary и распаковать его в %view-root/public/www.dobeash.com/RebGUI/dictionary/ (или в подкаталог /dictionary того места, где находится rebgui.r).

Взгляните на несколько других замечательных виджетов, встроенных в RebGUI:

do %rebgui.r  ; обязательно укажите путь, если необходимо

display "Pie Chart" [pie-chart data ["VID" yellow 19 "RebGUI" red 81]]
do-events

display "Spreadsheet" [
    sheet options [size 7x7] data [a1 "very " a2 "cool" a3 "=join a1 a2"]
]
do-events

display "Chat" [
    chat data ["Nick" blue "I like RebGUI" yellow 20-sep-2009/1:00]]
do-events

display/maximize "Menu" [
    menu data [
        "File" [
             "Open" [request-file]
             "Save" [request-file]
         ] 
        "About" ["Info" [alert "RebGUI is great!"]]
    ]
]
do-events

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

Уточнение "/close" функции "display" позволяет вам установить любое действие(я), которое вы хотите запустить при закрытии окна GUI. Это может помочь избежать потери данных из-за случайного закрытия окна и обеспечивает способ автоматической обработки данных или запуска других приложений при закрытии окна:

display/close "" [area] [question "Really Close?"] do-events

Обязательно попробуйте функцию запросчика "request-ui". Он позволяет легко настраивать глобальные настройки для общего внешнего вида макетов, созданных с помощью RebGUI на вашем компьютере. Настройки сохраняются в файле %ui.dat в текущем рабочем каталоге.

request-ui

RebGUI включает в себя множество "директив диапазона" для простой автоматизации изменения размера виджетов:

Эти директивы автоматически устанавливают начальный размер виджета:

#L - выровнять правый край виджета по смежному краю
#V - выровнять базовый край виджета по смежному краю
#O - выровнять левый край виджета по смежному краю

("смежный край" - это край соседнего виджета или край
графический интерфейс, если рядом нет виджета.)

Эти директивы автоматически регулируют размер и положение
виджет при изменении размера графического интерфейса:

#H - растянуть или сжать виджет по высоте окна
#W - растянуть или сжать виджет по ширине окна
#X x - переместить виджет на x количество пикселей вправо
#Y y - переместить виджет на y количество пикселей вниз

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

display "" [area #HW] do-events

Вот полнофункциональное приложение текстового редактора с изменяемым размером со встроенными функциями отмены/повтора, проверки орфографии и обработки событий закрытия:

do %rebgui.r
display/maximize/close "Text Editor" [
    menu #LHW data [
        "File" [
             "Open" [x/text: read to-file request-file show x]
             "Save" [write to-file request-file/save x/text]
         ] 
    ] return
    x: area #LHW
] [question "Really Close?"] do-events

Большие возможности программы для небольшого кода!

Этот пример демонстрирует, как использовать панели вкладок и различные полезные методы:

display "Tab Panel" [
    main-screen: tab-panel data [
        "Spreadsheet" [
            x: sheet data [
                A1 32 A2 12 A3 "=a1 + a2" A4 "=1.06 * to decimal! a3"
            ]
            a: area
            reverse
            button -1 " Show Data " [x/save-data set-text a x/data]
            button -1 " Quit! " [quit]
        ]
        "VID style"  [
            style -1 data [text bold "Back to Spreadsheet" [
                main-screen/select-tab 1
            ]]
        ]
        action [wait .2 face/color: 230.230.230]  "Text" [
            text "Tabs are a great way to maximize screen real estate."
        ]
        action [wait .2 set-focus z]  "Fields" [
            y: field
            z: field "Edit me"
        ]
    ]
] 
do-events

Чтобы по-настоящему познакомиться с RebGUI, изучите его основной объект ctx-rebgui:

? ctx-rebgui

Объект "ctx-rebgui" настраивается так же, как встроенный в REBOL объект "system/view/vid". Вы можете изучить его, используя обозначение пути. Обратите внимание, что встроенная справка включена в "подсказку" каждого виджета:

? ctx-rebgui/widgets/tree/tip

Вот быстрый и простой способ просмотреть справку по всем виджетам RebGUI:

foreach i (find first ctx-rebgui/widgets 'anim) [
    do compose/deep [print rejoin[i" - "(ctx-rebgui/widgets/(i)/tip)"^/"]]
]

Обязательно прочтите основное руководство пользователя RebGUI по адресу http://www.dobeash.com/RebGUI/user-guide.html и книгу рецептов по адресу http://www.dobeash.com/RebGUI/cookbook.html. Затем прочтите всю информацию в RebDoc.r, изучите код в tour.r и познакомьтесь с ctx-rebgui. Скорее всего, вы обнаружите, что RebGUI - лучший выбор для макета графического интерфейса во многих ситуациях.

16.23 Приложения RebGUI - электронная таблица, Rolodex, менеджер, редактор, POS-система

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

do %rebgui.r
display "Spreadsheet" [
    x: sheet options [size 3x3 widths [8 8 10]] data [
        A1 32 A2 12 A3 "=a1 + a2" A4 "=1.06 * to-integer a3"
    ]
    return 
    button "Save" [
        x/save-data
        save to-file request-file x/data
    ]
    button "Load" [
        x/load-data load to-file request-file
    ]
    button "View" [
        x/save-data
        alert form x/data
    ]
    button "Print" [
        save/png %sheet.png to image! x
        browse %sheet.png  ; or call %sheet.png
    ]
] 
do-events

Пример ниже аналогичен программе "База данных деталей", представленной ранее, с использованием RebGUI вместо VID. Он должен дать ряд практических сведений о том, как использовать RebGUI. Обратите внимание, что размер графического интерфейса можно изменять, текстовые поля имеют возможности отмены/повтора и проверки орфографии, запросчики являются модальными и доступны все другие функции RebGUI:

REBOL [title "RebGUI Card File"]

do load-thru http://re-bol.com/rebgui.r

write/append %data.txt ""
database: load %data.txt

display "RebGUI Card File" [
    text 20 "Select:"
    names: drop-list #LW data (sort extract copy database 4) [
        marker: find database pick names/data names/picked
        set-text n copy first marker
        set-text a copy second marker
        set-text p copy third marker
        set-text o copy fourth marker
    ]
    after 2
    text 20 "Name:"        n: field #LW ""
    text 20 "Address:"     a: field #LW ""
    text 20 "Phone:"       p: field #LW ""
    after 1 text "Notes:"  o: area #LW ""
    after 3
    button -1 "Save" [
        if (n/text = "") [alert "You must enter a name." return]
        if find (sort extract copy database 4) copy n/text [
            either true = question "Overwrite existing record?" [
               remove/part (find database n/text) 4
            ] [return]
        ]
        database: repend database [
            copy n/text copy a/text copy p/text copy o/text
        ]
        save %data.txt database
        set-data names (sort extract copy database 4)
        set-text names copy n/text
    ]
    button -1 "Delete" [
        if true = question rejoin ["Delete " copy  n/text "?"] [
            remove/part (find database n/text) 4
            save %data.txt database
            set-data names (sort extract copy database 4)
            set-values face/parent-face ["" "" "" "" ""]
        ]
    ]
    button -1 "New" [
            set-values face/parent-face ["" "" "" "" ""]
    ]
]
do-events

Вот полезный редактор текста/кода RebGUI:

REBOL []

unless exists? %ui.dat [
    write %ui.dat read http://re-bol.com/ui-editor.dat
]
do load-thru http://re-bol.com/rebgui.r    ; Build#117
; do %rebgui.r

filename: %temp.txt
make-dir %./edit_history/

backup: does [
    if ((length? x/text) > 0) [
        write rejoin [
            %./edit_history/ 
             last split-path filename 
             "_" now/date "_"
             replace/all form now/time ":" "-"
        ] x/text
    ]
]
ctx-rebgui/on-fkey/f5: does [
    backup
    write filename x/text
    launch filename
]

display/maximize/close "RebGUI Editor" [
    tight
    menu #LW data [
        "File" [
            "  New  " [
                if true = question "Erase Current Text?" [
                    backup
                    filename: %temp.txt set-text x copy ""
                ]
            ]
            "  Open  " [
                filetemp: to-file request-file/file filename
                if filetemp = %none [return]
                backup
                set-text x read filename: filetemp
            ]
            "  Save  " [
                backup
                write filename x/text
            ]
            "  Save As  " [
                filetemp: to-file request-file/save/file filename
                if filetemp = %none [return]
                backup
                write filename: filetemp x/text
            ]
            "  Save and Run  " [
                backup
                write filename x/text 
                launch filename
            ]
            "  Print  "  [
                write %./edit_history/print-file.html rejoin [
                    {<}{pre}{>} x/text {<}{pre}{>}
                ]
                browse %./edit_history/print-file.html
            ]
            "  Quit  " [
                if true = question "Really Close?" [backup quit]
            ]
        ]
        "Options" [
            "  Appearance  " [request-ui]
        ]
        "Help" [
            "  Shortcut Keys  " [
                alert trim {
                    F5:       Save and Run
                    Ctrl+Z:   Undo
                    Ctrl+Y:   Redo
                    Esc:      Undo All
                    Ctrl+S:   Spellcheck
                }
            ]
        ] 
    ] return
    x: area #LHW
] [
    if true = question "Really Close?" [backup quit]
]

do-events

Вот сценарий управления пользователями, вдохновлённый учебником на http://snappmx.com:

REBOL [title: "RebGUI User List Demo"]

do load-thru http://re-bol.com/rebgui.r    ; Build#117  ; do %rebgui.r
unless exists? %snappmx.txt [
    save %snappmx.txt [
        "user1" "pass1" "Bill Jones" "%bill--site--com" "Bill LLC" 
        "user2" "pass2" "John Smith" "%john--mail--com" "John LLC"
    ]
]
database: load %snappmx.txt
login: request-password
found: false
foreach [userid password name email company] database [
    either (login/1 = userid) and (login/2 = password) [found: true] []
]
if found = false [alert "Incorrect Login." quit]
add-record: does [
    display/dialog "User Info" [
        text 20 "User:" f1: field return
        text 20 "Pass:" f2: field return
        text 20 "Name:" f3: field return
        text 20 "Email:" f4: field return
        text 20 "Company:" f5: field reverse
        button -1 #XY " Clear " [clear-fields]
        button -1 #XY " Add " [add-fields]
    ]
]
edit-record: does [
    display/dialog "User Info" [
        text 20 "User:" f1: field (form pick t/selected 1) return
        text 20 "Pass:" f2: field (form pick t/selected 2) return
        text 20 "Name:" f3: field (form pick t/selected 3) return
        text 20 "Email:" f4: field (form pick t/selected 4) return
        text 20 "Company:" f5: field (form pick t/selected 5) reverse
        button -1 #XY " Delete " [
            t/remove-row t/picked
            save %snappmx.txt t/data
            hide-popup
        ]
        button -1 #XY " Save " [
            t/remove-row t/picked
            add-fields
            save %snappmx.txt t/data
            hide-popup
        ]
    ]
]
add-fields: does [
    t/add-row reduce [
        copy f1/text copy f2/text copy f3/text copy f4/text copy f5/text
    ]
    save %snappmx.txt copy t/data
]
clear-fields: does [
    foreach item [f1 f2 f3 f4 f5] [do rejoin [{set-text } item {""}]]
]
table-size: system/view/screen-face/size/1 / ctx-rebgui/sizes/cell
display/maximize "Users" [
    t: table table-size #LWH options [
        "" left .0  "" left .0   ; don't show the first 2 fields
        "Name" center .33  "Email" center .34  "Company" center .34
    ] data database [edit-record]
    reverse
    button -1 #XY " Add " [add-record]
]
do-events

Вот система торговой точки (касса продаж, принтер чеков и система хранения данных), написанная с использованием RebGUI. Обратите внимание, что информация об имени пользователя и пароле в файле posp.db должна быть создана и прочитана с использованием отдельного метода и зашифрована. Пример файла posp.db создан здесь в качестве демонстрации. Также обратите внимание, что первое поле в макете предназначено для приёма ввода от сканера штрих-кода клина клавиатуры с данными в формате: элемент (пробел) будка (пробел) цена (вставлен символ клавиши [ENTER]). Используя этот формат и автоматическую перефокусировку при входе, пользователь может постоянно сканировать несколько элементов в системе:

REBOL [title: "POINT OF SALE SYSTEM"]

write %posp.db {["username" "password"] ["username2" "password2"]} ; etc.
make-dir %./receipts/
write/append %./receipts/deleted.txt ""  ; create file if not exists

unless exists? %scheme_has_changed [
    write %ui.dat decompress #{
        789C9552CB92A3300CBCE72BBCDCA18084995A7E652A078115F08EB129592C33
        7FBFC24E32CC2387A5EC2A49ED6EB56C267845E5BB3FD8F32FF57250F2CD3060
        ABEAA629E23E95B1CAF8C6AD7A3A1571A5D28813E6D60CA32055752AAAE67751
        97CF3B5003BDB6EA5817CF821E9B8804067E484BE04F34BFB035EE4EACCB5371
        DD9FE044AD8E4FC5751FCE6AFA3E648FD6B62A51516F035731BE78B7B9AAEF49
        3EE2D5693A3CC02CCD63B8F5DB8CC464021A8CBB49066B3492901EB4879E8D77
        B92C74BC1D7CD1E467992DB0D8319CA28B41ABE53D42583D691566E31C521438
        7F9161E844241276780F84BCC117DF2F410E480E7BFCBDB7A697FA407E99F3CE
        BF493787568511919588E631DF5146131F602FFA1F8645B1437D35A2BA85D93B
        F5317A8C9810BF5DC240E6A1F0CF374CE4D790B31F507E45B9E10BD8801122D0
        6633DEEC5E3CFB8BA4C14176AF6D936540066CA6B2DE2F649094C35532361386
        EC0B270D18660B61CC355A78BFFD53ECBD6533DF8A655BCA4AD08A9D366E905E
        4C4B72B71AA7FDDA2AE71D1ECEFF004BE40F38A0030000
    }
]

do http://re-bol.com/rebgui.r

do login: [
    userpass: request-password
    if (length? userpass) < 2 [quit]
    posp-database: to-block read %posp.db
    logged-in: false
    foreach user posp-database [
        if (userpass/1 = user/1) and (userpass/2 = user/2) [
            logged-in: true
        ]
    ]
    either logged-in = true [] [
        alert "Incorrect Username/Password"
        do login
    ]
]
calculate-totals: does [
    tax: .06
    subtotal: 0
    foreach [item booth price] pos-table/data [
        subtotal: subtotal + to decimal! price
    ]
    set-text subtotal-f subtotal
    set-text tax-f (round/to (subtotal * tax) .01)
    set-text total-f (round/to (subtotal + (subtotal * tax)) .01)
    set-focus barcode
]
add-new-item: does [
    if ("" = copy f1/text) or ("" = copy f2/text) or (error? try [
        to-decimal copy f3/text
    ]) [
        alert trim/lines {You must enter a proper Item Description,
            Booth Number, and Price.}
        return
    ]
    pos-table/add-row/position reduce [
        copy f1/text copy f2/text copy f3/text
    ] 1
    calculate-totals
]
print-receipt: does [
    if empty? pos-table/data [
        alert "There's nothing to print." return
    ]
    html: copy rejoin [
        {<html><head><title>Receipt</title></head><body>
        <table width=40% border=0 cellpadding=0><tr><td>
        <h1>Business Name</h1>
        123 Road St.<br>
        City, State 98765<br>
        123-456-7890
        </td></tr></table><br><br>
        <center><table width=80% border=1 cellpadding=5>
        <tr>
        <td width=60%><strong>Item</strong></td>
        <td width=20%><strong>Booth</strong></td>
        <td width=20%><strong>Price</strong></td></tr>}
    ]    
    foreach [item booth price] pos-table/data [
        append html rejoin [
            {<tr><td width=60%>} item 
            {</td><td width=20%>} booth 
            {</td><td width=20%>} price {</td></tr>}
        ]
    ]
    append html rejoin [
        {<tr><td width=60%></td><td width=20%><strong>SUBTOTAL:
        </strong></td><td width=20%><strong>}
        copy subtotal-f/text 
        {</strong></td></tr>}
    ]
    append html rejoin [
        {<tr><td width=60%></td><td width=20%><strong>TAX:
        </strong></td><td width=20%><strong>}
        copy tax-f/text 
        {</strong></td></tr>}
    ]
    append html rejoin [
        {<tr><td width=60%></td><td width=20%><strong>TOTAL:
        </strong></td><td width=20%><strong>}
        copy total-f/text 
        {</strong></td></tr>}
    ]
    append html rejoin [
        {</table><br>Date: <strong>} now/date 
        {</strong>, Time: <strong>} now/time 
        {</strong>, Salesperson: } userpass/1
        {</center></body></html>}
    ]
    write/append to-file saved-receipt: rejoin [
        %./receipts/
        now/date "_"
        replace/all copy form now/time ":" "-"
        "+" userpass/1 
        ".html"
    ] html
    browse saved-receipt
]
save-receipt: does [
    if empty? pos-table/data [
        alert "There's nothing to save." return
    ]
    if allow-save = false [
        unless true = resaving: question trim/lines {
            This receipt has already been saved.  Save again?
        } [
            if true = question "Print another copy of the receipt?" [
                print-receipt
            ]
            return
        ]
    ] 
    if resaving = true [
        resave-file-to-delete: copy ""
        display/dialog "Delete" compose [
            text 150 (trim/lines {
                IMPORTANT - DO NOT MAKE A MISTAKE HERE!  
                Since you've made changes to an existing receipt,
                you MUST DELETE the original receipt.  The original
                receipt will be REPLACED by the new receipt (The
                original data will be saved in an audit history file,
                but will not appear in any future seaches or totals.)
                Please CAREFULLY choose the original receipt to DELETE:
            })
            return
            tl1: text-list 150 data [
                "I'm making changes to a NEW receipt that I JUST SAVED" 
                "I'm making changes to an OLD receipt that I've RELOADED"
            ] [
                resave-file-to-delete: tl1/selected
                hide-popup
            ]
            return
            button -1 "Cancel" [
                resave-file-to-delete: copy "" 
                hide-popup
            ]
        ]
        if resave-file-to-delete = "" [
            resaving: false
            return
        ]
        if resave-file-to-delete = trim/lines {
            I'm making changes to a NEW receipt that I JUST SAVED
        }  [
            the-file-to-delete: saved-file
        ]
        if resave-file-to-delete = trim/lines {
            I'm making changes to an OLD receipt that I've RELOADED
        } [
            the-file-to-delete: loaded-receipt
        ]
        if not question to-string the-file-to-delete [return]
        write %./receipts/deleted--backup.txt read %./receipts/deleted.txt
        write/append %./receipts/deleted.txt rejoin [
            newline newline newline
            to-string the-file-to-delete
            newline newline
            read the-file-to-delete
        ]
        delete the-file-to-delete
        alert "Original receipt has been deleted, and new receipt saved."
        resaving: false
    ]
    if true = question "Print receipt?" [print-receipt]
    saved-data: mold copy pos-table/data
    write/append to-file saved-file: copy rejoin [
        %./receipts/
        now/date "_"
        replace/all copy form now/time ":" "-"
        "+" userpass/1 
        ".txt"
    ] saved-data
    splash compose [
        size: 300x100
        color: sky
        text: (rejoin [{^/      *** SAVED ***^/^/      } saved-file {^/}])
        font: ctx-rebgui/widgets/default-font
    ]
    wait 1
    unview
    allow-save: false
    if true = question "Clear and begin new receipt?" [clear-new]
]
load-receipt: does [
    if error? try [
        loaded-receipt: to-file request-file/file/filter %./receipts/
            ".txt" "*.txt"
    ] [
        alert "Error selecting file"
        return
    ]
    if find form loaded-receipt "deleted" [
        alert "Improper file selection" 
        return
    ]
    if error? try [loaded-receipt-data: load loaded-receipt] [
        alert "Error loading data"
        return
    ]
    insert clear pos-table/data loaded-receipt-data
    pos-table/redraw
    calculate-totals
    allow-save: false
]
search-receipts: does [
    search-word: copy request-value/title "Search word:" "Search"
    ; if search-word = none [return]
    found-files: copy []
    foreach file read %./receipts/ [
        if find (read join %./receipts/ file) search-word [
            if (%.txt = suffix? file) and (file <> %deleted.txt) [
                append found-files file
            ]
        ]
    ]
    if empty? found-files [alert "None found" return]
    found-file: request-list "Pick a file to open" found-files
    if found-file = none [return]
    insert clear pos-table/data (
        load loaded-receipt: copy to-file join %./receipts/ found-file
    )
    pos-table/redraw
    calculate-totals
    allow-save: false
]
clear-new: does [
    if allow-save = true [
        unless (true = question "Erase without saving?") [return]
    ]
    foreach item [barcode f1 f2 f3 subtotal-f tax-f total-f] [
        do rejoin [{clear } item {/text show } item]
    ]
    clear head pos-table/data
    pos-table/redraw
    allow-save: true
]
change-appearance: does [
    request-ui
    if true = question "Restart now with new scheme?" [
        if allow-save = true [
            if false = question "Quit without saving?" [return]
        ]
        write %scheme_has_changed ""
        launch %pos.r  ; EDIT
        quit
    ]
]
title-text: "Point of Sale System"
if system/version/4 = 3 [
    user32.dll: load/library %user32.dll
    get-tb-focus: make routine! [return: [int]] user32.dll "GetFocus"
    set-caption: make routine! [
        hwnd [int] 
        a [string!]  
        return: [int]
    ] user32.dll "SetWindowTextA"
    show-old: :show
    show: func [face] [
        show-old [face]
        hwnd: get-tb-focus
        set-caption hwnd title-text
    ]
]

allow-save: true
resaving: false
saved-file: ""
loaded-receipt: ""

screen-size: system/view/screen-face/size
cell-width: to-integer (screen-size/1) / (ctx-rebgui/sizes/cell)
cell-height: to-integer (screen-size/2) / (ctx-rebgui/sizes/cell)
table-size: as-pair cell-width (to-integer cell-height / 2.5)
current-margin: ctx-rebgui/sizes/margin
top-left: as-pair negate current-margin negate current-margin

display/maximize/close "POS" [
    at top-left #L main-menu: menu data [
        "File" [
            "     Print      " [print-receipt]
            "     Save       " [save-receipt]
            "     Load       " [load-receipt]
            "     Search     " [search-receipts]
        ]
        "Options" [
            "     Appearance     " [change-appearance]
        ] 
        "About" [
            "     Info     " [
                alert trim/lines {
                    Point of Sale System. 
                    Copyright © 2010 Nick Antonaccio. 
                    All rights reserved.
                }
            ]
        ]
    ]
    return
    barcode: field #LW tip "Bar Code" [
        parts: parse/all copy barcode/text " "
        set-text f1 parts/1
        set-text f2 parts/2
        set-text f3 parts/3
        clear barcode/text
        add-new-item
    ]
    return
    f1: field tip "Item" 
    f2: field tip "Booth" 
    f3: field tip "Price (do NOT include '$' sign)" [
        add-new-item 
        set-focus add-button
    ]
    add-button: button -1 "Add Item" [
        add-new-item 
        set-focus add-button
    ]
    button -1 #OX "Delete Selected Item" [
        remove/part find pos-table/data pos-table/selected 3
        pos-table/redraw
        calculate-totals
    ]
    return
    pos-table: table (table-size) #LWH options [
        "Description" center .6
        "Booth" center .2
        "Price" center .2
    ] data []
    reverse
    panel sky #XY data [
        after 2
        text 20 "Subtotal:" subtotal-f: field 
        text 20 "     Tax:" tax-f: field
        text 20 "   TOTAL:" total-f: field
    ]
    reverse
    button -1 #XY "Lock" [do login]
    button -1 #XY "New" [clear-new]
    button -1 #XY "SAVE and PRINT" [save-receipt]
    do [set-focus barcode]
] [question "Really Close?"]

do-events

16.24 Создание файлов PDF с помощью pdf-maker.r

PDF - это стандартный формат файла, который используется для отображения и печати содержимого документа одинаковым образом на разных компьютерных платформах. В Windows и других операционных системах программа для чтения PDF-файлов от Adobe часто устанавливается по умолчанию. Другие бесплатные программы для чтения PDF, такие как Foxit, Sumatra и PDF-Xchange, позволяют просматривать и распечатывать документы PDF. Openoffice можно использовать для создания, преобразования и сохранения различных форматов документов (например, MS Word и других форматов текстовых редакторов) в PDF, чтобы их можно было просматривать/печатать в одном и том же визуальном макете на любом компьютере.

Габриэле Сантилли создал скрипт для создания PDF-файлов REBOL, который генерирует универсально читаемые и пригодные для печати PDF-файлы непосредственно из кода REBOL. Официальная документация доступна по адресу http://www.colellachiara.com/soft/Misc/pdf-maker-doc.pdf (источник REBOL, использованный для создания этого PDF-документа, доступен по адресу http://www.colellachiara.com/soft/Misc/pdf-maker-doc.r). Pdf-maker.r - это полноценное автономное многоплатформенное решение для создания PDF-файлов. Никакого другого программного обеспечения для создания PDF-файлов с помощью REBOL не требуется.

Базовая функциональность pdf-maker.r очень проста. Импортируйте pdf-maker.r с функцией "do" (или просто включите её прямо в свой код). Затем запустите функцию "layout-pdf", которая принимает один блок в качестве параметра, и запишите его вывод в файл .pdf, используя функцию "write/binary". Внутри блока, переданного в функцию "layout-pdf", различные функции форматирования могут быть включены в макет текста, изображений и графики, сгенерированной вручную. Вот базовый пример формата с одной простой функцией макета текста:

do http://www.colellachiara.com/soft/Misc/pdf-maker.r
write/binary %example.pdf layout-pdf [[textbox ["Hello PDF world!"]]]

; Откроем созданный документ в программе просмотра PDF по умолчанию:

call %example.pdf

Вот более сложный пример, который создаёт многостраничный файл PDF и демонстрирует многие из основных возможностей pdf-maker.r. Отдельное содержимое страницы содержится в отдельных подблоках. Все координаты записываются в МИЛЛИМЕТРОВОМ формате:

REBOL [title: "pdf-maker example"]

do http://www.colellachiara.com/soft/Misc/pdf-maker.r

write/binary %example.pdf layout-pdf compose/deep [
    [
        page size 215.9 279.4  ; American Letter Size!!!
        textbox ["Here is page 1.  It just contains this text."]
    ] 
    [
        textbox 55 55 90 100 [
            {Here's page 2.  This text box contains a starting
             XxY position and an XxY size.  Coordinates are in
             millimeters and extend from the BOTTOM LEFT of the
             page (this box extends from starting point 55x55
             and is 90 mm wide, 100 mm tall).

             *** NOTE ABOUT PAGE SIZES - IMPORTANT!!! *** 

             All the following page sizes are the default ISO A4,
             or 211×297 mm.  That is SLIGHTLY SMALLER than the
             standard American Letter page size.  If you are
             printing on American Letter sized paper, be sure to
             manually set your paper size, as is done on the first
             page of this example.}
        ]
    ]
    [
        textbox 0 200 200 50 [
            center font Helvetica 10.5
            {This is page 3.  The text inside this box is centered
             and formatted using Helvetica font, with a character
             size of 10.5 mm.}
        ]
    ]
    [
        apply rotation 20 translation 35 150 [
            textbox 4 4 200 20 [
                {This is page 4.  The textbox is rotated 20 degrees
                 and translated (moved over) 35x150 mm.  Graphics
                 and images can also be rotated and translated.}
            ]
        ]
    ]
    [
        textbox 5 200 200 40 [
            {Here's page 5.  It contains this textbox and several
             images.  The first image is placed at starting point
             50x150 and is 10mm wide by 2.4mm tall.  The second
             image is 2x bigger and rotated 90 degrees.  The last
             image is placed at starting point 100x150 and is
             streched to 10x its size.  Notice that this ENTIRE
             layout block has been evaluated with compose/deep to
             evaluate the images in the following parentheses.}
        ]
        image 50 150 10 2.4 (system/view/vid/image-stock/logo)
        image 50 100 20 4.8 rotated 90 
            (system/view/vid/image-stock/logo)
        image 100 150 100 24 (system/view/vid/image-stock/logo)
    ]
    [
        textbox [
            {This page contains this textbox and several generated
             graphics:  a line, a colored and filled box with a
             colored edge, and a circle.}
        ]
        line width 3  20 20 100 50   ; starting and ending XxY positions
        solid box edge width 0.2 edge 44.235.77 150.0.136 100 67 26 16
        circle 75 50 40   ; starting point 75x50, radius 40mm
    ]
]

call %example.pdf

Компоновка/глубокая оценка очень важна при использовании вычисленных значений в макетах PDF. Взгляните на следующий пример, в котором используются вычисленные координаты и значения изображения:

do http://www.colellachiara.com/soft/Misc/pdf-maker.r
xpos: 50  ypos: 200  offset: 5  
size: 5  width: (10 * size)  height: (2.4 * size)
page1: compose/deep [[
    image 
        (xpos + offset) (ypos + offset)
        (width) (height)
        (system/view/vid/image-stock/logo)
]]
write/binary %example.pdf layout-pdf page1
call %example.pdf

Вот программа, которую я написал для студентов-гитаристов. Он распечатывает схемы нот на грифе, которые можно вырезать, обернуть и наклеить непосредственно на грифы гитар различных размеров. Скрипт pdf-maker включён в сжатом встроенном формате:

REBOL [title:  "Guitar Fretboard Note Overlay Printer"]

chosen-scale: none
view center-face layout [
    h1 "Fretboard length:"
    text-list "25.5" "27.67" "30" [
        chosen-scale: join "scale" value
        unview
        alert rejoin [
            "Now printing " 
            value 
            " inch scale fretboard overlay to 'notes.pdf'"
        ]
    ]
]

notes: [
    [{F}{C}{ }{ }{ }{F}]
    [{ }{ }{A}{E}{B}{ }]
    [{G}{D}{ }{F}{C}{G}]
    [{ }{ }{B}{ }{ }{ }]
    [{A}{E}{C}{G}{D}{A}]
    [{ }{F}{ }{ }{ }{ }]
    [{B}{ }{D}{A}{E}{B}]
    [{C}{G}{ }{ }{F}{C}]
    [{ }{ }{E}{B}{ }{ }]
    [{D}{A}{F}{C}{G}{D}]
    [{ }{ }{ }{ }{ }{ }]
    [{E}{B}{G}{D}{A}{E}]
]

scale25.5: [
    36.35 70.66 103.05 133.62 162.47 189.71 215.41 239.67 262.58 284.19
    304.59 323.85 342.03 359.18 375.38 390.66 405.09 418.70 431.56 443.69
    455.14 465.95 476.15 485.77
]

scale27.67: [
    39.45 76.68 111.82 144.99 176.30 205.85 233.74 260.07 284.92 308.38
    330.51 351.41 371.13 389.75 407.32 423.91 439.56 454.34 468.28 481.45
    493.87 505.60 516.67 527.11
]

scale30: [
    42.77 83.14 121.24 157.20 191.15 223.18 253.43 281.97 308.91 334.34
    358.34 381.00 402.38 422.57 441.62 459.60 476.57 492.59 507.71 521.99
    535.46 548.17 560.17 571.50
]

x: 40  line-width: 30  text-width: 4  height: 5

make-overlay: does [
    page1: copy [
        textbox 40 0 4 6 [center font Helvetica 5 "E"]
        textbox 45 0 4 6 [center font Helvetica 5 "B"]
        textbox 50 0 4 6 [center font Helvetica 5 "G"]
        textbox 55 0 4 6 [center font Helvetica 5 "E"]
        textbox 60 0 4 6 [center font Helvetica 5 "A"]
        textbox 65 0 4 6 [center font Helvetica 5 "E"]
    ]
    output: copy []
    for i 1 10 1 [
        y: do compose [pick (to-word chosen-scale) i]
        notes-at-fret: pick notes i
        append page1 compose/deep [
            line width 4 (x) (y) (x + line-width) (y)
            textbox (x) (y - 10) (text-width) (height + 1) [
                center font Helvetica (height) (notes-at-fret/1)
            ]
            textbox (x + 5) (y - 10) (text-width) (height + 1) [
                center font Helvetica (height) (notes-at-fret/2)
            ]
            textbox (x + 10) (y - 10) (text-width) (height + 1) [
                center font Helvetica (height) (notes-at-fret/3)
            ]
            textbox (x + 15) (y - 10) (text-width) (height + 1) [
                center font Helvetica (height) (notes-at-fret/4)
            ]
            textbox (x + 20) (y - 10) (text-width) (height + 1) [
                center font Helvetica (height) (notes-at-fret/5)
            ]
            textbox (x + 25) (y - 10) (text-width) (height + 1) [
                center font Helvetica (height) (notes-at-fret/6)
            ]
        ]
    ]
    append/only output page1
    write/binary %notes.pdf layout-pdf output
    alert "Done"
]

do to-string load to-binary decompress 64#{
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
n6eu9vNOGX7AVA9A3+GqM+D/NK66uAGAr77UFK0+q6KPfPo3+qO/Qf8gQOO3XU5B
8PO2hyaoTjZ8GaDVqaTeX7tfm1MQ383A25UErpG2AtNw1dpprq9obcWucTdDVaVX
l5DG1Ya5L1U+7mqkLpe8tJv3cF4nsNbq8a+mrEboOk8vJ+WK7ffG+ip8JkoN/nyT
L/JUgj4elr/ca/88rkecLmR5I/OXM2jka7357fbrS6PLs6qHXpeiX1tPtzs9XOdU
V//dO50uCP7+nU7/xeqS//iOZl6tdq61vsS/vqV7Vk02WSV+vlzn/b3NinV94Xx/
WH3b0Jvp/vhS5wuJr8L6MT7Cmy4Bw32/3nmYSaSLwA/ofAyzen25rPZfNnY8GEPg
LP14Y77aFtXE/nJfrf63BF5uw/ygp0cNed/V5V7uc28PVX/d3Usr9O6Dn4+XLlWv
C2Nf5+YdzvtyrvbH/8WTD3XuHxWRD8TjQTTeu6GKrXTfjy+VCnyPyS+3WH25V7gv
evzgitvmIG8m/7Z3pvXZV49RnjVurL21uLu79lZ8MSexfRnbCwsf34/8XHhzDl8+
OZv4J0iX6Xhwdc9T9PDATr837F/TG2k4vLf6zYvYqsb3p/Pf7vG9x6BJujgxASH0
t4T1I/3Ouyhe/Ot8Lwj7tKn4vaT4/VSf9xFebtV6MDJ14vuMw3sXtf58IOsZs9eJ
7+qSuy+vToF+rRc4EQzBrqelHzcwN8NN0CX4//8Dw8SJGQWrAAA=
}

make-overlay

Больше информации тут и a href="https://web.archive.org/web/20090705092401/http://www.rebolfrance.info:80/org/articles/pdf1/pdf2.html>тут

16.25 Штрих-коды

Штрих-коды используются во многих бизнес-приложениях для ускорения ввода данных и уменьшения количества ошибок ввода. Было бы глупо создавать систему торговых точек или систему управления запасами без использования технологии штрих-кода.

Считывать данные штрих-кода в компьютер очень просто. USB-сканеры (аппаратные устройства) работают так же, как клавиатуры. Подключите их к компьютеру, и все, что они прочитают со штрих-кода, будет введено в ваше приложение как обычный текст, как если бы текст был набран вручную.

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

REBOL [title "Bar Code Reader"]
view layout [
    text "Plug in your scanner and scan:"
    f1: field [
        if f1/text <> "" [
            write/append %barcodes.txt rejoin [
                copy f1/text
                newline
            ]
        ]
        if "recall" = select ["123456" "ok" "234567" "recall"] f1/text [
            alert "That item has been recalled!"
        ]
        f1/text: copy ""
        focus f1
    ]
    do [focus f1]
    btn "View Scanned Bar Codes" [editor %barcodes.txt]
]

Для печати штрих-кодов требуется ещё немного кода. Скрипты на http://www.rebol.org/view-script.r?script=code39.r и http://www.rebol.org/view-script.r?script=ean13.r демонстрируют, как печатать несколько распространённых форматов штрих-кода для изображений.

Следующая программа берет заданную строку и координату XxY (в миллиметрах) и выводит файл PDF, содержащий штрих-код для печати в заданной позиции. Алгоритм штрих-кода заимствован непосредственно из "code39.r" Богдана Лехновского, а PDF-файл создаётся с использованием "pdf-maker.r" Габриэле Сантилли. Этот сценарий был создан потому, что изображения, выводимые исходным сценарием code39.r, становились размытыми при вставке и изменении размера с помощью pdf-maker.r. Здесь полосы отображаются в виде линий, прямо на диалекте PDF-Maker. Создаваемые изображения чёткие и легко поддаются сканированию:

REBOL [title: "PDF Bar Code Generator"]

text-string: "item2342"
x-offset: 10    ; миллиметры от левого края страницы
y-offset: 257   ; миллиметры от нижнего края страницы

create-pdf-barcode: func [barcode-string xshift yshift] [
    barcode-width: .3  barcode-height: 12  
    code39: first to-block decompress #{
    789C5D93490EC2400C04EF794514C10504D8EC1CD9F77D07F1FF6F30C9C4E3F6
    200529E54EA91D866F92BA4FC699BB989828FF6277EB793BE7EE3EE69D322F03
    E15D9F27629BEFA9DFE4FBEA377C103CC520F021F684FC087B0227EC037C2C9E
    F209E113F1447C1AF6F503E1B3D2CF517E1EFC36BF087ECB97E221BBEF0A7B42
    7E8D3D816FB00FF0AD7A8A89F09D7A0CDFC3BEF940F841FD267F847D317F827D
    919FC3BE6C3C17E889F92BF4447E833EC8EFDE43A212FE28F2C4317F4A9EED79
    7E95F9F83CBFD56FF21FF51BDE081EFBFB36B127E453EC09BC867D80578447E7
    B3051CDF4F5DFB185ED5FF9DE7C9EF0F6518AA1B22040000
    }
    convfrom: rejoin ["*" barcode-string "*"]
    pdf-dialect-out: copy []
    x: 0
    foreach char convfrom [
        pattern: select code39 form char
        foreach bit pattern [
            x: x + 1
            if bit = #"1" [
                append pdf-dialect-out compose [
                    line width (barcode-width)
                    line
                    (
                        (x * barcode-width) + xshift
                    ) (
                        yshift
                    )
                    (
                        (x * barcode-width) + xshift
                    ) (
                        yshift + barcode-height
                    )
                ]
            ]
        ]
        x: x + 1
    ]
    return pdf-dialect-out
]

do http://www.colellachiara.com/soft/Misc/pdf-maker.r 
barcode-layout: copy []
current-barcode-page: copy [page size 215.9 279.4 offset 0 0]
append current-barcode-page create-pdf-barcode 
    text-string x-offset y-offset

; Следующий блок не нужен. Он просто добавляет в распечатку читаемый 
; текст:

append current-barcode-page compose/deep [
    textbox 
    (x-offset - 9.5) (y-offset - 8)
    56 8
    [
        center font Helvetica 3 
        (mold text-string)
    ]
]

append/only barcode-layout current-barcode-page
write/binary %labels.pdf layout-pdf barcode-layout
call %labels.pdf

; Эта строка не нужна - она просто позволяет прочитать вывод диалекта pdf:

editor barcode-layout

Вот функция программного обеспечения Merchants 'Village, которая печатает штрих-коды поставщиков на страницах бумаги Avery Label (30 этикеток на страницу), с возможностью начать печать на любой выбранной позиции этикетки на первой странице, вставленной в принтер (чтобы продолжить печать на частично используемых страницах). Этот пример взят из более крупного приложения RebGUI и требует модуля .PDF. Несмотря на то, что она вырвана из контекста, логика полезна для всех, кто хочет распечатать страницы этикеток со штрих-кодом:

print-bar-codes: does [
    bar-code-data-to-print: copy []
    bar-code-human-readable-to-print: copy []
    either true = question "Print bar codes from vendor order file?" [
        if error? try [
            barcode-order: request-file/only
            barcode-order-data: load barcode-order
            temporary-barcode-data: copy second barcode-order-data
        ] [alert "Not a valid bar code order file."]
    ] [
        temporary-barcode-data: (copy pos-table/data)
    ]
    temporary-barcode-data-build: copy []
    foreach [booth item price] temporary-barcode-data [
        new-price: to-decimal copy price
        if new-price < .01  [
            new-price: request-value/type rejoin [
                "Enter Price for " booth ", " item ": "
            ] decimal!
        ]
        append temporary-barcode-data-build reduce [booth item new-price]
    ]
    display/maximize/dialog "Bar Codes" [
        barcode-pos-table: table (table-size) #LWOH options [
            "Booth" center .25
            "Item" center .55
            "Price" center .20
        ] data (copy temporary-barcode-data-build) [
            ; on-click, change the selected line price (request-text)
            ; probe barcode-pos-table/picked
            new-price: request-value/type rejoin [
                "Enter Price for " pick barcode-pos-table/selected 1
                ", " pick barcode-pos-table/selected 2
                ", " pick barcode-pos-table/selected 3 ": "
            ] decimal!
            if new-price = none [return]
            barcode-pos-table/alter-row barcode-pos-table/picked reduce [
                pick barcode-pos-table/selected 1
                pick barcode-pos-table/selected 2
                pick barcode-pos-table/selected 3
                new-price
            ]
            barcode-pos-table/redraw
        ]
        return
        button "Print" #XYO  [
            all-the-barcode-data: copy barcode-pos-table/data
            hide-popup
        ]
    ]
    unless value? 'all-the-barcode-data [return]
    if all-the-barcode-data = none [return]
    foreach [booth item price] all-the-barcode-data [
        bar-code-data: copy ""
        bar-code-human-readable: copy ""
        temp-booth: copy booth
        booth-name-to-print: copy temp-booth
        append bar-code-human-readable rejoin [
            "booth " temp-booth ", item: "
        ]
        if error? try [booth-index: to-integer copy temp-booth] [
            booth-index: 0
        ] 
        append bar-code-data booth-index
        append bar-code-data select [
            1 "  " 2 " " 3 ""
        ] (length? bar-code-data)
        temp-item: copy item
        append bar-code-human-readable join temp-item ", "
        if error? try [item-index: index? find items temp-item] [
            item-index: 0
        ]
        append bar-code-data item-index
        append bar-code-data select [
            4 "    " 5 "   " 6 "  " 7 " " 8 ""
        ] (length? bar-code-data)
        temp-price: to-decimal price
        temp-price-nodot: to-integer (price * 100)
        append bar-code-human-readable rejoin ["$" temp-price]
        append bar-code-data form temp-price-nodot
        append bar-code-data-to-print bar-code-data
        append bar-code-human-readable-to-print bar-code-human-readable
    ] ; probe bar-code-data
    ; Avery Address Label Edges:   left - 5 75 145   right - 71 141 211
    ; tops/bottoms - 13 38.5 64 89.5 115 140 165.5 191 216.5 241.5 267
    barcode-label-positions: [
        5 267    75 267    145 267      5 241.5  75 241.5  145 241.5
        5 216.5  75 216.5  145 216.5    5 191    75 191    145 191
        5 165.5  75 165.5  145 165.5    5 140    75 140    145 140
        5 115    75 115    145 115      5 89.5   75 89.5   145 89.5
        5 64     75 64     145 64       5 38.5   75 38.5   145 38.5 
    ]
    current-barcode-pos: request-value/default/type 
        "Position on page to begin printing: " "1" integer!
    if current-barcode-pos = none [return]
    if current-barcode-pos = "" [return]
    if current-barcode-pos = -1 [return]
    current-barcode-pos: (to-integer current-barcode-pos) * 2 - 1
    barcode-counter: 1
    barcode-list-index: 1
    barcode-layout: copy []
    current-barcode-page: copy [page size 215.9 279.4 offset 2 -17 ]
    loop (length? bar-code-data-to-print) [
        append current-barcode-page compose/deep [
            (create-pdf-barcode 
                (pick bar-code-data-to-print barcode-list-index)
                (pick barcode-label-positions current-barcode-pos)
                (pick barcode-label-positions (current-barcode-pos + 1))
            )
            textbox 
                (pick barcode-label-positions current-barcode-pos)
                ((pick barcode-label-positions 
                    (current-barcode-pos + 1)) - 10)
                56 8
                [
                    font Helvetica 3 
                    (mold pick bar-code-human-readable-to-print
                        barcode-list-index)
                ]
        ]
        barcode-counter: barcode-counter + 1
        barcode-list-index: barcode-list-index + 1
        current-barcode-pos: current-barcode-pos + 2
        if current-barcode-pos > 60 [
            current-barcode-pos: 1
            append/only barcode-layout current-barcode-page
            current-barcode-page: copy [
                page size 215.9 279.4 offset 2 -17
            ]
        ]
    ]
    append/only barcode-layout current-barcode-page
    make-dir %./barcodes/
    write/binary %./barcodes/labels.pdf layout-pdf barcode-layout
    current-barcode-file-name: to-file rejoin [
        "./barcodes/labels--booth_" booth-name-to-print "_" 
        booth-number-login/text "--" now/date "_" 
        replace/all copy form now/time ":" "-"
        ".pdf"
    ]
    write/binary current-barcode-file-name layout-pdf barcode-layout
    either pdf-choice = "none" [
        if error? try [call %./barcodes/labels.pdf] [
            write %pdf-choice.txt "SumatraPDF.exe"
            call rejoin [
                (to-local-file %SumatraPDF.exe) " " 
                (to-local-file %./barcodes/labels.pdf)
            ]
        ]
    ] [
        call rejoin [
            (to-local-file read %pdf-choice.txt) " " 
            (to-local-file %./barcodes/labels.pdf)
        ]
    ]
]

Сторонние библиотеки DLL и другие инструменты, такие как http://sourceforge.net/projects/openbarcodes/files/, предоставляют полезные решения для печати штрих-кодов всех типов.

16.26 Создание файлов .swf с помощью REBOL/Flash

Flash - это широко распространённый мультимедийный формат, используемый для доставки графики, звука, видео, игр и целых веб-сайтов в Интернете. Flash уже установлен на более чем 90% всех компьютеров, подключённых к Интернету. Он доступен в виде небольшого бесплатного плагина для всех основных веб-браузеров по адресу http://get.adobe.com/flashplayer. Существует множество других доступных флэш-плееров, которые могут отображать файлы в формате ".swf" в формате Flash на мобильных устройствах, в операционных системах для настольных ПК и в Интернете. Вездесущность, мощность и обширные мультимедийные возможности Flash делают его популярной платформой для разработки мультимедийных материалов, особенно в Интернете.

Первоначально Flash был создан компанией Macromedia, а сейчас принадлежит Adobe. Пакет разработки Adobe Flash CS4 - это дорогостоящая и тяжёлая среда разработки, требующая значительного обучения и владения языком "ActionScript". Есть много других коммерческих предложений и предложений с открытым исходным кодом, которые можно использовать для создания файлов flash .swf, но многие из них ориентированы на создание простой анимации с движущимися текстовыми эффектами, развёртками графики, панорамированием, затуханием и т.д.

CS4 - фантастически мощный инструмент (отраслевой стандарт), но для многих потребностей разработки Flash вы будете счастливы узнать, что вам не нужно выходить за пределы мира REBOL. У REBOLers есть свой собственный инструмент для создания Flash, который можно бесплатно загрузить и который не требует дополнительных языков или инструментов разработки для создания мультимедийных файлов .swf. Просто выполните сценарий REBOL/flash на http://box.lebeda.ws/~hmm/rswf/rswf_latest.r, и у вас под рукой будет мощная система разработки Flash.

Использовать REBOL/flash просто. Следующие 3 строки демонстрируют базовый процесс установки диалекта (выполнение файла сценария REBOL/flash), компиляции загруженного файла исходного кода REBOL/flash и последующего просмотра скомпилированного файла .swf в вашем браузере:

do http://box.lebeda.ws/~hmm/rswf/rswf_latest.r ; Установка
                                                ; REBOL/flash
make-swf/save/html http://tinyurl.com/yhex2cf   ; Компилируем 
                                                ; исходный код
browse %starfield1.html                         ; Отображаем 
                                                ; созданный .swf

Чтобы серьёзно начать работать с REBOL/flash, вам нужно сохранить копию диалекта REBOL/flash на свой жёсткий диск:

write %rswf.r read http://box.lebeda.ws/~hmm/rswf/rswf_latest.r

Приведённый выше файл является собственной программой REBOL, созданной Дэвидом Олдесом Оливой, которая принимает входные данные на диалекте REBOL/flash (мини-язык REBOL, созданный Oldes) и напрямую выводит файлы Flash .swf. Никаких других инструментов (кроме интерпретатора REBOL) не требуется для создания очень мощных Flash-приложений, которые вы можете использовать на своём веб-сайте, в настольных презентациях и т.д..

Вот несколько демонстрационных примеров для загрузки:

examples: [
    http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-eyeball.rswf
    http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-snow.rswf
    http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-starfield2.rswf
]
foreach file examples [write (to-file last split-path file) (read file)]

Это всего лишь несколько из 175 примеров кода на http://box.lebeda.ws/~hmm/rswf. Эта большая коллекция примеров кода представляет собой полную существующую документацию для диалекта REBOL/flash. Их чтение и эксперименты помогут вам познакомиться с REBOL/flash API и продемонстрируют, как использовать основные строительные блоки, необходимые для выполнения многих необходимых задач разработки REBOL/flash. Обратите внимание, что предыдущий веб-сайт REBOL/flash Олдеса все ещё доступен по адресу http://oldes.multimedia.cz/swf. На этом сайте есть загружаемый zip-файл со всеми примерами кода и некоторыми дополнительными более старыми инструментами, такими как компилятор swf-to-exe (этот сайт больше не обновляется).

После загрузки/выполнения сценария по адресу http://box.lebeda.ws/~hmm/rswf/rswf_latest.r вы можете скомпилировать файлы исходного кода REBOL/flash в рабочие файлы .swf с помощью функции "make-swf". Чаще всего используются уточнения этой функции /save и /html, поскольку они генерируют HTML-код, необходимый для вставки объекта Flash на ваши собственные веб-страницы. Функция make-swf/save/html создаёт полнофункциональный файл .swf Flash и необходимый HTML-файл контейнера в той же папке, что и rswf.r. Вот простой скрипт, который я использую для компиляции исходных файлов REBOL/flash, а затем просматриваю скомпилированные результаты в браузере:

REBOL [title: "Compile and View Flash Files"]

my-rswf-folder: %./
; my-rswf-folder: %/C/12-2-09/My_Docs/WORK/rswf/  ; choose your own
change-dir my-rswf-folder
do %rswf.r  ; предполагается, что вы уже сохранили его на жёсткий диск
make-swf/save/html to-file request-file/filter "*.rswf"
browse to-file request-file/filter "*.html"

Попробуйте прямо сейчас - используйте приведённый выше сценарий для компиляции исходных файлов, загруженных ранее. Процесс компиляции и запуска невероятно прост! Следующий скрипт представляет собой инструмент сборки, который помогает автоматизировать процесс создания, редактирования и компиляции исходных файлов REBOL/flash, просмотра результатов, а затем повторного выполнения всего процесса многократно для быстрой отладки и завершения проектов:

REBOL [title: "REBOL/flash Build Tool"]

; Следующая папка должна быть настроена для хранения файлов вашего 
; проекта REBOL/flash:: 

my-rswf-folder: %./
; my-rswf-folder: %/C/12-2-09/My_Docs/WORK/rswf/
change-dir my-rswf-folder
do %rswf.r
current-source: to-file request-file/filter/file "*.rswf" %test.rswf
unset 'output-html

do edit-compile-run: [
    editor current-source
    if error? err: try [make-swf/save/html current-source] [
        err: disarm :err
        alert reform [
            "The following compile error occurred: "
            err/id err/where err/arg1
        ]
        either true = request "Edit/Compile/Run Again?" [
            do edit-compile-run quit
        ] [
            quit
        ]
    ]
    unless value? 'output-html [
        output-html: to-file request-file/filter "*.html"
    ]
    browse output-html
    if true = request "Edit/Compile/Run Again?" [do edit-compile-run]
]

Чтение и корректировка 175 онлайн-примеров кода - важная часть обучения написанию собственных сценариев REBOL/flash. Попробуйте этот пример, полученный из http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-soundstream.rswf:

write %mp3.rswf {
    REBOL [
        type: 'swf5
        file: %mp3.swf
        background: 200.200.200
        rate: 12
        size: 1x1
    ]
    mp3Stream http://re-bol.com/example.mp3
    finish stream
    end
}

Обратите внимание, что я изменил исходное имя файла, изменил файл .mp3 на URL-ссылку, изменил размер изображения и цвет фона, а также обрезал часть заголовка. Я скомпилировал этот скрипт с помощью make-swf/save/html, добавил текст в сгенерированный HTML-файл и загрузил оба файла на http://re-bol.com/examples/mp3.html, используя скрипт ниже:

REBOL [title: "Generate from source and upload SWF and HTML to web site"]

; Отредактируйте имена файлов и информацию о FTP как Вам нужно
source-file: %mp3.rswf
output-html: %mp3.html
output-swf:  %mp3.swf
inserted-html: {<center><h1>MP3 Example!</h1></center>}
insert-at: {<BODY bgcolor="#C8C8C8">}
my-ftp-info: ftp://username:password@site.com/public_html/folder/
destination-url: http://re-bol.com/examples/mp3.html
do http://box.lebeda.ws/~hmm/rswf/rswf_latest.r  ; do %rswf.r
make-swf/save/html source-file 
content: read output-html
insert (skip find content insert-at length? insert-at) inserted-html
write output-html content
write (join my-ftp-info form output-html) (read output-html)
write/binary (join my-ftp-info output-swf) (read/binary output-swf)
browse destination-url

Это полностью основанная на REBOL система разработки и развёртывания Flash, состоящая всего из нескольких строк кода, и для неё требуется всего несколько сотен килобайт простых в использовании автономных инструментов разработки. Невероятный! Подумайте на мгновение о возможности автоматического создания и развёртывания пользовательских файлов .swf прямо на вашем веб-сервере в режиме реального времени, используя такие простые скрипты ...

Теперь взгляните на http://box.lebeda.ws/~hmm/rswf/example/swf5-3dtext. Обратите внимание, что исходный код этого примера (http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-3dtext.rswf) импортирует некоторые ресурсы из отдельного включенного файла, хранящегося на веб-сервере (по адресу %includes/fnt_euromode_b.swf). Такие файлы, как изображения и другие двоичные ресурсы, часто включаются в код REBOL/flash, поэтому их можно скомпилировать в окончательный файл .swf. Если вы хотите скомпилировать этот исходный код на своём локальном компьютере, вы должны загрузить включённые файлы, а также исходный код (ПОПРОБУЙТЕ ЭТОТ ПРИМЕР!):

; Обратите внимание на папку, содержащую включённый ресурс. Сохраняя 
; загруженные ресурсы в той же структуре папок, вам вообще не нужно
; изменять исходный код:

make-dir %./includes/
web-resource: http://box.lebeda.ws/~hmm/rswf/includes/fnt_euromode_b.swf
local-resource: %./includes/fnt_euromode_b.swf
write/binary local-resource read/binary web-resource

source: http://box.lebeda.ws/~hmm/rswf/examples/swf5/swf5-3dtext.rswf
do %rswf.r
make-swf/save/html source

; Обратите внимание на имя выходного файла в заголовке файла 
; исходного кода:

browse %3dtext.html

Приведённый выше пример кода сложен, но вы должны заметить, что большая его часть находится в стандартном формате REBOL (двоеточия используются для создания переменных, код содержится в блоках в квадратных скобках, функции и структуры, такие как цикл "for", отформатированы в стандартном формате синтаксиса REBOL и т.д.). В этом примере вы увидите много слов и фраз, которые используются в других примерах: ImportAssets, EditText, sprite, place, at, "show 2 frames", doAction, "goto 1 and play", showFrame и т.д.. Понять, как использовать диалект, начать с более простых примеров и искать общие слова, изучать их синтаксис и экспериментировать с настройкой их параметров. Ниже приведены несколько примеров, демонстрирующих основы использования текста, изображений, графики, звуков и методов анимации в REBOL/flash. Сначала прямоугольник:

REBOL [
    type: 'swf
    file: %shape.swf
    background: 230.230.230
    rate: 40
    size: 320x240
]
a-rectangle: Shape [
    Bounds 0x0 110x50
    fill-style [color 255.0.0]
    box 0x0 110x50
]
place [a-rectangle] at 105x100
showFrame
end

Вот какой-то текст:

REBOL [
    type: 'swf
    file: %text.swf
    background: 255.255.255
    rate: 40
    size: 320x240
]
fnt_Arial: defineFont2 [name "Arial"]
some-text: EditText 'the-text 110x18 [
    Color 0.0.0
    ReadOnly
    NoSelect
    Font [fnt_Arial 12]
    Layout [align: 'center]
]
place [some-text] at 105x100
doAction [the-text: "Hello world!"]
showFrame
end

Попробуйте изменить все приведённые выше примеры (скопируйте/вставьте и отредактируйте их напрямую с помощью "REBOL/flash Build Tool", предоставленного ранее). Они должны предоставить достаточно базовых знаний, чтобы начать читать примеры API на http://box.lebeda.ws/~hmm/rswf.

Чтобы увидеть, насколько мощен REBOL/flash, загляните на http://machinarium.net/demo/. Machinarium - абсолютно красиво оформленная, коммерчески успешная игра (см. Обзоры), созданная с помощью REBOL/flash. С помощью REBOL/flash также был создан ряд законченных и сложных веб-сайтов на Flash. См. Ссылки на http://oldes.multimedia.cz/swf и http://miss3.cz для нескольких примеров.

16.27 Печать с REBOL

REBOL - это кроссплатформенное решение, работающее в более чем 40 операционных системах. Поскольку каждая операционная система требует различных интерфейсов ОС для печати (совместно используемых библиотек, DLL и других фреймворков), важно разработать универсальные подходы, позволяющие печатать выходные данные на бумажных бумажных копиях.

16.27.1 Печать в изображения

Быстрый способ разметки простого печатаемого содержимого страницы - это сохранить графический интерфейс в виде изображения, а затем распечатать изображение, используя стандартное или установленное системное программное обеспечение. Возможности графического интерфейса REBOL предоставляют быстрые и лаконичные методы позиционирования текста и изображений, указания шрифтов и т.д. на экране. Просто создайте макет содержимого, которое вы хотите распечатать, сохраните его с помощью функций "save/png" и "to-image" и используйте функцию "call/show", чтобы открыть его для печати с помощью системного средства просмотра по умолчанию. В этом примере изменяется размер и положение изображения, а также устанавливается размер шрифта для блока форматированного текста:

REBOL [title: "Simple Print Example"]
some-text: copy {}
repeat i 10 [append some-text rejoin ["This is line #: " i newline]]
save/png %printout.png to-image layout/tight [
    backdrop white 
    area 640x480 white font-color red font-size 40 some-text
    at 400x150 image logo.gif 200x100 pink
]
call/show %printout.png

Вы также можете открыть сгенерированное изображение для печати в системном браузере:

browse %printout.png

Такие программы, как Irfanview, обеспечивают пакетную обработку, автоматическое определение размеров печати и другие функции, которые могут быть полезны при автоматизации повторяющихся задач печати. Just Print the Image позволяет печатать файлы изображений непосредственно на принтере, не открывая графический интерфейс.

16.27.2 Печать в HTML

Makedoc.r, рассмотренный ранее в этом руководстве, предоставляет простой способ создания вывода HTML для содержания документации. Этот контент можно распечатать с помощью любого браузера, часто с различными вариантами макета и функциями. Чтобы принудительно разорвать печатную страницу в печатной документации, вставьте следующий код:

<p style="page-break-before: always"></p>

Используя бесплатные драйверы принтера сторонних производителей, такие как CutePDF, файлы HTML можно легко сохранять в PDF и другие форматы. Это быстрая и эффективная цепочка инструментов для создания высококачественных печатных книг и документации, которые можно просматривать во всех распространённых операционных системах.

HTML также можно использовать для создания более конкретных макетов для прямой печати. Просто соедините вместе нужный HTML-код и данные, сохраните их в файле .html и откройте в браузере для печати. Таблицы HTML особенно полезны для печати строк и столбцов данных. Чтобы увидеть практический пример, изучите функцию печати в приложении электронных таблиц Nano-Sheets, показанном ранее в руководстве.

Чтобы заставить страницу печататься автоматически, добавьте функцию Javascript "printthispage()" к тегам head и body вашего HTML-документа:

REBOL [title: "Automatic HTML Printing"]
write %autoprint.html {
    <html>
        <head>
            <title>Automatically Printed Page</title>
            <script type="text/javascript">
            <!--
              function printthispage() {
              window.print();  
              }
              //-->
            </script>
        </head>
        <body onload="printthispage()">
            This Page Will Print Automatically.
        </body>
    </html>
}
browse %autoprint.html

16.27.3 Принтер для гитарных аккордов

Приведённая ниже программа создаёт, сохраняет и распечатывает коллекции диаграмм гитарных аккордов грифа. Он был разработан, чтобы помочь студентам-гитаристам распечатать диаграммы аккордов в авторском бизнесе по обучению музыке. Код демонстрирует некоторые общие и полезные методы работы с файлами, данными и графическим интерфейсом, в том числе технику перетаскивания (drag-and-drop) "feel", используемую здесь для перемещения элементов по экрану. Он также демонстрирует технику вывода на печать в HTML, а затем предварительного просмотра в браузере (для печати на бумаге, загрузки на веб-сайт и т.д.):

REBOL [Title: "Guitar Chord Diagram Printer"]

; Загрузите несколько файлов изображений, которые были встроены с 
; помощью сценария "встраивание двоичных ресурсов", описанного ранее 
; в руководстве:

fretboard: load 64#{
iVBORw0KGgoAAAANSUhEUgAAAFUAAABkCAIAAAB4sesFAAAACXBIWXMAAAsTAAAL
EwEAmpwYAAAA2UlEQVR4nO3YQQqDQBAF0XTIwXtuNjfrLITs0rowGqbqbRWxEEL+
RFU9wJ53v8DN7Gezn81+NvvZXv3liLjmPX6n/4NL//72s9l/QGbWd5m53dbc8/kR
uv5RJ/QvzH42+9nsZ7OfzX62nfOPzZzzyNUxxh8+qhfVHo94/rM49y+b/Wz2s9nP
Zj+b/WzuX/cvmfuXzX42+9nsZ7OfzX4296/7l8z9y2Y/m/1s9rPZz2Y/m/vX/Uvm
/mWzn81+NvvZ7Gezn8396/4l2/n+y6N/f/vZ7Gezn81+tjenRWXD3TC8nAAAAABJ
RU5ErkJggg==
}

barimage: load 64#{
iVBORw0KGgoAAAANSUhEUgAAAEoAAAAFCAIAAABtvO2fAAAACXBIWXMAAAsTAAAL
EwEAmpwYAAAAHElEQVR4nGNsaGhgGL6AaaAdQFsw6r2hDIa59wCf/AGKgzU3RwAA
AABJRU5ErkJggg==
}

dot: load 64#{
iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAACXBIWXMAAAsTAAAL
EwEAmpwYAAAAFElEQVR4nGNsaGhgwA2Y8MiNYGkA22EBlPG3fjQAAAAASUVORK5C
YII=
}

; Следующие строки определяют дизайн графического интерфейса. 
; Приведённая ниже процедура была определена в разделе о "feel" :
movestyle: [
    engage: func [f a e] [
        if a = 'down [
            initial-position: e/offset
            remove find f/parent-face/pane f
            append f/parent-face/pane f
        ]
        if find [over away] a [
            f/offset: f/offset + (e/offset - initial-position)
        ]
        show f
    ]
]

; С этим определением добавление "feel movestyle" к любому виджету 
; делает его подвижным в графическом интерфейсе. Это очень полезно 
; для всех видов графических приложений. Если вы хотите продолжить 
; создание графических макетов, которые реагируют на 
; пользовательские события, очень важно узнать все о том, как 
; "feel" (ощущения/чувство) работают в REBOL. См. URL-адрес выше для 
; получения дополнительной информации.
gui: [

    ; Делаем фон графического интерфейса белым:

    backdrop white

    ; Покажите изображение грифа и измените его размер (сохранённое 
    ; изображение на самом деле составляет всего 85x100 пикселей):

    currentfretboard: image fretboard 255x300

    ; Покажем изображение "барэ", измените его размер и сделайте 
    ; подвижным. Обратите внимание на "feel movestyle" (стиль 
    ; движения). Вот что позволяет перетаскивать:

    currentbar: image barimage 240x15 feel movestyle

    ; Небольшой текст инструкции:

    text "INSTRUCTIONS:" underline
    text "Drag dots and other widgets onto the fretboard."
    across    
    text "Resize the fretboard:"

    ; "tab" выравнивает следующий элемент графического интерфейса 
    ; пользователя с предварительно заданным разделителем столбцов:

    tab 

    ; Поворотная кнопка ниже позволяет выбрать размер грифа. В блоке 
    ; действий изменяется размер изображения грифа, а затем размер 
    ; изображения панели также изменяется в соответствии с выбранным 
    ; значением. Благодаря этому размер такта будет правильно 
    ; пропорционален изображению грифа. После каждого изменения 
    ; размера графический интерфейс обновляется, чтобы фактически 
    ; отображать изменённое изображение. Слово "show" (показать) 
    ; обновляет отображение графического интерфейса пользователя. 
    ; Это необходимо делать всякий раз, когда виджет изменяется в 
    ; графическом интерфейсе. Имейте в виду, что отсутствие 
    ; показа (show) изменённого элемент графического интерфейса - 
    ; это источник ошибок, который легко упустить из виду:
rotary "255x300" "170x200" "85x100" [
    currentfretboard/size: to-pair value show currentfretboard
    switch value [
        "255x300" [currentbar/size: 240x15 show currentbar]
        "170x200" [currentbar/size: 160x10 show currentbar]
        "85x100" [currentbar/size: 80x5 show currentbar]
    ]
]

return

; Блок действий кнопки ниже запрашивает имя файла у
; пользователя, а затем сохраняет текущее изображение грифа в 
; это имя файла:

button "Save Diagram" [
    filename: to-file request-file/save/file "1.png"
    save/png filename to-image currentfretboard
]

tab

; Блок действий кнопки ниже распечатывает выбранный 
; пользователем набор изображений на HTML-страницу, где их можно 
; просматривать вместе, загружать в Интернет, отправлять на 
; принтер и т.п.
button "Print" [

    ; Получите список файлов для печати:

    filelist: sort request-file/title "Select image(s) to print:"

    ; Начните создавать блок для размещения HTML-макета для 
    ; печати и присвойте ему метку "html":

    html: copy "<html><body>"

    ; Этот цикл foreach создаёт макет HTML, который отображает 
    ; каждое из выбранных изображений:

    foreach file filelist [
        append html rejoin [
            {<img src="file:///} to-local-file file {">}
        ]
    ]

    ; Следующая строка завершает макет HTML:

    append html [</body></html>]

    ; Теперь переменная "html" содержит полный HTML-документ, 
    ; который можно записать на жёсткий диск и открыть в 
    ; браузере по умолчанию. Приведённый ниже код выполняет это:
write %chords.html trim/auto html
browse %chords.html 

]
]

; Каждый из следующих циклов помещает 50 подвижных точек в 
; графический интерфейс пользователя в одних и тех же местах. Это 
; создаёт три стопки точек, которые пользователь может перемещать по 
; экрану и помещать на гриф. Есть три размера, чтобы приспособить 
; функцию изменения размера изображения грифа. Обратите внимание на 
; код "feel movestyle" в конце каждой строки. Опять же, это то, что 
; делает каждую точку перетаскиваемой:
loop 50 [append gui [at 275x50 image dot 30x30 feel movestyle]]
loop 50 [append gui [at 275x100 image dot 20x20 feel movestyle]]
loop 50 [append gui [at 275x140 image dot 10x10 feel movestyle]]

; Следующие циклы добавляют в графический интерфейс несколько 
; дополнительных перетаскиваемых виджетов:

loop 6 [append gui [at 273x165 text "X" bold feel movestyle]]
loop 6 [append gui [at 273x185 text "O" bold feel movestyle]]

view layout gui

16.27.4 Направление печати в системный браузер по умолчанию или в установленные приложения

Как было продемонстрировано ранее, вы можете открывать и распечатывать созданные HTML-документы, используя ваш системный браузер по умолчанию:

browse %example.html

Сторонние браузеры, такие как Off By One, обеспечивают согласованный вывод и предоставляют параметры командной строки, такие как автоматическая печать, так что файлы печатаются без видимого стороннего интерфейса.

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

REBOL [title: "HTML printing"]
unless exists? %/OB1.exe [
    write/binary %/OB1.exe to-binary decompress {...сжатый файл...}
]
unless exists? %html-choice.txt [
    write %html-choice.txt "none" ; or for example "%OB1.exe"
]
browser-choice: read %browser-choice.txt
view layout [
    btn "Choose HTML Browser Application" [
        html-choice-file: to-file request-file/filter "*.exe" "*.exe"
        write %html-choice.txt html-choice-file
        html-choice: to-local-file to-file read %html-choice.txt
    ]
    btn "Print" [
        either browser-choice = "none" [
            if error? try [browse current-file] [
                write %browser-choice.txt "OB1.exe"
                call/show rejoin [
                    "OB1.exe file:/// " 
                    (to-local-file current-file)
                ]
            ]
        ] [
            call/show browser-string: rejoin [
                (to-local-file browser-choice)
                " file:///C:/" 
                (replace current-file "./" "")
            ]
        ]

    ]
]

Обязательно изучите приложение RebGUI "СИСТЕМА ПРОДАЖИ", представленное ранее, чтобы увидеть ещё один пример печати HTML. В этом примере печатаются красиво оформленные кассовые чеки для клиентов.

16.27.5 Печать в PDF

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

Как было показано ранее, вы можете открывать и распечатывать созданные документы с помощью установленного в системе средства просмотра PDF-файлов:

call %example.pdf

Сторонние программы чтения, такие как программа чтения Foxit, работают быстро и предоставляют такие параметры командной строки, как автоматическая печать, чтобы файлы печатались автоматически. Sumatra PDF - это небольшая бесплатная программа для чтения PDF-файлов с открытым исходным кодом, которую можно сжимать и встраивать непосредственно в код REBOL. Вы можете скачать её здесь.

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

REBOL [title: "PDF printing"]
current-file: %file.pdf
unless exists? %SumatraPDF.exe [
    write/binary %SumatraPDF.exe to-binary decompress {.сжатый файл.}
]
unless exists? %pdf-choice.txt [
    write %pdf-choice.txt "none" ; or for example "%SumatraPDF.exe"
]
pdf-choice: read %pdf-choice.txt
view layout [
    btn "Choose PDF Application" [
        pdf-choice-file: to-file request-file/filter "*.exe" "*.exe"
        write %pdf-choice.txt pdf-choice-file
        pdf-choice: to-local-file to-file read %pdf-choice.txt
    ]
    btn "Print" [
        either pdf-choice = "none" [
            if error? try [call current-file] [
                write %pdf-choice.txt "SumatraPDF.exe"
                call/show rejoin [
                    (to-local-file %SumatraPDF.exe) " " 
                    (to-local-file )
                ]
            ]
        ] [
            call/show rejoin [
                (to-local-file read %pdf-choice.txt) " " 
                (to-local-file current-file)
            ]
        ]
    ]
]

16.27.6 Печать в текст

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

REBOL [title: "Simple POS"]
make-dir %./receipts/
make-dir %./inventory/
make-dir %./users/
write/append %./inventory/sales.txt ""
; Вот несколько примеров данных для тестирования программы. 
; Отредактируйте распечатанный файл% header.txt. Войдите в 
; программу, используя "user"/"user" в качестве 
; имени пользователя/пароля. Введите "11111111" и "22222222" в 
; качестве штрих-кода. 
; --------------------------------------------------------------------
write/binary %./users/users compress trim {"user" "user" "" ""}
save %./inventory/inventory.txt [
    ; barcode description price taxable cost date qty (8 blank fields)
    "11111111" "Item 1" $1.00 "y" $5.00 "24-jun-2011" 40 
    "" "" "" "" "" "" "" ""
    "22222222" "Item 2" $1.00 "y" $.50  "23-jun-2011" 999 
    "" "" "" "" "" "" "" ""
]
write %header.txt rejoin [
    "Company Name" newline
    "1234 Street Road" newline
    "Townsville, PA  98765" newline
    "800-833-7273"
]
; --------------------------------------------------------------------
users: load decompress read/binary %./users/users
if not find users user: request-pass [alert "Incorrect Login" quit]
database: load %./inventory/inventory.txt
divider: "  |  "
blank-line: 
    "___________________________________________________________________"
pad: func [strng lngth] [
    insert/dup tail strng " " lngth
    copy/part strng lngth 
]
display-qty: does [
    if error? try [idx: index? find database f1/text] [
        alert "barcode error"
        focus f1 return
    ]
    qty: pick database (idx + 6)
    alert join "Quantity: " qty
]
submit: does [
    either f1/text <> "" [
        if error? try [idx: index? find database f1/text] [
            alert "barcode error"
            focus f1 return
        ]
        item: pick database (idx + 1)
        price: pick database (idx + 2)
        taxable: pick database (idx + 3)
        f2/text: copy form item
        f3/text: copy form price
    ] [
        if ((f2/text = "") or 
        (f3/text = "") or 
        (error? try [to-money f3/text])) [
            alert "Data Entry Error" 
            focus f1 return
        ]
        item: f2/text
        price: f3/text
        taxable: either true = request "Taxable?" ["y"] ["n"]
    ]
    insert head a1/text rejoin [
        (pad form item 25) divider
        (pad form f1/text 15) divider 
        (pad form price 10) divider
        taxable newline
    ]
    recalculate-totals
    focus f1
    show gui
]
recalculate-totals: does [
    my-items: copy []
    my-lines: parse/all (copy a1/text) "^/"
    f7/text: copy form length? my-lines
    foreach line my-lines [
        current-line: parse/all line "|"
        foreach item current-line [
            append my-items trim item
        ]
    ]
    subtotal: $0
    tax: 0
    total: $0
    foreach [item barcode price taxable] my-items [
        subtotal: subtotal + to-decimal (replace form price "$" "")
        if taxable = "y" [
            tax: tax + ((to-decimal replace form price "$" "") * .06)
        ]
        total: to-money subtotal + tax
    ]
    f4/text: form subtotal
    f5/text: form tax
    f6/text: form total
    show gui
]
saveprint: does [
    x: now  y: now/time
    write to-file filename: rejoin [
        %./receipts/
        x/year "-" x/month "-" x/day "_" 
        y/hour "-" y/minute "-" to-integer y/second "_" 
        user/1 ".txt"
    ] rejoin [
        (read %header.txt) newline
        newline newline
        "Item:                        Barcode:"
                                          "             Price:       Tax:"
        newline    
        blank-line
        newline newline
        a1/text
        newline
        blank-line
        newline newline
        "Subtotal:  " f4/text "  (" f7/text " items)    Tax:  " f5/text 
        "       TOTAL:  " f6/text newline
        blank-line
        newline newline newline
        "Receipt:  " (replace copy filename "./receipts/" "") 
        "    Payment Type:  " d1/text "  $" f8/text " -" f9/text 
        newline a2/text
    ]
    write to-file rejoin [
        %./inventory/sales--
        x/year "-" x/month "-" x/day "_" 
        y/hour "-" y/minute "-" to-integer y/second "_" 
        user/1 ".txt"
    ] read %./inventory/sales.txt
    write/append %./inventory/sales.txt rejoin [x newline a1/text]
    f1/text: copy "" f2/text: copy "" f3/text: copy "" 
    f4/text: copy "" f5/text: copy "" f6/text: copy "" f7/text: copy ""
    a1/text: copy " " a2/text: copy "" f8/text: copy "" f9/text: copy ""
    show gui
    replace a1/text " " "" show gui  ; eliminate area bug
    call filename  ; call/show join "metapad " filename  ; specify editor
]
delete-item: does [
    my-items: copy []
    my-lines: parse/all (copy a1/text) "^/"
    foreach item my-lines [append item newline]
    a1/text: form copy at my-lines 2
    show a1
    recalculate-totals
]
tend: does [
    f9/text: form ((to-money f8/text) - (to-money f6/text)) show f9
    if (to-money f9/text) < $0 [
        alert "Not enough money tendered to pay for items!"
    ]
]
svv/vid-styles/area/colors: [240.240.240 255.255.255]
svv/vid-styles/field/colors: [240.240.240 255.255.255]
svv/vid-styles/area/font/name: "courier"
svv/vid-styles/area/font/size: 12
svv/vid-styles/field/font/size: 20
svv/vid-face/color: white
insert-event-func [
    either event/type = 'close [
        if true = request "Really close the program?" [quit]
    ] [event]
]
view center-face gui: layout [
    size 1024x550
    across
    style txt text bold right 90x35 font-size 14 middle
    style fld field 100x35 
    box black 984x2 return
    txt 70 "Barcode:" [display-qty] f1: fld 120
    txt "Description:" f2: fld 200
    txt "Price:" 50 f3: fld 100
    loc: at txt 10 "" btn 130x35 font-size 14 "Delete Item" [delete-item]
    btn 145x35 font-size 14 "Save and Print" [saveprint]
    return
    box black 984x2 return
    text "Description:" pad 280 text "Bar Code:" pad 180 
    text "Price:" pad 100 
    text "Taxable:" return
    a1: area 984x300 [recalculate-totals]
    return
    loc2: at txt "" (loc/1 + 20) txt "SubTotal:" f4: fld 160 return 
    txt "" (loc/1 + 20) txt "Tax:" f5: fld 160 return
    txt "" (loc/1 + 20) txt "Total:" f6: fld 160 return
    at loc2 a2: area 400x70 "" font-size 12
    text "" 75 loc3: at txt "Item count: " 100 f7: fld 50
    at (loc3 + 20x50) d1: drop-down 138 "Cash" "Credit" "Check" "Other"
    at (loc2 + 0x80) txt 70 "Tendered:" font-size 12 f8: field 120 [tend]
    txt 60 "Change:" font-size 12 f9: field 120 
    key #"^M" [if form system/view/focal-face/var = "f1" [submit]]
    do [focus f1] 
]

Одним из преимуществ этого варианта печати является то, что данные в сохранённых документах для печати структурированы и могут использоваться для создания отчётов. Эта программа печатает отчёты о продажах на основе квитанций, созданных с помощью указанной выше системы:

REBOL [title: "Simple POS Sales Report Printer"]
nl: newline
start-date: request-date
if start-date = none [quit]
start-time: to-time request-text/title/default "Start Time:" "12:01am"
if ((start-time = none) or (start-time = 0:00)) [quit]
end-date: request-date
if end-date = none [quit]
end-time: to-time request-text/title/default "End Time:" "11:59pm"
if ((end-time = none) or (end-time = 0:00)) [quit]
flash "Processing..."
files: read %./receipts/
files-within-timeframe: copy [] 
foreach file files [
    if 
    (%.txt = suffix? file) and 
    (file <> %deleted.txt) and 
    (file <> %deleted--backup.txt) [
        parsed-filename: parse file "_." 
        the-date: to-date first parsed-filename 
        the-time: to-time replace (second parsed-filename) "-" ":" 
        username: third parsed-filename 
        if (the-date > start-date) and (the-date < end-date) [
            append files-within-timeframe file
        ]
        if (the-date = start-date) and (the-date < end-date) [
            if the-time > start-time [
                append files-within-timeframe file
            ]
        ]
        if (the-date > start-date) and (the-date = end-date) [
            if the-time < end-time [
                append files-within-timeframe file
            ]
        ] 
        if (the-date = start-date) and (the-date = end-date) [
            if (the-time > start-time) and (the-time < end-time) [
                append files-within-timeframe file
            ]
        ]            
    ]  
]
total-items: 0
total-sales: $0
total-tax: 0
total-sales-plus-tax: $0
total-taxable: 0
total-nontaxable: 0
total-cash: $0
subtotal-cash: $0
tax-cash: 0
total-credit: $0
subtotal-credit: $0
tax-credit: 0
total-check: $0
subtotal-check: $0
tax-check: 0
total-other: $0
subtotal-other: $0
tax-other: 0
a-line: copy {}  loop 67 [append a-line "_"]
items-sold: to-string rejoin [
    {REPORT PERIOD:  } start-date {, } start-time { to } end-date {, }
    end-time nl nl a-line nl
]
foreach file files-within-timeframe [
    sale: read/lines join %./receipts/ file
    items-start: copy next next find sale a-line
    items-end-index: (index? find items-start a-line) - 2
    items-in-sale: copy/part items-start items-end-index
    either items-in-sale <> [] [
        num-of-items: length? items-in-sale
        totals: parse (pick items-start (items-end-index + 4)) none
        other-info: parse (pick items-start (items-end-index + 8)) none
        subtotal: totals/2
        current-tax: totals/6
        receipt-grand-total: totals/8
        filename: other-info/2
        parsed-filename: parse file "_." 
        the-date: to-date first parsed-filename 
        the-time: to-time replace (second parsed-filename) "-" ":" 
        username: third parsed-filename 
        payment-type: other-info/5
        append items-sold "^/--------------------------------------------"
        append items-sold rejoin [
            nl the-date ", " the-time ", " username ", " payment-type
        ]
        append items-sold "^/--------------------------------------------"
        foreach i items-in-sale [
            append items-sold rejoin [nl i]
        ]
        total-items: total-items + num-of-items
        total-sales: total-sales + (to-money subtotal)
        total-tax: total-tax + to-decimal current-tax  
        ; (to-decimal replace (to-string current-tax) "$" "")
        total-sales-plus-tax: 
            total-sales-plus-tax + (to-money receipt-grand-total)
        switch payment-type [
            "Cash" [
                total-cash: total-cash + (to-money receipt-grand-total)
                subtotal-cash: subtotal-cash + (to-money subtotal)
                tax-cash: tax-cash + (to-decimal current-tax)
            ]
            "Credit" [
                total-credit: 
                    total-credit + (to-money receipt-grand-total)
                subtotal-credit: subtotal-credit + (to-money subtotal)
                tax-credit: tax-credit + (to-decimal current-tax)
            ]
            "Check" [
                total-check: 
                    total-check + (to-money receipt-grand-total)
                subtotal-check: subtotal-check + (to-money subtotal)
                tax-check: tax-check + (to-decimal current-tax)
            ]
            "Other" [
                total-other: total-other + (to-money receipt-grand-total)
                subtotal-other: subtotal-other + (to-money subtotal)
                tax-other: tax-other + (to-decimal current-tax)
            ]
        ]
    ] [
        if true = request rejoin ["Delete empty receipt " file "?"] [
            delete rejoin [%./receipts/ file]
        ]
    ]
]
total-taxable: total-tax / .06  
total-nontaxable: 
    (to-decimal replace (to-string total-sales) "$" "") - total-taxable
items-sold: rejoin [
    items-sold nl nl a-line nl nl
    "        Subtotal:  " total-sales "  ("  total-items " items)" nl nl
    "         Taxable:  $" total-taxable nl
    "     Non-Taxable:  $" total-nontaxable nl nl
    "             Tax:  $" total-tax  nl a-line nl nl
    "     GRAND TOTAL:  " total-sales-plus-tax nl a-line nl nl
    "   Cash Subtotal:  " subtotal-cash nl 
    "        Cash Tax:  $" tax-cash nl 
    "      Cash Total:  " total-cash nl nl
    " Credit Subtotal:  " subtotal-credit nl 
    "      Credit Tax:  $" tax-credit nl 
    "    Credit Total:  " total-credit nl nl
    "  Check Subtotal:  " subtotal-check nl 
    "       Check Tax:  $" tax-check nl 
    "     Check Total:  " total-check nl nl
    "  Other Subtotal:  " subtotal-other nl
    "       Other Tax:  $" tax-other nl
    "     Other Total:  " total-other
]
unview
; editor items-sold
write %temp.txt items-sold
call/show rejoin ["notepad " to-local-file what-dir "\temp.txt"]
wait 1 delete %temp.txt

16.27.7 CUPS, Crystal Reports и другие решения

Ниже приведены несколько ссылок на скрипты на rebol.org, которые помогают создавать печатные документы. Обучение использованию других библиотек и инструментов, специфичных для других операционных систем, обычно требует дополнительной работы, но устраняет необходимость в сторонних программных интерфейсах, таких как средства просмотра изображений, HTML и PDF:

https://github.com/dockimbel/printer-driver (CUPS - работает в Windows, Linux, Mac) http://www.rebol.org/view-script.r?script=can-rebol-print.r http://www.rebol.org/view-script.r?script=crystal-reports.r http://www.rebol.org/view-script.r?script=make-doc-pro.r

16.28 Приложение для удалённой печати чеков

Приведённое ниже приложение позволяет работникам сайта составлять чеки. Владелец или менеджер могут утверждать чеки удалённо (онлайн), а затем работники могут загружать и распечатывать их локально. Эта система использует RebGUI, структуру PDF и функцию "вербализации", продемонстрированную ранее в этом тексте. Точные позиции печати хранятся в строке контрольных позиций (её можно загрузить из текстового файла для облегчения редактирования):

REBOL [title: "Remote Check Writer"]
do %rebgui.r
do %pdf-maker.r
bank-balance: 1000000
write/binary %bank-balance compress copy to-string bank-balance
default-amount: 99
check-memo-text: "Sales 12-1-2012 to 12-31-2012"
signature-bmp: to-binary decompress 64#{
    eJztyrERQGAMgNGgt4LOmUKhN4X1jGEaS0T4d1C9l/suRbLt9xyftVqqaYg4anc1
    r7Pu19jq22tkZgAAAAAAAAAAAMAfHpszCALqJgAA
}
ftp1: ftp://user:pass@site.com/folder/
booths: [
    "1" "John Smith" "1" "1 Street Rd. Larville, PA 98765" 
        "123-555-1212" "john@site.com" {Notes about John.}
    "2" "Paul Jones" "2" "2 Road Pl. Bonville, PA 87654" 
        "234-555-2323" "paul@site.net" {Notes about Paul.}
    "3" "Tim Maul" "3" "3 Lane Rd. Panville, PA 76543" 
        "345-555-3434" "tim@site.org" {Notes about Tim.}
]
check-positions: do {
    check-date-position-x: 135 
    check-date-position-y: 228 
    check-payable-position-x: 30 
    check-payable-position-y: 228 
    check-amount-position-x: 175 
    check-amount-position-y: 228 
    check-amount-text-position-x: 25 
    check-amount-text-position-y: 215 
    check-notes-position-x: 25 
    check-notes-position-y: 190 
    check-signature-position-x: 130 
    check-signature-position-y: 205
}
verbalize: func [a-number] [
    if error? try [a-number: to-decimal a-number] [
        return "** Error **  Input must be a decimal value"
    ]
    if a-number = 0 [return "Zero"]
    the-original-number: round/down a-number
    pennies: a-number - the-original-number
    the-number: the-original-number
    if a-number < 1 [
        return join to-integer ((round/to pennies .01) * 100) "/100"
    ] 
    small-numbers: [
        "One" "Two" "Three" "Four" "Five" "Six" "Seven" "Eight"
        "Nine" "Ten" "Eleven" "Twelve" "Thirteen" "Fourteen" "Fifteen"
        "Sixteen" "Seventeen" "Eighteen" "Nineteen"
    ]
    tens-block: [
        { } "Twenty" "Thirty" "Forty" "Fifty" "Sixty" "Seventy" "Eighty"
        "Ninety"
    ]
    big-numbers-block: ["Thousand" "Million" "Billion"]    
    digit-groups: copy []
    for i 0 4 1 [
        append digit-groups (round/floor (mod the-number 1000))
        the-number: the-number / 1000
    ]    
    spoken: copy ""
    for i 5 1 -1 [
        flag: false
        hundreds: (pick digit-groups i) / 100
        tens-units: mod (pick digit-groups i) 100
        if hundreds <> 0 [
            if none <> hundreds-portion: (pick small-numbers hundreds) [
                append spoken join hundreds-portion " Hundred "
            ]
            flag: true
        ]
        tens: tens-units / 10
        units: mod tens-units 10
        if tens >= 2 [
            append spoken (pick tens-block tens)
            if units <> 0 [
                if none <> last-portion: (pick small-numbers units) [
                    append spoken rejoin [" " last-portion " "]
                ]
                flag: true
            ]
        ]
        if tens-units <> 0 [
            if none <> tens-portion: (pick small-numbers tens-units) [
                append spoken join tens-portion " "
            ]
            flag: true
        ]
        if flag = true [
            commas: copy {}    
            case [
                ((i = 4) and (the-original-number > 999999999)) [
                    commas: {billion, }
                ]
                ((i = 3) and (the-original-number > 999999)) [
                    commas: {million, }
                ]
                ((i = 2) and (the-original-number > 999)) [
                    commas: {thousand, }
                ]
            ]
            append spoken commas
        ]
    ]
    append spoken rejoin [
        "and " to-integer ((round/to pennies .01) * 100) "/100"
     ]
    return spoken
]
print-check: does [
    unless ["admin" "admin"] = request-password [return]
    display/dialog "Write Check" [
        after 1
        text (join "Bank Account Balance: $" (to-string bank-balance))
        text "Pay To:"
        check-payable-list: drop-list "" 50x5 data (
            sort (extract/index (copy booths) 7 2)
        ) [
            set-text check-payable pick 
                check-payable-list/data check-payable-list/picked
        ]
        check-payable: field
        text "Amount:"
        check-amount: field (join "$" (default-amount)) [
            set-text check-amount-text (
                verbalize replace (copy check-amount/text) "$" ""
            )
        ]
        text "Amount (text, written out):"
        check-amount-text: field (verbalize copy total-f/text)
        text "Check Date:"
        check-date: field (form now/date)
        text "Check #:"
        check-number: field
        text "Notes:" 
        check-notes: field (check-memo-text)
        text ""
        after 2
        reverse
        button "Cancel" [hide-popup return]
        button "Print" #Y  [
            if (find reduce [
                check-payable/text check-amount/text 
                check-amount-text/text
                check-date/text check-number/text
            ] "" ) [alert "Every field must be filled." return]
            bank-balance: to-decimal decompress read/binary %bank-balance
            new-bank-balance: (
               bank-balance - (
                   to-decimal (replace/all check-amount/text "$" "")
               )
            )
            if new-bank-balance < 10.00 [
                alert "Bank account balance too low to write this check!"
                return
            ]
            write/binary %bank-balance 
                compress copy to-string new-bank-balance
            bank-balance: to-decimal decompress read/binary %bank-balance
            make-dir %./checks/
            current-check: to-file rejoin [
                %./checks/ check-number/text ".pdf"
            ]
            if exists? current-check [
                either true = trim question {
                     This check # already exists.  Overwrite?
                } [
                    either true = question {Are you sure?} [
                        write/binary (to-file (rejoin [
                            %./checks/
                            check-number/text
                            "_overwritten_" now/date "_"
                            (replace/all (to-string now/time) ":" "-")
                            ".pdf"
                        ])) read/binary to-file rejoin [
                            %./checks/ check-number/text ".pdf"
                        ]
                        delete to-file rejoin [
                            %./checks/ check-number/text ".pdf"
                        ]
                    ] [return]
                ] [return]
            ]
            error-uploading: false
            if error? try [
                write/binary (
                   the-ftp1: rejoin [
                       ftp1 (to-string second split-path current-check)
                   ]
                ) 
                the-current-pdf: layout-pdf 
                entire-check-layout: compose/deep [[
                    page size 215.9 279.4
                    textbox (check-date-position-x) 
                    (check-date-position-y) 75 10 
                    [font Helvetica 3 (check-date/text)]
                    textbox (check-payable-position-x)
                    (check-payable-position-y) 90 10 
                    [font Helvetica 3 (check-payable/text)]
                    textbox (check-amount-position-x)
                    (check-amount-position-y) 75 10 
                    [font Helvetica 3 (check-amount/text)]
                    textbox (check-amount-text-position-x)
                    (check-amount-text-position-y) 90 10 
                    [font Helvetica 3 (check-amount-text/text)]
                    textbox (check-notes-position-x)
                    (check-notes-position-y) 70 20 
                    [font Helvetica 3 (check-notes/text)]
                    image (check-signature-position-x) 
                    (check-signature-position-y) 75 12.5 
                    (load signature-bmp)
                ]]
            ][
                alert {
                    *** ERROR *** Check NOT saved!!!
                    Check the Internet connection and try again.
                }
                error-uploading: true
            ]
            save to-file (replace copy current-check ".pdf" ".un") 
                the-compressed-check: compress to-string the-current-pdf
            if error? try [
                save rejoin [
                    ftp1 to-string second split-path 
                    (to-file (replace copy current-check ".pdf" ".un"))
                ] 
                the-compressed-check: compress to-string the-current-pdf
            ] [
                alert {
                    *** ERROR *** Check NOT saved!!! 
                    Check the Internet connection and try again.
                }  
                error-uploading: true
            ]
            hide-popup
            if error-uploading = false [
                alert "Saved (awaiting approval)"
            ]
        ]
    ]
]
publish-checks: does [
    current-approved-check-list: copy {Approved Checks:^/^/}
    if error? try [
        approved-checks: to-block load http://site.com/register.txt
    ] [
        alert "*** ERROR: Check the Internet connection and try again."
        return
    ]
    foreach file read %./checks/ [
        if %.un = suffix? file [
            current-check-un: join %./checks/ file
            current-check: to-file replace 
                    copy current-check-un ".un" ".pdf"
            current-check-conf-num: to-integer second split-path to-file
                    replace copy current-check-un ".un" ""
            if find approved-checks current-check-conf-num [
                unless exists? current-check [
                    append current-approved-check-list rejoin [
                        current-check newline
                    ]
                    write/binary current-check 
                        to-binary decompress load current-check-un
                ]
            ]
        ]
    ]
    current-approved-check-list-file: to-file rejoin [
        "./checks/" now/date "_" 
        replace/all copy form now/time ":" "-"
        ".txt"
    ]
    write current-approved-check-list-file current-approved-check-list
    attempt [call current-approved-check-list-file]
]
view-checks: does [
    check-to-view: to-file request-file/file/filter 
        %./checks/1.pdf "*.pdf" "*.pdf"
    either pdf-choice = "none" [
        if error? try [call check-to-view] [
            write %pdf-choice.txt "SumatraPDF.exe"
            call rejoin [
                "SumatraPDF.exe " (to-local-file check-to-view)
            ]
        ]
    ] [
        call (to-local-file check-to-view)
    ]
]

16.29 Создание приложений на платформах, не поддерживающих графические интерфейсы пользователя

REBOL с открытым исходным кодом в настоящее время переносится на многие платформы, которые ещё не поддерживают графический интерфейс. Часто приложения CGI можно запускать в Интернет-браузерах по умолчанию в большинстве новых операционных систем без каких-либо изменений. Многие из программ REBOL CGI в этом руководстве были запущены в коммерческих средах в браузерах iPhone и Android с момента появления этих платформ.

В системах, которые в настоящее время не имеют портированной системы графического интерфейса REBOL, а также для программ, которые должным образом не разработаны как сценарии CGI, взаимодействие с основной консолью может использоваться для обеспечения практического пользовательского интерфейса. Приведённый ниже сценарий является простой заменой графического интерфейса пользователя, который собирает вводимый текст из полей и раскрывающихся списков. Просто укажите блок меток и блок значений по умолчанию для каждого поля (ответы). Если блок ответов содержит вложенный блок, значения в этом блоке будут использоваться в качестве элементов в селекторе списка (так что пользователям не нужно вводить длинный ответ и/или они могут выбирать из заранее определённого списка параметров. - аналогично текстовому списку графического интерфейса или раскрывающемуся селектору). Эти скрипты работают во всех версиях REBOL, как R2, так и R3, с минимальной поддержкой консоли:

REBOL [title: "Textual User Interface"]
; ------------------------------------------------------------------------
labels: ["First Name" "Last Name" "Favorite Color" "Address" "Phone"]
answers: copy ["" "" ["Red" "Green" "Blue" "Tan" "Black"] "" ""]
; ------------------------------------------------------------------------
; if system/build/date > 1-jan-2011 [
    newpage: copy {} loop 50 [append newpage "^/"]
; ]
if (length? labels) <> (length? answers) [
    print join newpage "'Labels and 'answers blocks must be equal length."
    halt
]
len: length? labels
lngth: 0
spaces: "    "
foreach label labels [
    if (l: length? label) > lngth [lngth: l]
]
pad: func [strng] [
    insert/dup tail str: join copy strng "" " " lngth
    join copy/part str (lngth) spaces
]
forever [
    prin newpage
    repeat i len [
        either ((answers/:i = "") or ((type? answers/:i) = block!)) [
            ans: ""
        ][
            ans: answers/:i
        ]
        prin rejoin [i ")  " pad labels/:i "|" spaces ans newline]
    ]
    prin rejoin [newline (len + 1) ")  SUBMIT"]
    choice: ask {^/^/}
    either error? try [num: to-integer choice] [] [
        either block? drop-down: answers/:num [
            print ""
            repeat i l: length? drop-down [
                prin rejoin [i ")  " pad form drop-down/:i newline]
            ]
            prin rejoin [(l + 1) ")  " pad "Other" newline]
            drop-choice: ask rejoin [{^/Select } labels/:num {:  }]
            either error? try [d-num: to-integer drop-choice] [] [
                either d-num = (l + 1) [
                    if "" <> resp: ask rejoin [
                        {^/Enter } labels/:num {:  }
                    ] [answers/:num: resp]
                ][
                    chosen: pick drop-down d-num
                    if ((chosen <> none) and (chosen <> (l + 1))) [
                        answers/:num: chosen
                    ]
                ]
            ]
        ][
            if ((num > 0) and (num <= (len + 1))) [
                either num = (len + 1) [
                    prin newpage probe answers halt  ; END ROUTINE
                ][
                    either answers/:num = "" [
                        ans: ""
                    ][
                        ans: answers/:num
                    ]
                    write clipboard:// ans
                    line: copy {}
                    loop ((length? labels/:num) + 1) [append line "-"]
                    answers/:num: ask rejoin [
                        newpage labels/:num ":  " ans "^/" line "^/^/"
                    ]
                ]
            ]
        ]
    ]
]

Вот консольная версия программы "Групповые заметки", переделанная так, что графический интерфейс не требуется. Вы увидите, что весь вывод данных обрабатывается путём вывода текста на консоль, а весь ввод данных и управление потоком программы обрабатываются с помощью функции "ask" (спросить). Как и в программе "FTP Chat Room", весь ввод и вывод выполняется в рамках цикла "forever" (вечно), который бесконечно запрашивает у пользователя ввод и выводит вывод на основе ответа пользователя. Пользователь может ввести десятичное число, чтобы стереть выбранное сообщение, слово "all", чтобы стереть все сообщения, или слово "name", чтобы переключить имена пользователей:

REBOL [title: "Group Notes (Console)"]
u: ftp://user:pass@site.com/public_html/Notes
if "" = form url: to-url ask join u ":  " [url: u]
name: copy ask "^LName:  "
forever [
    if error? try [notes: copy read/lines url] [write url notes: ""]
    display: copy {}  count: 0
    foreach note reverse notes [
        either note = "" [
            note: "^/"
        ] [
            count: count + 1
            note: rejoin [count ") "note]
        ]
        append display note
    ]
    message: ask rejoin [newpage display "^/Message:  "]
    case [
        message = "" []
        not error? try [indx: to-integer message] [
            remove/part at notes (3 * indx - 2) 3 
            write/lines url reverse notes
            ; write/lines/append join url "-bak" reverse notes
        ]
        message = "erase" [write url ""]
        message = "name" [name: copy ask "^LName:  "]
        message = "url" [
            u: url
            if "" = form url: to-url ask rejoin ["^L" u ":  "] [url: u]
        ]
        true [
            if error? try [
                write/lines/append url rejoin [
                    "^/^/" now " (" name "):  " message
                ]
            ] [print "ERROR: Not Saved" quit]
        ]
    ]
]

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

16.30 Шифрование и безопасность

Чтобы защитить данные от посторонних глаз, REBOL имеет ряд функций шифрования с промышленной надежностью. В простых ситуациях, когда требуется только базовая защита, можно использовать функции "encloak" и "decloak" для шифрования строк и двоичных данных с помощью пароля:

REBOL [title: "Simple Encrypt"]
view layout [
    across
    f1: field 
    btn "Select File" [f1/text: request-file/only  show f1]
    return
    f2: field "(enter password)"
    btn "Encrypt" [
        save request-file/only/save/title/file 
            "Enter encrypted file save name:" ""
            to-file f1/text
            to-binary encloak read to-file f1/text f2/text
        alert "Saved"
    ]
    btn "Decrypt" [editor to-string decloak load to-file f1/text f2/text]
]

Для случаев, когда необходимо обеспечить полную безопасность данных, REBOL обеспечивает шифрование с симметричным ключом (Blowfish и Rijndael/AES), шифрование с открытым ключом RSA, алгоритм цифровой подписи DSA, обмен ключами DH (Diffie Hellman) и доступ SSL/HTTPS/TLS для безопасной передачи данных через сетевые соединения TCP. Также была реализована поддержка OAuth для различных API веб-сайтов.

Приведённый ниже сценарий, написанный Карлом Сассенратом, демонстрирует все основные методы шифрования, описанные в статье с практическими рекомендациями здесь.

REBOL [title: "Encryption"]
request-key: has [pass] [
    pass: request-text/title "Enter a pass-phrase:"
    if pass [checksum/secure pass]
]
crypt: func [
    "Encrypts or decrypts with compression. Returns result."
    data [any-string!] "Data to encrypt or decrypt"
    akey [binary!] "The encryption key"
    /decrypt "Decrypt the data"
    /binary "Produce binary decryption result."
    /local port
][
    port: open [
        scheme: 'crypt
        direction: pick [encrypt decrypt] not decrypt
        key: akey
        padding: true
    ]
    if not decrypt [data: compress data]
    insert port data
    update port
    data: copy port
    close port
    if decrypt [
        data: decompress data
        if not binary [data: to-string data]
    ]
    data
]
if none? op: request ["Select action:" "Encrypt" "Decrypt" "Cancel"][quit]
action: pick ["Encrypt" "Decrypt"] op
if none? files: request-file/title join "Select Files to " action action [
    quit
]
if none? key: request-key [quit]
foreach file files [
    data: read/binary file
    data: either op [crypt data key][crypt/decrypt/binary data key] 
    write/binary file data
]
alert join "File has been " pick ["encrypted." "decrypted."] op

Этот сценарий, также написанный Карлом Сассенратом, демонстрирует, как отправлять зашифрованные данные по электронной почте. Обратите внимание, что здесь используется та же функция "crypt", что и в приведённом выше примере:

REBOL [title: "Send Encrypted Email"]
crypt: func [
    "Encrypts or decrypts with compression. Returns result."
    data [any-string!] "Data to encrypt or decrypt"
    akey [binary!] "The encryption key"
    /decrypt "Decrypt the data"
    /binary "Produce binary decryption result."
    /local port
][
    port: open [
        scheme: 'crypt
        direction: pick [encrypt decrypt] not decrypt
        key: akey
        padding: true
    ]
    if not decrypt [data: compress data]
    insert port data
    update port
    data: copy port
    close port
    if decrypt [
        data: decompress data
        if not binary [data: to-string data]
    ]
    data
]
view layout [
    style lab label right 80x24
    across space 0x4
    vh2 "Send Encrypted Email File:" return
    lab "To:"      f-to: field return
    lab "Subject:" f-sub: field return
    lab "Key:"     f-key: field hide return
    lab "File:"    f-file: text 200x24 white black middle
        "click to pick" [f-file/text: request-file/keep] return
    lab
    button "Send" [unview]
    button "Close" [quit]
]
dest: to-email f-to/data
file: to-file f-file/data
key: checksum/secure f-key/data
subject: f-sub/data
msg: enbase/base crypt read/binary file 64
send/subject email msg subject

Этот пример демонстрирует, как расшифровать защищённые данные, отправленные по электронной почте указанной программой:

REBOL [title: "Decrypt Secure Email"]
crypt: func [
    "Encrypts or decrypts with compression. Returns result."
    data [any-string!] "Data to encrypt or decrypt"
    akey [binary!] "The encryption key"
    /decrypt "Decrypt the data"
    /binary "Produce binary decryption result."
    /local port
][
    port: open [
        scheme: 'crypt
        direction: pick [encrypt decrypt] not decrypt
        key: akey
        padding: true
    ]
    if not decrypt [data: compress data]
    insert port data
    update port
    data: copy port
    close port
    if decrypt [
        data: decompress data
        if not binary [data: to-string data]
    ]
    data
]
view layout [
    style lab label right 80x24
    across space 0x4
    vh2 "Decrypt Email File:" return
    lab "Key:"     f-key: field hide return
    lab "File:"    f-file: text 200x24 white black middle
        "click to pick" [f-file/text: request-file/keep] return
    lab "Data:"    f-data: area return
    lab
    button "Decrypt" [unview]
    button "Close" [quit]
]
file: to-file f-file/data
key: checksum/secure f-key/data
data: f-data/data
data: debase/base data 64
write/binary file crypt/decrypt/binary data key

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

http://www.rebol.com/how-to/encrypt.html

http://www.rebol.com/docs/encryption.html

http://www.rebol.net/cookbook/recipes/0023.html

http://www.rebol.net/cookbook/recipes/0050.html

http://www.rebol.com/docs/ssl.html

http://www.ross-gill.com/page/OAuth_and_REBOL

http://www.ross-gill.com/page/Twitter_API_and_REBOL

http://www.rebol.org/view-script.r?script=rc4.r

http://www.rebol.org/view-script.r?script=arcfour.r

http://www.rebol.org/view-script.r?script=encrypt.r

http://www.rebol.org/view-script.r?script=sha1.r

http://www.rebol.org/view-script.r?script=md5.r

http://www.rebol.org/view-script.r?script=steganography.r

http://www.rebol.org/view-script.r?script=rugby4.r

http://www.rebol.org/view-script.r?script=my-http.r

http://www.rebol.org/view-script.r?script=s-field.r

http://www.rebol.com/docs/services/security.html

http://www.rebol.com/security.html

http://www.rebol.it/power-mezz/#section-5.13

http://www.rebolforces.com/zine/rzine-1-04.html#sect5.

http://stackoverflow.com/questions/3434164/rebol-and-oauth-how-to

16.31 Rebcode

REBOL обеспечивает быструю производительность для наиболее распространённых задач написания сценариев. В ситуациях, когда требуются более высокопроизводительные вычисления (для обработки изображений, математических вычислений с большим циклом и т.д.), виртуальная машина REBOL "Rebcode" действует как своего рода собственный кроссплатформенный язык ассемблера, который может значительно повысить скорость обработки задач, интенсивно использующих ЦП, которые выгода от оптимизации на низком уровне. Rebcode использует синтаксис, аналогичный типичному коду блока/функции REBOL, и позволяет вам получить доступ к переменным, используемым вне контекста Rebcode, но он не предназначен для начинающих программистов. Ребкод структурирован так же, как язык ассемблера, с некоторыми дополнительными преимуществами, такими как возможность использовать встроенные математические функции, циклы и условные оценки, встроенную документацию и возможность работать одинаково на всех процессорах. Ребкод низкого уровня обычно увеличивает скорость работы в 10-30 раз. Использование Rebcode выходит за рамки этого руководства. Для получения дополнительной информации посетите http://www.rebol.com/docs/rebcode.html и http://www.rebol.net/rebcode/, а также выполните поиск в списке рассылки на сайте rebol.org. Обязательно посмотрите примеры на http://www.rebol.net/rebcode/docs/rebcode-demos.html.

Чтобы использовать ребкод, вы должны использовать версию REBOL, загруженную с http://www.rebol.net/builds/, в разделе, помеченном "Каталоги загрузки" (другие не содержат ребкод ВМ). Получите самую последнюю доступную устаревшую версию (для Windows, по крайней мере, rebview1361031.exe). После того, как вы скачали интерпретатор с поддержкой ребкода, попробуйте следующий пример:

do http://www.rebol.net/rebcode/demos/dot-flowers.r

16.32 Полезные инструменты REBOL: XML, Zip, база данных, сеть, веб-сервер и др.

Вот несколько веб-ссылок, содержащих бесплатные модули кода и различные программы, которые могут помочь вам выполнять полезные задачи в REBOL:

http://www.hmkdesign.dk/rebol/list-view/list-view.r - мощный виджет списка для отображения и управления форматированными данными в приложениях с графическим интерфейсом. Пожалуй, самое полезное дополнение к встроенному в REBOL языку графического интерфейса "VID".

http://www.dobeash.com/rebdb.html - модуль базы данных, полностью написанный на собственном коде REBOL, который позволяет легко хранить и систематизировать данные. Это тот же веб-сайт, где вы найдёте модуль REBOL sqlite и RebGUI (рассмотренный ранее): http://www.dobeash.com/rebgui.html

http://www.rebol.org/cgi-bin/cgiwrap/rebol/view-script.r?script=rebzip.r - модуль для сжатия/распаковки файлов в формате zip.

http://www.colellachiara.com/soft/Misc/pdf-maker.r - диалект для создания файлов PDF непосредственно в REBOL (рассмотрено ранее).

http://softinnov.org/rebol/mysql.shtml - модуль для прямого управления базами данных mysql в REBOL (рассмотрено ранее). Модуль для баз данных postgre также находится в свободном доступе на том же сайте.

http://www.rebol.org/cgi-bin/cgiwrap/rebol/view-script.r?script=menu-system.r - диалект для создания всех типов полезных меню GUI в REBOL (рассмотрено ранее).

http://softinnov.org/rebol/uniserve.shtml - фреймворк, помогающий создавать сетевые приложения клиент-сервер.

http://softinnov.org/cheyenne.shtml - полнофункциональный веб-сервер, полностью написанный на собственном языке REBOL. Он позволяет создавать встроенные серверные сценарии, подобные PHP.

http://www.rebol.net/demos/BF02D682713522AA/i-rebot.r http://www.rebol.net/demos/BF02D682713522AA/objective.r и http://www.rebol.net/demos/BF02D682713522AA/histogram.r - эти примеры содержат модуль 3D-движка, полностью написанный на собственном диалекте отрисовки REBOL. Модуль позволяет вам легко добавлять объекты 3D-графики и управлять ими в ваших приложениях REBOL (о которых говорилось ранее).

http://web.archive.org/web/20030411094732/www3.sympatico.ca/gavin.mckenzie/ - библиотека синтаксического анализатора REBOL XML.

http://earl.strain.at/space/rebXR - полная клиент-серверная реализация XML-RPC для REBOL (содержит указанную выше библиотеку парсеров). Учебники (переведённые с французского на Google) доступны здесь и здесь.

http://box.lebeda.ws/~hmm/rswf/ - диалект для создания файлов flash (SWF) непосредственно из сценариев REBOL (описанных ранее).

libwmp3.dll - самый простой способ управлять полнофункциональным воспроизведением mp3 в REBOL. http://www.rebol.org/view-script.r?script=mp3-player-libwmp.r демонстрирует, как использовать его в REBOL.

http://www.rebolforces.com/articles/tui-dialect/ - диалект для позиционирования символов на экране в версиях REBOL для командной строки.

http://www.rebol.net/docs/makedoc.html - преобразует текстовые файлы в хорошо отформатированные файлы HTML. Эта страница руководства полностью написана и поддерживается с помощью makedoc (вы можете увидеть исходный код makedoc на http://re-bol.com/rebol.txt).

http://www.rebol.org/cgi-bin/cgiwrap/rebol/view-script.r?script=layout-1.8.r - простой дизайнер визуального макета для кода REBOL GUI. Не достаточно стабилен для коммерческого использования, но полезен для быстрого создания простых дизайнов графического интерфейса.

http://www.crimsoneditor.com/ - редактор исходного кода для Windows с выделением цветом специально для синтаксиса REBOL. Инструкции по быстрому запуску доступны на http://www.rebol.net/article/0187.html.

http://www.rebol.org - официальная библиотека REBOL, полная множества дополнительных модулей и полезных фрагментов кода. Первое, на что следует обратить внимание при поиске исходного кода REBOL.

16.33 6 ароматов REBOL

В этом руководстве рассматривается версия интерпретатора языка REBOL под названием REBOL/View. REBOL/View на самом деле является лишь одним из нескольких доступных релизов REBOL. Вот краткое описание различных версий:

  1. View - бесплатно для загрузки и использования, он включает языковые конструкции, используемые для создания и управления графическими элементами. View поставляется со встроенным диалектом под названием "VID", который представляет собой сокращённый мини-язык, используемый для отображения общих виджетов GUI. Концепции View и диалекта VID были объединены в этом документе. Слово "layout" (макет) в типичном дизайне графического интерфейса "view layout" фактически означает использование кода диалекта VID во вложенном блоке. Диалект VID используется внутри интерпретатора REBOL для анализа и преобразования простого кода VID в команды просмотра нижнего уровня, которые составляются с нуля рудиментарным механизмом отображения в REBOL. VID упрощает создание графического интерфейса пользователя без необходимости иметь дело с графикой на элементарном уровне. Но для точного управления всеми графическими операциями полный язык просмотра доступен в REBOL/View и может быть смешан с кодом VID. View также имеет встроенный диалект "draw" (рисования), который используется для создания и изменения изображений на экране. Помимо графических эффектов, View имеет встроенный звук и доступ к функции "call" (вызов) для выполнения приложений командной строки. Начиная с версии 2.76 REBOL/View содержит множество возможностей, которые ранее были доступны только в коммерческих версиях (dll, база данных, шифрование, SSL и другие - см. Ниже). Последние официальные выпуски View можно загрузить с http://rebol.com/view-platforms.html. Последние тестовые версии находятся на http://www.rebol.net/builds/. Более старые версии находятся на http://rebol.com/platforms-view.html.
  2. Core - текстовая версия языка, обеспечивающая базовую функциональность. Он меньше, чем View (примерно от 1/3 до 1/2 размера файла), без расширений графического интерфейса, но по-прежнему полностью включён в сеть и может запускать все не графические конструкции кода REBOL. Он предназначен для консольных и серверных приложений, таких как сценарии CGI, в которых средства графического интерфейса не нужны. Core также бесплатен и может быть загружен с http://rebol.com/platforms.html. Последние версии находятся на http://www.rebol.net/builds/. Более старые версии находятся на http://rebol.com/platforms-core.html.
  3. View/Pro - создан для профессиональных разработчиков, он добавляет функции шифрования, доступ к Dll и многое другое. Лицензии Pro не бесплатны. См. Http://www.rebol.com/purchase.html. ПРИМЕЧАНИЕ. НАЧИНАЯ В ВЕРСИИ 2.76, ЭТИ ФУНКЦИИ ДОСТУПНЫ В БЕСПЛАТНО ЗАГРУЖАЕМЫХ ВЕРСИЯХ REBOL!
  4. SDK - также предназначен для профессионалов, он добавляет возможность создавать автономные исполняемые файлы из сценариев REBOL, а также доступ к реестру Windows и многое другое для View/Pro. Лицензии SDK не бесплатны.
  5. Command - ещё одно коммерческое решение, оно добавляет встроенный доступ к общим системам баз данных, SSL, FastCGI и другим функциям для View/Pro. ПРИМЕЧАНИЕ. НАЧИНАЯ В ВЕРСИИ 2.76, ЭТИ ФУНКЦИИ ДОСТУПНЫ В БЕСПЛАТНО ЗАГРУЖАЕМЫХ ВЕРСИЯХ REBOL!
  6. Command/SDK - сочетает в себе функции SDK и Command.

Некоторые функции, предоставляемые версиями SDK и Command REBOL, были включены модулями, патчами и приложениями, созданными сообществом пользователей REBOL. Например, доступ к базе данных mysql и postgre, доступ к dll и автономная упаковка исполняемых файлов могут управляться бесплатными сторонними творениями (параметры можно найти на сайте rebol.org). Поскольку эти решения не соответствуют официальным стандартам REBOL и REBOL Technologies не поддерживает их, для критически важных работ рекомендуются коммерческие решения RT.

16.34 Биндология, диалекты, метапрограммирование и другие продвинутые темы

На первый взгляд REBOL представляет собой простой, практичный и полезный инструмент. Многие разработчики склонны отклонять его из-за его простого внешнего вида, небольшого размера файла и нетипичного синтаксиса языка. Однако те, кто придерживается REBOL, в конце концов обнаруживают, что в нем есть действительно глубокие и мощные языковые особенности, которые не сразу бросаются в глаза. Было написано несколько замечательных статей, которые хорошо освещают эти темы. Обязательно прочтите http://blog.revolucent.net/search/label/REBOL. Статья по биндологии на http://www.rebol.net/wiki/Bindology и другие страницы на http://www.fm.vslib.cz/~ladislav/rebol дают больше понимания. Не забудьте также просмотреть все дополнительные ссылки в последнем разделе этого руководства. Если ваши интересы лежат глубже, чем простые сценарии и разработка пользовательских приложений, REBOL предлагает уникальную пищу для размышлений.

16.34.1 Метапрограммирование

Метапрограммирование было определено как "написание компьютерных программ, которые пишут или манипулируют другими программами (или самими собой) в качестве своих данных или которые выполняют часть работы во время компиляции, которая в противном случае выполнялась бы во время выполнения. Во многих случаях это позволяет программистам чтобы сделать больше за то же время, которое потребовалось бы для написания всего кода вручную, или это даёт программам большую гибкость для эффективной обработки новых ситуаций без перекомпиляции".

Многие РЕБОЛеры склонны интуитивно использовать методы метапрограммирования в результате дизайна языка. Основная концепция REBOL заключается в том, что данные - это код, а код - это данные. Вы можете легко создавать блоки и строки, которые могут быть выполнены просто с помощью функции "do":

REBOL []

function: ask "'print' or 'editor':  "
text: ask "Enter some text:  "
do compose [(to-word function) (text)]
halt

Вышеупомянутая задача также может быть выполнена "DO" в составной строке:

do rejoin [function { "} text {"}]

Понимание "reduce" (сокращения), "compose" (компоновки) и связанных с ними функций является ключом к вышеупомянутой концепции, наряду с пониманием того, как создавать и манипулировать строками и блоками, которые могут быть выполнены (серийные функции действительно являются основой языка). Найдите в этом руководстве примеры, содержащие функции "compose", "reduce" и "rejoin", и вы увидите множество простых, но полезных примеров "do compose [some (сгенерированный) код]" и "do rejoin [{some} сгенерированный] {} код]". Также обратите внимание на скрипты "Voice Alarms" и "VOIP", а также на финальную программу веб-камеры в разделе "многозадачность". Эти примеры запускают отдельные сценарии .r с использованием функции "launch" (запуск), которые были созданы динамически основным сценарием. Точно так же для просмотра динамически созданного HTML-кода часто используется функция "browse" (обзор), как в "Создателе диаграмм гитарных аккордов", "Система торговых точек", "Гитарные аккорды" и других примерах в этом тексте. Этот метод часто используется для создания документов для печати в REBOL.

Другой распространённый метод - это написание кода, способного создавать динамически изменяемые блоки макета графического интерфейса. Вот простой пример:

view center-face layout [
    text "Select a button width, in pixels:"
    d: drop-down data [250 400 550]
    text "Enter any number of button labels (text separated by spaces):"
    f: field 475
    btn "Generate GUI" [
        created-buttons: copy compose [
            style new-btn btn (to-integer d/text) [
                alert join "This button's label is: " face/text
            ]
        ]
        foreach item to-block f/text [
            append created-buttons compose [
                new-btn (form item)
            ]
        ]
        view/new center-face layout created-buttons
    ]
]

Вся функция VID "layout" на самом деле является инструментом метапрограммирования. Чтобы использовать VID, вы составляете блоки, которые оцениваются функцией компоновки, а сгенерированный собственный код REBOL/view затем запускается механизмом компоновки нижнего уровня REBOL. RebGUI работает точно так же. Таким образом, многие диалекты REBOL являются инструментами метапрограммирования. Диалекты ("DSL"), как правило, в значительной степени полагаются на использование функции синтаксического анализа "parse" для (ре) организации значения ввода, заданного в указанном синтаксисе. На http://computer-programming-languages.suite101.com/article.cfm/how_to_createa_rebol_dialect есть небольшая вводная статья, которая может быть полезна в связи с этой темой. REBOL/flash и pdf-maker.r - мощные примеры инструментов метапрограммирования REBOL, которые продуктивны за пределами области кодирования REBOL. Оба этих инструмента используют диалекты в качестве входных данных и фактически выводят интерпретируемые форматы (REBOL/flash создает файлы SWF, а программа pdf-maker создаёт файлы PDF). Это можно считать "многоуровневым" метапрограммированием. Если вы хотите узнать больше о метапрограммировании с помощью диалектов, изучите синтаксический анализ, контексты и связывание, а также изучите различные инструменты диалекта, созданные сообществом REBOL. Чтобы понять, насколько мощной может быть эта концепция, взгляните на красиво оформленную игру "Machinarium", созданную с использованием диалекта REBOL/flash.

17. REBOL для Android, R3 с открытым исходным кодом (сборки Saphirion) и RED

17.1 Открытый исходный код

Карл Сассенрат выпустил REBOL3 как проект с открытым исходным кодом 12-12-2012, поэтому база кода теперь может быть изменена, улучшена, портирована, разветвлена ​​и использована любым разработчиком, который хочет настроить работу интерпретатора. Проект был выпущен под лицензией Apache 2.0, поэтому его можно свободно использовать в коммерческих целях, но он по-прежнему совместим с GPL и другими общими лицензиями на проекты с открытым исходным кодом. Исходный код доступен по адресу https://github.com/rebol/rebol,

Одним из больших преимуществ выпуска с открытым исходным кодом является то, что REBOL можно навсегда переносить на новые платформы (новое оборудование, новые операционные системы и т.д.), а продуктивный язык REBOL может продолжать идти в ногу с изменениями и достижениями в современных технологиях. интерфейс с новыми библиотеками кода, новыми языковыми конструкциями, новыми форматами данных и т.д. К сожалению, версия 2.xx интерпретатора REBOL ("R2") обременена коммерческими лицензиями, контрактами с инвесторами и другими ограничениями, поэтому, вероятно, никогда не будет быть выпущенным как проект с открытым исходным кодом (хотя двоичный интерпретатор по-прежнему находится в свободном доступе, периодически обновляется и может использоваться для коммерческой работы на существующих настольных платформах на сайте rebol.com). Долгая история инструментов и кода R2, предоставленных сообществом, также продолжает поддерживаться на rebol.org и других пользовательских веб-сайтах. Для мобильных телефонов, планшетов и других новых платформ REBOL версии 3 ("R3") - это путь вперёд.

На момент выпуска R3 был свежим новым дизайном, нацеленным на решение проблем, с которыми пришлось столкнуться в течение десятилетия обширного коммерческого опыта разработки REBOL. Дизайн R3 предназначен для того, чтобы лучше подходить для разработки более крупных программных проектов, поэтому в языке R3 есть некоторые изменения, которые наиболее очевидны в диалекте графического интерфейса по умолчанию. Многие из встроенных в REBOL 2 "наворотов" также не сразу доступны в новой версии, но R3 уже вполне пригоден для коммерческой работы. Пользователи перенесли его на Raspberry PI и различные встроенные системы, и основная разработка активно ведётся группой Saphirion под названием "Saphir R3". Новые рабочие версии Saphirion для Android, Windows, Mac, Linux и других распространённых платформ доступны по адресу:

http://development.saphirion.com/experimental/builds/

В настоящее время выпуски Saphirion являются наиболее функциональными упакованными версиями REBOL3 с мощной рабочей системой графического интерфейса, которая доступна даже на Android. С появлением повсеместной популярности ОС Android (это, безусловно, самая распространённая мобильная платформа), этот выпуск стал важным для разработчиков REBOL. Вы можете загрузить небольшой интерпретатор Saphirion R3 на свой телефон, планшет или другое устройство Android и создавать мощные консольные и графические приложения так же легко, как и на настольных ПК. Существующая документация для Saphirion версии REBOL доступна по адресу http://development.saphirion.com/rebol/r3gui/. Исходный код графического интерфейса пользователя Saphirion находится по адресу https://github.com/saphirion/r3-gui, а новая документация - по адресу https://github.com/saphirion/documentation. Изменения диалекта графического интерфейса сначала могут показаться запутанными, но это главным образом потому, что существует всего несколько примеров, написанных в определённом стиле, который может быть незнаком пользователям R2. Основы использования графического интерфейса Saphirion R3 на самом деле очень просты и сразу доступны кодировщикам R2, а подавляющее большинство базового языка REBOL работает почти так, как вы ожидаете. В этом разделе содержится важное введение, которое поможет быстро приступить к работе с диалектом графического интерфейса пользователя Saphirion R3. Основное внимание здесь уделяется использованию среды Android, поскольку это наиболее распространённая новая платформа.

17.2 Создание рабочей среды Android - необходимые инструменты

Сначала загрузите и установите версию APK-файла R3/Droid от Saphirion (перейдите по этой ссылке в веб-браузере Android). Щёлкните загруженный файл и примите параметры установки по умолчанию. Это займёт всего несколько секунд. После установки вы увидите значок приложения "R3/Droid" на вашем устройстве Android. Вы можете щёлкнуть значок, чтобы запустить консоль REBOL. Как и в R2, вы можете найти справку и списки исходного кода для встроенных функций, изучить системные объекты и даже запустить простые сценарии непосредственно из командной строки консоли. Попробуйте запустить несколько примеров:

print "hello world"

cd %./sdcard/

list-dir

help

? system

help write

source load-gui

write %r3-gui.r3 read http://development.saphirion.com/resources/r3-gui.r3

Вы увидите, что большинство основных функций и синтаксиса, с которыми вы знакомы в R2, точно такие же, как и в R3. Изучите "help" (справку) и "source" (источник) для любой нужной вам функции, и вы быстро освоите новые параметры и изменения. По большей части, если вы написали код для R2, ​​вы сразу почувствуете себя комфортно в среде ядра Saphirion R3/Droid.

Хороший текстовый редактор необходим для продуктивного написания сценариев REBOL на Android. Текущее любимое приложение этого автора для текстового редактора для Android - это Jota, которое бесплатно доступно в магазинах приложений для Android. Вы можете найти редактор Jota в Google Play по адресу https://play.google.com/store/apps/details?id=jp.sblo.pandora.jota&hl=en. Подойдёт любой текстовый редактор, но Jota обеспечивает беспроблемную интеграцию с интерпретатором REBOL, так что скрипты могут запускаться автоматически из среды редактирования. Jota также предоставляет фантастические средства управления просмотром и редактированием, такие как простой размер шрифта и параметры переноса слов, точные элементы управления навигацией, отмена/возврат, настраиваемая панель инструментов и другие функции, которые делают его приятной средой, в которой вы можете быстро набирать код.

Предлагается настроить на панели инструментов Jota следующие кнопки: Сохранить, Открыть приложение, Перенос слов, Отменить, Повторить, Сохранить как, Font-, Font +, Search, End, Home, Up, Down, Left, Right (нажмите Menu -> Настройки -> Настройки панели инструментов для настройки макета панели инструментов). Это обеспечит быстрый доступ ко многим функциям, необходимым при редактировании скриптов.

Функцию Jota "Открыть приложение" также можно найти, щёлкнув Меню -> Файл -> Открыть приложением. Эта функция позволяет запускать редактируемый в данный момент файл сценария REBOL с помощью установленного приложения-интерпретатора REBOL. Чтобы включить эту возможность, не забудьте сохранить свои сценарии с именами файлов, которые заканчиваются расширением .r3 (например, "myscript.r3"). Попробуйте вставить следующий скрипт в редактор Jota на вашем устройстве Android:

REBOL []
load-gui
view [text "Hello Android!"]

Сохраните код с именем файла, например hello.r3 (любое имя файла, заканчивающееся на ".r3"), затем нажмите Меню -> Файл -> Открыть приложением (или нажмите кнопку на панели инструментов "Открыть приложение", если вы настройте эту кнопку) и выберите "R3/Droid" (при появлении запроса "Завершить действие с помощью:"). Вы увидите запущенную программу с небольшим графическим интерфейсом и текстом "Hello Android!". Вы также можете запустить любой сценарий, который заканчивается на ".r3", щёлкнув значок его файла в файловом менеджере.

Простая процедура редактирования с помощью Jota, сохранения и открытия с помощью R3/Droid может быть выполнена за секунды, если вы настроите панель инструментов Jota с легкодоступными кнопками "сохранить" и "открыть приложение". Меню "Файл" -> "История" - ещё одна опция, с помощью которой можно быстро перемещаться между недавно отредактированными скриптами. Вместе со справочной системой, доступной в консоли R3/Droid, эта установка предоставляет быстрый и полный набор инструментов для обработки механики программирования Android.

Несколько других полезных инструментов Android включают ES File Explorer и Wifi File Transfer. ES File explorer - это файловый менеджер, который позволяет копировать/перемещать/удалять/выполнять и т.д. файлы на вашем устройстве Android. Он имеет встроенные возможности, которые позволяют сохранять файлы на общих сетевых ресурсах и FTP-серверах, а также передавать по электронной почте, текстовым сообщениям и другим соединениям. Wifi File Transfer - наиболее эффективное приложение, которое нашёл автор, для обмена файлами между настольными компьютерами и устройствами Android. Для любого приложения не требуется никаких кабелей или специальных действий по настройке сети, и оба приложения являются бесплатными, быстрыми и надёжными.

Вы можете загрузить настольные версии сборки Saphirion R3 и редактировать/запускать сценарии на своём домашнем компьютере, а затем использовать Wifi File Transfer или ES File Explorer, чтобы скопировать их на своё устройство Android, редактировать, запускать и отправлять файлы туда и обратно между каждой платформой. быстро. Эти инструменты значительно повышают производительность при совместном использовании/перемещении работы между рабочим столом и рабочей средой Android.

17.3 Основы графического интерфейса пользователя R3

Обратите внимание на функцию load-gui в приведённом выше примере. Если вы наберёте "source load-gui" в консоли R3, вы увидите, что все, что он делает, это загружает и запускает файл по адресу http://development.saphirion.com/resources/r3-gui.r3. Рекомендуется загрузить и сохранить этот файл в той же папке, что и сценарии, которые вы создаёте, и использовать следующий код для включения функциональности графического интерфейса в сценариях R3:

REBOL []
do %r3-gui.r3
view [text "Hello Android!"]

Вышеупомянутая строка "do %r3-gui.r3" делает то же самое, что и встроенная функция load-gui, без необходимости загрузки библиотеки графического интерфейса пользователя при каждом запуске сценария. Это увеличивает скорость и устраняет необходимость подключения к Интернету каждый раз, когда вы запускаете свой скрипт(ы). Вы также можете вставить код на http://development.saphirion.com/resources/r3-gui.r3 прямо в любой из ваших скриптов, если вы хотите устранить необходимость в каких-либо дополнительных импортированных файлах библиотеки (это не рекомендуется).

Обратите внимание, что в приведённом выше примере одно слово "view" используется в R3 для замены двух слов R2 "view layout". Ещё одно важное отличие состоит в том, что R3 использует "on-action" субъекта, чтобы позволить виджетам выполнять действия. Итак, этот код в R2:

REBOL []
view layout [
    text "First Name:"
    f1: field
    text "Last Name:"
    f2: field
    btn "Submit" [
        write/append %cntcts.txt rejoin [
            mold f1/text " " mold f2/text newline
        ]
    ]
    a1: area
    btn "Load" [a1/text: read %cntcts.txt  show a1]
]

В R3 Saphirion выглядит так:

REBOL []
do %r3-gui.r3   ; или используйте load-gui
view [
    text "First Name:"
    f1: field
    text "Last Name:"
    f2: field
    button "Submit" on-action [
        write/append %cntcts.txt rejoin [
            mold get-face f1 " " mold get-face f2 newline
        ]
    ]
    a1: area
    button "Load" on-action [set-face a1 to-string read %cntcts.txt]
]

Обратите внимание также на использование функций "get-face" и "set-face" (например, "get-face f1" вместо "f1/text"). В диалекте графического интерфейса R3 это предпочтительный синтаксис, используемый для получения и установки текста и данных в виджетах графического интерфейса. По большей части, как видите, основы создания макетов графического интерфейса в R3/Droid так же просты, знакомы и дружелюбны, как и в R2.

Некоторые свойства графического интерфейса можно установить с помощью уточнения "view/options". Вот как вы устанавливаете текст заголовка окна графического интерфейса пользователя:

REBOL []
do %r3-gui.r3
view/options [text-list] [title: "My Title"]

Следующий код делает то же самое, что и выше:

REBOL [title: "My Title"]
do %r3-gui.r3
view [text-list]

Вы можете получить доступ к определённым свойствам любого виджета с помощью функции "get-facet":

REBOL []
do %r3-gui.r3
view [t1: text-list on-action [probe get-facet t1 'atts]]

Вы можете увидеть фасеты (и все другие свойства) любого виджета, используя ключевое слово "debug" (используйте CTRL + C, чтобы остановить прокрутку вывода консоли). Это, пожалуй, самый важный инструмент, с которым нужно освоиться при изучении диалекта графического интерфейса пользователя Saphirion R3. Наряду с функциями "source" и "help", он предоставляет незаменимую справочную информацию, которая поможет узнать о новых свойствах графического интерфейса:

REBOL []
do %r3-gui.r3
view [text-list debug]

Вот сценарий, который позволяет прокручивать и просматривать все встроенные стили виджетов:

REBOL [title: "View All Styles"]
do %r3-gui.r3
all-styles: find extract to-block guie/styles 2 'clicker
view [
    title "Pick a style:"
    text-list all-styles on-action [
        style-name: pick all-styles get-face face
        view/modal reduce [
            'title reform ["Example of a" style-name "style:"]
            style-name
        ]
    ]
]

Более подробную информацию о лицах, фасетах, актерах и других параметрах графического интерфейса можно найти на http://development.saphirion.com/rebol/r3gui/faces/index.shtml и http://development.saphirion.com/rebol/r3gui/actors/index.shtml. Эти тексты предоставляют более полную информацию о том, что вы здесь узнали, об основных функциях диалекта графического интерфейса R3.

17.4 Простые запрощики

Одна вещь, которую вы заметите, в настоящее время отсутствует в сборке Saphirion R3/Droid, - это широкий набор диалогов встроенных запросов. Функция "request" (запрос) в R3/Droid очень мощная. В простейшей форме вы можете использовать его как функцию "alert" (предупреждения) R2:

REBOL []
do %r3-gui.r3
view [
    button "Click me" on-action [request "Ok" "You clicked the button."]
]

Уточнение "/ask" полезно для получения ответов на вопросы типа да/нет:

REBOL []
do %r3-gui.r3
x: request/ask "Question" "Do you like this?."
either x = false [print "Boo!"] [print "Yay!"]

Уточнение "/custom" позволяет вам выбрать альтернативный текст для кнопок:

REBOL []
do %r3-gui.r3
x: request/custom "" "Do you like this?" ["Yay" "Boo"]
either x = false [print "Boo!"] [print "Yay!"]
halt

Этот request/custom пример взят из демонстрационного кода Cyphre, встроенного в R3/Droid:

REBOL []
do %r3-gui.r3
site: http://development.saphirion.com/experimental/
view [
    button "Tile Game" on-action [
        request/custom "Downloading files..." [
            title "Loading game..."
            when [enter] on-action [
                game: load/all site/tile-game.r
                unview/all
                gui-metric/set 'unit-size 1x1
                do game
            ]
        ]["" "Close"]
    ]
]

В ситуациях, когда вам нужно собрать больше информации или предоставить специально выбираемые варианты, пользовательские средства запроса текста и другие базовые диалоговые окна ввода и вывода легко создать, если это необходимо. Например, этот сценарий содержит простой диалог, который запрашивает текст у пользователя при нажатии кнопки (в данном случае имя файла, используемое для сохранения текста заметки). Обратите внимание на использование уточнения view/modal окна. Это удерживает диалог поверх основного макета, пока он не будет закрыт:

REBOL [title: "Tiny Text Editor"]
do %r3-gui.r3
view [
    text "Notes:"
    a1: area
    button "Save" on-action [
        view/modal [
            text "File Name:"
            f1: field "notes.txt"
            button "Submit" on-action [
                write (to-file get-face f1) (get-face a1)
                unview
            ]
        ]
    ]
    button "Load" on-action [set-face a1 to-string read %notes.txt]
]

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

REBOL []
do %r3-gui.r3
view [
    text "Notes:"
    a1: area
    button "Save" on-action [
        files: read %./
        view/modal [
            text "File Name:"
            t1: text-list files
            button "Submit" on-action [
                write (to-file pick files get-face t1) (get-face a1)
                request "" "Saved"
                unview
            ]
        ]
    ]
    button "Load" on-action [
        files: read %./
        view/modal [
            text "File Name:"
            t2: text-list files
            button "Submit" on-action [
                set-face a1 to-string read(to-file pick files get-face t2)
                unview
            ]
        ]
    ]
]

Вы можете объединить как загрузку, так и сохранение диалоговых подпрограмм в одну функцию "request-file", например:

REBOL [title: "request-file"]
do %r3-gui.r3
request-file: func [opt] [
    files: read %./
    view/modal [
        text "File Name:"
        t1: text-list files
        button "Submit" on-action [
            either opt = "save" [
                write (to-file pick files get-face t1) (get-face a1)
                request "" "Saved"
            ][
                set-face a1 to-string read(to-file pick files get-face t1)
            ]
            unview
        ]
    ]
]
view [
    text "Notes:"
    a1: area
    button "Load" on-action [request-file "load"]
    button "Save" on-action [request-file "save"]
]

Как видите, развернуть собственные запрашивающие элементы достаточно просто, используя простые встроенные возможности графического интерфейса пользователя R3.

17.5 Макет (layout)

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

REBOL []
do %r3-gui.r3
view [
    vpanel [
        f1: field
        hpanel [
            button "Reset" on-action [set-face f1 ""]
            button "Clear" on-action [set-face f1 ""]
        ]
    ]
]

Обратите внимание, что слово "return" по-прежнему работает как в R2, в "hgroup":

REBOL []
do %r3-gui.r3
view [
    hgroup [
        f1: field
        return
        button "Reset" on-action [set-face f1 ""]
        button "Clear" on-action [set-face f1 ""]
    ]
]

Обратите внимание, что многие параметры макета и изменения размера обрабатываются автоматически (попробуйте изменить размер этой сетки и обратите внимание на функции автоматической сортировки и отображения виджета текстовой таблицы text-table):

REBOL []
do %r3-gui.r3
view [
    text-table ["1" 200 "2" 100 "3"][
        ["asdf" "a" "4"]
        ["sdfg" "b" "3"]
        ["dfgh" "c" "2"]
        ["fghj" "d" "1"]
    ] 
]

Поскольку Android и другие новые устройства поставляются с огромным разнообразием разрешений экрана, в R3 были добавлены некоторые положения, позволяющие плавно обрабатывать эти параметры, с небольшим количеством кода, необходимого для простых сценариев, и дополнительными подробными параметрами управления, доступными для создания более сложных приложений. Функцию "gui-metric" важно изучить (введите "help gui-metric" в консоли R3, чтобы увидеть её параметры). В следующем коде посмотрите, как используется функция "gui-metric" вместе с параметром "max-hint". Этот пример макета прекрасно подходит для телефона с маленьким экраном в портретном режиме:

REBOL [title: "Edit Downloaded File"]
do %r3-gui.r3 
gui-metric/set 'unit-size (gui-metric 'screen-dpi) / 96
view/options [
    hgroup [
        button "Load" on-action [
            set-face a to-string read http://rebol.com
        ]
        button "Open" on-action [
            view [
                text "File:"
                f: field
                button "Submit" on-action [
                    file: get-face f
                    set-face a (to-string read file)
                    unview
                ]
            ]
        ]
        return
        button "New" on-action [
            set-face a  ""
        ]
        button "Save-As" on-action [
            view [
                text "File:"
                f: field
                button "Submit" on-action [
                    file: get-face f
                    write file (get-face a)
                    unview
                ]
            ]
        ]
        button "Quit" on-action [
            unview/all
        ]
    ]
    a: area ""  on-key [
        do-actor/style face 'on-key arg 'area
        if arg/type = 'key [ 
            if arg/key = 'f5 [try load face/names/tb/state/value]    
        ]    
    ]
][
    max-hint: round/floor (gui-metric 'work-size) - gui-metric 'title-size
]

Для получения дополнительной информации о макете R3 и изменении размера см. Http://development.saphirion.com/rebol/r3gui/layouts/index.shtml и http://development.saphirion.com/rebol/r3gui/resizing/index.shtml.

17.6 Стили

"Стили" - это синоним R3 для "виджетов" в других языках. Функция "stylize" (стилизовать) позволяет создавать собственные варианты виджетов или даже совершенно новые определения виджетов с помощью команд рисования. Фасеты по умолчанию, субъекты и другие свойства существующего стиля могут быть установлены и присвоены новому имени виджета. Здесь создается стиль синей кнопки "blue-button":

REBOL [title: "Blue Button"]
do %r3-gui.r3
stylize [
    blue-button: button [
        facets: [bg-color: blue]
    ]
]
view [blue-button]

Вот версия R3 простого калькулятора с графическим интерфейсом, представленного ранее в этом тексте. Для стиля "btn" задан размер, и определено действие по умолчанию (текст лица нажатой кнопки добавляется к тексту лица виджета поля):

REBOL [title: "CALCULATOR"]
do %r3-gui.r3
stylize [
    btn: button [
        facets: [init-size: 50x50]
        actors: [on-action:[set-face f join get-face f get-face face]]
    ]
]
view [
    hgroup [
        f: field return
        btn "1"  btn "2"  btn "3"  btn " + "  return
        btn "4"  btn "5"  btn "6"  btn " - "  return
        btn "7"  btn "8"  btn "9"  btn " * "  return
        btn "0"  btn "."  btn " / "   btn "=" on-action [
            attempt [set-face f form do get-face f]
        ]
    ]
]

Вы можете создавать свои собственные стили виджетов, используя команды рисования:

REBOL [title: "Blue Button"]
do %r3-gui.r3
stylize [
    circle: [
        draw: [
            fill-pen blue
            circle 50x50 30
        ]
    ]
]
view [circle circle circle]

См. Http://development.saphirion.com/rebol/r3gui/styles/index.shtml для получения дополнительной информации о создании стилей.

17.7 Ещё несколько простых примеров

Вот ещё несколько примеров, преобразованных из сценариев R2 VID ранее в этом тексте. Вы можете видеть, что необходимые корректировки кода выполняются быстро и просто. Во-первых, небольшой тест по математике для детей:

REBOL [title: "Math Test"]
do %r3-gui.r3
random/seed now
x: does [rejoin [random 10 " + " random 20]]
view [
    f1: field (x)
    text "Answer:"
    f2: field on-action [
        either (get-face f2) = (form do get-face f1) [
            request "Yes!" "Yes!"][request "No!" "No!"
        ]
        set-face f1 x
        set-face f2 ""
        focus f2
    ]
]

Небольшая игра с раздвижной плиткой:

REBOL [title: "Sliding Tile Puzzle"]
do %r3-gui.r3
stylize [
    p: button [
        facets: [init-size: 60x60  max-size: 60x60]
        actors: [
            on-action: [
                t: face/gob/offset
                face/gob/offset: x/gob/offset
                x/gob/offset: t
            ]
        ]
    ]
]
view/options [
    hgroup [ 
        p "8"   p "7"   p "6"   return
        p "5"   p "4"   p "3"   return
        p "2"   p "1"   x: box 60x60 white
    ]
] [bg-color: white]

Минимальное кассовое приложение:

REBOL [title: "Minimal Cash Register"]
do %r3-gui.r3
stylize [fld: field [init-size: 80]]   
view [
    hgroup [
        text "Cashier:"   cashier: fld 
        text "Item:"      item: fld 
        text "Price:"     price: fld on-action [
            if error? try [to-money get-face price] [
                request "Error" "Price error" 
                return none
            ]
            set-face a rejoin [
                get-face a mold get-face item tab get-face price newline
            ]
            set-face item copy "" set-face price copy ""
            sum: 0
            foreach [item price] load get-face a [
                sum: sum + to-money price
            ]
            set-face subtotal form sum
            set-face tax form sum * .06
            set-face total form sum * 1.06 
            focus item
        ]
        return
        a: area 600x300
        return
        text "Subtotal:"   subtotal: fld 
        text "Tax:"        tax: fld 
        text "Total:"      total: fld
        button "Save" on-action [
            items: replace/all (mold load get-face a) newline " "
            write/append %sales.txt rejoin [
                items newline get-face cashier newline now/date newline
            ]
            set-face item copy "" set-face price copy "" 
            set-face a copy ""    set-face subtotal copy ""
            set-face tax copy "" set-face total copy ""
        ]
    ]
]

Небольшое приложение для хранения и извлечения данных:

REBOL [title: "Parts"]
do %r3-gui.r3
write/append %data.txt ""
database: read/lines %data.txt
clear-fields: does [
    set-face n copy  ""
    set-face a copy  ""
    set-face p copy  ""
    set-face o copy  ""
]
view [
    text "Parts in Stock:"
    name-list: text-list (extract database 4) on-action [
        if none = marker: get-face face [return none]
        marker: (marker * 4) - 3
        set-face n pick database marker
        set-face a pick database (marker + 1)
        set-face p pick database (marker + 2)
        set-face o pick database (marker + 3)
    ]
    text "Part Name:"       n: field
    text "Manufacturer:"    a: field
    text "SKU:"             p: field
    text "Notes:"           o: area
    hgroup [
        button "Save" on-action [
            if "" = get-face n [
                request "Error" "You must enter a Part name."
                return none
            ]
            if find (extract database 4) get-face n [
                either false = request/ask """Overwrite existing record?"[
                   return none
                ] [
                   remove/part (find database (get-face n)) 4
                ]
            ]
            append database reduce [
                get-face n  get-face a  get-face p  get-face o
            ]
            write/lines %data.txt database
            set-facet name-list 'list-data (extract copy database 4)
        ]
        button "Delete" on-action [
            if not (false = request/ask "?" (rejoin [
                "Delete " get-face n "?"
            ])) [
                remove/part (find database (copy get-face n)) 4
                write/lines %data.txt database
                clear-fields
                set-facet name-list 'list-data (extract copy database 4)
            ]
        ]
        button "New" on-action [clear-fields]
    ]
]

17.8 Дополнительные важные ресурсы

На этом этапе вы должны уметь создавать базовые формы ввода данных графического интерфейса пользователя R3, которые будут полезны в служебных сценариях и простых бизнес-приложениях. Обязательно ознакомьтесь со следующими примерами, чтобы увидеть более полезный код для сборок R3/Droid и Saphirion R3. Обратите особое внимание на мозаичную игру и нарисуйте тестовые сценарии Cyphre, чтобы увидеть, как реализованы графика, макет экрана, обработка событий и другие полезные функции:

http://development.saphirion.com/experimental/demo.r

http://development.saphirion.com/experimental/tile-game.r

http://development.saphirion.com/experimental/draw-test.r

https://raw.github.com/angerangel/r3bazaar/master/builds/windows/editor.r

17.9 RED

До того, как Карл Сассенрат выпустил REBOL как проект с открытым исходным кодом, Ненад Ракочевич ("Док Кимбел") намеревался создать замену языка REBOL с открытым исходным кодом, основываясь на его лучших достоинствах. Этот язык, который сейчас называется "Red", все ещё находится в зачаточном состоянии, но показывает признаки огромного будущего потенциала для разработчиков REBOL. Одним из больших преимуществ языка Red является то, что он перекрёстно компилируется непосредственно с собственными исполняемыми файлами для каждой целевой платформы, поэтому небольшие, быстрые, собственные двоичные программы могут быть созданы с помощью простой и небольшой цепочки инструментов, знакомой REBOLers. Также доступна интерпретируемая консоль для быстрой работы и тестирования кода, и планируется профессиональная IDE. Вместе с Red/System цель Red состоит в том, чтобы предоставить полный кросс-платформенный инструментарий, который можно использовать для создания всего, от низкоуровневых/высокопроизводительных системных драйверов до высокоуровневых пользовательских приложений, и все это с использованием крошечного и чрезвычайно производительного инструментария, который не имеет абсолютно никаких зависимостей от C или других исторически раздутых цепочек инструментов. Red уже работает в определённой степени со всеми популярными операционными системами (включая мобильные), и его создатель регулярно проводит стабильную работу. Red уже использовался для создания как минимум одного коммерческого приложения, и сообщество работает над тем, чтобы инструменты R3 хорошо с ним взаимодействовали. Следите за этим на http://www.red-lang.org/. Вероятно, это будет важная часть будущего развития всех языков, связанных с REBOL.

18. Реализация приложений для управления многопользовательскими данными с помощью Rebol

Автономная копия этого раздела руководства с множеством снимков экрана доступна по адресу http://re-bol.com/rebol-multi-client-databases.html. Возможно, вам будет легче следить за этой темой, добавив снимки экрана.

18.1 Многопользовательские системы баз данных в Rebol

Простые однопользовательские приложения Rebol обычно используют сохранённые структуры блоков для постоянного хранения данных. Процедура проста: создайте блок, манипулируйте блоком в памяти по мере необходимости вашего приложения (добавление значений, поиск, сортировка, удаление значений и т.д.) используя собственные функции серии, а затем используйте функцию сохранения для сохранения блока в текстовый или двоичный файл. В этой статье показано, как расширить использование простых блоков, чтобы они могли работать с аналогичными функциями в многопользовательских сетевых приложениях для управления данными. Во многих случаях необходимость в СУБД сторонних производителей, таких как MySQL, может быть устранена.

18.2 Типичный 101-й пример REBOL

Ниже приведен пример простого однопользовательского приложения для управления данными. Новые реболлеры могут научиться писать такой код за несколько дней:

rebol [title: "Single User Contacts App"]
do %rebgui.r    ; http://re-bol.com/rebgui.r
if not exists? %contacts [write %contacts ""]
display/close "Contacts" [
    t: table 78x34 options [
        "Name" left .3 "Address" left .4 "Phone" left .3
    ] data (load %contacts) [set-texts [n a p] t/selected]
    f: panel data [
        after 2
        text 18 "Name:"     n: field
        text 18 "Address:"  a: field
        text 18 "Phone:"    p: field
        reverse
        button " Submit " [
            attempt [t/remove-row (index? find t/data t/selected) + 2 / 3]
            t/add-row/position reduce [
                copy n/text copy a/text copy p/text
            ] 1
            set-texts [n a p] ""
        ]
        button " New " [set-texts [n a p] ""  t/redraw]
        button " Delete "  [
            t/remove-row (index? find t/data t/selected) + 2 / 3
            set-texts [n a p] ""
        ]
    ]
] [save %contacts t/data  alert "saved"  quit]
do-events

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

Работа со строками, списками каталогов, электронной почтой, сетевыми соединениями, виджетами в графическом интерфейсе, графикой, звуками и даже базовыми структурами кода в программах Rebol требует управления сериями, поэтому навыки работы с сериями повышаются за счёт постоянного использования в большинстве видов кодирования. Поскольку собственные последовательные операции настолько распространены в Rebol, для разработчиков Rebol естественно использовать "собственные" структуры данных серий и методы манипуляции, а также использовать сохранённые блоки как простой метод для постоянного хранения (в отличие от использования сторонних СУБД), особенно в небольших приложениях и утилитах.

Как правило, проблем с приложением и моделью данных выше, если только один пользователь запускает приложение на одном компьютере.

18.3 Многопользовательские базы данных

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

Кроме того, например, если пользователю Пользователь2 необходимо выполнить поиск в базе данных для части информации, в то время как Пользователь1 находится в процессе добавления данных, изменения, сделанные пользователем Пользователь1, не будут отображаться в поиске Пользователь2, пока пользователь Пользователь1 не сохранит свою уникальную версию данных.

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

rebol [title: "Test for concurrent file write errors"]
x: copy []
insert/dup x 0 10000000
write %testdata.txt x
write %concurrent-file-write-errors.r {
    rebol [title: "Test for concurrent file write errors"]
    script-name: form now
    repeat i 25 [
       probe i
       write/append %testdata.txt a: rejoin [script-name "  " i newline]
       if not find read %testdata.txt a [
           alert rejoin ["Error: " script-name ", " i]
       ]
    ]
    print "Done"
    halt
}
loop 10 [launch %concurrent-file-write-errors.r]

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

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

Вот пример сервера, который загружает файл %contacts, как в примере с графическим интерфейсом выше. Сетевой цикл загружает строку кода, отправленную от клиента, оценивает код с помощью 'do и возвращает сформированные результаты оценки 'do обратно клиенту. Некоторые напечатанные отзывы также представлены в консоли сервера, поэтому администратор сервера может наблюдать за любой активностью (функции проверки можно удалить для повышения производительности):

rebol [title: "Simple Data Server"]
if not exists? %contacts [write %contacts ""]
contacts: load %contacts
print "waiting..."
port: open/lines tcp://:55555
forever [
    probe commands: load data: first wait connect: first wait port
    probe result: mold do commands
    insert connect result
    close connect
]

Вот пример клиентского скрипта, который отправляет некоторый код на указанный выше сервер для выполнения. Сервер оценивает код с помощью 'do и возвращает результат (последнее выражение в строке кода, которое в приведённом ниже случае представляет собой просто число 1). Пока сервер работает и клиент может подключаться по сети, блок данных ["Jim" "" ""] будет добавлен к блоку контактов, загруженному в память сервера, обновлённый блок контактов будет будут сохранены в файле %contacts на сервере, а целочисленное значение 1 будет отправлено обратно в клиентский сценарий. Когда клиент получает ответ, если это целое число 1, клиентский скрипт печатает сообщение об успешном завершении, после чего соединение с сервером закрывается:

rebol [title: "Simple Client 1"]
print "sending..."
serverip: "localhost"
port: open/lines rejoin [tcp:// serverip ":55555"]
insert port {append contacts ["Jim" "" ""] save %contacts contacts 1}
data-response: load first connect: wait port [
either 1 = data-response [print "Success"] [print "Failure"]
close connect
close port
halt

Следующий клиентский сценарий расширяет идею, описанную выше, отправляя на сервер 3 блока данных. Обратите внимание, что поскольку метка контактной переменной в цикле foreach существует только в контексте клиентского сценария (а не где-либо в памяти сервера), значения, которые она представляет на каждой итерации цикла, должны быть объединены (воссоединены) в строку отправлено на сервер. Вам нужно будет сделать это в любом клиентском коде, который отправляет данные, представленные локальной переменной:

rebol [title: "Simple Client 2"]
print "sending..."
serverip: "localhost"
foreach [contact] [
    ["Jim" "" "123-1234"]
    ["Bob" "" "234-2345"]
    ["Joe" "" "345-3456"]
] [
    port: open/lines rejoin [tcp:// serverip ":55555"]
    insert port rejoin [
        {append contacts } mold contact { save %contacts contacts 1}
    ]
    if 1 = data-response: load first connect: wait port [print "Success"]
    close connect
    close port
]
halt

Вышеупомянутая идея может быть расширена для создания обобщённой функции server-exec, которая отправляет любую строку кода на сервер и получает ответ сервера. Обратите внимание, что, поскольку порт сервера открыт в режиме /lines, функция 'trim/lines' используется для удаления всех возвратов каретки из строки кода (режим /lines - чистый и простой способ передачи формованных данных - просто всегда обязательно удалите все разрывы строк, используя эту функцию 'trim/lines'). Эта функция server-exec также обрабатывает ошибки, записывает текущее время и дату, отправленную строку кода и информацию об ошибке в файл и предупреждает клиента, если когда-либо возникает сетевая ошибка:

server-ip: "localhost"
server-exec: func [strng] [
    commands: trim/lines strng
    if error? err: try [
        port: open/lines rejoin [tcp:// serverip ":55555"]
        insert port mold commands
        data-response: first connect: wait port
        close connect
        close port
        return data-response
    ] [
        err: disarm :err
        write/append %net-err.txt rejoin [
            now newline 
            commands newline
            err newline newline
        ]
        alert "** Network Connection Error **  Try Again"
        return none
    ]
]

Вот немного более надёжный обобщённый серверный скрипт с аналогичной обработкой ошибок:

REBOL [title: "Server"]
print "waiting..."
port: open/lines tcp://:55555
forever [
    if error? er: try  [
        probe commands: load data: first wait connect: first wait port
        probe result: mold do commands
        insert connect result
        close connect
        true
    ] [
        er: disarm :er
        net-error: rejoin [
            form now newline
            mold commands newline 
            er newline newline 
        ]
        write %server-error.txt net-error
        print net-error
        close connect
    ]
]

В качестве примера мы можем отправлять команды из клиентского сценария графического интерфейса, представленного ранее, для обработки с помощью обобщённого серверного сценария, описанного выше. Все, что нам нужно сделать, это загрузить данные контактов в серверное приложение и использовать обобщённую функцию server-exec для отправки любых изменений, которые необходимо внести в данные контактов, от клиента к серверу. В приведённом ниже примере операция сохранения (которая происходит при закрытии окна графического интерфейса пользователя) и все изменения, внесённые, когда пользователь-клиент нажимает любую кнопку в графическом интерфейсе, отправляются по сети для обработки сервером. Этот код выглядит странно, но это просто копия функции server-exec, а затем копия графического интерфейса контактов с добавленными несколькими операциями server-exec:

rebol [title: "Network Contacts"]
serverip: "localhost"
server-exec: func [strng] [
    commands: trim/lines strng
    if error? err: try [
        port: open/lines rejoin [tcp:// serverip ":55555"]
        insert port mold commands
        data-response: first connect: wait port
        close connect
        close port
        return data-response
    ] [
        err: disarm :err
        write/append %net-err.txt rejoin [
            now newline 
            commands newline
            err newline newline
        ]
        alert "** Network Connection Error **  Try Again"
        return none
    ]
]
do %rebgui.r
contacts: load server-exec {to-block load %contacts}
display/close "Contacts" [
    t: table 78x34 options [
        "Name" left .3 "Address" left .4 "Phone" left .3
    ] data contacts [set-texts [n a p] t/selected]
    f: panel data [
        after 2
        text 18 "Name:"     n: field
        text 18 "Address:"  a: field
        text 18 "Phone:"    p: field
        reverse
        button " Submit " [
            server-exec rejoin [{
                remove/part find contacts } mold t/selected { 3 
                insert contacts [} 
                    mold copy n/text { } 
                    mold copy a/text { } 
                    mold copy p/text 
                {]
                to-block contacts
            }]
            attempt [t/remove-row (index? find t/data t/selected) + 2 / 3]
            t/add-row/position reduce [
                copy n/text copy a/text copy p/text
            ] 1
            set-texts [n a p] ""
        ]
        button " New " [set-texts [n a p] ""  t/redraw]
        button " Delete " [
            server-exec rejoin [{
                remove/part find/skip contacts } mold t/selected { 3 3
                to-block contacts
            }]
            t/remove-row (index? find t/data t/selected) + 2 / 3
            set-texts [n a p] ""
        ]
    ]
] [
    if 1 = load server-exec {save %contacts contacts 1} [alert "Saved"]
    quit
]
do-events

Вот конкретный вариант сервера, который мы будем использовать для обработки клиентского скрипта выше. Единственная разница между ним и обобщённым сервером в том, что файл %contacts загружается в память:

rebol [title: "Contacts Data Server"]
if not exists? %contacts [write %contacts ""]
contacts: load %contacts
print "waiting..."
port: open/lines tcp://:55555
forever [
    if error? er: try  [
        probe commands: load data: first wait connect: first wait port
        probe result: mold do commands
        insert connect result
        close connect
        true
    ] [
        er: disarm :er
        net-error: rejoin [
            form now newline
            mold commands newline 
            er newline newline 
        ]
        write %server-error.txt net-error
        print net-error
        close connect
    ]
]

Запустите указанный выше сервер, затем запустите столько разных экземпляров клиентского скрипта, все на разных машинах, если хотите, и все они будут хорошо работать вместе.

Вышеупомянутая модель полезна для обработки широкого спектра действий по управлению данными, и она может обрабатывать множество одновременных пользователей, потому что все сетевые запросы естественным образом ставятся в очередь на сервере. Это результат присущей циклу кода сервера синхронной природы. В любой момент времени может быть открыто только одно соединение, и только одна операция выполняется одновременно с данными. Таким образом, в любой момент каждый сетевой запрос работает только с самым последним блоком данных, хранящимся в памяти сервера. Это гарантирует, что все пользователи имеют одновременный доступ к самым актуальным данным в любой момент, когда они выполняют операцию манипулирования данными, что несколько пользователей никогда не перезаписывают изменения, внесённые другими, которые работают одновременно с данными, что система никогда не испытывает одновременные ошибки записи данных и т.д..

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

Операции манипулирования данными в этой модели выполняются быстро, поскольку вся база данных по-прежнему управляется в памяти серверного компьютера. Единственная дополнительная нагрузка на производительность связана с передачей значений запроса и результата по сети. Запросы обычно состоят из очень маленьких пакетов кода и данных, а результаты, как правило, ограничиваются кодами ответов и/или небольшими подмножествами полей или строками значений, выбранными из базы данных, поэтому производительность сетевой модели имеет тенденцию хорошо расти. в большинстве типичных приложений. Полный спектр последовательных функций, наряду с циклическими структурами, условными выражениями, синтаксическим анализом, математикой и другими функциями нативного языка в Rebol, обеспечивает все возможности манипулирования данными, обычно предоставляемые СУБД SQL (и другими).

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

Вы можете рассматривать эту крошечную родную клиент-серверную "платформу" как замену более тяжёлым инструментам СУБД, таким как MySQL. Поскольку это весь собственный код Rebol, у вас есть полный контроль над каждой операцией. Любой код, который можно запустить в простом однопользовательском приложении, можно отправить на сервер с помощью функции server-exec. Вы можете выбрать, какой код поместить в своё клиентское приложение, а какой переместить в сценарий сервера, какие функции безопасности включить, как оптимизировать производительность, как улучшить читаемость и т.д. - всё это просто чистый простой код Rebol.

18.4 Более длинный пример

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

Вот общий код, используемый обоими клиентскими приложениями:

REBOL [title: "Common Functions and Code"]

; FOLDER, FILE, AND VARIABLE INITIALIZATION ==============================

unless exists? %serverip.txt [write %serverip.txt "localhost"]
serverip: read %serverip.txt

; FUNCTIONS ==============================================================

server-exec: func [strng] [
    commands: trim/lines strng
    if error? err: try [
        port: open/lines rejoin [tcp:// serverip ":55555"]
        insert port mold commands
        data-response: first connect: wait port
        close connect
        close port
        return data-response
    ] [
        err: disarm :err
        write/append %net-err.txt rejoin [
            now newline 
            commands newline
            err newline newline
        ]
        alert "** Network Connection Error **  Try Again"
        return none
    ]
]
change-adminpw: func [/dlt] [
    if error? try [
        old-userpass-block: copy load server-exec {get-old-userpass-block}
    ] [return]
    alert rejoin [
        "Current Usernames and Passwords:^/^/" (mold old-userpass-block)
    ]
    title-text: either dlt [
        "Remove username/password:  "
    ] [
        "New username/password:  "
    ]
    new-userpass: request-password/verify
    title-text: copy system/script/header/title  show main-screen
    if any [
       new-userpass = none 
       new-userpass/1 = "" 
       new-userpass/2 = ""
    ] [
        alert "Changes NOT SAVED (both fields must be completed)."
        return
    ]
    either dlt [
        if error? try [
            server-exec rejoin [
                {delete-adminpw } 
                mold old-userpass-block { } 
                mold new-userpass " true"
            ]
        ] [
            alert "Admin username/password NOT removed."  
            return
        ]
        alert "Admin username/password removed." 
    ] [
        if error? try [
            server-exec rejoin [
                {add-adminpw } 
                mold old-userpass-block { } 
                mold new-userpass " true"
            ]
        ] [
           alert "New Admin username/password NOT saved."  
           return
        ]
        alert "New admin username/password saved."
    ]
]

; LOGIN ==================================================================

do %rebgui.r

if error? try [admin-database: load server-exec {get-admin-database}] [
    quit
]
do login: [
    userpass: request-password
    if any [userpass = none find userpass ""] [quit]
    logged-in: false
    foreach [user pass] admin-database [
        if (userpass/1 = user) and (userpass/2 = pass) [
            logged-in: true 
            break
        ]
    ]
    either logged-in [] [alert "Incorrect Username/Password" do login]
]

Вот серверное приложение:

REBOL [title: "Work Order Server"]

; FOLDER, FILE, AND VARIABLE INITIALIZATION ==============================

unless exists? %adminpw [
    save %adminpw to-binary encloak mold ["admin" "password"] "1234"
]   
unless exists? %superpw [
    save %superpw to-binary encloak mold ["super" "secret"] "1234"
]     
make-dir %./history/
if not exists? %orders-data.txt [write %orders-data.txt ""]
orders: load %orders-data.txt
if not exists? %ordernum.txt [save %ordernum.txt 1]

; FUNCTIONS ==============================================================

get-super-pass: does [load decloak to-string load %superpw "1234"]
get-admin-database: does [load decloak to-string load %adminpw "1234"]
get-old-userpass-block: does [load decloak to-string load %adminpw "1234"]
delete-adminpw: func [old-userpass-block new-userpass] [
    save %adminpw to-binary encloak (
        mold admin-database: head remove/part find/skip old-userpass-block
        new-userpass/1 2 2
    ) "1234"
]
add-adminpw: func [old-userpass-block new-userpass] [
    save %adminpw to-binary encloak (
        mold admin-database: append old-userpass-block new-userpass
    ) "1234"
]

; SERVER LOOP ============================================================

print "waiting..."

port: open/lines tcp://:55555
forever [
    if error? er: try  [
        probe commands: load data: first wait connect: first wait port
        probe result: mold do commands
        insert connect result
        close connect
        true
    ] [
        er: disarm :er
        net-error: rejoin [
            form now newline
            mold commands newline 
            er newline newline 
        ]
        write %server-error.txt net-error
        print net-error
        close connect
    ]
]

Вот первое клиентское приложение, которое позволяет пользователям вводить заказы на работу:

REBOL [title: "New Work Orders"]

; FUNCTIONS ==============================================================

save-order: func [order-data] [
    server-exec rejoin [{
        order-data: } mold order-data {
        write to-file rejoin [
            %./history/ 
            now/year "-" now/month "-" now/day 
            "_" replace/all form now/time ":" "-"
            "_" "orders-data.txt"
        ] read %orders-data.txt
        append orders order-data 
        save %orders-data.txt orders
        order-transaction: rejoin [
            form now " - Saved Order:" newline 
            mold order-data newline newline
        ]
        write/append %order-history.txt order-transaction
        print order-transaction
        true
    }]
]
submit-all: does [
    submitted: copy []
    repeat i 2 [append submitted get-values main-screen/pane/:i]
    replace/all submitted 1 "Yes"
    replace/all submitted 2 "No"
    insert head submitted get-values customer-panel
    if any [
        find submitted none
        find submitted ""
    ] [
        if not question "Submit incomplete data?" [return]
    ]
    insert head submitted now
    unless next-inv: server-exec {
        ordernum: load %ordernum.txt
        ordernum: ordernum + 1
        save %ordernum.txt ordernum
        ordernum
    } [return]
    insert head submitted next-inv
    either save-order submitted [alert "Saved"] [return]
]

do %common.r

; GUI ====================================================================

ctx-rebgui/on-fkey/f3: make function! [face event] [submit-all]

display/maximize/close/min-size copy system/script/header/title [
    main-screen: tab-panel #LVHW data [
        action [wait .2  set-focus name-field] " Customer " [
            after 1
            customer-panel: panel 87 data [
                after 2
                text 23 "Name:"   name-field: field
                text 23 "Address:"   field
                text 23 "City, State:"   field
                text 23 "Zip code:"   field
            ]
            text   ; placeholder
            after 2
            text 28 "Drop Down 1:"   drop-list data ["aaa" "bbb" "ccc"]
            text 28 "Drop Down 2:"   drop-list data ["ddd" "eee" "fff"]
            text 28 "Edit List 1:"   edit-list data ["111" "222" "333"]
            text 28 "Edit List 2:"   edit-list data ["444" "555" "666"]
            text 28 "Yes/No 1:"   radio-group 30x5 data ["Yes" "No"] 
            text 28 "Yes/No 2:"   radio-group 30x5 data ["Yes" "No"] 
        ]
        action [wait .2  face/color: 244.241.255] " Order Info " [
            after 3
            text 28 "Date 1:"   day-field1: field
                text "..." [set-text day-field1 request-date]
            text 28 "Date 2:"   day-field2: field
                text "..." [set-text day-field2 request-date] 
            after 2
            text 28 "Area1:" area
            text 28 "Area2:" area
            text text bar text
            text 44 button 35x15 " Submit All Entries " [submit-all] 
        ]
        action [
            if error? try [
                unless (
                    load server-exec {get-super-pass}
                ) = request-password [
                    main-screen/select-tab 1
                ]
            ] [main-screen/select-tab 1 return]
            wait .2 face/color: 240.255.240
        ] " Options " [
            after 1
            text bold "Add New Admin Username/Password" [
                change-adminpw
            ]
            text bold "Remove Admin Username/Password" [
                change-adminpw/dlt
            ]
            bar
            text bold "Version" [
                alert form system/script/header/version
            ]
        ]
    ] 
] [question "Really Close?"] (system/view/screen-face/size / 4)
set-focus name-field
do-events

Вот второе клиентское приложение, которое позволяет пользователям просматривать выбранные рабочие задания:

REBOL [title: "Order Viewer"]

; FUNCTIONS ==============================================================

extract-table-data: does [
    if error? try [
        ordrs: load server-exec {to-block load %orders-data.txt}
    ] [
        quit
    ]
    gui-table-data: copy []
    forskip ordrs 16 [
        append gui-table-data reduce [
            ordrs/1 ordrs/3 ordrs/2 ordrs/7 ordrs/9 ordrs/11 ordrs/13
        ]
    ]
    gui-table-data
]

do %common.r

; GUI ====================================================================

screen-size: system/view/screen-face/size
cell-width: to-integer (screen-size/1) / (ctx-rebgui/sizes/cell) - 16
cell-height: to-integer (screen-size/2) / (ctx-rebgui/sizes/cell)
table-size: as-pair cell-width (to-integer cell-height / 1.21)

extracted-table-data: extract-table-data

display/maximize/close/min-size copy system/script/header/title [
    main-screen: tab-panel #LVHW data [
        action [
            wait .2 
        ] " Orders " [
            orders-table: table table-size options [
                "Order #" left .1
                "Name" left .3
                "Date" left .2
                "Info 1" left .1
                "Info 2" left .1
                "Info 3" left .1
                "Info 4" left .1
            ] data extracted-table-data 
        ]
        action [
            if error? try [
                unless (
                    load server-exec {get-super-pass}
                ) = request-password [
                    main-screen/select-tab 1
                ]
            ] [main-screen/select-tab 1]
            wait .2 face/color: 240.255.240
        ] " Options " [
            after 1
            text bold "Add New Admin Username/Password" [
                change-adminpw
            ]
            text bold "Remove Admin Username/Password" [
                change-adminpw/dlt
            ]
            bar
            text bold "Version" [
                alert form system/script/header/version
            ]
        ]
    ] 
] [question "Really Close?"] (system/view/screen-face/size / 2)
do-events

18.5 Получение динамически назначаемых адресов серверов

TCP-сервер должен быть найден по известному IP-адресу (например, 192.168.1.10). Если сетевой маршрутизатор настроен на назначение IP-адресов с использованием DHCP (опция по умолчанию для большинства стандартных маршрутизаторов), то IP-адрес, к которому подключаются клиенты, может потребоваться изменить в клиентской программе, возможно, в любое время, когда сервер компьютер перезагружается.

Одно из решений - настроить серверную машину со статическим IP-адресом в маршрутизаторе. Этот шаг настройки отличается для каждого производителя маршрутизатора, он часто выходит за рамки технических возможностей пользователя клиентского приложения, маршрутизатор может быть недоступен из-за проблем безопасности в корпоративной среде и т.д.

Другое решение - загрузить текущий IP-адрес сервера на сервер по известному URL-адресу (веб-сервер, FTP и т.д., управляемый вне сайта), но это просто распространяет проблему на другой сетевой сервер, требует подключения к Интернету и т.д.

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

Грубое решение для простых приложений - просто вручную ввести IP-адрес сервера (т.е. Джо кричит Джону в коридоре: "IP-адрес сервера - 192.168.1.10").

Приведенный ниже код демонстрирует постоянно используемое решение для всех приложений TCP, которое не требует настройки маршрутизатора или внешней сети. Он создает два отдельных сценария, которые запускаются на клиенте и сервере, для управления всеми обновлениями IP-адресов сервера. Сценарий %send-ip.r запускается на сервере и непрерывно транслирует IP-адрес через UDP. Сценарий %receive-ip.r запускается на клиенте, получает текущий IP-адрес и записывает его в файл. Поскольку UDP является широковещательным протоколом, для его работы не требуются известные IP-адреса. Как только серверный сценарий запущен, все клиенты могут просто запустить и получить текущий транслируемый IP-адрес. Этот пример включает отдельное приложение чата TCP, которое просто считывает сохранённый IP-адрес и подключается к серверу. Никакой другой конфигурации сети не требуется. Чтобы реализовать эту процедуру в любом приложении TCP, просто запустите сценарий% send-ip.r на любом сервере, запустите сценарий %receive-ip.r на любом клиенте(ах), и вы сможете прочитать файл %local-ip.r файл в ваших клиентских приложениях для подключения к текущему IP-адресу сервера:

REBOL [title: "UDP Communicate Server IP"]
write %receive-ip.r {rebol []
net-in: open udp://:9905
print "waiting..."
forever [
    received: wait [net-in]
    probe join "Received: " trim/lines ip: copy received
    write %local-ip.r ip
    wait 2
]}
launch %receive-ip.r
write %send-ip.r {rebol []
net-out: open/lines udp://255.255.255.255:9905
set-modes net-out [broadcast: on]
print "Sending..."
forever [
    insert net-out form read join dns:// read dns://
    wait 2
]}
launch %send-ip.r
write %tcp-chat.r {rebol [title: "TCP-Chat"]
view layout [ across
    q: btn "Serve"[focus g p: first wait open/lines tcp://:8 z: 1]text"OR"
    k: btn "Connect"[focus g p: open/lines rejoin[tcp:// i/text ":8"]z: 1]
    i: field form read %local-ip.r  return  ; read join dns:// read dns://
    r: area rate 4 feel [engage: func [f a e][if a = 'time and value? 'z [
        if error? try [x: first wait p] [quit]
        r/text: rejoin [x newline r/text] show r
    ]]]  return
    g: field "Type message here [ENTER]" [insert p value  focus face]
]}
wait 2
launch %tcp-chat.r
launch %tcp-chat.r

18.6 Интерфейсы HTML-форм для клиентов

Ещё одним полезным решением в некоторых многопользовательских средах является создание серверного приложения, которое обслуживает HTML-формы и обрабатывает данные, возвращаемые пользователями из этих форм. Это может быть полезно при сборе данных от пользователей, которые подключаются к вашей сети с помощью различных специальных мобильных устройств. Клиенты могут вводить данные с любого устройства (iPhone, Android, нетбук и т.д.) Для ввода информации в серверное приложение, если на устройстве есть базовый веб-браузер и подключение к Wi-Fi (или другой сети). Это практично в средах, где клиентам, клиентам, студентам или другим группам пользователей необходимо взаимодействовать с серверным приложением в вашей локальной среде. Поскольку мобильные устройства с Wi-Fi и браузеры настолько распространены, это часто жизнеспособный и удобный способ для пользователей с улицы немедленно взаимодействовать с вашим серверным приложением, без необходимости предоставлять какой-либо общедоступный доступ к оборудованию и не требуя от пользователей установки какого-либо программного обеспечения. Кроме того, этот вариант может быть полезен в средах, где многим внутренним пользователям необходимо иметь недорогие устройства для ввода данных. Например, различные устройства Android доступны в ценовом диапазоне от 50 до 70 долларов. Их можно легко переносить, использовать без стола, мыши или клавиатуры и т.д., поэтому они отлично подходят для ситуаций, когда сотрудники перемещаются по месту работы, проводят инвентаризацию и т.п.

В приведённом ниже примере представлена ​​структура сокращённого кода, позволяющая реализовать эту возможность. Просто отредактируйте пример HTML-формы и делайте всё, что хотите, с блоком данных переменной z, возвращаемым пользователем(ами). Помимо основного кода сетевого порта и цикла сервера, большую часть работы выполняют функции decode-cgi и write-io. Функция 'decode-cgi' обрабатывает входящие данные, отправленные HTML-формой. Функция write-io отправляет HTML-строку клиенту через сетевой порт:

REBOL [title: "HTML Form Server"]
l: read join dns:// read dns://
print join "Waiting on:  " l
port: open/lines tcp://:80
browse join l "?"
forever [
    connect: first port
    if error? try [
        z: decode-cgi replace next find first connect "?" " HTTP/1.1" ""
        prin rejoin ["Received: " mold z newline]
        my-form: rejoin [
            {HTTP/1.0 200 OK^/Content-type: text/html^/^/
            <HTML><BODY><FORM ACTION="} l {">Server:  } l {<br><br>
                Name:<br><INPUT TYPE="TEXT" NAME="name" SIZE="35"><br>
                Address:<br><INPUT TYPE="TEXT" NAME="addr" SIZE="35"><br>
                Phone:<br><INPUT TYPE="TEXT" NAME="phone" SIZE="35"><br>
                <br><input type="checkbox" name="checks" value="i1">Item 1
                <input type="checkbox" name="checks" value="i2">Item 2
                <input type="radio" name="radios" value="yes">Yes
                <input type="radio" name="radios" value="no">No<br><br>
                <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
            </FORM></BODY></HTML>}
        ]
        write-io connect my-form (length? my-form)
    ] [print "(empty submission)"]
    close connect
]

В приведённом ниже коде показано полезное приложение, созданное из приведённого выше сценария Form Server. При каждом запуске он генерирует уникальную HTML-форму на основе пользовательских спецификаций (любое количество проверок, радио и текстовых элементов ввода) и запускает сервер для получения ответов на опрос от аудитории (все они подключаются к серверу LAN с помощью телефонов или любое другое устройство для подключения к Интернету с Wi-Fi). Все ответы на опрос сохраняются в файле, указанном пользователем, а в включённом демонстрационном отчёте отображаются все отправленные записи, а также общий список всех проверок и переключателей. Затем он представляет столбчатую диаграмму, отображающую проверку опроса и результаты переключателей:

REBOL [title: "Room Poll (HTML Survey Generator for LANs)"]
view center-face layout [
    style area area 500x100
    across
    h4 200 "SURVEY TOPIC:" 
    h4 200 "Response File:" return
    f1: field 200 "Survey #1"
    f2: field 200 "survey1.db"
    below
    h4 "SURVEY CHECK BOX OPTIONS:"
    a1: area "Check Option 1^/Check Option 2^/Check Option 3"
    h4 "SURVEY RADIO BUTTON OPTIONS:"
    a2: area "Radio Option 1^/Radio Option 2^/Radio Option 3"
    h4 "SURVEY TEXT ENTRY FIELDS:"
    a3: area "Text Field 1^/Text Field 2^/Text Field 3"
    btn "Submit" [
        checks: parse/all a1/text "^/" remove-each i checks [i = ""]
        radios: parse/all a2/text "^/" remove-each i radios [i = ""]
        texts: parse/all a3/text "^/" remove-each i texts [i = ""]
        title: join uppercase f1/text ":"
        response-file: to-file f2/text
        unview
    ]
]
write response-file ""
write %poll-report.r rejoin [{
rebol [title: "Poll Report"]
view center-face layout [
    btn 100 "Generate Report" [
        all-checks: copy []
        all-radios: copy []
        print newpage
        print {All Entries:^/}
        foreach response load %} response-file {[
            x: construct response
            ?? x
            if find first x 'checks [
                either block? x/checks [
                    foreach check x/checks [
                        append all-checks check
                    ]
                ][
                    append all-checks x/checks
                ]
            ] 
            if find first x 'radios [
                either block? x/radios [
                    foreach radio x/radios [
                        append all-radios radio
                    ]
                ][
                    append all-radios x/radios
                ]
            ]            
        ]
        alert rejoin [
            "All Checks: "  mold all-checks
            " All Radios: " mold all-radios
        ]
        check-count: copy []
        foreach i unique all-checks [
            cnt: 0
            foreach j all-checks [
                if i = j [cnt: cnt + 1]
            ]
            append check-count reduce [i cnt]
        ]
        radio-count: copy []
        foreach i unique all-radios [
            cnt: 0
            foreach j all-radios [
                if i = j [cnt: cnt + 1]
            ]
            append radio-count reduce [i cnt]
        ]
        bar-size: to-integer request-text/title/default
            "Bar Chart Size:" "40"
        g: copy [backdrop white text "Checks:"] 
        foreach [m v] check-count [
            append g reduce ['button m v * bar-size]
        ]
        append g [text "Radios:"]
        foreach [m v] radio-count [
            append g reduce ['button gray m v * bar-size]
        ]
        view/new center-face layout g
    ]
    btn 100 "Edit Raw Data" [
       alert "Be careful!"
       editor %} response-file {
    ]
]
}]
launch %poll-report.r
poll: copy ""
repeat i len: length? checks [
    append poll rejoin [
        {<input type="checkbox" name="checks" value="} i {">}
        checks/:i {<br>} newline
    ]
]
append poll {<br>}
repeat i len: length? radios [
    append poll rejoin [
        {<input type="radio" name="radios" value="} i {">}
        radios/:i {<br>}
        newline
    ]
]
append poll {<br>}
repeat i len: length? texts [
    append poll rejoin [
        texts/:i {:<br><INPUT TYPE="TEXT" NAME="text} i 
        {" SIZE="35"><br>} newline
    ]
]
append poll {<br><INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">}
l: read join dns:// read dns://
print join "Waiting on:  " l
port: open/lines tcp://:80
browse join l "?"
responses: copy []
forever [
    q: first port
    if error? try [
        z: decode-cgi replace next find first q "?" " HTTP/1.1" ""
        if not empty? z [
            append/only responses z
            save response-file responses
            print newpage
            entry-received: construct z
            ?? entry-received
        ]
        d: rejoin [
            {HTTP/1.0 200 OK^/Content-type: text/html^/^/
            <HTML><BODY><FORM ACTION="} l {">} title {<br><br>}
            poll
            {</FORM></BODY></HTML>}
        ] 
        write-io q d length? d
    ] [] ;[print "(empty submission)"]
    close q
]
halt

Сочетание Rebol и HTML - это простой способ расширить охват и возможности ваших многопользовательских приложений для управления данными. Научитесь использовать "write/custom", "decode-cgi" и "write-io", и вы сможете создавать серверы, которые работают как с веб-клиентами, так и с чистыми клиентами Rebol. Подробнее об этом можно узнать на http://www.rebol.com/docs/quick-start6.html. Обратите особое внимание на функцию send-server в этом руководстве.

18.7 Простота

Представленный здесь пример кода предоставляет всё необходимое для создания сетевых многопользовательских приложений для управления данными. Общая модель использования функции server-exec и небольшого количества серверного кода для обработки запросов обеспечивает основу для обработки всего необходимого разделения кода клиент-сервер. Вся структура состоит всего из нескольких десятков строк полностью нативного кода Rebol. Нет необходимости иметь дело с проблемами установки и поддержки стороннего программного обеспечения, проблемами обновления, проблемами совместимости или другими дилеммами обслуживания. Клиентские и серверные машины могут состоять из любого сочетания оборудования и платформ ОС, на которых установлен Rebol. Поскольку любое программное обеспечение, созданное с использованием этого дизайна, предназначено для обработки только необходимых операций, оно абсолютно не более сложное или тяжёлое, чем необходимо для выполнения его цели, и оно настолько гибкое и способное, насколько это необходимо для удовлетворения его функциональности. А поскольку код и все части системы построены исключительно из конструкций собственного языка Rebol, код можно ещё больше упростить за счёт разработки диалектов, если это окажется стоящим делом (например, если API разработчика был необходим для интерфейса с более сложной распределённой системой). Полностью устранены сложности, связанные с интеграцией сторонних моделей данных и API. Это все просто базовый Ребол. Всё, что может быть выполнено с помощью простого однопользовательского сценария, можно легко разделить на мультиклиентские компоненты (и многосерверные компоненты ... но это тема для целой другой статьи) применив простые концепции, продемонстрированные здесь.

19. Создание мобильных и веб-приложений с помощью jsLinb и Sigma Visual Builder

Автономная копия этого раздела руководства с множеством снимков экрана доступна по адресу http://re-bol.com/jslinb/. Эту тему, в частности, будет намного легче отслеживать, добавив эти снимки экрана.

Видео, демонстрирующее части этой темы, также можно найти по адресу https://youtu.be/6yrDNluQSwo.

19.1 Что такое библиотека jsLinb и Sigma Visual Builder?

Для создания клиентских приложений с графическим интерфейсом пользователя для операционных систем, которые R2/View и R3-GUI не поддерживают, простое решение - использовать инструментарий по адресу http://sourceforge.net/projects/ajaxuibuilder/(6 МБ). Эта загрузка содержит библиотеку JavaScript под названием "jsLinb" и IDE "Sigma Visual Builder", которую можно использовать для создания приложений с графическим интерфейсом jsLinb путём нажатия и перетаскивания. И библиотека, и визуальная IDE лёгкие, но при этом поразительно мощные. Не только созданные клиентские приложения работают практически в любом браузере с поддержкой JavaScript (от Firefox 1.5 и IE6 до практически всех существующих современных мобильных и настольных браузеров), но и вся IDE, инструменты документации и все необходимое для создания приложений также запускаются на всех тех же платформах.

Это означает, что вы можете использовать любую версию iPhone, iPad, телефонов и планшетов Android, устройств Blackberry, телефонов и планшетов с Windows или любую версию любой старой или новой операционной системы для настольных ПК для создания интерфейсных приложений с графическим интерфейсом, которые подключаются к коду сервера Rebol. Созданные клиентские приложения могут быть немедленно запущены в любой другой операционной системе или на вашем веб-сайте без каких-либо изменений в коде графического интерфейса пользователя. Вы также можете использовать jsLinb/Sigma IDE для простого подключения и перемещения данных между серверными приложениями, написанными на любом другом языке. Вся система полностью независима от сервера и использует стандартные протоколы http:// и методы Ajax для передачи данных в виде обычного текста, JSON, XML и других распространённых форматов данных. Библиотека jsLinb и IDE Sigma builder полностью документированы, они бесплатны для использования в коммерческих проектах (под лицензией LGPL), а с помощью сторонних инструментов, таких как Cordova, Phonegap Build, Node Webkit или других, вы можете легко упаковать созданные клиентские пользовательские интерфейсы на основе браузера в автономные приложения для распространения в магазинах приложений любой современной платформы ОС.

Библиотеку jsLinb можно использовать полностью самостоятельно с помощью кода JavaScript на любой веб-странице. Никаких других инструментов не требуется, кроме чисто рукописного кода API jsLinb (просто распространите копию файла(ов) времени выполнения jsLinb JavaScript с вашим кодом). Программное обеспечение Sigma Visual Builder - это необязательная IDE на основе браузера, созданная с использованием jsLinb, которая упрощает жизнь, визуально генерируя даже самый сложный код jsLinb. Это может помочь вам приступить к написанию кода API jsLinb, повысить общую продуктивность и уменьшить синтаксические ошибки за счёт визуальной компоновки пользовательских интерфейсов jsLinb даже после того, как вы полностью ознакомитесь с API. Код любого приложения jsLinb/Sigma может полностью содержаться в одном текстовом файле, а синтаксис необычайно прост для чтения и записи вручную, с визуальной IDE или без неё.

Вам не нужен предыдущий опыт работы с JavaScript, чтобы использовать Sigma IDE или API кода jsLinb. Этот текст объяснит, как начать использовать визуальный конструктор, а также основные конструкции кода, необходимые для подключения к серверным приложениям Rebol. Вы можете приступить к созданию полезных браузерных клиентов для приложений управления данными сервера Rebol примерно за час.

19.2 Установка Sigma Builder на веб-сервер

Чтобы использовать конструктор Sigma Visual Builder, содержимое указанного выше zip-пакета необходимо распаковать в общедоступную папку на любом веб-сервере (например, в .../www/ или .../htdocs/ и т.д.). Вы можете установить его на любой знакомый вам веб-сервер в любой операционной системе, если там могут работать сценарии PHP и Rebol CGI. Чтобы использовать все возможности Sigma Visual IDE, на вашем сервере должен быть запущен минимум PHP4 + (рекомендуется PHP5 +). Вам не нужно ничего знать о PHP, чтобы использовать jsLinb или конструктор Sigma, и ваши созданные приложения не будут иметь абсолютно никаких зависимостей от PHP. Вы не увидите и не будете использовать какой-либо PHP-код при разработке приложений jsLinb. Код PHP в построителе Sigma просто предоставляет некоторые функции управления файлами в среде IDE.

Примеры в этом руководстве были протестированы с использованием недорогих учётных записей общего хостинга LAMP в Lunarpages и HostGator, на устройствах Android с использованием серверного приложения KSWeb и на настольных компьютерах Windows с использованием пакета Uniserver WAMP: http://sourceforge.net/projects/miniserver/files/Uniform%20Server/3.3/ (эта старая версия работает нормально и выбрана здесь для демонстрации из-за её небольшого размера загрузки 6 МБ). Несколько других пакетов LAMP и WAMP для различных ОС были протестированы этим автором, и все они работают из коробки, с каждым протестированным браузером (от IE6 и Firefox 1.5 до браузеров по умолчанию, которые поставляются с Android 2.2 и самого первого iPhone, а также широкий ассортимент различных новых и старых настольных и мобильных браузеров).

Вот краткое объяснение того, как установить jsLinb/Sigma builder на Uniform Server в Windows для целей этого руководства:

На самом деле никакой реальной установки не требуется - просто распакуйте указанный выше пакет Uniform Server в любую папку на вашем жёстком диске, а затем распакуйте zip-файл Sigma Builder в какую-нибудь подпапку, куда вы распаковали .../UniformServer/udrive/www/. Вы можете переименовать папку .../UniformServer/udrive/www/sigma-visual-builder-2.0.2_full/ на что-нибудь более короткое (в этом руководстве мы переименуем её в .. / UniformServer/udrive/www/sigma/). Щёлкните файл Server_Start.bat в папке UniformServer, затем откройте в браузере адрес http://localhost/sigma/. Там вы увидите домашнюю страницу визуального конструктора сигма. Щёлкните ссылку "Попробовать сейчас" или перейдите прямо на страницу http://localhost/sigma/VisualJS/index.html в своем браузере, и вы увидите приложение Sigma Visual Builder. Перейдите на вкладку "Обычный просмотр", и вы сможете редактировать код API jsLinb, из которого состоит ваше текущее приложение. Вернитесь на вкладку "Дизайн", и любые изменения, внесённые в код, появятся в Visual Builder и наоборот. Код вашего приложения может быть создан/отредактирован вручную с нуля или создан/отредактирован полностью визуально с помощью операций перетаскивания мышью и настроек флажков свойств/событий в среде IDE. Вы можете переключаться между полностью визуальной разработкой и разработкой, полностью основанной на коде, или смешивать любой из двух подходов в любой момент в процессе разработки.

19.3 Базовый код jsLinb и примеры Sigma IDE

Минимальный шаблонный код, который по умолчанию отображается на вкладке редактора кода "Обычное представление" в Sigma IDE:

/*
* The default code is a com class (inherited from linb.Com)
*/
Class('App', 'linb.Com',{
    Instance:{
        //base Class for this com
        base:["linb.UI"],
        //requried class for this com
        required:[],

        properties:{},
        events:{},
        iniResource:function(com, threadid){
        },
        iniComponents:function(com, threadid){
        },
        iniExComs:function(com, hreadid){
        }
    }
});

Замените этот код следующим (скопируйте/вставьте этот код прямо во вкладку Обычный просмотр):

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:[], 
        events:{
            onReady:'_onready'
        }, 
        _onready:function(){
            alert("hi");
        }
    }
});

Обратите внимание, что блок events: {} в приведённом выше коде теперь содержит событие onReady, которое запускает функцию "_onready" ниже. Также обратите внимание, что функция _onready () запускает функцию alert (), которая отображает сообщение "привет" (hi).

Теперь нажмите кнопку "Выполнить" в среде IDE, и вы увидите, как созданное приложение открывается и запускается в новой вкладке браузера. Затем вставьте следующий код во вкладку IDE Normal View (полностью замените предыдущий код):

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:["linb.UI.Button"], 
        events:{
        }, 
        iniComponents:function(){

            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.Button)
                .host(host,"button1")
                .setLeft(70)
                .setTop(110)
                .setCaption("Say Hi")
                .onClick("_button1_onclick")
            );

            return children;

        }, 
        _button1_onclick:function (profile,e,src,value){
            alert ("Hi");
        }
    }
});

Обратите внимание, что приведённая выше функция iniComponents() теперь содержит функцию append(). Эта функция append() используется для добавления всех возможных виджетов из библиотеки jsLinb в любой макет пользовательского интерфейса. Обратите внимание, что виджет добавленной кнопки помечен как "button1", свойства позиции и заголовка установлены, а событие onClick настроено на запуск функции "_button1_onclick". Обратите внимание, что приведённая ниже функция _button1_onclick() просто запускает функцию alert(), которая выдаёт сообщение "Hi". Обратите внимание: поскольку это набор инструментов JavaScript, параметры функций заключаются в круглые скобки, а строки заканчиваются точкой с запятой.

Теперь вернитесь на вкладку Design View, и вы увидите, что кнопка с текстом "Say Hi" появилась на холсте визуального конструктора. С помощью мыши выберите новую кнопку в визуальном построителе, а затем щёлкните раскрывающееся дерево "Свойства" в правой части экрана. Вы увидите, что настройки .properties во вставленном коде приложения точно отражаются в этих визуально редактируемых настройках свойств.

Если вы отредактируете свойства в настройках визуальной среды IDE, изменения будут немедленно отражены в коде всякий раз, когда вы вернётесь на вкладку "Обычный просмотр" - и наоборот. Если вы внесёте изменения в код, они будут немедленно отражены в визуальном макете и в указанном дереве свойств. Например, попробуйте изменить заголовок или положение кнопки как в коде, так и в визуальном редакторе, и вы увидите, что эти изменения отражаются между кодом и визуальным редактором. Таким образом вы можете работать со всеми аспектами любого приложения jsLinb - перемещаться туда и обратно, между кодом и визуальным редактированием, используя любой подход, который вы предпочитаете для данной задачи. Визуальный редактор часто может создавать код, защищающий от ошибок, намного быстрее, чем вы могли бы ввести его вручную.

Теперь щелкните раскрывающееся дерево "событий" в правой части экрана и дважды щелкните событие "onClick". Вы увидите всплывающее окно редактора кода с кодом, который нужно выполнить для этого события. Это тот же самый код, который вы видите на вкладке "Обычный просмотр" в функции _button1_onclick (). Нажмите кнопку "Выполнить" в верхней части экрана, и вы увидите, что созданное приложение открывается и запускается в новой вкладке. Попробуйте нажать кнопку.

19.4 Подключение приложений jsLinb к серверным приложениям Rebol CGI

Чтобы узнать об основах использования Rebol на веб-сервере с интерфейсом CGI, см. Http://business-programming.com/business_programming.html#section-14. Чтобы использовать Rebol с Uniform Server, который мы установили ранее, копию интерпретатора Rebol/Core (rebol.exe) необходимо поместить в папку .../UniformServer/udrive/usr/bin/. Если вы используете какой-либо другой веб-сервер, просто поместите интерпретатор Rebol/Core для операционной системы, в которой вы запускаете веб-сервер, в папку, которую программное обеспечение веб-сервера может использовать для обработки запросов CGI (проверьте программное обеспечение вашего сервера. документацию, если вы не уверены, где это должно быть). Не забудьте указать в первой строке каждого сценария Rebol CGI местоположение вашего интерпретатора Rebol.

Теперь скопируйте этот сценарий CGI Rebol в файл с именем echo.cgi в папке cgi-bin вашего веб-сервера (.../UniformServer/udrive/cgi-bin/ в Uniserver - проверьте документацию вашего сервера, если вы не знаете, где Приложения CGI могут быть расположены в вашем серверном программном обеспечении):

#!/usr/bin/rebol.exe -cs
REBOL []
print "content-type: text/html^/"
data: decode-cgi raw: read-cgi

save %data.txt raw
prin data/2

Обратите внимание на несколько моментов в приведённом выше коде. Во-первых, линия shebang указывает на местоположение интерпретатора Rebol, описанного выше. Первые четыре строки сценария - это всего лишь стандартный шаблонный код для приложений Rebol CGI. Две следующие после этого строки сохраняют отправленные данные в текстовый файл (data.txt), а затем распечатывают отправленное значение данных. Итак, это приложение отображает любой отправленный ему текст. Вы можете проверить это, отправив строку запроса GET непосредственно в виде URL-адреса в вашем браузере: http://localhost/cgi-bin/echo.cgi?Data=Hello!

Когда у вас настроен сценарий Rebol CGI, указанный выше, вставьте следующий код во вкладку просмотра кода Sigma IDE:

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:[
            "linb.UI.Label", "linb.UI.Button", 
            "linb.UI.Input", "linb.UI.Tag"
        ], 
        properties:{}, 
        events:{}, 
        iniResource:function(com, threadid){}, 
        iniComponents:function(){

            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.Input)
                .host(host,"input1")
                .setLeft(30)
                .setTop(40)
                .setWidth(550)
                .setHeight(150)
                .setMultiLines(true)
                .setValue("6ct7y6g78u8hi9o0")
            );

            append((new linb.UI.Button)
                .host(host,"button1")
                .setLeft(30)
                .setTop(210)
                .setCaption("button1")
                .onClick("btn6c")
            );

            append((new linb.UI.Input)
                .host(host,"input2")
                .setLeft(30)
                .setTop(250)
                .setWidth(550)
                .setHeight(130)
                .setMultiLines(true)
            );

            return children;

        }, 
        btn6c:function(){
            var self=this;
            linb.Ajax(
                "http://localhost/cgi-bin/echo.cgi", 
                ('data=' + self.input1.getUIValue()),
                function(s){
                    self.input2.setUIValue(s);
                }
            ).start();
        }
    }
});

Если вы обнаружите, что верхняя часть Sigma IDE больше не видна после вставки или редактирования более длинных фрагментов кода, просто уменьшите масштаб в окне браузера в несколько раз ([CTRL] + [-] в большинстве браузеров для настольных компьютеров или ущипнуть + sqeeze на мобильных экранах), а затем сбросьте масштаб до 100% ([CTRL] + [0] в настольных браузерах или ущипнуть + растянуть на мобильных устройствах). В большинстве браузеров вы также можете щёлкнуть и прокрутить мышью до верхней части IDE.

Обратите внимание, что в приведённом выше коде к макету холста добавлены 3 виджета: ввод текста, кнопка и дополнительный ввод текста. Оба виджета ввода имеют свойство multiline, установленное на true, а значение по умолчанию, отображаемое в первом виджете, было установлено на "6ct7y6g78u8hi9o0".

ВАЖНО: Обратите внимание, что событие onClick виджета кнопки настроено на запуск функции "btn6c". Эта функция - самая важная часть этого примера. Он запускает метод linb.Ajax, который мы будем использовать в оставшейся части этого руководства для подключения к коду Rebol, запущенному на сервере. Обратите внимание, что первым аргументом функции linb.Ajax является URL-адрес сценария Rebol echo.cgi, который мы установили выше. Второй аргумент - это данные, которые мы отправляем этому сценарию CGI. В этом случае мы отправляли некоторый составной текст, 'data =' плюс self.input1.getUIValue(), который является значением, отображаемым в виджете input1. Третий аргумент - это действие, которое может быть выполнено с данными, возвращаемыми серверным скриптом Rebol, в данном случае эти данные помечены как "s". Эти возвращённые данные отображаются в виджете input2 с использованием кода API jsLinb "self.input2.setUIValue(s)".

Эта базовая конструкция linb.Ajax - это главное, чему нужно научиться, чтобы передавать данные между вашим графическим интерфейсом jsLinb и вашими серверными приложениями Rebol. Обратите внимание, что метод linb.Ajax предназначен для использования только для пользовательских интерфейсов, которые исходят с сервера в том же домене, что и серверное приложение CGI. Например, если ваш код jsLinb обслуживается с http: // localhost, он не будет подключаться к сценарию сервера Rebol, размещенному на http://yourdomain.com. Существуют и другие вызовы API jsLinb, которые обрабатывают междоменные вызовы, но в большинстве случаев ваши пользовательские интерфейсы jsLinb и ваши сценарии Rebol CGI будут обслуживаться с одного и того же сервера (т.е. из одного домена), поэтому мы будем иметь дело с перекрёстным доменом Методы Ajax далее в этом руководстве.

Нажмите кнопку запуска в построителе Sigma и запустите приведённый выше сценарий. Вы увидите, что любой текст, введённый в виджет input1, передаётся в сценарий сервера Rebol, обрабатывается, а затем отражённые данные возвращаются в приложение jsLinb и отображаются в виджете input2. Вы можете проверить, что это так, посмотрев файл data.txt в папке cgi-bin на вашем веб-сервере (помните, сценарий echo.cgi содержал 1 строку кода, которая сохраняла отправленные данные в файл data.txt).

Играя с указанным выше приложением, вы можете заметить небольшую проблему. Возвращённые данные не содержат разрывов строк, введённых в виджет input1.

Если вы проверите файл data.txt в папке cgi-bin вашего сервера, вы увидите, что необработанные данные были отправлены без разрывов строк. Чтобы исправить это, замените приведённый выше код следующим:

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:[
            "linb.UI.Label", "linb.UI.Button", 
            "linb.UI.Input", "linb.UI.Tag"
        ], 
        properties:{}, 
        events:{}, 
        iniResource:function(com, threadid){}, 
        iniComponents:function(){

            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.Input)
                .host(host,"input1")
                .setLeft(30)
                .setTop(40)
                .setWidth(550)
                .setHeight(150)
                .setMultiLines(true)
                .setValue("6ct7y6g78u8hi9o0")
            );

            append((new linb.UI.Button)
                .host(host,"button1")
                .setLeft(30)
                .setTop(210)
                .setCaption("button1")
                .onClick("btn6c")
            );

            append((new linb.UI.Input)
                .host(host,"input2")
                .setLeft(30)
                .setTop(250)
                .setWidth(550)
                .setHeight(130)
                .setMultiLines(true)
            );

            return children;

        }, 
        btn6c:function(){
            var self=this;
            linb.Ajax(
                "http://localhost/cgi-bin/echo.cgi", 
                ('data=' + _.serialize(self.input1.getUIValue())),
                function(s){
                    self.input2.setUIValue(_.unserialize(s));
                }
            ).start();
        }
    }
});

Единственная разница в приведённом выше коде заключается в вызове linb.Ajax. Обратите внимание, что функция self.input1.getUIValue() была заключена в функцию _.serialize(). Эта функция является частью linb API и гарантирует, что все данные, полученные от виджета input1, должным образом упакованы перед отправкой на сервер. Обратите внимание, что функция _.unserialize() используется для распаковки данных с сервера (содержащихся в этой переменной 's') перед их отображением обратно в виджете input2. Использование функций _.serialize() и _.unserialize() избавит вас от головной боли при отправке данных между пользовательскими интерфейсами jsLinb и серверными приложениями.

Простых конструкций кода, которые вы видели до сих пор, достаточно, чтобы приступить к созданию многих видов полезных приложений для управления данными. Вы можете использовать виджеты GUI в jsLinb/Sigma, чтобы получать данные от пользователей, отправлять их на серверные сценарии Rebol, чтобы выполнять грязную работу по обработке данных, постоянному хранению и т.д., а затем отправлять результаты обратно для отображения в браузере пользователя. API jsLinb Ajax предоставляет все необходимое для обмена данными с кодом вашего сервера Rebol. Используя визуальный конструктор для компоновки виджетов GUI, вы уже знаете достаточно, чтобы создавать некоторые базовые приложения CRUD. Следующий раздел содержит несколько примеров приложений.

19.5 Примеры приложений, созданных с использованием кода CGI jsLinb и Rebol

В этом разделе демонстрируются некоторые простые приложения, созданные с использованием уже знакомых вам функций jsLinb/Sigma. Несколько дополнительных виджетов и методов jsLinb будут объяснены по пути.

19.5.1 Список дел

Сохраните следующий сценарий Rebol в файл с именем todo.cgi в папке cgi-bin вашего веб-сервера:

#!/usr/bin/rebol.exe -cs
REBOL []
print "content-type: text/html^/"
data: decode-cgi raw: read-cgi
save %todo.log raw
either data/2 = "loaddata" [
    either error? try [
        sv: read %todo.txt
    ][prin "[]"] [prin sv]
][
    write %todo.txt data/2
    prin "Saved!"
]
quit

Вставьте приведённый ниже код на вкладку Normal View в Sigma IDE:

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:["linb.UI.List", "linb.UI.Button", "linb.UI.Input"], 
        properties:{}, 
        events:{"onReady":"_onready"}, 
        iniResource:function(com, threadid){}, 
        iniComponents:function(){

            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.List)
                .host(host,"list1")
                .setTips("double-click to remove item")
                .setLeft(20)
                .setTop(20)
                .setWidth(460)
                .setHeight(190)
                .onDblclick("_list1_ondblclick")
            );

            append((new linb.UI.Input)
                .host(host,"input1")
                .setLeft(20)
                .setTop(230)
                .setWidth(460)
                .onBlur("_input1_onblur")
            );

            append((new linb.UI.Button)
                .host(host,"button1")
                .setLeft(360)
                .setTop(270)
                .setHeight("22")
                .setCaption("Save To Server")
                .onClick("_button1_onclick")
            );

            return children;
        }, 
        iniExComs:function(com, hreadid){
        }, 
        _list1_ondblclick:function (profile,item,src){
            this.list1.removeItems([item.id])
        }, 
        _input1_onblur:function (profile){
            this.list1.insertItems([{
                id : this.input1.getUIValue(),
                caption : this.input1.getUIValue()
            }],null,false)
            this.input1.setUIValue("")
        }, 
        _button1_onclick:function (profile,e,src,value){
            linb.Ajax(
                "http://localhost/cgi-bin/todo.cgi",
                ('savedata=' + _.serialize(this.list1.getItems())),
                function(s){
                    alert(s);
                }
            ).start();
        }, 
        _onready:function (com,threadid){
            var self=this
            linb.Ajax(
                "http://localhost/cgi-bin/todo.cgi",
                'loaddata=loaddata',
                function(s){
                    self.list1.setItems(_.unserialize(s));
                }
            ).start();
        }
    }
});

Теперь переключитесь на вкладку Design View в Sigma IDE и нажмите кнопку Run. Вы можете протестировать приложение, введя несколько записей в поле ввода текста. Нажимайте клавишу [ENTER] после каждого ввода текста (или опцию "перейти" на всплывающей клавиатуре Android, либо вы также можете щёлкнуть в любом месте за пределами текстового виджета, чтобы отправить каждую запись, если вы используете мобильное устройство, которое не поддерживает какой-то вариант клавиатуры go / enter). Вы увидите, что каждая из представленных записей добавляется в виджет списка. Чтобы удалить любой элемент в виджете списка, дважды щёлкните этот элемент. Когда вы закончите добавлять/удалять элементы, нажмите кнопку "Сохранить на сервере". Закройте приложение в браузере и запустите его снова (или просто обновите страницу браузера приложения), и вы увидите, что ваши сохранённые данные автоматически загружаются с сервера и отображаются в виджете списка.

Откройте файл todo.txt в папке cgi-bin на вашем веб-сервере, и вы увидите, что данные были сохранены в этот файл на сервере (это просто какой-то простой Json).

Теперь давайте подробнее рассмотрим как код графического интерфейса jsLinb, так и код сервера Rebol CGI. Как в визуальном конструкторе Sigma, так и в чистом коде jsLinb вы можете видеть, что к холсту добавлены три виджета (список, ввод и кнопка). Вот соответствующий код виджета jsLinb из приведённого выше примера. Все это было создано путём перетаскивания виджетов на холст на вкладке Design View в Sigma IDE (визуальный конструктор), а затем визуального щелчка/редактирования свойств добавленных виджетов с использованием дерева визуальных свойств:

append((new linb.UI.List)
    .host(host,"list1")
    .setTips("double-click to remove item")
    .setLeft(20)
    .setTop(20)
    .setWidth(460)
    .setHeight(190)
    .onDblclick("_list1_ondblclick")
);

append((new linb.UI.Input)
    .host(host,"input1")
    .setLeft(20)
    .setTop(230)
    .setWidth(460)
    .onBlur("_input1_onblur")
);

append((new linb.UI.Button)
    .host(host,"button1")
    .setLeft(360)
    .setTop(270)
    .setHeight("22")
    .setCaption("Save To Server")
    .onClick("_button1_onclick")
);

Обратите внимание, что каждый из этих виджетов обрабатывает какое-либо событие onXX (onDblclick, onBlur, onClick). Каждый из этих обработчиков событий и пустые скелеты функций были созданы путем нажатия на визуальное дерево событий в представлении "Дизайн". Когда одно из этих событий происходит на любом из виджетов, вызывается соответствующая функция. Пока код в этом приложении не написан вручную. Весь процесс был завершён всего за пару минут перетаскивания виджетов и щелчка/редактирования свойств. Нам просто нужно добавить код к каждой функции, чтобы сделать что-то полезное для наших целей:

_list1_ondblclick:function (profile,item,src){
    this.list1.removeItems([item.id])
}, 
_input1_onblur:function (profile){
    this.list1.insertItems([{
        id : this.input1.getUIValue(),
        caption : this.input1.getUIValue()
    }],null,false)
    this.input1.setUIValue("")
}, 
_button1_onclick:function (profile,e,src,value){
    linb.Ajax(
        "http://localhost/cgi-bin/todo.cgi",
        ('savedata=' + _.serialize(this.list1.getItems())),
        function(s){
            alert(s);
        }
    ).start();
},

Когда событие onDblclick происходит в виджете списка, метод .removeItems() вызывается для выбранного элемента (на который указывает свойство [item.id]). Этот процесс удаляет текущий выбранный элемент в виджете списка.

Когда событие onBlur происходит в виджете ввода текста (когда фокус перемещается с этого виджета), для виджета списка вызывается метод .insertItems(). Данные, вставленные в список, получают с помощью метода .getUIValue() виджета ввода текста. Наконец, значение, отображаемое в виджете ввода текста, стирается (устанавливается в "") с помощью метода .setUIValue(). Этот процесс добавляет отправленную текстовую запись в виджет списка.

Когда событие onClick происходит на виджете кнопки, запрос linb.Ajax отправляется сценарию Rebol todo.cgi на сервере. Обратите внимание, что для ключа 'savedata =' заданы данные, полученные с помощью метода .getItems() виджета списка. Этот список элементов сериализуется и отправляется в серверное приложение. Этот процесс сохраняет данные списка на сервере. Данные, возвращаемые приложением(-ями) CGI, предупреждаются для пользователя. В случае успешного сохранения возвращаемыми данными является текст "Сохранено!", Который предупреждается пользователю.

Затем визуальный редактор используется для добавления события onReady на холст приложения. Это событие активируется, когда приложение полностью загружено в браузере пользователя. Это событие запускает автоматически созданную функцию _onready(). Нам просто нужно добавить туда код, чтобы функция делала что-то полезное. Для наших целей мы запустим метод linb.Ajax, который отправляет значение loaddata в серверное приложение todo.cgi и получает список данных в ответ. Затем эти данные десериализуются и устанавливаются для отображения в виджете list1 с помощью метода .setItems(). Конечным эффектом этого кода является то, что сохранённые данные загружаются с сервера и отображаются в виджете списка всякий раз, когда загружается приложение браузера:

_onready:function (com,threadid){
    var self=this
    linb.Ajax(
        "http://localhost/cgi-bin/todo.cgi",
        'loaddata=loaddata',
        function(s){
            self.list1.setItems(_.unserialize(s));
        }
    ).start();
}

Это касается всех настраиваемых частей приложения jsLinb GUI, которые не являются просто стандартным стандартным кодом.

Теперь, если вы посмотрите на сценарий сервера todo.cgi, вы увидите, как он обрабатывает каждый из двух возможных запросов из кода браузера, приведенного выше. Он начинается с 4 обычных строк стандартного кода CGI. Затем он оценивает, было ли отправлено значение loaddata из браузера. Если это так, он пытается прочитать локальный файл todo.txt. Если во время этого процесса возникает ошибка (предположительно из-за того, что приложение никогда не запускалось и этот файл еще не существует), он распечатывает пустой блок для отображения обратно в виджете списка браузера. В противном случае он распечатывает список данных, считанных из сохраненного файла на сервере:

either data/2 = "loaddata" [
    either error? try [
        saved-data: read %todo.txt
    ][prin "[]"] [prin saved-data]
]...

Если отправленное значение данных не является loaddata (т.е. Запрос на сохранение был отправлен браузером), то отправленное значение данных (сериализованный список элементов из виджета списка) записывается в локальный файл todo.txt, и сообщение "Сохранено!" отправляется обратно в браузер, чтобы предупредить пользователя.

... [
    write %todo.txt data/2
    prin "Saved!"
]

Вот и всё. Как в приложении с графическим интерфейсом, так и в коде сервера требуется всего несколько строк настраиваемого кода. Все остальное создаётся автоматически конструктором или является стандартным кодом. После того, как процедура станет понятной, на её создание уйдёт всего несколько минут. Процесс кодирования аналогичен тому, который требуется для сборки гораздо более сложных интерфейсов управления данными. В более сложных приложениях вы просто будете использовать больше виджетов и макетов для получения ввода от пользователя и для отображения вывода данных. И, конечно же, манипуляции с данными, которые происходят на сервере, могут включать в себя любую вычислительную работу, которую вы требуете или можете себе представить. Вся любимая продуктивность Rebol может быть задействована там, а результаты отображаются в виджетах jsLinb.

19.5.2 Галерея изображений

Сохраните следующий код Rebol в файл с именем gallery.cgi в папке cgi-bin вашего веб-сервера:

#!/usr/bin/rebol.exe -cs
REBOL [title: "Image Gallery"]
print "content-type: text/html^/"
data: decode-cgi raw: read-cgi

folder: data/2
count: 1
json: copy "["
files: read to-file join "../www/" folder
foreach file files [
    if find [%.jpg %.gif %.png %.bmp] suffix? file [
        append json rejoin [
            "{" 
            {"id" : "} count 
            {", "caption" : "} count " -- " file 
            {", "image" : "http://localhost/} folder file {"}
            "},"
        ]
        count: count + 1
    ]
]
append json "]"
prin json
quit

Вставьте следующий код jsLinb во вкладку Normal View в Sigma IDE:

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:[
            "linb.UI.Gallery", "linb.UI.Label", 
            "linb.UI.Input", "linb.UI.Button"
        ], 
        properties:{}, 
        events:{"onReady":"_onready"}, 
        iniResource:function(com, threadid){}, 
        iniComponents:function(){
            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.Label)
                .host(host,"label1")
                .setLeft(10)
                .setTop(10)
                .setWidth("60")
                .setHeight("20")
                .setCaption("Folder:")
            );

            append((new linb.UI.Input)
                .host(host,"input1")
                .setLeft(80)
                .setTop(10)
                .setWidth("220")
                .setValue("someimages/")
            );

            append((new linb.UI.Button)
                .host(host,"button1")
                .setLeft(310)
                .setTop(10)
                .setCaption("Submit")
                .onClick("_button1_onclick")
            );

            append((new linb.UI.Gallery)
                .host(host,"gallery1")
                .setLeft(10)
                .setTop(50)
                .setWidth("700")
                .setHeight("400")
                .setItemWidth("auto")
                .setItemHeight("auto")
                .setImgWidth("auto")
                .setImgHeight("auto")
            );

            return children;
        }, 
        iniExComs:function(com, hreadid){}, 
        _button1_onclick:function (profile,e,src,value){
            var self=this
            linb.Ajax(
                "http://localhost/cgi-bin/gallery.cgi",
                ("data=" + self.input1.getUIValue()),
                function(s){
                    self.gallery1.setItems(_.unserialize(s));
                }
            ).start();
        }
    }
});

Создайте папку .../www/someimages/ на своем веб-сервере и скопируйте в неё несколько случайных файлов изображений. Затем нажмите кнопку "Выполнить" в Sigma IDE, и вы увидите приложение, отображающее простой макет.

Нажмите кнопку "Отправить", и если вы добавили несколько изображений в папку someimages/, вы увидите, что все они отображаются в виджете прокручиваемой галереи в приложении.

Давайте сначала взглянем на код jsLinb. Как видите, на холст добавлено 4 виджета. Как и в предыдущих примерах, все это было сделано с помощью визуального построителя. Сгенерированный код легко читается, его свойства отражаются непосредственно из визуально редактируемых настроек:

append((new linb.UI.Label)
    .host(host,"label1")
    .setLeft(10)
    .setTop(10)
    .setWidth("60")
    .setHeight("20")
    .setCaption("Folder:")
);

append((new linb.UI.Input)
    .host(host,"input1")
    .setLeft(80)
    .setTop(10)
    .setWidth("220")
    .setValue("someimages/")
);

append((new linb.UI.Button)
    .host(host,"button1")
    .setLeft(310)
    .setTop(10)
    .setCaption("Submit")
    .onClick("_button1_onclick")
);

append((new linb.UI.Gallery)
    .host(host,"gallery1")
    .setLeft(10)
    .setTop(50)
    .setWidth("700")
    .setHeight("400")
    .setItemWidth("auto")
    .setItemHeight("auto")
    .setImgWidth("auto")
    .setImgHeight("auto")
);

Обратите внимание, что к виджету button1 было добавлено событие onClick. Пользовательский код, который нам нужно создать для этой функции, - это метод linb.Ajax, который отправляет текст виджета input1 в приложение CGI сервера. Данные, возвращаемые с сервера, десериализованы и настроены для отображения в виджете галереи:

var self=this
linb.Ajax(
    "http://localhost/cgi-bin/gallery.cgi",
    ("data=" + self.input1.getUIValue()),
    function(s){
        self.gallery1.setItems(_.unserialize(s));
    }
).start();

Это всё, что есть в коде jsLinb. Это во многом похоже на все другие примеры, которые вы видели до сих пор.

Код Rebol gallery.cgi начинается с присвоения папки метки значению, отправленному из браузера (имя папки, которое пользователь ввёл в виджет input1). Для переменной count установлено значение 1, создаётся строка с меткой json и считываются файлы из представленной папки:

folder: data/2
count: 1
json: copy "["
files: read to-file join "../www/" folder

Затем цикл foreach перебирает все имена файлов в папке. Если файл имеет тип изображения (jpg, gif, png или bmp), то к строке json добавляется некоторый текст. Добавленный текст - это просто несколько сцепленных символов в формате, требуемом свойством "items" виджета галереи для отображения изображений. Если вы щёлкните виджет "Галерея" в визуальном конструкторе Sigma и посмотрите на его свойство "items", вы увидите, что для списка элементов требуется следующий формат: [{"id": "id text", "caption": "caption text ", "URL"},]. Переменная count также обновляется на каждой итерации цикла foreach (и включается в текст заголовка изображения). Когда цикл завершён, последняя строка json печатается обратно в браузер, где возвращённый список элементов отображается в виджете галереи:

foreach file files [
    if find [%.jpg %.gif %.png %.bmp] suffix? file [
        append json rejoin [
            "{" 
            {"id" : "} count 
            {", "caption" : "} count " -- " file 
            {", "image" : "http://localhost/} folder file {"}
            "},"
        ]
        count: count + 1
    ]
]
append json "]"
prin json
quit

Это вся галерея. На этом этапе вы должны получить хорошее представление о том, как пользовательские интерфейсы jsLinb и серверные приложения Rebol взаимодействуют для формирования законченных клиент-серверных приложений.

19.5.3 Растояние между двумя датами

Вот простое приложение, которое вычисляет количество дней между двумя датами. Это строго приложение jsLinb (сервер Rebol не требуется). Вставьте следующий код в редактор кода Sigma:

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:["linb.UI.Button", "linb.UI.DatePicker"], 
        properties:{}, 
        events:{}, 
        iniResource:function(com, threadid){}, 
        iniComponents:function(){
            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.DatePicker)
                .host(host,"datepicker1")
                .setLeft(10)
                .setTop(10)
            );

            append((new linb.UI.DatePicker)
                .host(host,"datepicker2")
                .setLeft(250)
                .setTop(10)
            );

            append((new linb.UI.Button)
                .host(host,"button")
                .setLeft(170)
                .setTop(190)
                .setCaption("Calculate")
                .onClick("_button_onclick")
            );

            return children;

        }, 
        iniExComs:function(com, hreadid){}, 
        _button_onclick:function (profile,e,src,value){
            alert(
                linb.Date.diff(
                    this.datepicker1.getUIValue(),
                    this.datepicker2.getUIValue(),
                    'd'
                )
            );
        }
    }
});

Вы можете видеть, что на холст добавлены 2 виджета Datepicker и 1 кнопка. При нажатии кнопки вычисляется разница между датами, выбранными пользователем, с помощью метода linb.Date.diff (). Этот результат сообщается пользователю. Это всё, что есть во всём приложении.

19.5.4 Калькулятор чаевых

Вот ещё одно простое приложение jsLinb (сервер Rebol не нужен). Код выглядит длинным, но почти весь он был создан с помощью визуального конструктора:

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:["linb.UI.Label", "linb.UI.Input", "linb.UI.Button"], 
        properties:{}, 
        events:{}, 
        iniResource:function(com, threadid){}, 
        iniComponents:function(){
            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.Label)
                .host(host,"label1")
                .setLeft(20)
                .setTop(30)
                .setWidth(70)
                .setCaption("Price ($):")
            );

            append((new linb.UI.Label)
                .host(host,"label2")
                .setLeft("20")
                .setTop(70)
                .setWidth(70)
                .setCaption("Tip (%):")
            );

            append((new linb.UI.Input)
                .host(host,"input1")
                .setLeft(100)
                .setTop(30)
                .setValueFormat(
                 "^-?(\\d\\d*\\.\\d*$)|(^-?\\d\\d*$)|(^-?\\.\\d\\d*$)"
                 )
                .setValue("100")
            );

            append((new linb.UI.Input)
                .host(host,"input2")
                .setLeft(100)
                .setTop(70)
                .setValueFormat(
                 "^-?(\\d\\d*\\.\\d*$)|(^-?\\d\\d*$)|(^-?\\.\\d\\d*$)"
                 )
                .setValue(".20")
                .onBlur("_button1_onclick")
            );

            append((new linb.UI.Button)
                .host(host,"button1")
                .setLeft(100)
                .setTop(110)
                .setCaption("Calculate Tip")
                .onClick("_button1_onclick")
            );

            append((new linb.UI.Input)
                .host(host,"input3")
                .setLeft(100)
                .setTop(150)
            );

            return children;
        }, 
        iniExComs:function(com, hreadid){}, 
        _button1_onclick:function (profile,e,src,value){
            this.input3.setUIValue(
                this.input1.getUIValue() * this.input2.getUIValue()
            )
        }
    }
});

Обратите внимание, что свойства .valueFormat для первых двух полей ввода были установлены с помощью визуальных селекторов в среде IDE. Это требует от пользователя ввода числовых значений. Существует ряд других параметров valueFormat по умолчанию (и эти параметры регулярного выражения можно редактировать вручную в коде).

Единственный настраиваемый код, необходимый для всего этого приложения, находится в функции _button1_onclick. Он умножает числовые значения в виджетах ввода цены и чаевых и отображает результат этого расчёта в виджете input3:

this.input3.setUIValue(
    this.input1.getUIValue() * this.input2.getUIValue()
)

Наконец, обратите внимание, что событие .onBlur было добавлено к виджету input2, который также запускает указанную выше функцию. Таким образом, пользователь может либо щёлкнуть кнопку, либо просто нажать [ENTER] после ввода значения чаевых, и расчёт будет запущен.

19.6 Сохранение и развёртывание ваших приложений jsLinb в Sigma IDE

Sigma Builder IDE предоставляет несколько вариантов сохранения кода и развёртывания готовых приложений. Одна хорошая вещь в jsLinb заключается в том, что код для целых приложений может быть сохранен как один непрерывный текстовый файл (есть интересные варианты разделения кода на несколько файлов и динамической загрузки удалённого кода, но эти темы сохранены для другого учебника). Как вы уже видели, весь код макета и логики/действия в полном приложении jsLinb можно скопировать и вставить непосредственно на/из вкладки кода обычного просмотра в Sigma IDE. Вы можете перемещать код таким образом, редактировать свой код в любом предпочтительном локальном текстовом редакторе, IDE и т.д. и копировать/вставлять назад между файлами, веб-страницами, другими текстовыми дисплеями и Sigma IDE без использования каких-либо функции сохранения/загрузки Sigma. Например, вставив примеры с этой веб-страницы, вы увидели, что единый файл, чистый код приложений jsLinb может упростить механизм. Вы также можете найти эту функцию полезной, например, при работе на небольших мобильных устройствах, где приложение для редактирования собственного кода может быть проще, чем вкладка кода Sigma IDE.

Если вы нажмёте кнопку "Загрузить" в правом верхнем углу Sigma IDE, вы увидите, что файлы можно загружать из любого URL-адреса (например, http://localhost/myapp.js).

Кнопка загрузки также позволяет просматривать и загружать любые демонстрационные приложения фрагментов кода, которые поставляются с Sigma IDE. Эти демонстрационные примеры кода действительно полезны при изучении того, как использовать все функции виджетов и пользовательского интерфейса в библиотеке jsLinb (подробнее о библиотеке фрагментов кода мы поговорим позже).

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

Если вы нажмёте кнопку "Сохранить" в правом верхнем углу Sigma IDE, вы увидите, что есть четыре варианта, доступных для сохранения и развёртывания кода вашего приложения.

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

Второй вариант сохранения позволяет загрузить весь код приложения на локальный компьютер в виде одного файла .js (то есть позволяет просто загружать содержимое вкладки кода обычного просмотра в Sigma IDE). Это наиболее вероятный метод, которым вы будете регулярно сохранять свою работу.

Третий вариант сохранения позволяет упаковать код в файл HTML для развёртывания на сервере. Эта опция включает в ваш код весь необходимый HTML, включая код, который загружает среду выполнения jsLinb и файлы скинов (внешний вид) (CSS, изображения и т. Д.). Вы можете загрузить файлы времени выполнения jsLinb только один раз в одну папку на сервере развёртывания, а затем просто загрузить любые новые одностраничные приложения с оболочкой HTML, и это все, что необходимо для развёртывания. Это действительно полезно, когда вы создаёте множество приложений, запускаемых с одного сервера.

Четвёртый вариант сохранения создаёт полностью автономный zip-файл со всем кодом вашего приложения, документом-оболочкой HTML и полной средой выполнения jsLinb с файлами скинов - абсолютно всем, что требуется для запуска созданного вами приложения jsLinb. Для большинства приложений размер zip-файла составляет примерно 200 КБ (0,2 МБ - крошечный). Вы можете просто распаковать zip-файл в папку на любом веб-сервере, указать браузеру URL-адрес созданного файла .html, и ваше приложение будет запущено. Обратите внимание, что для запуска вашего приложения jsLinb не требуются абсолютно никакие другие серверные технологии. Ни PHP, ни какие-либо другие серверные языки или среда развёртывания не требуются вообще. Помните, что файлы PHP в приложении Sigma Builder используются только для обеспечения функций управления файлами для IDE (где бы вы ни запускали Visual Builder IDE для создания своих приложений). JavaScript в браузере вашего пользователя - единственное требование для запуска созданных приложений jsLinb. Фактически, созданным приложениям даже не нужен веб-сервер для работы. Вы можете распаковать zip-файл в папку на жёстком диске, SD-карте и т.д., открыть HTML-страницу прямо из местоположения файла, и он будет работать без проблем.

ZIP-файлы, созданные Sigma IDE, также можно загружать непосредственно в упаковщики приложений, такие как сборка Phonegap. В zip-пакете есть все, что нужно этим упаковщикам для создания автономных приложений, которые можно распространять в iTunes, Google Play, Blackberry World и других магазинах приложений.

ПРИМЕЧАНИЕ: формат файла zip, созданный Sigma IDE, не распаковывается должным образом с некоторыми версиями мастера извлечения Windows, и аналогичная проблема возникла при использовании инструмента распаковки в Lunarpages cPanel (не все файлы скинов распаковываются должным образом). Однако Tugzip и несколько других различных zip-приложений работают без проблем. Если вы столкнётесь с этой проблемой на своём сервере развёртывания, попробуйте другое zip-приложение - все необходимые файлы включены в zip-файл. Другой вариант - просто скопируйте файлы времени выполнения jsLinb на сервер развёртывания вручную, переупакуйте их в zip-файл с файлами приложения вручную и т.д.

19.7 Некоторые примеры Data Grid

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

Следующий пример кода находится в ...\sigma\Samples\comb\GridEditor\App\js\index.js:

Class('App', 'linb.Com',{
    Instance:{
        //base Class for linb.Com
        base:["linb.UI"], 
        //requried class for the App
        required:["linb.UI.Block", "linb.UI.TreeGrid", "linb.UI.Div"], 
        iniComponents:function(){
            // [[code created by jsLinb UI Builder
            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.Block)
                .host(host,"block1")
                .setLeft(70)
                .setTop(50)
                .setWidth(540)
                .setHeight(270)
            );

            host.block1.append((new linb.UI.TreeGrid)
                .host(host,"tg")
                .setTabindex("2")
                .setHeader([])
                .setRows([])
                .onDblClickRow("_tg_ondblclickrow")
            );

            append((new linb.UI.Div)
                .host(host,"div9")
                .setLeft(70)
                .setTop(10)
                .setWidth(270)
                .setHeight(30)
                .setHtml("DblClick row  to open edit dialog!")
            );

            return children;
            // ]]code created by jsLinb UI Builder
        }, 
        _changeRow:function(v1,v2,v3){
            var cells=this._activeRow.cells;
            if(cells[0]!=v1)
                this.tg.updateCell(cells[0],{value:v1});
            if(cells[1]!=v2)
                this.tg.updateCell(cells[1],{value:v2});
            if(cells[2]!=v3){
                this.tg.updateCell(cells[2],{value:v3});
            }
        }, 
        _tg_ondblclickrow:function (p,row, e, src) {
            this._activeRow = row;
            var self=this;
            linb.ComFactory.newCom('App.Dlg' ,function(){
                this.$parent=self;
                this
                .setProperties({
                    fromRegion:linb([src]).cssRegion(true),
                    col1:row.cells[0].value,
                    col2:row.cells[1].value,
                    col3:row.cells[2].value
                })
                .setEvents('onOK', self._changeRow);
                this.show(linb([document.body]));
            });
        }, 
        events:{
            onReady:'_onready'
        }, 
        _onready:function(){
            var self=this;
            linb.Ajax("data/data.js",'',function(s){
                var hash=_.unserialize(s);
                self.tg.setHeader(hash.header).setRows(hash.rows);
            }).start();
        }
    }
});

Этот код представляет собой упрощённую версию приведённого выше кода, который демонстрирует только те части, которые необходимы для использования виджета Treegrid, а также код, необходимый для заполнения сетки данными из серверного приложения Rebol CGI:

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:["linb.UI.TreeGrid"], 
        iniComponents:function(){

            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.TreeGrid)
                .host(host,"tg")
                .setHeader([])
                .setRows([])
            );

            return children;

        }, 
        events:{
            onReady:'_onready'
        }, 
        _onready:function(){
            var self=this;
            linb.Ajax("
                http://localhost/cgi-bin/readjsonfile.cgi",
                '',
                function(s){
                    var hash=_.unserialize(s);
                    self.tg.setHeader(hash.header).setRows(hash.rows);
                }
            ).start();
        }
    }
});

Обратите внимание, что в приведённом выше коде вызов linb.Ajax выполняется для сценария Rebol readjsonfile.cgi в папке cgi-bin на веб-сервере. Вставьте следующий код в текстовый файл и сохраните его как readjsonfile.cgi в папке cgi-bin вашего сервера:

#!/usr/bin/rebol.exe -cs
REBOL []
print "content-type: application/json^/"
prin read %data.js

Приведённый выше код делает только одно. Он печатает содержимое файла data.js. Вставьте следующие данные в текстовый файл и сохраните его как data.js в папке cgi-bin вашего сервера (вы можете найти этот исходный файл в zip-пакете jsLinb/Sigma, в ...\sigma\Samples\comb\GridEditor\App\js\index.js):

{
    header: [
       {
           "id" : "col1",
           "caption" : "col1",
           "type" : "input",
           "width" : 50
       },
       {
           "id" : "col2",
           "caption" : "col2",
           "type" : "number",
           "format":"^-?\\d\\d*$",
           "width" : 80
       },
       {
           "id" : "col3",
           "caption" : "col3",
           "type" : "checkbox",
           "width" : 40
       },
       {
           "id" : "col4",
           "caption" : "col4",
           "type" : "label",
           "width" : 40
       }
   ],
   rows: [
       {
           "id" : "row1",
           "cells" : ["cell11",1,true,'label1']
       },
       {
           "id" : "row2",
           "cells" : ["cell21",2,true,'label2']
       },
       {
           "id" : "row3",
           "cells" : ["cell31",3,false,'label3']
       },
       {
           "id" : "row4",
           "cells" : ["cell41",4,false,'label4'],
           "sub" : [
               {
                   "id" : "row5",
                   "cells" : ["in51",5,false,'label5']
               },
               {
                   "id" : "row6",
                   "cells" : ["in61",6,false,'label6']
               },
               {
                   "id" : "row7",
                   "cells" : ["in71",7,false,'label7']
               },
               {
                   "id" : "row8",
                   "cells" : ["in81",8,false,'label8']
               }
           ]
       },
       {
           "id" : "row9",
           "cells" : ["cell91",9,false,'label9'],
           "sub" : [
               {
                   "id" : "row10",
                   "cells" : ["in101",10,false,'label10']
               },
               {
                   "id" : "row11",
                   "cells" : ["in111",11,false,'label11']
               },
               {
                   "id" : "row12",
                   "cells" : ["in121",12,false,'label12']
               },
               {
                   "id" : "row13",
                   "cells" : ["in131",13,false,'label13']
               }
           ]
       }
   ]
}

Теперь запустите приведённый выше пример кода jsLinb, и вы увидите сетку данных.

Формат кода файла данных должен иметь смысл при просмотре отображения данных. Обратите внимание, что блок заголовка в файле данных описывает формат строки заголовка, отображаемой в сетке данных. Также обратите внимание, что структура отображаемых данных сетки, включая сетки поддеревьев, отражена в иерархической структуре кода в файле данных. Вот сценарий Rebol, который создаёт аналогичный файл данных:

rebol [title: "Create jsLinb json grid data"]

data: [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]
cols: 4

header: copy "{header: ["
repeat i cols [
    append header rejoin [
        "{" {"id" : "col} i {", "caption" : "col} i {"} "},"
    ]
]
append header {],}

griddata: copy "rows: ["
repeat i ((length? data) / cols) [
    append griddata rejoin [ "{" {"id" : "row} i {", "cells" : [} ]
    for j cols 1 -1 [
        append griddata rejoin [
           {"} form data/(i * cols - j + 1) {",} 
        ]    
    ]
    append griddata "]},"
]
append griddata "]}"

editor join header griddata

Обратите внимание, что каждый цикл for выше объединяет список значений вместе с соответствующими символами форматирования, необходимыми для формирования структуры данных, используемой виджетом Treegrid. Один цикл создаёт структуру данных заголовка, а другой цикл создаёт структуру данных строки. Запустите этот код в интерпретаторе Rebol на своём рабочем столе, и вы увидите некоторые выходные данные, которые плохо отформатированы для просмотра людьми, но синтаксис данных соответствует правилам, продемонстрированным в исходном файле демонстрационных данных выше (обратите внимание, что набор данных немного упрощён, без любых подстрок).

Мы можем преобразовать приведённый выше код для запуска в качестве сценария CGI, просто добавив правильный шаблон CGI и используя 'prin для вывода окончательной структуры данных. Сохраните следующий код как createjson.cgi в папке cgi-bin вашего сервера:

#!/usr/bin/rebol.exe -cs
REBOL []
print "content-type: application/json^/"         ; text/html^/"
submitted: decode-cgi submitted-bin: read-cgi

data: [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]
cols: 6

header: copy "{header: ["
repeat i cols [
    append header rejoin [
        "{" {"id" : "col} i {", "caption" : "col} i {"} "},"
    ]
]
append header {],}

griddata: copy "rows: ["
repeat i ((length? data) / cols) [
    append griddata rejoin [ "{" {"id" : "row} i {", "cells" : [} ]
    for j cols 1 -1 [
        append griddata rejoin [
           {"} form data/(i * cols - j + 1) {",} 
        ]    
    ]
    append griddata "]},"
]
append griddata "]}"

prin join header griddata
quit

Теперь запустите следующий код jsLinb в Sigma IDE:

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:["linb.UI.Block", "linb.UI.TreeGrid", "linb.UI.Div"], 
        iniComponents:function(){

            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.TreeGrid)
                .host(host,"tg")
                .setHeader([])
                .setRows([])
            );

            return children;

        }, 
        events:{
            onReady:'_onready'
        }, 
        _onready:function(){
            var self=this;
            linb.Ajax(
                "http://localhost/cgi-bin/createjson.cgi",
                '',
                function(s){
                    var hash=_.unserialize(s);
                    self.tg.setHeader(hash.header).setRows(hash.rows);
                }
            ).start();
        }
    }
});

Обратите внимание, что вызов linb.Ajax в приведённом выше коде получает данные из сценария Rebol CGI, который мы только что сохранили на сервере. Этот сценарий CGI отправляет обратно отформатированные данные, которые в коде помечены буквой "s", как и в предыдущих примерах. И, как и в предыдущих примерах, функция _.unserialize () используется для распаковки возвращённой структуры данных, и эти распакованные данные сохраняются в переменной "hash". Заголовку отображения сетки данных, помеченному в этом примере "tg", назначаются данные hash.header, а строки настроены на отображение данных hash.rows. Обратите внимание, что все это происходит в функции, запускаемой событием onReady, когда страница завершает загрузку в вашем браузере.

Если вы измените код в Rebol createjson.cgi, а затем обновите работающее приложение jsLinb, вы увидите обновление отображения сетки, чтобы показать новые данные, выводимые сценарием CGI. Попробуйте изменить строку "cols: 6" в файле createjson.cgi на "cols: 3". Обновите приложение jsLinb, запущенное в вашем браузере, и вы увидите количество обновлённых столбцов в отображаемой сетке.

Следует знать, что вы можете либо поместить информацию о макете заголовка в данные, возвращаемые сервером, как в приведённых выше примерах, либо вы можете установить её непосредственно в коде jsLinb. Как вы видели в приведённых выше примерах, сетка (пере)настраивается для отображения данных в соответствии с инструкциями, если файл данных содержит информацию заголовка. Чтобы упростить вывод данных, вы можете указать макет заголовка в коде jsLinb (или изменив настройки сетки в Visual IDE) и просто вывести данные из сценария сервера, который соответствует этому формату. Сохраните следующий код как createjson2.cgi в папке cgi-bin:

#!/usr/bin/rebol.exe -cs
REBOL []
print "content-type: application/json^/"

data: [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]
cols: 4

griddata: copy "["
repeat i ((length? data) / cols) [
    append griddata rejoin [ "{" {"cells" : [} ]
    for j cols 1 -1 [
        append griddata rejoin [
           {"} form data/(i * cols - j + 1) {",} 
        ]    
    ]
    append griddata "]},"
]
append griddata "]"

prin griddata
quit

Сравните приведённый выше код со сценарием createjson.cgi, показанным ранее. Есть только один цикл, который используется для создания строковых данных, отображаемых в Treegrid. Присмотритесь немного внимательнее, и вы увидите, что формат данных выше также немного проще, чем в предыдущем примере (идентификаторов ключей меньше, помечены только блоки "ячейки:"). Вот настольная версия приведённого выше скрипта, которую вы можете использовать для игры с форматом данных:

REBOL [title: "simpler jsLinb treegrid data format"]

data: [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]
cols: 4

griddata: copy "["
repeat i ((length? data) / cols) [
    append griddata rejoin [ "{" {"cells" : [} ]
    for j cols 1 -1 [
        append griddata rejoin [
           {"} form data/(i * cols - j + 1) {",} 
        ]    
    ]
    append griddata "]},"
]
append griddata "]"

editor griddata
quit

Теперь вставьте следующий код приложения jsLinb в Sigma IDE:

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:["linb.UI.TreeGrid", "linb.UI.Button"], 
        properties:{}, 
        events:{"onReady":"_onready"}, 
        iniResource:function(com, threadid){
        }, 
        iniComponents:function(){

            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.TreeGrid)
                .host(host,"treegrid1")
                .setDock("none")
                .setWidth(370)
                .setHeader(["col1", "col2", "col3", "col4"])
            );

            return children;

        }, 
        iniExComs:function(com,threadid){
        }, 
        _onready:function (com,threadid){
            self=this;
            linb.Ajax(
                "http://localhost/cgi-bin/createjson2.cgi",
                '',
                function(s){
                    var hash=_.unserialize(s);
                    self.treegrid1.setRows(hash);
                }
            ).start();
        }
    }
});

Обратите внимание, что в приведённом выше коде также используется упрощённый формат данных для макета заголовка сетки. Именуются только заголовки столбцов. Вы можете включить дополнительное описание, как в предыдущем примере, но это не обязательно. Если вы переключитесь на вкладку Design View в Sigma IDE, выберите виджет Treegrid на визуальном холсте, а затем щёлкните свойство "header" справа, вы увидите, что структура данных полностью заполнена jsLinb.

Теперь запустите приведённый выше код jsLinb, чтобы увидеть отображение сетки.

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

Это выходит за рамки данного руководства, но создатели jsLinb/Sigma создали полностью автономную библиотеку Grid и API, которая имеет множество мощных и полезных функций (печать в PDF, Excel и другие форматы, фильтрация, множество расширенных параметров отображения, возможности взаимодействия с сервером и т.д.). Этот инструмент также можно свободно использовать для коммерческой работы (LGPL), он очень хорошо документирован и заслуживает внимания, если вы планируете выполнять много работы с сеткой данных. Это тема отдельного урока:

http://www.sigmawidgets.com/products/sigma_grid2/

19.8 Мощные виджеты макета

После этого беглого взгляда на виджет Treegrid вы, возможно, начали понимать, что библиотека jsLinb довольно мощная. Далее мы кратко рассмотрим несколько других интересных виджетов в jsLinb, которые упрощают размещение большого количества информации и элементов управления на одном экране интуитивно понятными способами, которые легко контролируются пользователем, а также быстрое и простое создание макета с помощью Sigma IDE или чистого кода API jsLinb.

Если вы посмотрите на панель инструментов в представлении "Дизайн" (вкладка визуального редактора) Sigma IDE, вы увидите, что виджеты и инструменты разделены на 6 категорий:

  1. Данные
  2. Элементы формы
  3. Контейнеры
  4. Навигаторы
  5. Расписания
  6. Медиа

Откройте аккордеон "Контейнеры" и перетащите виджет ButtonViews на визуальный холст.

Установите для свойства barLocation значение "left" в дереве свойств IDE.

Установите для свойства barSize значение "120".

Щелкните свойство "items" и отредактируйте пару ключ/значение следующим образом:

[{
    "id" : "a",
    "caption" : "item a",
    "image" : "img/demo.gif"
},
{
    "id" : "b",
    "caption" : "item b",
    "image" : "img/inedit.gif"
},
{
    "id" : "c",
    "caption" : "item c",
    "image" : "img/app.gif"
}]

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

Запустите приложение и просмотрите каждую страницу виджета ButtonViews.

Как видите, создание многостраничного макета занимает всего несколько минут. Вот код для приведённого выше примера макета:

Class('App', 'linb.Com',{
    Instance:{
        //base Class for this com
        base:["linb.UI"], 
        //requried class for this com
        required:[
            "linb.UI.ButtonViews", "linb.UI.Button", 
            "linb.UI.ProgressBar", "linb.UI.DatePicker"
        ], 

        properties:{}, 
        events:{}, 
        iniResource:function(com, threadid){
        }, 
        iniComponents:function(){
            // [[code created by jsLinb UI Builder
            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.ButtonViews)
                .host(host,"buttonviews6")
                .setItems([
             {"id":"a", "caption":"item a", "image":"img/demo.gif"},
             {"id":"b", "caption":"item b", "image":"img/inedit.gif"},
             {"id":"c", "caption":"item c", "image":"img/app.gif"}
                 ])
                .setBarLocation("left")
                .setBarSize("120")
                .setValue("a")
            );

            host.buttonviews6.append((new linb.UI.Button)
                .host(host,"button13")
                .setLeft(10)
                .setTop(10)
                .setCaption("button13")
            , 'a');

            host.buttonviews6.append((new linb.UI.ProgressBar)
                .host(host,"progressbar3")
                .setLeft(10)
                .setTop(10)
            , 'b');

            host.buttonviews6.append((new linb.UI.DatePicker)
                .host(host,"datepicker1")
                .setLeft(10)
                .setTop(10)
            , 'c');

            return children;
            // ]]code created by jsLinb UI Builder
        }, 
        iniExComs:function(com, hreadid){
        }
    }
});

Вот ещё один небольшой пример, демонстрирующий некоторые дополнительные виджеты и их свойства. Откройте "Контейнеры" и перетащите виджет "Макет" на визуальный холст.

Установите для свойства "тип" значение "горизонтальный" в дереве свойств IDE.

Щёлкните свойство "items" и отредактируйте пары ключ/значение следующим образом (удалите третий разделитель макета):

[{
    "id" : "before",
    "pos" : "before",
    "min" : 10,
    "size" : 20,
    "locked" : false,
    "hide" : false,
    "cmd" : true,
    "caption" : "before"
},
{
    "id" : "main",
    "min" : 10,
    "caption" : "main"
}]

Перетащите виджет вкладок в правую (большую) часть виджета макета.

Щёлкните и перетащите панель разделителя макета вправо, чтобы освободить больше места в левой части виджета макета.

Перетащите виджет галереи в левую часть виджета макета.

Щёлкните свойство "Док" и выберите "Влево". Это заставит размер виджета галереи растянуться и уместиться вдоль всей левой стены контейнера макета.

Установите для свойств itemHeight и itemWidth значения 250 и 200 (или любой другой размер, который вам нужен).

Щёлкните свойство "items" и измените пары ключ/значение на следующие (или любые другие подписи и изображения, которые вы предпочитаете):

[{
 "id" : "a",
 "caption" : "Nick Guitar",
 "image" : "http://guitarz.org/www/jscript/uploads/collection_vaa.jpg"
},
{
 "id" : "b",
 "caption" : "Nick's Dog",
 "image" : "http://guitarz.org/www/jscript/uploads/dew_electric3.jpg"
}]

Измените размер виджета галереи и разделителя макета, чтобы изображения соответствовали друг другу.

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

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

Просмотрите содержимое каждой страницы вкладки в виджете вкладок.

Отрегулируйте размер экрана браузера и посмотрите, как все виджеты изменяют размер / выравнивание соответствующим образом.

Вот код для приведённого выше примера макета:

Class('App', 'linb.Com',{
    Instance:{
        //base Class for this com
        base:["linb.UI"], 
        //requried class for this com
        required:[
            "linb.UI.Layout", "linb.UI.Tabs", "linb.UI.Button", 
            "linb.UI.List", "linb.UI.ComboInput", "linb.UI.DatePicker",
            "linb.UI.Gallery"
        ], 
        properties:{}, 
        events:{}, 
        iniResource:function(com, threadid){
        }, 
        iniComponents:function(){
            // [[code created by jsLinb UI Builder
            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.UI.Layout)
                .host(host,"layout3")
                .setItems([
                    {"id":"before", "pos":"before", "min":10, 
                    "size":237, "locked":false, "hide":false, 
                    "cmd":true, "caption":"before"}, 
                    {"id":"main", "min":10, "caption":"main"}
                 ])
                .setType("horizontal")
            );

            host.layout3.append((new linb.UI.Tabs)
                .host(host,"tabs2")
                .setItems([
                    {"id":"a", "caption":"item a", 
                    "image":"img/demo.gif"}, 
                    {"id":"b", "caption":"item b", 
                    "image":"img/demo.gif"}, 
                    {"id":"c", "caption":"item c", 
                    "image":"img/demo.gif"}, 
                    {"id":"d", "caption":"item d", 
                    "image":"img/demo.gif"}
                ])
                .setValue("a")
            , 'main');

            host.tabs2.append((new linb.UI.Button)
                .host(host,"button10")
                .setLeft(30)
                .setTop(30)
                .setCaption("button10")
            , 'a');

            host.tabs2.append((new linb.UI.List)
                .host(host,"list3")
                .setItems([
                    {"id":"a", "caption":"item a", 
                    "image":"img/demo.gif"}, 
                    {"id":"b", "caption":"item b", 
                    "image":"img/demo.gif"}, 
                    {"id":"c", "caption":"item c", 
                    "image":"img/demo.gif"}, 
                    {"id":"d", "caption":"item d", 
                    "image":"img/demo.gif"}
                ])
                .setLeft(30)
                .setTop(30)
                .setValue("a")
            , 'b');

            host.tabs2.append((new linb.UI.ComboInput)
                .host(host,"comboinput6")
                .setLeft(30)
                .setTop(30)
                .setItems([
                    {"id":"a", "caption":"item a", 
                    "image":"img/demo.gif"}, 
                    {"id":"b", "caption":"item b", 
                    "image":"img/demo.gif"}, 
                    {"id":"c", "caption":"item c", 
                    "image":"img/demo.gif"}, 
                    {"id":"d", "caption":"item d", 
                    "image":"img/demo.gif"}
                ])
                .setValue("a")
            , 'c');

            host.tabs2.append((new linb.UI.DatePicker)
                .host(host,"datepicker2")
                .setLeft(30)
                .setTop(30)
            , 'd');

            host.layout3.append((new linb.UI.Gallery)
                .host(host,"gallery2")
                .setItems([
                    {"id":"a", "caption":"Nick",
"image":"http://guitarz.org/www/jscript/uploads/collection_vaa.jpg"},
                    {"id":"b", "caption":"Nick's Dog",
"image":"http://guitarz.org/www/jscript/uploads/dew_electric3.jpg"}
                ])
                .setDock("left")
                .setWidth(230)
                .setItemWidth("200")
                .setItemHeight("250")
                .setValue("a")
            , 'before');

            return children;
            // ]]code created by jsLinb UI Builder
        }, 
        iniExComs:function(com, hreadid){
        }
    }
});

Это всего лишь несколько быстрых импровизированных примеров, которые помогут поэкспериментировать с некоторыми функциями виджета макета jsLinb. По мере того, как вы привыкаете к использованию конструктора Sigma, создание более сложных макетов экрана часто занимает всего несколько минут. Вы можете уместить огромное количество страниц и огромное количество макетов виджетов в одно приложение страницы, быстро и легко используя функции макета jsLinb. Изучение того, как управлять и редактировать свойства каждого из доступных виджетов, является самой большой частью изучения того, как использовать весь инструментарий jsLing/Sigma. В следующем разделе учебника будет объяснено, как изучить все функции библиотеки и Visual Builder, используя включённые функции документации.

19.9 Возможности документации jsLinb и Sigma Builder

Библиотека jsLinb и Sigma Visual IDE тщательно задокументированы. Все, что вам нужно, содержится в небольшом zip-файле размером 6 Мб, который вы уже установили. Когда ваш Uniform Server запущен, перейдите по адресу http://localhost/sigma в своем браузере (или по любому другому URL-адресу, по которому у вас установлен jsLinb/Sigma, в Интернете, в вашей локальной сети, на вашем локальном компьютере и т.д.). Щёлкните ссылку "Руководство", и вы найдёте краткий обзор Sigma IDE.

Теперь вернитесь на главную страницу http://localhost/sigma и щёлкните ссылки "Дополнительные образцы >>" и "Дополнительные образцы и материалы >>" или перейдите непосредственно по адресу http://localhost/sigma/Samples/all-samples.html. Эта страница содержит ряд сложных примеров приложений (Sigma IDE на самом деле является одним из них!), Которые вы можете изучить, чтобы увидеть, как функции jsLinb API могут быть объединены для создания приложений "реального мира". Обратите особое внимание на примеры, которые демонстрируют, как выполнять определённые задачи с помощью jsLinb, такие как стили CSS, ввод-вывод (включая междоменные вызовы Ajax и загрузка файлов), методы перетаскивания графического интерфейса пользователя, методы распределённого пользовательского интерфейса и т.д. полезных фрагментов кода в примерах, которые вы можете скопировать и вставить прямо в свои приложения. Некоторые из приложений сами по себе являются полезными инструментами для документации и повышения производительности, которые расширяют набор Sigma IDE.

Ещё одна золотая жила полезного кода jsLinb находится в приложении фрагментов кода. На странице примеров щёлкните ссылку "Фрагменты кода" или перейдите непосредственно на страницу http://localhost/sigma/CodeSnip/index.html. Эта коллекция примеров кода демонстрирует основные функции всех виджетов jsLinb. Вы будете широко использовать эти примеры при изучении наиболее часто используемых свойств, методов и конструкций кода, используемых для реализации виджетов jsLinb в макетах пользовательского интерфейса. Отображается код для каждого из примеров виджетов, а ссылка в правом верхнем углу примера позволяет вам открывать любой из фрагментов непосредственно в Sigma IDE, чтобы вы могли манипулировать им и экспериментировать с ним, использовать его сразу в ваши собственные приложения и т.д.

Примеры из библиотеки CodeSnip также интегрированы непосредственно в визуальный конструктор Sigma. Просто нажмите кнопку "Открыть" в правом верхнем углу среды IDE и выберите "Открыть из образцов".

Наконец, что наиболее важно, на странице примеров щёлкните ссылку "Sigma Linb API" или перейдите непосредственно по адресу http://localhost/sigma/API/index.html. Это программа просмотра API для библиотеки jsLinb, которую можно запустить как отдельное приложение. Это приложение также интегрировано непосредственно в визуальный конструктор Sigma.

Приложение для просмотра API предоставляет подробные объяснения и справочную документацию по каждой отдельной функции библиотеки jsLinb. Он также содержит тысячи коротких примеров кода, которые кратко демонстрируют необходимый синтаксис, необходимый для использования каждого свойства, события и функции библиотеки jsLinb. Примеры кода можно щёлкнуть и запустить прямо в программе просмотра API. Это огромная экономия времени при поиске и обучении использованию любого вызова API.

Программа просмотра API также интегрирована непосредственно в Sigma IDE. Вы можете дважды щёлкнуть любое свойство или событие в дереве в правой части экрана, и программа просмотра API откроется непосредственно к этому элементу, чтобы вы могли точно увидеть, как его использовать, с подробным описанием, ссылкой на параметры, примерами кода. , так далее.

Вместе инструменты и примеры документации предоставляют всю необходимую документацию не только для полной справки, но и для руководства о том, как можно собрать более крупные приложения, а также для некоторого творческого вдохновения. Обучение использованию функций документации - приятный и продуктивный процесс, который должен предоставить все необходимое для полноценного использования jsLinb и Sigma IDE после завершения этого вводного руководства.

19.10 Использование jsLinb Databinder для сбора и установки данных формы

JsLinb Databinder позволяет легко собирать несколько фрагментов информации, введённых в формы, и сериализовать их в одну строку Json (и наоборот). Вы можете перетащить объект базы данных на холст с панели инструментов.

Объект не будет отображаться в визуальном макете, но вы можете увидеть его добавленным в код.

Вставьте следующий код на вкладку "Обычный просмотр" в Sigma IDE. Обратите внимание, что на холст был добавлен объект Databinder с меткой "myformdata" и что свойства .setDataBinder () и .setDataField () были установлены на "field1" и "field2" соответственно для виджетов input1 и input2. Также обратите внимание, что методы this.myformdata.getValue () и this.myformdata.resetValue () использовались в приведённых ниже функциях onClick для сбора и установки данных в этих виджетах:

Class('App', 'linb.Com',{
    Instance:{
        base:[], 
        required:[
            "linb.DataBinder", "linb.UI.Input", "linb.UI.Button"
        ],
        events:{}, 
        iniComponents:function(){
            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.DataBinder)
                .host(host,"myformdata")
                .setName("myformdata")
            );

            append((new linb.UI.Input)
                .host(host,"input1")
                .setDataBinder("myformdata")
                .setDataField("field1")
                .setLeft(21)
                .setTop(20)
            );

            append((new linb.UI.Input)
                .host(host,"input2")
                .setDataBinder("myformdata")
                .setDataField("field2")
                .setLeft(21)
                .setTop(51)
            );

            append((new linb.UI.Button)
                .host(host,"button1")
                .setLeft(21)
                .setTop(80)
                .setCaption("Get Values")
                .onClick("_button1_onclick")
            );

            append((new linb.UI.Input)
                .host(host,"input3")
                .setLeft(21)
                .setTop(150)
                .setValue("{field1:'1234', field2:'adsf'}")
            );

            append((new linb.UI.Button)
                .host(host,"button2")
                .setLeft(21)
                .setTop(181)
                .setCaption("Set Values")
                .onClick("_button2_onclick")
            );

            return children;
        }, 
        _button1_onclick:function (profile, e, value) {
            var data=this.myformdata.getValue();
            if(!data)
                alert('Ensure all the fields are valid first!');
            else
                alert(_.serialize(data),true);
        }, 
        _button2_onclick:function (profile, e, value) {
            this.myformdata.resetValue(
                _.unserialize(this.input3.getUIValue())
            );
        }
    }
});

Когда вы запустите приведённый выше код, вы увидите, что два верхних виджета ввода текста пусты. Нажмите кнопку "Получить значения", и вы увидите пустое содержимое данных, предупреждённое в виде списка значений Json {"field1": "", "field2": ""}.

Теперь введите текст в виджеты input1 и input2, снова нажмите кнопку "Получить значения", и вы увидите новые собранные значения данных, предупреждённые как объект Json.

Нажмите кнопку "Установить значения", и вы увидите, что значения, которые вы ввели в виджеты input1 и input2, были заменены значениями, представленными строкой Json в виджете input3.

Вручную отредактируйте строку Json в виджете input3, снова нажмите кнопку "Установить значения", и вы увидите, что отредактированные значения данных отображаются соответствующим образом обратно в виджетах input1 и input2.

Вы можете добавить сколько угодно данных в свои приложения jsLinb, каждое из которых сериализует/десериализует любые выбранные значения из / в любой набор виджетов в любом месте холста. Это позволяет очень просто вводить, редактировать, собирать и/или представлять несколько фрагментов данных, используя любые различные виджеты jsLinb. Взгляните на примеры приложений jsLinb и средство просмотра API, чтобы увидеть больше примеров кода Databinder. Это одна из самых полезных функций библиотеки jsLinb.

19.11 Пример приложения большего размера

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

  1. Два объекта базы данных добавляются для сбора и установки данных в 2 отдельных формах.
  2. Виджет макета используется для размещения всех записей в особенно большой форме на одной странице (форма профиля учётной записи).
  3. Представленный макет прекрасно умещается на небольшом экране телефона, без прокрутки.
  4. Данные в форме профиля учётной записи сохраняются не только на сервере, но и в локальных файлах cookie браузера. При запуске приложения, если пользователь сохранил информацию профиля на локальном компьютере, она загружается из файлов cookie, а затем отображается. Пользователь также может создавать и сохранять новые профили учётных записей, а также загружать, обновлять и переключаться между существующими профилями, хранящимися на сервере (введите существующее имя пользователя/пароль, нажмите кнопку "загрузить", отредактируйте, затем нажмите "сохранить" " кнопка).
  5. Код сервера демонстрирует один способ синтаксического анализа данных в сохранённом сериализованном формате, отправленных из форм, а также логику, необходимую для сохранения и загрузки существующих данных профиля, сравнения имён пользователей и паролей и т.д.
  6. Некоторые другие данные загружаются с сервера, и результаты отображаются при запуске. Флажок на странице "макет данных" устанавливается на основе значения истина/ложь, сохранённого в текстовом файле на сервере (.../www/checkbox.txt). Некоторый текст в виджете метки на странице макета данных считывается из текстового файла и отображается в пользовательском интерфейсе (.../www/text.txt). Первая страница виджета buttonviews также фокусируется и отображается при запуске.
  7. Иконки в виджете buttonviews берутся из одного файла карты изображений (коллекции изображений, находящейся в /img/widgets.gif). Обратите особое внимание на синтаксис свойства "items" виджета buttonviews, чтобы увидеть, как это выполняется (каждый значок в этом наборе изображений имеет ширину 16 пикселей).
  8. Демонстрируются ещё несколько простых идей компоновки. Некоторый текст отображается в блоках div с использованием форматирования HTML. Также отображаются некоторые групповые виджеты, один из которых можно открывать и скрывать, нажимая отдельные кнопки или щёлкая непосредственно на виджете группы.
  9. Обратите внимание, что виджеты в формах и данные, включённые в базы данных, к которым они прикреплены, включают такие элементы, как средства выбора даты и времени, а также поля пароля, многострочные поля и т.д. Вся эта графически представленная информация может быть сериализована и десериализовать, включить в базу данных и т.д. так же легко, как и любое текстовое поле.

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

Class('App', 'linb.Com',{
    Instance:{
        base:["linb.UI"], 
        required:[
            "linb.UI.ButtonViews", "linb.UI.Div", "linb.UI.Label",
            "linb.UI.Input", "linb.UI.Group", "linb.UI.DatePicker",
            "linb.UI.Tabs", "linb.UI.Stacks", "linb.UI.TextEditor",
            "linb.UI.Button", "linb.UI.TimePicker", "linb.UI.Pane",
            "linb.UI.Layout", "linb.DataBinder", "linb.UI.CheckBox"
        ], 
        properties:{}, 
        events:{"onReady":"_onready", "onRender":"_onrender"}, 
        iniResource:function(com, threadid){}, 
        iniComponents:function(){
            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.DataBinder)
                .host(host,"databinder1")
                .setName("databinder1")
            );

            append((new linb.DataBinder)
                .host(host,"databinder2")
                .setName("databinder2")
            );

            append((new linb.UI.ButtonViews)
                .host(host,"buttonviews1")
                .setItems([
                  {"id":"1", "caption":"MultiPage Form", 
                  "image":"img/widgets.gif", "imagePos":"-192px top"},
                  {"id":"2", "caption":"Text Layout", 
                  "image":"img/widgets.gif", "imagePos":"-48px top"},
                  {"id":"3", "caption":"Data Layout", 
                  "image":"img/widgets.gif", "imagePos":"-64px top"},
                  {"id":"4", "caption":"Form", 
                  "image":"img/widgets.gif", "imagePos":"-96px top"},
                  {"id":"6", "caption":"Groups", 
                  "image":"img/widgets.gif", "imagePos":"-112px top"}
                ])
                .setBarLocation("left")
                .setBarSize("130")
                .setValue("a")
            );

            host.buttonviews1.append((new linb.UI.DatePicker)
                .host(host,"datepickerWriteYourOwn")
                .setDataBinder("databinder2")
                .setDataField("requestdate")
                .setLeft(20)
                .setTop(170)
                .setCloseBtn(false)
            , '4');

            host.buttonviews1.append((new linb.UI.Pane)
                .host(host,"pane8")
                .setLeft(10)
                .setTop(10)
                .setWidth("469")
                .setHeight(320)
            , '1');

            host.pane8.append((new linb.UI.Layout)
                .host(host,"layout3")
                .setItems([
                    {"id":"before", "pos":"before", "min":10, 
                    "size":318, "locked":false, "hide":false, 
                    "cmd":true, "caption":"before"}, 
                    {"id":"main", "min":1, "caption":"main"}
                ])
            );

            host.layout3.append((new linb.UI.Label)
                .host(host,"label213")
                .setLeft(230)
                .setTop(20)
                .setCaption("Notes:")
                .setHAlign("left")
            , 'main');

            host.layout3.append((new linb.UI.Label)
                .host(host,"label212")
                .setLeft(10)
                .setTop(20)
                .setCaption("Birthdate:")
                .setHAlign("left")
            , 'main');

            host.layout3.append((new linb.UI.DatePicker)
                .host(host,"datepicker19")
                .setDataBinder("databinder1")
                .setDataField("birthdate")
                .setLeft(10)
                .setTop(40)
            , 'main');

            host.layout3.append((new linb.UI.Group)
                .host(host,"groupProfile")
                .setLeft("10")
                .setTop("10")
                .setWidth(450)
                .setHeight(270)
                .setCaption("Account Info:")
                .setToggleBtn(false)
            , 'before');

            host.groupProfile.append((new linb.UI.Label)
                .host(host,"labelLastName")
                .setLeft(10)
                .setTop(100)
                .setWidth(80)
                .setCaption("Last Name:")
            );

            host.groupProfile.append((new linb.UI.Label)
                .host(host,"labelCity")
                .setLeft(10)
                .setTop(160)
                .setWidth(80)
                .setCaption("City:")
            );

            host.groupProfile.append((new linb.UI.Label)
                .host(host,"labelAddress")
                .setLeft(10)
                .setTop(130)
                .setWidth(80)
                .setCaption("Address:")
            );

            host.groupProfile.append((new linb.UI.Label)
                .host(host,"labelPassword")
                .setLeft(10)
                .setTop(40)
                .setWidth(80)
                .setCaption("Password:")
            );

            host.groupProfile.append((new linb.UI.Label)
                .host(host,"label1")
                .setLeft(10)
                .setTop(10)
                .setWidth(80)
                .setCaption("Username:")
            );

            host.groupProfile.append((new linb.UI.Label)
                .host(host,"labelZip")
                .setLeft(10)
                .setTop(220)
                .setWidth(80)
                .setCaption("Zip Code:")
            );

            host.groupProfile.append((new linb.UI.Label)
                .host(host,"labelFirstName")
                .setLeft(10)
                .setTop(70)
                .setWidth(80)
                .setCaption("First Name:")
            );

            host.groupProfile.append((new linb.UI.Label)
                .host(host,"labelState")
                .setLeft(10)
                .setTop(190)
                .setWidth(80)
                .setCaption("State:")
            );

            host.groupProfile.append((new linb.UI.Input)
                .host(host,"inputUsername")
                .setDataBinder("databinder1")
                .setDataField("username")
                .setLeft(110)
                .setTop(10)
                .setWidth(320)
            );

            host.groupProfile.append((new linb.UI.Input)
                .host(host,"inputPassword")
                .setDataBinder("databinder1")
                .setDataField("password")
                .setLeft(110)
                .setTop(40)
                .setWidth(320)
                .setTabindex("2")
                .setType("password")
            );

            host.groupProfile.append((new linb.UI.Input)
                .host(host,"inputFirstName")
                .setDataBinder("databinder1")
                .setDataField("firstname")
                .setLeft(110)
                .setTop(70)
                .setWidth(320)
                .setTabindex("3")
            );

            host.groupProfile.append((new linb.UI.Input)
                .host(host,"inputLastName")
                .setDataBinder("databinder1")
                .setDataField("lastname")
                .setLeft(110)
                .setTop(100)
                .setWidth(320)
                .setTabindex("4")
            );

            host.groupProfile.append((new linb.UI.Input)
                .host(host,"inputAddress")
                .setDataBinder("databinder1")
                .setDataField("address")
                .setLeft(110)
                .setTop(130)
                .setWidth(320)
                .setTabindex("5")
            );

            host.groupProfile.append((new linb.UI.Input)
                .host(host,"inputCity")
                .setDataBinder("databinder1")
                .setDataField("city")
                .setLeft(110)
                .setTop(160)
                .setWidth(320)
                .setTabindex("6")
            );

            host.groupProfile.append((new linb.UI.Input)
                .host(host,"inputState")
                .setDataBinder("databinder1")
                .setDataField("state")
                .setLeft(110)
                .setTop(190)
                .setWidth(320)
                .setTabindex("7")
            );

            host.groupProfile.append((new linb.UI.Input)
                .host(host,"inputZipCode")
                .setDataBinder("databinder1")
                .setDataField("zipcode")
                .setLeft(110)
                .setTop(220)
                .setWidth(320)
                .setTabindex("8")
            );

            host.layout3.append((new linb.UI.TextEditor)
                .host(host,"texteditor39")
                .setDataBinder("databinder1")
                .setDataField("notes")
                .setLeft(230)
                .setTop(40)
                .setWidth("230")
                .setHeight(150)
                .setTabindex("10")
                .setBorder(true)
            , 'main');

            host.layout3.append((new linb.UI.Label)
                .host(host,"label58")
                .setLeft("176")
                .setTop(290)
                .setWidth("120")
                .setCaption("(more)")
                .setHAlign("center")
            , 'before');

            host.buttonviews1.append((new linb.UI.Tabs)
                .host(host,"tabs")
                .setItems([
                    {"id":"a", "caption":"One", "image":"img/run.gif"},
                    {"id":"b", "caption":"Two", "image":"img/run.gif"}
                ])
                .setValue("a")
            , '3');

            host.tabs.append((new linb.UI.Stacks)
                .host(host,"stacks1")
                .setItems([
                    {"id":"1", "caption":"1"}, 
                    {"id":"2", "caption":"2"},
                    {"id":"3", "caption":"3"}, 
                    {"id":"4", "caption":"4"}
                ])
                .setValue("a")
            , 'a');

            host.stacks1.append((new linb.UI.CheckBox)
                .host(host,"checkbox1")
                .setLeft(20)
                .setTop(20)
                .setCaption("checkbox1")
            , '1');

            host.stacks1.append((new linb.UI.Label)
                .host(host,"label26")
                .setLeft(20)
                .setTop(50)
                .setWidth(340)
                .setCaption(
                    "This checkbox value is loaded from the server"
                )
                .setHAlign("left")
            , '1');

            host.tabs.append((new linb.UI.Label)
                .host(host,"labelGetFromServer")
                .setLeft(20)
                .setTop(20)
                .setWidth(410)
                .setBorder(true)
                .setCaption("Data Page 2")
                .setHAlign("left")
                .setVAlign("middle")
            , 'b');

            host.tabs.append((new linb.UI.Label)
                .host(host,"label27")
                .setLeft(20)
                .setTop(50)
                .setWidth(410)
                .setCaption(
                    "The text above was loaded from a" +
                    "plain text file on the server."
                )
                .setHAlign("left")
            , 'b');

            host.buttonviews1.append((new linb.UI.Group)
                .host(host,"groupShare")
                .setLeft(20)
                .setTop(120)
                .setWidth(400)
                .setHeight(250)
                .setCaption("Text Entry Group")
                .setToggle(false)
            , '6');

            host.groupShare.append((new linb.UI.TextEditor)
                .host(host,"texteditorShare")
                .setLeft("20")
                .setTop("10")
                .setWidth(360)
                .setHeight(170)
                .setVisibility("visible")
                .setBorder(true)
            );

            host.groupShare.append((new linb.UI.Button)
                .host(host,"buttonShareSubmitText")
                .setLeft(260)
                .setTop(190)
                .setVisibility("visible")
                .setCaption("Submit")
            );

            host.buttonviews1.append((new linb.UI.Div)
                .host(host,"divText1")
                .setLeft(40)
                .setTop(89)
                .setWidth(390)
                .setHeight(160)
                .setHtml(
                    "Here's some info about this app:  " +
                    "<br><br>1) Item 1 <br>2) Item 2 <br>3) Item 3"
                )
            , '2');

            host.buttonviews1.append((new linb.UI.Div)
                .host(host,"divText2")
                .setLeft("20")
                .setTop(10)
                .setWidth(310)
                .setHeight(60)
                .setHtml("<font size=20>Some Text</font>")
            , '2');

            host.buttonviews1.append((new linb.UI.Button)
                .host(host,"buttonLoadDatabinder")
                .setLeft(350)
                .setTop(370)
                .setCaption("Load")
                .onClick("_buttonloaddatabinder_onclick")
            , '1');

            host.buttonviews1.append((new linb.UI.TimePicker)
                .host(host,"timepickerWriteYourOwn")
                .setDataBinder("databinder2")
                .setDataField("requesttime")
                .setLeft(230)
                .setTop(170)
                .setWidth(220)
                .setCloseBtn(false)
            , '4');

            host.buttonviews1.append((new linb.UI.Div)
                .host(host,"div24")
                .setLeft("20")
                .setTop(20)
                .setWidth(430)
                .setHeight(20)
                .setHtml("Submit text, date, and time:")
            , '4');

            host.buttonviews1.append((new linb.UI.TextEditor)
                .host(host,"texteditorMakeaRequest")
                .setDataBinder("databinder2")
                .setDataField("requesttext")
                .setLeft(20)
                .setTop(50)
                .setWidth(430)
                .setHeight(100)
                .setBorder(true)
            , '4');

            host.buttonviews1.append((new linb.UI.Button)
                .host(host,"buttonWriteYourOwn")
                .setLeft(320)
                .setTop(340)
                .setCaption("Submit")
                .onClick("_buttonwriteyourown_onclick")
            , '4');

            host.buttonviews1.append((new linb.UI.Group)
                .host(host,"groupShareText")
                .setLeft(20)
                .setTop(20)
                .setWidth(400)
                .setHeight(70)
                .setCaption("Button Group")
                .setToggleBtn(false)
            , '6');

            host.groupShareText.append((new linb.UI.Button)
                .host(host,"buttonShareUploadText")
                .setLeft(20)
                .setTop(10)
                .setWidth(170)
                .setCaption("Open Text Entry Group")
                .onClick("_buttonshareuploadtext_onclick")
            );

            host.groupShareText.append((new linb.UI.Button)
                .host(host,"buttonShareReadTexts")
                .setLeft(200)
                .setTop(10)
                .setWidth(180)
                .setCaption("Close Text Entry Group")
                .onClick("_buttonsharereadtexts_onclick")
            );

            host.buttonviews1.append((new linb.UI.Button)
                .host(host,"buttonSubmitDatabinder")
                .setLeft(350)
                .setTop(340)
                .setTabindex("9")
                .setCaption("Save")
                .onClick("_buttonSubmitDatabinder_onclick")
            , '1');

            return children;
        }, 
        iniExComs:function(com, hreadid){
        }, 
        _buttonshareuploadtext_onclick:function (profile,e,src,value){
            this.groupShare.setToggle(true); ;
        }, 
        _onready:function (com,threadid){
            this.buttonviews1.setValue('1',true);
        }, 
        _buttonSubmitDatabinder_onclick:function (profile,e,src,value){
            var data=this.databinder1.getValue();
            if(!data)
                alert('Ensure all the fields are valid first!');
            else
                var cookie1 = linb.Cookies;
                cookie1.set('profile',_.serialize(data));
                linb.Ajax(
                    "http://localhost/cgi-bin/generic-profile.cgi",
                    (
                        "f=saveprofile&user=" + 
                        this.inputUsername.getUIValue() + "&pass=" +
                        this.inputPassword.getUIValue() + "&d=" + 
                        cookie1.get('profile')
                    ),
                    function(s){
                        alert(s);
                    }
                ).start();
         }, 
        _onrender:function (com,threadid){
            self=this;
            var cookie1 = linb.Cookies;
            this.databinder1.resetValue(
                _.unserialize(cookie1.get('profile'))
            );
            linb.Ajax(
                "http://localhost/text.txt",
                "",
                function(s){
                    self.labelGetFromServer.setCaption(s);
                }
            ).start();
            linb.Ajax(
                "http://localhost/checkbox.txt",
                "",
                function(s){
                    self.checkbox1.setValue(s);
                }
            ).start();
        }, 
        _buttonwriteyourown_onclick:function (profile,e,src,value){
            var data=this.databinder2.getValue();
            if(!data)
                alert('Ensure all the fields are valid first!');
            else
                linb.Ajax(
                    "http://localhost/cgi-bin/generic-form.cgi",
                    (
                        "f=submitrequest&user=" + 
                        this.inputUsername.getUIValue() + "&d=" + 
                        _.serialize(data)
                    ),
                    function(s){
                        alert(s);
                    }
                ).start();
        }, 
        _buttonsharereadtexts_onclick:function (profile,e,src,value){
            this.groupShare.setToggle(false); ;
        }, 
        _buttonloaddatabinder_onclick:function (profile,e,src,value){
            self=this
            linb.Ajax(
                "http://localhost/cgi-bin/generic-profile.cgi",
                (
                    "f=loadprofile&user=" + 
                    this.inputUsername.getUIValue() + 
                    "&pass=" + this.inputPassword.getUIValue()
                ),
                function(s){
                    alert(s);
                    self.databinder1.resetValue(_.unserialize(s));
                }
            ).start();
        }
    }
});

Вот файл Rebol generic-profile.cgi:

#!/usr/bin/rebol.exe -cs
REBOL []
print "content-type: text/html^/"
data: decode-cgi raw: read-cgi
save %log.txt raw

make-dir %./profiles/
if data/2 = "loadprofile" [
    either error? try [
        sv: read rejoin [%./profiles/ data/4 ".txt"]
        parse sv [thru {"password":} copy pass to {,}]
        if not ((mold data/6) = pass) [
            prin "Incorrect username/password"
            quit
        ]
    ][prin "That user profile does not exist"] [prin sv]
    quit
]
if data/2 = "saveprofile"[
    if exists? existing-file: rejoin [%./profiles/ data/4 ".txt"] [
        sv: read existing-file
        parse sv [thru {"password":} copy pass to {,}]
        if not ((mold data/6) = pass) [
            prin "Incorrect username/password"
            quit
        ]
    ]
    write rejoin [%./profiles/ data/4 ".txt"] data/8
    prin "Your profile info has been saved in local cookies"
    prin ", and in .../cgi-bin/profiles/"
]
quit

Rebol generic-form.cgi скрипт:

#!/usr/bin/rebol.exe -cs
REBOL []
print "content-type: text/html^/"
data: decode-cgi raw: read-cgi
save %log.txt raw

if data/2 = "readrequest" [
    either error? try [
        sv: read %request.txt
    ][prin "Error Reading!"] [prin sv]
    quit
]
if data/2 = "submitrequest"[
    write/append %request.txt join "^/^/" mold data/4
    write/append %request.txt join "^/^/" data/6
    prin "Your form data has been saved in "
    prin ".../cgi-bin/request.txt on the server!"
]
quit

Содержимое файла .../www/text.txt:

This text was loaded from .../www/text.txt

И содержимое файла .../www/checkbox.txt:

true

Код в этом примере должен быть повторно использован во многих типах приложений.

20. Подключение к автономным серверным приложениям Rebol

В руководстве по адресу http://re-bol.com/rebol-multi-client-databases.html объясняется, как полностью создавать многопользовательские сетевые приложения на Rebol. В таких приложениях клиентские и серверные приложения Rebol подключаются напрямую через сеть. Не требуется сторонний веб-сервер (т.е. не используется стек Apache, например Uniform Server), не требуется шаблон CGI, и даже модель данных приложения создаётся полностью с использованием чистого кода Rebol (собственные структуры данных и последовательные функции, в отличие от к СУБД стороннего производителя) для выполнения операций по манипулированию данными, постоянного хранения и т. д. Производительность такого сервера можно настроить так, чтобы он соответствовал задачам обычных ситуаций производственного трафика. Мы можем расширить идею автономного сервера Rebol для использования в сочетании с интерфейсными пользовательскими интерфейсами jsLinb.

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

REBOL [title: "HTML Form Server"]
l: read join dns:// read dns://
print join "Waiting on:  " l
port: open/lines tcp://:80
browse join l "?"
forever [
  connect: first port
  if error? try [
    z: decode-cgi replace next find first connect "?" " HTTP/1.1" ""
    prin rejoin ["Received: " mold z newline]
    my-form: rejoin [
      {HTTP/1.0 200 OK^/Content-type: text/html^/^/
      <HTML><BODY><FORM ACTION="} l {">Server:  } l {<br><br>
          Name:<br><INPUT TYPE="TEXT" NAME="name" SIZE="35"><br>
          Address:<br><INPUT TYPE="TEXT" NAME="addr" SIZE="35"><br>
          Phone:<br><INPUT TYPE="TEXT" NAME="phone" SIZE="35"><br>
          <br><input type="checkbox" name="checks" value="i1">Item 1
          <input type="checkbox" name="checks" value="i2">Item 2
          <input type="radio" name="radios" value="yes">Yes
          <input type="radio" name="radios" value="no">No<br><br>
          <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
      </FORM></BODY></HTML>}
    ]
    write-io connect my-form (length? my-form)
    ] [print "(empty submission)"]
    close connect
]

Для наших целей здесь нам не нужно печатать пользователю HTML-форму, потому что вместо этого мы используем jsLinb для отправки данных. Итак, мы немного скорректируем приведённый выше код, чтобы просто обработать данные в формате Json, отправленные приложением jsLinb. Запустите этот код непосредственно в консоли Rebol на вашем компьютере (обратите внимание, что этот сервер работает на порту 55555, поэтому он не мешает порту 80 Uniform Server):

REBOL [title: "jsLinb Server"]
l: read join dns:// read dns://
print rejoin ["Waiting on:  " l ":55555"]
port: open/lines tcp://:55555
forever [
  connect: first port
  if error? try [
    z: decode-cgi replace next find first connect "?" " HTTP/1.1" ""
    prin rejoin ["Received: " mold z newline]
    r: rejoin [
      ; {HTTP/1.0 200 OK^/}
      {content-type: text/html^/^/}
      "{" {"data" : "Saved!"} "}"
    ] 
    write-io connect r (length? r)
  ] []    ;[print "(empty submission)"] 
  close connect
]

После запуска указанного выше сервера вставьте следующий код jsLinb во вкладку кода Sigma IDE. Обратите внимание, что этот макет собирает некоторые данные от пользователя через 2 виджета ввода, к каждому из которых прикреплён модуль данных myformdata. При нажатии кнопки данные формы сериализуются в строку Json с помощью метода myformdata.getValue(), а затем эта строка отправляется на сервер с помощью запроса linb.Ajax (вы видели эту процедуру несколько раз сейчас же):

Class('App', 'linb.Com',{
    Instance:{
        base:[], 
        required:[
            "linb.DataBinder", "linb.UI.Input", "linb.UI.Button"
        ],
        events:{}, 
        iniComponents:function(){
            var host=this, children=[], 
            append=function(child){children.push(child.get(0))};

            append((new linb.DataBinder)
                .host(host,"myformdata")
                .setName("myformdata")
            );

            append((new linb.UI.Input)
                .host(host,"input1")
                .setDataBinder("myformdata")
                .setDataField("field1")
                .setLeft(21)
                .setTop(20)
            );

            append((new linb.UI.Input)
                .host(host,"input2")
                .setDataBinder("myformdata")
                .setDataField("field2")
                .setLeft(21)
                .setTop(51)
            );

            append((new linb.UI.Button)
                .host(host,"button1")
                .setLeft(21)
                .setTop(80)
                .setCaption("Send Form Values")
                .onClick("_button1_onclick")
            );

            return children;
        }, 
        _button1_onclick:function (profile, e, value) {
            var data=this.myformdata.getValue();
            if(!data)
                alert('Ensure all the fields are valid first!');
            else
                linb.Ajax( 
                    "http://localhost:55555", 
                    ("data=" + _.serialize(data)),
                    function(s){
                        alert(s);    
                    } 
                ).start();
        }, 
    }
});

Запустите приведённый выше код jsLinb, чтобы увидеть, как он взаимодействует с сервером, написанным на чистом коде Rebol.

Встроенные функции сетевого ввода-вывода Rebol упрощают отправку данных туда и обратно с помощью приложений jsLinb.

21. CrossUI

Следует отметить, что библиотека jsLinb и Sigma IDE являются предшественниками более свежего продукта под названием CrossUI. Большинство функций библиотеки jsLinb и Sigma IDE работают точно так же в наборе инструментов CrossUI. Sigma Toolkit, рассмотренный в этом руководстве, на самом деле достаточно зрелый и содержит многое из того, что делает CrossUI привлекательным. Cross UI предлагает ряд новых виджетов и функций, включая виджеты для рисования и функции анимации, а также улучшенные версии многих виджетов jsLinb. Менеджер проекта CrossUI также более зрелый, чем тот, который поставляется с Sigma IDE. Он имеет прямую интеграцию с Phonegap Build, так что созданные вами графические интерфейсы могут быть автоматически преобразованы в мобильные приложения, которые могут быть отправлены в магазины приложений практически для любой современной платформы. Менеджер проекта также может создавать настольные приложения для Windows, Mac и Linux (32- и 64-битные версии каждой ОС), используя Node-Webkit, и он делает кроссплатформенную упаковку из любой настольной ОС в любую другую (т.е. вы можете создавать приложения для Mac и Linux из Windows, приложения для Windows на Mac и т. д.)

Функция "прототипирования" в новейших версиях CrossUI - это полное решение для разработки без кода, которое позволяет пользователям создавать мощную логику приложения без написания ни одной строчки кода jsLinb. Он выходит далеко за рамки настроек свойств/событий визуального компоновщика Sigma и полностью инкапсулирует все функции jsLinb API, а также создание переменных, создание условных выражений и другие фундаментальные структуры кода, и всё это управляется щелчком по спискам выбора в интерфейс мастера, который автоматически создаёт любой требуемый код приложения. Это поистине уникальная и глубокая функция, которая весьма практична для таких пользователей, как графические дизайнеры, которые работают над дизайном пользовательского интерфейса, но не заинтересованы в глубоком погружении в код.

Новые возможности CrossUI имеют свою цену. Хотя библиотека "xui" (новая версия jsLinb, входящая в состав инструментария CrossUI) имеет лицензию LGPL, IDE имеет коммерческую лицензию. Его необходимо приобрести, если вы используете его для создания коммерческих приложений (это НЕ относится к Sigma IDE).

Следует отметить, что инструменты CrossUI также поставляются в виде единого установочного пакета для Windows, Mac или Linux. Вам не нужно устанавливать какой-либо отдельный веб-сервер, например Uniform Server. Это может быть как полезным, так и вредным. Одним из больших преимуществ Sigma IDE является то, что её можно запускать из любого места, в любой операционной системе, используя любой веб-сервер с PHP (доступный на локальном компьютере, в локальной интрасети или в Интернете). Это означает, что его можно разместить практически в любом месте, а IDE может работать практически на любом настольном компьютере или мобильном устройстве с веб-браузером. Это одна из самых убийственных функций инструментария. Он полностью переносится между любым местом и любым устройством и работает одинаково везде, от машин с устаревшими браузерами до новейших мобильных устройств. В этом отношении нигде нет ничего похожего на Sigma IDE (и это совершенно бесплатно - лучше не найти).

Если вам нужна коммерческая поддержка используемого набора инструментов, обратите внимание на CrossUI. Вы сами по себе, если решите использовать Sigma IDE. Предположительно, будущие обновления и поддержка будут производиться только для линейки продуктов CrossUI.

21.1 Мощное дополнение к набору инструментов Rebol

Комбинация jsLinb/Sigma IDE и серверного кода Rebol образует очень производительный и мощный, современный и полностью переносимый инструментарий разработки для самого широкого спектра современных и устаревших операционных систем. Приложения JsLinb UI могут подключаться к CGI-коду Rebol, работающему даже на самых дешёвых учётных записях общего хостинга Apache, или к сетевым приложениям с чистым кодом Rebol, работающим на вашем собственном выделенном сервере в вашей собственной сети или в Интернете. Это позволяет создавать как серверные приложения, так и внешние интерфейсы практически в любом месте, на любых типах доступных операционных систем, а также запускать их где угодно. Его легко освоить, он действительно продуктивен и бесплатен для использования в коммерческих целях. Это отличное дополнение к набору инструментов Rebol - определённо стоит потраченного времени, необходимого для изучения того, как им пользоваться.

22. НАСТОЯЩИЕ МИРОВЫЕ ПРИМЕРЫ - Обучение мыслить кодом

К этому моменту вы увидели наиболее важные части синтаксиса языка REBOL, но, вероятно, все ещё говорите себе: "Это здорово ... но как мне написать полную программу, которая выполняет ______". Чтобы материализовать любое работающее программное обеспечение на основе воображаемого проекта, очевидно, важно знать, какие языковые конструкции доступны для построения частей программы, но "мышление в коде" - это не менее важно организовать эти части в более крупные структуры, зная, с чего начать, и возможность разбить процесс на управляемую, повторяемую рутину. Этот раздел предназначен для того, чтобы дать общее представление о том, как преобразовать концепции человеческого дизайна в код REBOL и как организовать рабочий процесс, чтобы подойти к любой уникальной ситуации. Представлен ряд тематических исследований, чтобы понять, насколько удовлетворены конкретные реальные жизненные ситуации.

22 20 Обобщённый подход с использованием контуров и псевдокода

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

  1. Начните с детального определения того, что должно делать приложение, в человеческих терминах. Вы ничего не добьётесь в процессе проектирования, пока не сможете описать какую-либо форму воображаемой окончательной программы. Запишите своё объяснение и максимально подробно изложите детали воображаемой программы. Включите как можно больше деталей: как должна выглядеть программа, как пользователь будет с ней взаимодействовать, какие данные она будет принимать, обрабатывать и возвращать и т.д.
  2. Составьте список общего кода и структур данных, относящихся к каждой из "описанных человеком" программных целей выше. Оцените все общие шаблоны кода, которые относятся к работе каждого воображаемого программного компонента. Подумайте о том, как пользователь будет получать данные в программу и из неё. Будет ли пользователь работать с окном графического интерфейса рабочего стола, веб-формами, которые подключаются к сценарию CGI, или напрямую через командную строку в консоли интерпретатора?
  3. Подумайте, как данные, используемые в программе, могут быть представлены в коде, организованы и ими можно управлять. Какие типы данных будут задействованы (типы текста, такие как строки, значения времени или URL-адреса, двоичные типы, такие как изображения и звуки, и т.д.). Может ли программный код потенциально использовать переменные, структуры и функции блоков/серий или структуры объектов? Будут ли данные храниться в локальных файлах, в удалённой базе данных или просто во временной памяти? Подумайте, как программа будет переходить от одной операции к другой. Как нужно будет сортировать, группировать и связывать фрагменты данных друг с другом, какие типы условных операций и операций цикла необходимо выполнять, какие типы повторяющихся функций необходимо изолировать и кодифицировать? Подумайте обо всем, что должно произойти в воображаемом программном обеспечении, и начните думать: "Вот как я мог бы потенциально реализовать это в коде ...".
  4. Начните писать набросок кода. Часто проще всего сделать это, обрисовав в общих чертах пользовательский интерфейс, но блок-схема операций также может оказаться полезной. Идея здесь в том, чтобы начать писать контейнер обобщённого кода для вашей рабочей программы. На этом этапе схему можно заполнить простым КОДОМ ПСЕВДО на естественном языке, который описывает, как может быть организован реальный код. Особенно полезно начинать с схемы пользовательского интерфейса, поскольку она обеспечивает отправную точку для написания больших структур кода и заставляет вас решать, как программа будет обрабатывать ввод, манипулирование и вывод данных. Простые структуры, такие как "вид макета [кнопка [которая делает это при нажатии ...]]", "блок: [с метками и субблоками, организованными следующим образом ...]", функция: (которая проходит через этот блок и сохраняет эти элементы в другую переменную ...) "можно дополнить позже полным кодом.
  5. Наконец, перейдите к замене псевдокода реальным рабочим кодом. Это не так сложно, как только вы выполните предыдущие шаги. На этом этапе очень полезен языковой словарь / руководство с функциями перекрёстных ссылок. И как только вы по-настоящему ознакомитесь со всеми доступными конструкциями в языке, все, что вам, вероятно, понадобится, - это периодические напоминания о синтаксисе из встроенной справки REBOL. В конце концов, вы пройдёте через другие этапы проектирования гораздо более интуитивно и очень быстро перейдёте на этот этап или пройдёте его через него.
  6. В качестве последнего шага отлаживайте рабочий код и добавляйте/изменяйте функциональные возможности по мере тестирования и использования программы.

Основной план атаки состоит в том, чтобы всегда объяснять себе, что предполагаемая программа должна делать в человеческих терминах, а затем продумывать, как все необходимые структуры кода должны быть организованы для достижения этой цели. По мере того, как воображаемая программа обретает форму, организуйте свой рабочий процесс, используя подход сверху вниз: воображаемая концепция -> общий план -> описание псевдокода / мыслительный процесс -> рабочий код -> готовый код.

Большая часть кода, который вы пишете, будет переходить от одного пользовательского ввода, определения данных или внутренней функции к другому. Начните составлять карту всего, что должно "произойти" в программе, и информации, которой нужно манипулировать на этом пути, чтобы это произошло, от начала до конца. Процесс написания схемы может быть облегчён, если подумать о том, как должна начинаться программа и что нужно сделать до того, как пользователь начнёт взаимодействовать с приложением. Подумайте о любых данных или действиях, которые необходимо определить до запуска программы. Затем подумайте, что должно произойти, чтобы приспособиться к каждому возможному взаимодействию, которое может выбрать пользователь. В некоторых случаях, например, все возможные действия могут происходить в результате того, что пользователь щёлкает различные виджеты графического интерфейса. Это должно вызвать мысль об определённых битах структуры кода графического интерфейса пользователя, и вы можете начать писать схему для разработки интерфейса графического интерфейса. Если вы представите онлайн-приложение CGI, пользователь, скорее всего, будет работать с формами на веб-странице. Вы можете приступить к разработке HTML-форм, что неизбежно приведёт к указанию переменных, которые передаются в приложение CGI. Если ваша программа будет работать как простое приложение командной строки, пользователь может отвечать на текстовые вопросы. Опять же, вам следует вспомнить некоторый код из примеров приложений в этом руководстве, и вы можете начать формировать структуру кодирования, которая обеспечивает общий пользовательский интерфейс и рабочий процесс.

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

Работая над каждой строчкой кода, используйте псевдокод на естественном языке, чтобы упорядочить свои мысли. Например, если вы представляете себе кнопку в интерфейсе GUI, выполняющую что-то для вашего пользователя, вам не нужно сразу писать код REBOL, который запускается кнопкой. Сначала просто напишите описание того, что вы хотите, чтобы кнопка делала. То же самое верно для функций и других фрагментов кода. По мере того, как вы конкретизируете свой план, опишите элементы языка и код, которые вы задумали для выполнения различных действий или для представления различных структур данных. Смысл написания псевдокода - чётко сосредоточиться на общем дизайне программы на каждом этапе процесса разработки. Это поможет вам не потеряться в мельчайших деталях синтаксиса реального кода. Каждый раз, когда вы занимаетесь написанием каждой строчки кода, легко упустить из виду общую картину.

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

Ниже приводится ряд тематических исследований, в которых описывается, как я продуктивно подходил к различным задачам программирования. В каждом примере прослеживается ход моих мыслей от организационного процесса до завершённого кода.

22.1 Случай: Расписание учителей

В моем бизнесе по урокам музыки учителя были знакомы с расписаниями, написанными от руки на бумаге, которые выглядели так:

Monday:

3      student1, 555-1234, parent's names, payment history, notes
3:30   student2, 555-1234, parent's names, payment history, notes
4      (gone 3-17)  student3, 555-1234, payment history, notes
4:30   student4, 555-1234, parent's names, payment history, notes
5      student5, 555-1234, parent's names, payment history, notes


Tuesday:

3      ----
3:30   ----
4      (john doe 3-18) ----
4:30   ----
5      student1, 555-1234, parent's names, payment history, notes
5:30   student2, 555-1234, parent's names, payment history, notes
6      student3, 555-1234, parent's names, payment history, notes
6:30   ----
7      student4, 555-1234, parent's names, payment history, notes
7:30   ----
8      student5, 555-1234, parent's names, payment history, notes
.
.
.

Чтобы вести свой бизнес, я хотел создать указанный выше формат расписания на веб-странице и поместить его в HTML-документ, содержащий некоторую постоянную информацию, которую учителя не будут изменять. Я хотел, чтобы каждый учитель мог вносить коррективы в своё расписание, не вмешиваясь в ftp или что-то ещё, связанное с веб-сайтом. Я просто хотел, чтобы они могли щёлкнуть значок на рабочем столе, внести изменения в своё расписание и отобразить их на веб-странице. Я вообразил простое приложение, которое будет делать эти вещи, и придумал этот базовый план того, как оно может работать:

  1. Загрузите текстовый файл текущего расписания учителя.
  2. На всякий случай сделайте резервную копию существующего расписания.
  3. Отредактируйте расписание.
  4. Загрузите изменённые данные расписания обратно на сайт.
  5. Включите новый текст расписания в шаблон HTML, сохранив правильный формат строки.
  6. Убедитесь, что изменения были внесены правильно и правильно отображаются на веб-странице.
  7. Сделайте интерфейс учителя простым и интуитивно понятным, как если бы вы писали на листе бумаги.

Посмотрев на вышеприведённый план, я просто сделал каждый шаг самым прямым способом в коде REBOL:

; сначала установим некоторые начальные требуемые переменные:

url: http://website.com/teacher
ftp-url: ftp://user:pass@website.com/public_html/teacher

; ... и дадим учителю инструкции: 

alert {Edit your schedule, then click save and quit.
    The website will be automatically updated.}

; 1) скачиваем файл с текстом расписания:

write %schedule.txt read rejoin [url "/schedule.txt"]

; 2) создаём резервную копию с отметкой времени на веб-сервере:

write rejoin [ftp-url "/" now/date "_" now/time ".txt"] read %schedule.txt

; 3 и 7) редактируем текст:

editor %schedule.txt

; 4) сохраним отредактированный текст обратно на веб-сайт:

write rejoin [ftp-url "/schedule.txt"] read %schedule.txt

; 6) убедимся, что изменения отображаются правильно:

browse url

Чтобы выполнить шаг 5 схемы, я создал загружаемый исполняемый файл (файл ".exe") вышеуказанной программы (с помощью XpackerX) и загрузил его на веб-сайт. В папке http://website.com/teacher на веб-сайте я создал сценарий index.cgi, содержащий следующий код:

#!/home/path/public_html/rebol/rebol -cs
REBOL []
print "content-type: text/html^/"

print {<a href="./scheduler.exe" target=_blank>Download Scheduler</a><br>}
print rejoin ["<pre>" read %schedule.txt "</pre>"]

Первая строка HTML создаёт ссылку для загрузки, чтобы учитель мог загрузить и запустить свою программу-планировщик в любом удалённом месте. Вторая строка включает предварительно отформатированный текст расписания на веб-странице. Я могу разместить на этой странице любой другой HTML-код, который я хочу, чтобы учитель никогда не касался его (их контактная информация, стоимость уроков, информация о датах каникул, типы студентов, которых они хотят преподавать, и т.д.).

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

22.2 Случай: простая программа CGI для галереи изображений

Создавая веб-сайт для своего бизнеса по урокам музыки, я хотел регулярно добавлять фотографии студентов, выступающих на различных мероприятиях. Сначала я просто загрузил фотографии по отдельности и добавил ссылку на папку, в которой они были. По мере роста коллекции я хотел, чтобы пользователям было легче просматривать изображения без необходимости нажимать на каждое отдельное имя файла. Итак, я собрал простую флеш-презентацию, в которой изображения показывались одно за другим. Но обновление этой презентации потребовало слишком большого обслуживания. Я хотел просто загружать фотографии и отображать их в удобном формате на одной веб-странице без какого-либо обслуживания. Этот тип небольшого CGI-приложения идеально подходил для REBOL. Написание заняло всего несколько минут, и теперь им пользуются каждый день.

Вот схема и псевдокод для этой программы, над которыми я работал в своей голове:

  1. Начинаем с создания простого сценария командной строки на моем домашнем компьютере, который читает список каталогов и использует цикл foreach для просмотра файлов и выполнения необходимых действий.
  2. В цикле foreach проверим указанные типы изображений (расширения в каждом имени файла) и работайте только с этими файлами. Добавить счётчик для отображения общего количества изображений. Для этого используем переменную-счётчик и увеличивайте её каждый раз в цикле.
  3. В цикле foreach оберните каждое изображение в списке в теги HTML, необходимые для отображения их на веб-странице. Добавьте необходимые заголовки для создания сценария CGI, который запускается на веб-сайте. Сценарий должен распечатать HTML-код в браузере посетителя, чтобы он увидел веб-страницу, содержащую все изображения.

Вот код для шага 1:

REBOL []

folder: read %.
foreach file folder [
    print file 
    ; это просто фиктивное действие, чтобы убедиться, что цикл 
    ; работает правильно
]
halt

На шаге 2 я добавил переменную счётчика и проверил указанные типы изображений с помощью условного выражения "if any" (если любой)

REBOL []

folder: read %.
count: 0
foreach file folder [
    if any [
        find file ".jpg" 
        find file ".gif" 
        find file ".png" 
        find file ".bmp"
    ] [
        print file
        count: count + 1
    ]
]
print rejoin [newline "Total Images: " count]
halt

Я немного сократил этот сценарий, используя альтернативную версию, основанную на вложенных циклах foreach. Альтернативный код упрощает расширение списка потенциальных типов изображений в будущем:

REBOL []

folder: read %.
count: 0
foreach file folder [
    foreach ext [".jpg" ".gif" ".png" ".bmp"] [
        if find file ext [
            print file
            count: count + 1
        ]
    ]
]
print rejoin [newline "Total Images: " count]
halt

Для последнего шага я позаимствовал строку из более раннего примера "Создателя диаграмм гитарных аккордов". Он создаёт HTML-код, необходимый для отображения каждого изображения на веб-странице. Я заменил фиктивную функцию печати выше на этот код:

print rejoin [{<img src="} file {">}]

Наконец, я добавил типичные заголовки CGI и код форматирования страницы, необходимые для правильной работы сценариев REBOL CGI (похожие шаблоны см. В предыдущих примерах CGI в этом руководстве):

#! /home/path/public_html/rebol/rebol -cs
REBOL [title: "Photo Viewer"]
print "content-type: text/html^/"
print [<HTML><HEAD><TITLE>"Jam Session Photos"</TITLE></HEAD><BODY>]
print read %pageheader.html

folder: read %.
count: 0
foreach file folder [
    foreach ext [".jpg" ".gif" ".png" ".bmp"] [
        if find file ext [
            print [<BR> <CENTER>]
            print rejoin [{<img src="} file {">}]
            print [</CENTER>]
            count: count + 1
        ]
    ]
]
print [<BR>]
print rejoin ["Total Images: " count]
print read %pagefooter.html

Я загрузил этот сценарий в папку с изображениями на нашем веб-сервере и обновил ссылку на фотографии на нашем веб-сайте. Теперь мы просто загружаем новые изображения прямо на сервер, и когда посетители веб-сайта щёлкают ссылку "Фотографии" на нашем сайте, они мгновенно видят динамически созданную веб-страницу, полную всех изображений, содержащихся в данный момент в этой папке.

22.3 Случай: Калькулятор дней между двумя датами

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

Это приложение создавалось поэтапно. Я начал с этой очень простой идеи псевдокода для сценария:

  1. Использовать функцию "request-date", чтобы получить дату начала от пользователя. Назначьте ответ переменной.
  2. Снова запустить функцию request-date, чтобы получить дату окончания от пользователя. Назначьте этот ответ другой переменной.
  3. Вычтите переменную даты окончания из переменной даты начала. Присвойте результат третьей переменной.
  4. Оповестите пользователя о результате.

Все очень просто. Вот рабочий код:

sd: request-date   ; запрашиваем начальную дату
ed: request-date   ; запрашиваем конечную дату
db: ed - sd        ; вычисляем разность
alert rejoin ["Days between " sd " and " ed ": " db]  ; display the result

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

  1. Создайте окно "view layout" и запустите каждую из функций даты запроса (дата начала и дата окончания) отдельной кнопкой.
  2. Выполните расчёт дней между периодами после выбора конечной даты и отобразите результат в текстовом поле. Чтобы это произошло, числовой результат между днями должен быть преобразован в текстовую строку (поскольку поля могут отображать только значения текстовой строки). Не забудьте обновить отображаемые результаты с помощью функции "show" (показать).

Вот код:

REBOL [title: "Days Between"]

view layout [
    btn "Select Start Date" [sd: request-date]
    btn "Select End Date" [
        ed: request-date
        db/text: to-string (ed - sd)
        show db
    ]
    h1 "Days Between:"
    db: field
]

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

  1. Я добавлю ещё два текстовых поля в макет графического интерфейса.
  2. Всякий раз, когда пользователь выбирает новую дату начала/окончания, я обновляю соответствующее текстовое поле для отображения выбранной даты. Для того, чтобы это работало правильно, мне снова нужно использовать функцию "to-string" для преобразования выбранной даты в текстовое значение.

Вот код, который я придумал, чтобы внести эти изменения:

REBOL [title: "Days Between"]

view layout [

    btn "Select Start Date" [
        sd: request-date

        ; Обновляем текстовое поле даты начала

        sdt/text: to-string sd
        show sdt

    ]

    ; Вот поле для отображения выбранной даты начала:
    sdt: field

    btn "Select End Date" [

        ed: request-date

        ; Обновляем текстовое поле даты окончания

        edt/text: to-string ed
        show edt

        db/text: to-string (ed - sd)
        show db

    ]

    ; Вот поле для отображения выбранной даты окончания:
    edt: field

    h1 "Days Between:"
    db: field
]

В нынешнем виде программа выйдет из строя, если я выберу дату окончания перед установкой даты начала (поскольку расчёт дней между ними пытается выполняться без какого-либо значения, установленного для переменной даты начала). Чтобы исправить это, вот мысленный процесс псевдокода, через который я прошёл:

  1. Я начну программу, установив для переменных "st" и "ed" (start-date и end-date) начальное значение сегодняшней даты ("now/date").
  2. Я покажу начальную дату начала и дату окончания в текстовых полях графического интерфейса. Для того, чтобы это работало правильно, мне снова нужно использовать функцию "to-string" для преобразования даты в текстовое значение.

Вот как выглядит программа, когда я вношу эти изменения:

REBOL [title: "Days Between"]

; установим значения для начальной и конечной даты:
sd: ed: now/date

view layout [
    btn "Select Start Date" [
        sd: request-date
        sdt/text: to-string sd
        show sdt
    ]

    ; покажем начальную дату в этом поле:
    sdt: field to-string sd

    btn "Select End Date" [
        ed: request-date
        edt/text: to-string ed
        show edt
        db/text: to-string (ed - sd)
        show db
    ]

    ; покажем конечную дату в этом поле:
    edt: field to-string ed

    h1 "Days Between:"
    db: field
]

Отлично, это работает, но расчёт дней между ними по-прежнему выполняется только тогда, когда я меняю дату окончания. Я добавлю код расчёта дней между кнопкой "Выбрать дату начала":

REBOL [title: "Days Between"]

sd: ed: now/date
view layout [
    btn "Select Start Date" [
        sd: request-date
        sdt/text: to-string sd
        show sdt

        ; Выполним расчёт дней между интервалами и обновим отображение:

        db/text: to-string (ed - sd)
        show db

    ]
    sdt: field to-string sd
    btn "Select End Date" [
        ed: request-date
        edt/text: to-string ed
        show edt
        db/text: to-string (ed - sd)
        show db
    ]
    edt: field to-string ed
    h1 "Days Between:"
    db: field
]

Немного поиграв с программой, я понял, что было бы здорово, если бы пользователь мог вручную вводить/редактировать выбранные даты. Вот мой мыслительный процесс:

  1. Я буду проводить расчёт дней между ними всякий раз, когда пользователь вносит изменения в текстовое поле.
  2. Мне нужно прекратить использовать переменные "sd" и "ed" для выполнения вычислений и вместо этого использовать текст, содержащийся в полях графического интерфейса, чтобы быть уверенным, что я работаю с любыми потенциально редактируемыми текстовыми значениями.
  3. Опять же, мне нужно обратить внимание на преобразование дат между типами данных text и date. Данные, отображаемые в текстовых полях графического интерфейса пользователя, необходимо преобразовать в текстовую строку с помощью функции "to-text", а данные, используемые для вычисления промежуточных дней, должны быть преобразованы в значение даты с использованием "to-date" функция. REBOL автоматически умеет вычитать и складывать даты, но не знает, как выполнять эти типы вычислений с текстовыми строками. Просто используйте функцию "to-date" для выполнения соответствующих вычислений, и она работает как по волшебству.
REBOL [title: "Days Between"]

sd: ed: now/date
view layout [
    btn "Select Start Date" [
        sd: request-date 
        sdt/text: to-string sd
        show sdt

        ; Выполните расчёт дней между днями, используя значение, 
        ; содержащееся в текстовом поле конечной даты (сначала 
        ; преобразуйте это текстовое значение в значение даты):
db/text: to-string ((to-date edt/text) - sd)

show db
]
sdt: field to-string sd [

; Выполните расчёт дней между датами, используя значения, 
; содержащиеся в текстовых полях начальной и конечной даты 
; (сначала преобразуйте эти текстовые значения в значения даты):
db/text: to-string ((to-date edt/text) - (to-date sdt/text))

show db
]
btn "Select End Date" [
ed: request-date
edt/text: to-string ed 
show edt

; Выполните расчёт дней между днями, используя значение, 
; содержащееся в текстовом поле даты начала (сначала 
; преобразуйте это текстовое значение в значение даты):
db/text: to-string (ed - (to-date sdt/text)) 

show db
]
edt: field to-string ed [

; Выполните расчёт дней между датами, используя значения, 
; содержащиеся в текстовых полях начальной и конечной даты 
; (сначала преобразуйте эти текстовые значения в значения даты):
db/text: to-string ((to-date edt/text) - (to-date sdt/text))

show db
]
h1 "Days Between:"
db: field 
]

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

  1. Отобразить начальное значение "0" дней в текстовом поле "db" (это количество дней между начальной и конечной датами (сегодня - сегодня)).
  2. Если пользователь вручную вводит количество дней, добавьте данное количество дней к дате начала и обновите текстовое поле конечной даты, указав результат (опять же, не забудьте выполнить преобразование между текстовыми значениями и значениями даты, как в каждом предыдущем примере).

Просто. Вот обновлённый код:

REBOL [title: "Days Between"]

sd: ed: now/date    
view layout [

    btn "Select Start Date" [
        sd: request-date 
        sdt/text: to-string sd 
        show sdt
        db/text: to-string ((to-date edt/text) - sd)
        show db
    ]
    sdt: field to-string sd [
        db/text: to-string ((to-date edt/text) - (to-date sdt/text))
        show db
    ]
    btn "Select End Date" [
        ed: request-date
        edt/text: to-string ed 
        show edt 
        db/text: to-string (ed - (to-date sdt/text)) 
        show db
    ]
    edt: field to-string ed [
        db/text: to-string ((to-date edt/text) - (to-date sdt/text))
        show db
    ]
    h1 "Days Between:"
    db: field "0" [

        ; Добавим введённое вручную количество дней к дате начала и 
        ; обновите отображение:
edt/text: to-string ((to-date sdt/text) + (to-integer db/text))
show edt

]
]

Когда я протестировал приведённый выше код, я обнаружил одну ошибку. Если дата введена вручную неправильно (например, я попробовал "267-авг-2009"), программа остановится при сбое с сообщением об ошибке. Чтобы исправить это, я обернул каждое вычисление даты, которое включало ввод текста вручную, в процедуру "either error? try", и предупредил пользователя приятным сообщением, если он ввёл что-либо, кроме правильной даты:

sdt: field to-string sd [
    either error? try [to-date sdt/text] [
        alert "Improper date format."
    ] [
        db/text: to-string ((to-date edt/text) - (to-date sdt/text))
        show db
    ]
]

edt: field to-string ed [
    either error? try [to-date edt/text] [
        alert "Improper date format."
    ] [
        db/text: to-string ((to-date edt/text) - (to-date sdt/text))
        show db
    ]
]

Я также добавил процедуру проверки ошибок в текстовое поле "db" на случай, если пользователь ввёл что-то, отличное от допустимого количества дней:

db: field "0" [
    either error? try [to-integer db/text] [
        alert "Please enter a number."
    ] [
        edt/text: to-string (
            (to-date sdt/text) + (to-integer db/text)
        )
    ]
    show edt
]

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

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

REBOL [title: "Days Between"]

sd: ed: now/date    
view layout [
    btn "Select Start Date" [
        sd: request-date 
        sdt/text: to-string sd
        show sdt 
        db/text: to-string ((to-date edt/text) - sd)
        show db
    ]
    sdt: field to-string sd [
        either error? try [to-date sdt/text] [
            alert "Improper date format."
        ] [
            db/text: to-string ((to-date edt/text) - (to-date sdt/text))
            show db
        ]
    ]
    btn "Select End Date" [
        ed: request-date
        edt/text: to-string ed 
        show edt 
        db/text: to-string (ed - (to-date sdt/text)) 
        show db
    ]
    edt: field to-string ed [
        either error? try [to-date edt/text] [
            alert "Improper date format."
        ] [
            db/text: to-string ((to-date edt/text) - (to-date sdt/text))
            show db
        ]
    ]
    h1 "Days Between:"
    db: field "0" [
        either error? try [to-integer db/text] [
            alert "Please enter a number."
        ] [
            edt/text: to-string (
                (to-date sdt/text) + (to-integer db/text)
            )
        ]
        show edt
    ]
]

Я упаковал этот сценарий как исполняемую программу, используя XpackerX, и разослал его всем учителям. Мы пользуемся им каждый день. (... Конечно, я всё ещё использую командную строку REBOL для вычисления даты :)

22.4 Случай: простой поиск

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

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

  1. Получить список каталогов всех элементов в данной начальной папке.
  2. Для каждого элемента в списке, если элемент является файлом, прочитать/просканировать его, чтобы увидеть, содержит ли он заданный текст для поиска.
  3. Для каждого элемента в списке, если элемент является папкой, войти в эту папку и повторить шаги 1-3 (я должен включить шаг 3 в шаг 3, если я хочу сделать то же самое для каждой подпапки, иначе процесс будет остановиться на 1 подпапке - очень важно!). Когда закончим, вернёмся в родительскую папку.

Шаг 1 прост в коде REBOL:

; определяем начальную папку:
    current-folder:  %.\
; прочтите список файлов и каталогов:
    read current-folder

Шаг 2 не намного сложнее:

; определяем текст для поиска:
phrase: "the"

; для каждого элемента в списке каталогов: 
foreach item (read current-folder) [
    ; если элемент является файлом:
    if not dir? item [
        ; читаем/сканируем файл по заданной фразе:
        if find (read to-file item) phrase [
            ; отобразим путь/имя файла, в котором найден искомый текст:
            print rejoin [{"} phrase {" found in:  } what-dir item]
        ]
    ]
]

Шаг 3 является рекурсивным - действия на шаге 3 включают выполнение действий на шаге 3. Такие операции рекурсии обычно требуют создания функции, которая содержит желаемые действия, включая вызов самой функции, в которой содержатся эти действия. Вот новый код, необходимый для шага 3 - обратите внимание, что функция называется "recurse" (рекурсивная) и что эта функция "recurse" вызывается в теле этой рекурсивной функции:

; Создаём функцию recurse:
recurse: func [current-folder] [
    ; для каждого элемента в списке каталогов:
    foreach item (read current-folder) [ 
        ; если элемент является папкой:
        if dir? item [
            ; переходим в эту папку: 
            change-dir item
            ; и повторите все шаги функции: 
            recurse %.\
            ; вернуться в родительский каталог, когда подпапок 
            ; больше нет:
            change-dir %..\
        ] 
    ]
]

При тестировании функции я обнаружил, что некоторые системные файлы не читаются. Произошла ошибка чтения. Устраним эту ошибку, добавив немного кода "if error? try []":

foreach item (read current-folder) [ 
    if not dir? item [  if error? try [
        if find (read to-file item) phrase [
            print rejoin [{"} phrase {" found in:  } what-dir item]
        ]] [print rejoin ["error reading " item]]
    ]
]

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

REBOL [title: "Simple Search"]

phrase: request-text/title/default "Text to Find:" "the"
start-folder: request-dir/title "Folder to Start In:"
change-dir start-folder
found-list: ""

recurse: func [current-folder] [ 
    foreach item (read current-folder) [ 
        if not dir? item [  if error? try [
            if find (read to-file item) phrase [
                print rejoin [{"} phrase {" found in:  } what-dir item]
                found-list: rejoin [found-list newline what-dir item]
            ]] [print rejoin ["error reading " item]]
        ]
    ]
    foreach item (read current-folder) [ 
        if dir? item [
            change-dir item 
            recurse %.\
            change-dir %..\
        ] 
    ]
]

print rejoin [{SEARCHING for "} phrase {" in } start-folder "...^/"]
recurse %.\
print "^/DONE^/"
editor found-list
halt

Затем я хотел, чтобы на моих веб-сайтах работала CGI-версия. Мне нужно будет ввести текст для поиска и начальную папку, используя HTML-форму:

print [<CENTER><TABLE><TR><TD>]
print [<FORM ACTION="./search.cgi">]
print ["Text to search for:" <BR> 
    <INPUT TYPE="TEXT" NAME="phrase"><BR><BR>]
print ["Folder to search in:" <BR> 
    <INPUT TYPE="TEXT" NAME="folder" VALUE="../yourfolder/" ><BR><BR>]
print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">]
print [</FORM>]
print [</TD></TR></TABLE></CENTER>]

Чтобы эта работа работала на веб-сайте, мне нужно включить все стандартные заголовки CGI и декодировать отправленные данные (этот стандартный формат кода скопирован из предыдущих примеров CGI в этом руководстве):

#! /home/yourpath/public_html/rebol/rebol -cs
REBOL []
print "content-type: text/html^/"
print [<HTML><HEAD><TITLE>"Search"</TITLE></HEAD><BODY>]
; print read %template_header.html

submitted: decode-cgi system/options/cgi/query-string

Вот финальная версия CGI:

#! /home/yourpath/public_html/rebol/rebol -cs
REBOL []
print "content-type: text/html^/"
print [<HTML><HEAD><TITLE>"Search"</TITLE></HEAD><BODY>]
; print read %template_header.html

submitted: decode-cgi system/options/cgi/query-string

if not empty? submitted [
    phrase: submitted/2
    start-folder: to-file submitted/4
    change-dir start-folder
    found-list: ""

    recurse: func [current-folder] [ 
        foreach item (read current-folder) [ 
            if not dir? item [  if error? try [
                if find (read to-file item) phrase [
                    print rejoin [{"} phrase {" found in:  }
                        what-dir item {<BR>}]
                    found-list: rejoin [found-list newline 
                        what-dir item]
                ]] [print rejoin ["error reading " item]]
            ]
        ]
        foreach item (read current-folder) [ 
            if dir? item [
                change-dir item 
                recurse %.\
                change-dir %..\
            ] 
        ] 
    ]

    print rejoin [{SEARCHING for "} phrase {" in } 
        start-folder {<BR><BR>}]
    recurse %.\
    print "<BR>DONE <BR>"
    ; save %found.txt found-list
    ; print read %template_footer.html
    quit
]

print [<CENTER><TABLE><TR><TD>]
print [<FORM ACTION="./search.cgi">]
print ["Text to search for:" <BR> 
    <INPUT TYPE="TEXT" NAME="phrase"><BR><BR>]
print ["Folder to search in:" <BR> 
    <INPUT TYPE="TEXT" NAME="folder" VALUE="../yourfolder/" ><BR><BR>]
print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">]
print [</FORM>]
print [</TD></TR></TABLE></CENTER>]
; print read %template_footer.html

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

22.5 Случай: простое приложение-калькулятор

Далее приводится краткий пример того, как создать небольшое приложение-калькулятор на REBOL. Это не столько реальный бизнес-пример - просто кажется, что создание приложения-калькулятора с графическим интерфейсом пользователя является обязательным клише среди учебных пособий по компьютерному программированию. Фактически, такой калькулятор можно легко дополнить специальными возможностями, которые, например, делают его очень полезным для агентов по недвижимости для расчёта ставок по ипотечным кредитам или для любых персональных вычислений, которые владельцу бизнеса может потребоваться регулярно выполнять.

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

  1. Должна быть область отображения для отображения числовых цифр по мере их ввода, а также результатов вычислений. Для этого дисплея отлично подойдёт простое текстовое поле графического интерфейса.
  2. Должны быть кнопки графического интерфейса для ввода числовых цифр и десятичной точки, а также кнопки для математических операторов и кнопка для выполнения вычислений (знак "="). Каждая из этих трёх категорий кнопок обычно выполняет одни и те же типы действий, поэтому я создам их как отдельные стили графического интерфейса, каждый с общими блоками действий.

Этого было достаточно, чтобы приступить к написанию реального кода графического интерфейса REBOL. Я играл с различными размерами/цветами окон, кнопок и шрифтов, пока макет не стал приемлемым. Вот что я придумал, используя приведённый выше псевдокод:

view center-face layout/tight [
    size 300x350 space 0x0 across   ; основные размеры окна и 
                                    ; расстояния между элементами
    basic window sizing/spacing
    display: field 300x50 font-size 28 "0" return ; дисплей
    style butn button 100x50 [
        ; add the action code here for number buttons
    ]
    style eval button 100x50 brown font-size 13 [
        ; add the action code here for operator buttons
    ]
    butn "1"  butn "2"  butn "3"  return   ; расставим кнопки
    butn "4"  butn "5"  butn "6"  return   ; в окне программы
    butn "7"  butn "8"  butn "9"  return 
    butn "0"  butn "."  eval "+" return
    eval "-" eval "*" eval "/" return
    button 300x50 gray font-size 16 "=" [
        ; add the action code here for "=" sign button
    ]
]

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

  1. Пользователь должен иметь возможность вводить числа, которые длиннее одной цифры, поэтому каждый раз, когда нажимается цифровая кнопка, эта цифровая цифра должна добавляться к цифрам на дисплее. Я использую "rejoin" для построения отображаемого числа, а затем я устанавливаю переменную для хранения этого числа каждый раз, когда нажимается новая цифра.
  2. В приведённом выше коде графического интерфейса пользователя я начал с "0" в поле отображения. Это нужно для того, чтобы понимать, когда нужно очистить поле перед отображением любых других чисел.

Это всё достаточно просто сделать в коде REBOL:

if display/text = "0" [display/text: ""]    ; очищаем дисплей "0"
display/text: rejoin [display/text value]   ; изобразим цифру #
show display
cur-val: display/text   ; используем переменную для сохранения 
                        ; отображаемого #

Теперь мне нужно подумать о том, что должно произойти при нажатии кнопок оператора:

  1. Мне нужно назначить переменную для сохранения числа, введённого в данный момент на дисплее графического интерфейса пользователя (этот номер уже временно сохранён в переменной "cur-val" выше).
  2. Очищаем дисплей, чтобы подготовить его к выводу нового числа.
  3. Назначьте переменную для сохранения выбранного оператора.

Всё очень просто - на самом деле, в реальном коде REBOL это проще, чем в псевдокоде:

prev-val: cur-val  ; сохраним отображаемое число # в переменной
display/text: "" show display  ; очистим дисплей
cur-eval: value  ; сохраним выбранный оператор в переменной

Наконец, мне нужно подумать о том, что происходит при нажатии кнопки "=":

  1. Вычисление должно быть оценено с использованием первого введённого числа (переменная "prev-val" выше), введённого оператора (переменная "cur-eval") и второго введённого числа ("cur-val").
  2. Область отображения необходимо обновить, чтобы отобразить значение этого вычисления.

Самый простой способ построить вычисление, который я мог придумать, - это использовать функцию "rejoin" для построения строки, представляющей первое введённое число, введённый оператор и второе введённое число. Затем я мог бы оценить это вычисление, просто используя функцию "do" для построенной строки:

cur-val: do rejoin [prev-val " " cur-eval " " cur-val]
display/text: cur-val
show display

Всё было очень просто. Вот код, который у нас есть:

view center-face layout/tight [
    size 300x350 space 0x0 across
    display: field 300x50 font-size 28 "0" return
    style butn button 100x50 [
        if display/text = "0" [display/text: ""]  ; удаляем "0"
        display/text: rejoin [display/text value]  ; добавляем #
        show display
        cur-val: display/text  ; use a variable to save the displayed #
    ]
    style eval button 100x50 brown font-size 13 [
        prev-val: cur-val
        display/text: "" show display
        cur-eval: value
    ]
    butn "1"  butn "2"  butn "3"  return
    butn "4"  butn "5"  butn "6"  return
    butn "7"  butn "8"  butn "9"  return 
    butn "0"  butn "."  eval "+" return
    eval "-" eval "*" eval "/" return
    button 300x50 gray font-size 16 "=" [
        cur-val: do rejoin [prev-val " " cur-eval " " cur-val]
        display/text: cur-val
        show display
    ]
]

Немного протестировав код, я обнаружил ошибку. Каждый раз, когда первое вычисление завершается, любые дополнительные введенные цифры добавляются к итоговой сумме, отображаемой из первого вычисления. Это происходит в этой строке кода в определении цифровых кнопок (стиль "butn"):

display/text: rejoin [display/text value]

Эту проблему легко решить, установив флаговую переменную при нажатии кнопки "=":

display-flag: true

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

if display-flag = true [display/text: "" display-flag: false]

Это исправляет первую ошибку. Ещё немного протестировав программу, я обнаружил ещё одну небольшую ошибку. Калькулятор выйдет из строя с ошибкой, если по знаку "=" или любой из кнопок оператора щёлкнуть до того, как числовые цифры будут введены правильно. Это было легко исправить, просто установив некоторые переменные по умолчанию в начале программы - это принципиально хорошая практика в любом виде программирования:

prev-val: cur-val: 0 cur-eval: "+" display-flag: false

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

cur-val: do rejoin [prev-val " " cur-eval " " cur-val]

Переменная "cur-val" обновлялась каждый раз при нажатии кнопки "=" вне зависимости от того, был введено новое число или оператор. Чтобы устранить эту ошибку, я просто использовал переменную "display-flag", созданную ранее, чтобы проверить, отображается ли общая сумма. Я заключил весь код действий, выполняемый при щелчке по знаку "=", в условное "if" и выполнял эти действия только в том случае, если флаг был сброшен (только если не отображалось общее количество):

if display-flag <> true [ ... ]

Наконец, была ошибка, о которой я думал с самого начала: если пользователь попытается разделить на 0, программа выйдет из строя. Чтобы справиться с этой ситуацией, я добавил следующую условную проверку в приведённый выше код:

if ((cur-eval = "/") and (cur-val = "0")) [
    alert "Division by 0 is not allowed." break
]

На этом этапе программа казалась достаточно свободной от ошибок, поэтому я решил добавить дополнительную функцию, которая показалась мне полезной при тестировании кода. Мне нужна была текущая распечатка всех выполненных расчётов, как на бумажной ленте на традиционных калькуляторах. Добавить эту функцию было максимально просто. В начале программы я добавил строку "print 0", а затем добавил следующие изменения строки к кнопке "=":

prin rejoin [prev-val " " cur-eval " " cur-val " = "]
print display/text: cur-val: do rejoin [
    prev-val " " cur-eval " " cur-val
]

Вот окончательный код программы калькулятора:

REBOL [title: "Calculator"]

prev-val: cur-val: 0 cur-eval: "+" display-flag: false
print "0"
view center-face layout/tight [
    size 300x350 space 0x0 across
    display: field 300x50 font-size 28 "0" return
    style butn button 100x50  [
        if display-flag = true [display/text: "" display-flag: false]
        if display/text = "0" [display/text: ""]
        display/text: rejoin [display/text value] 
        show display
        cur-val: display/text
    ]
    style eval button 100x50 brown font-size 13 [
        prev-val: cur-val
        display/text: "" show display
        cur-eval: value
    ]
    butn "1"  butn "2"  butn "3"  return
    butn "4"  butn "5"  butn "6"  return
    butn "7"  butn "8"  butn "9"  return 
    butn "0"  butn "."  eval "+" return
    eval "-" eval "*" eval "/" return
    button 300x50 gray font-size 16 "=" [
        if display-flag <> true [ 
            if ((cur-eval = "/") and (cur-val = "0")) [
                alert "Division by 0 is not allowed." break
            ]
            prin rejoin [prev-val " " cur-eval " " cur-val " = "]
            print display/text: cur-val: do rejoin [
                prev-val " " cur-eval " " cur-val
            ]
            show display
            display-flag: true
        ]
    ]
]

22.6 Случай: музыкальный генератор фона(исполнитель аккордового аккомпанемента)

В моем бизнесе, посвящённом урокам музыки, мы обучаем навыкам импровизации ("джем-сейшна"). Для того, чтобы начинающие студенты могли практиковаться, я создал простую программу, которую они могли использовать, чтобы слышать и подыгрывать любой заданной последовательности аккордов в любом заданном темпе. Создать такую ​​программу с REBOL было легко. Разработка приложения для воспроизведения предварительно записанных аккордов из заданного текстового списка заняло менее получаса.

Вот основная схема, которую я придумал для начала (для этого примера требуются очень базовые знания нотации аккордов):

  1. Запишем волновые файлы мажорных, минорных, доминантных 7-х, полууменьшенных, уменьшенных 7-х, минорных 7-х и мажорных 7-х аккордов на всех 12 основных нотах (A, A#, B, C, C#, D, D#, E, F, F#, G и G#), а также несколько других часто используемых аккордов. Все записи должны были состоять из коротких блок-аккордов, одинаковой продолжительности и громкости.
  2. Сожмём и вставим волновые файлы с помощью средства внедрения двоичных ресурсов, описанного ранее в этом тексте.
  3. Загрузим каждый звук в память и присвойте каждому переменную метку.
  4. Создадим графический интерфейс с текстовыми полями для воспроизведения аккордов и темпа. Добавим кнопки "воспроизведение" и "стоп" для управления действием.
  5. Когда нажата кнопка "play", воспроизведём данные волны для каждого аккорда в данной прогрессии, используя заданный временной интервал. Потребуется некоторый многозадачный код, позволяющий останавливать зацикливание аккордов.
  6. Добавим несколько кнопок для сохранения и загрузки последовательностей аккордов, а также кнопку для вызова отображения помощи/инструкций.

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

Для шага 2 в схеме я использовал вариант программы встраивания бинарных ресурсов, описанный ранее в этом тексте, для циклического просмотра файлов в каталоге:

REBOL []

system/options/binary-base: 64
sounds: copy []
foreach file load %./  [
    print file
    uncompressed: read/binary file
    compressed: compress to-string uncompressed
    if ((length? uncompressed) > 5000) [
        append sounds compressed
    ]
]
editor sounds

Он предоставил один огромный блок данных, содержащий каждый из этих звуков во встроенном формате. Вывод этих данных аккорда можно увидеть на http://musiclessonz.com/rebol_tutorial/backup.r (http://rockfactory.us/files/chords.r):

64#{
eJxEd2VUW833ddyIE8fdHYoVCi1S2mJ1F+ruTvvU3d3dC1RpC7S4ExwSJFgSSEhC
EmIQIvfl9//yrr32mTMz58vcNXfP2XOTEhIQx0CgRbEL4zds32dPBIFA4EnEZYFA...

Для шага 3 я поместил код "load to-binary decompress" (найденный ранее в этом тексте) перед каждым фрагментом данных встроенного звукового файла (чтобы распаковать данные и загрузить звук в память для быстрого использования). Я дал каждому аккорду соответствующую метку аккорда (A major (ля мажор), Bd major (си мажор), C minor (до минор), G7 (соль 7) и т.д.). При этом я решил использовать все плоские символы для любых корневых нот, которые были случайными (например, F # = Gb, C # = Db и т.д. (Без диеза)). Вот как выглядел код для аккордов ля мажор и си минор:

a: load to-binary decompress 64#{
eJxEd2VUW833ddyIE8fdHYoVCi1S2mJ1F+ruTvvU3d3dC1RpC7S4ExwSJFgSSEhC
EmIQIvfl9//yrr32mTMz58vcNXfP2XOTEhIQx0C ...

bbm: load to-binary decompress 64#{
eJwstwVcU9//P757d9eMbQwYMWB0d4cgraCogIgdKHY38lb0rZjYXW8VOxGbDukc
3TVqCWPdv32+///j9Tj3nnvuOa9+Pc85iQtjYkr ...

Вот полный список созданных мною ярлыков аккордов (символ подчёркивания - это ярлык, который я присвоил записанному мною безмолвному звуку, который будет использоваться для ритма отдыха). Я вручную пометил каждый аккорд следующими метками (используя средства поиска, копирования и вставки в моем текстовом редакторе, что заняло около десяти минут):

a bb b c db d eb e f gb g ab 
am bbm bm cm dbm dm ebm em fm gbm gm abm 
a7 bb7 b7 c7 db7 d7 eb7 e7 f7 gb7 g7 ab7 
adim7 bbdim7 bdim7 cdim7 dbdim7 ddim7 
ebdim7 edim7 fdim7 gbdim7 gdim7 abdim7
am7b5 bbm7b5 bm7b5 cm7b5 dbm7b5 dm7b5 
ebm7b5 em7b5 fm7b5 gbm7b5 gm7b5 abm7b5 
am7 bbm7 bm7 cm7 dbm7 dm7 ebm7 em7 fm7 gbm7 gm7 abm7
amaj7 bbmaj7 bmaj7 cmaj7 dbmaj7 dmaj7
ebmaj7 emaj7 fmaj7 gbmaj7 gmaj7 abmaj7
_

Шаг 4 в схеме просто потребовал создания следующего простого графического интерфейса. Он состоит из нескольких ярлыков, текстовой области для хранения введённых пользователем аккордов, текстового поля для темпа и пары кнопок для остановки и запуска музыкального действия. Я также решил добавить кнопки из шага 6 - я даже поместил сюда весь этот код - всё, что требовалось, - это сохранить и загрузить содержимое текстовой области. Простой:

view center-face layout [
    across
    h2 "Chords:"
    tab
    chords: area 392x300 trim {}
    return
    h2 "Delay:"
    tab
    tempo: field 50 "0.35" text "(seconds)"
    tabs 40 tab
    btn "PLAY" []
    btn "STOP" []
    btn "Save" [save to-file request-file/save chords/text]
    btn "Load" [chords/text: load read to-file request-file show chords]
    btn "HELP" [
        alert {}
    ]
]

Теперь всё, что осталось, это шаг 5. Я начал с загрузки введённого пользователем списка аккордов в блок:

sounds: to-block chords/text

Я также дал ярлык для темпа и убедился, что он обрабатывается как десятичное значение:

the-tempo: to-decimal tempo/text

Я взял функцию воспроизведения звука, которую вы видели ранее, и использовал её код внутри цикла foreach, который воспроизводил каждый из звуков из предоставленного пользователем списка (теперь в блоке "sounds" (звуки)). Поскольку эти метки аккордов теперь относятся к реальным фрагментам звуковых данных, которые могут быть вставлены и воспроизведены непосредственно через звуковой порт, это было просто:

wait 0
sound-port: open sound://
foreach sound sounds [
    do rejoin ["insert sound-port " reduce [sound]]
    wait sound-port
    wait the-tempo 
]

Я заключил вышеупомянутый цикл foreach в цикл forever, потому что я хотел, чтобы последовательность аккордов повторялась непрерывно. Чтобы остановить музыку, я сначала подумал, что мне понадобится многозадачный код, но оказалось, что это было проще, чем ожидалось. Все, что я сделал, - это создал переменную флага (слово "play"), которой было присвоено значение false при нажатии кнопки остановки графического интерфейса. Внутри вышеупомянутого цикла foreach я проверил, установлено ли для переменной play значение false, и если да, то вышел из цикла. Затем кнопка остановки просто закрывала звуковой порт после установки флага переменной в значение false. Ниже приведён полный код кнопок PLAY и STOP в графическом интерфейсе. Простой :)

btn "PLAY" [
    play: true
    the-tempo: to-decimal tempo/text
    sounds: to-block chords/text
    wait 0
    sound-port: open sound://
    forever [
        foreach sound sounds [
            if play = false [break]
            do rejoin ["insert sound-port " reduce [sound]]
            wait sound-port
            wait the-tempo 
        ]
        if play = false [break]
    ]
]
btn "STOP" [
    play: false
    close sound-port
]

Чтобы завершить программу, я добавил текст инструкции к предупреждению, которое появляется при нажатии кнопки справки, и я добавил пример последовательности аккордов, которая появляется в текстовой области по умолчанию (аккорды в "Hotel California"). При тестировании программы я понял, что если графический интерфейс был закрыт до того, как музыка была остановлена, воспроизведение продолжится без возможности его остановить. Диспетчер задач операционной системы был единственным способом остановить музыку на этом этапе. Чтобы исправить эту ошибку, я добавил код для перехвата кнопки закрытия и остановки музыки (установите флаг воспроизведения в значение false и закройте звуковой порт) вместе с запросом "действительно закрыть программу?". Вы видели такой код в нескольких предыдущих примерах.

Вот последняя программа (сокращенная версия, БЕЗ данных об аккордах, необходимых для воспроизведения примера):

REBOL [title: "Chord Accompaniment Player"]

play: false
insert-event-func [
    either event/type = 'close [
        if play = true [play: false close sound-port]
        really: request "Really close the program?"
        if really = true [quit]
    ][
        event
    ]
]

{
    .
    .
    .

    все данные аккордов идут тут

    bm: load to-binary decompress 64#{
    eJw8dgdUU0/w7k0vJNTQe++9SpUqCogIiiKgIIq9YAdU7L397NgVRAQEpEnvvfca
    CCUhQEhCIJUk9/E/7/3f2bN7z92d...
    } 

    .
    .
    .

}

view center-face layout [
    across
    h2 "Chords:"
    tab
    chords: area 392x300 trim {
        bm bm bm bm
        gb7 gb7 gb7 gb7
        a a a a 
        e e e e
        g g g g
        d d d d 
        em em em em
        gb7 gb7 gb7 gb7
        g g g g
        d d d d
        gb7 gb7 gb7 gb7
        bm bm bm bm
        g g g g
        d d d d
        em em em em
        gb7 gb7 gb7 gb7
    }
    return
    h2 "Delay:"
    tab
    tempo: field 50 "0.35" text "(seconds)"
    tabs 40 tab
    btn "PLAY" [
        play: true
        the-tempo: to-decimal tempo/text
        sounds: to-block chords/text
        wait 0
        sound-port: open sound://
        forever [
            foreach sound sounds [
                if play = false [break]
                do rejoin ["insert sound-port " reduce [sound]]
                wait sound-port
                wait the-tempo 
            ]
            if play = false [break]
        ]
    ]
    btn "STOP" [
        play: false
        close sound-port
    ]
    btn "Save" [save to-file request-file/save chords/text]
    btn "Load" [chords/text: load read to-file request-file show chords]
    btn "HELP" [
        alert {
            This program plays chord progressions.  Simply type in
            the names of the chords that you'd like played, with a
            space between each chord.  For silence, use the
            underscore ("_") character.  Set the tempo by entering a 
            delay time (in fractions of second) to be paused between
            each chord.  Click the start button to play from the 
            beginning, and the stop button to end.  Pressing start
            again always begins at the first chord in the 
            progression.  The save and load buttons allow you to 
            store to the hard drive any songs you've created.
            Chord types allowed are major triad (no chord symbol - 
            just a root note), minor triad ("m"), dominant 7th 
            ("7"), major 7th ("maj7"), minor 7th ("m7"), diminished
            7th ("dim7"), and half diminished 7th ("m7b5").
            *** ALL ROOT NOTES ARE LABELED WITH FLATS (NO SHARPS)
            F# = Gb, C# = Db, etc...
         }
    ]
]

Полную версию для воспроизведения с полным набором данных встроенных аккордов можно найти на http://musiclessonz.com/rebol_tutorial/backup.r (http://rockfactory.us/files/chords.r).

Вот несколько примеров аккордов для загрузки. Все аккорды:

a bb b c db d eb e f gb g ab 
am bbm bm cm dbm dm ebm em fm gbm gm abm 
a7 bb7 b7 c7 db7 d7 eb7 e7 f7 gb7 g7 ab7 
adim7 bbdim7 bdim7 cdim7 dbdim7 ddim7 
ebdim7 edim7 fdim7 gbdim7 gdim7 abdim7
am7b5 bbm7b5 bm7b5 cm7b5 dbm7b5 dm7b5 
ebm7b5 em7b5 fm7b5 gbm7b5 gm7b5 abm7b5 
am7 bbm7 bm7 cm7 dbm7 dm7 ebm7 em7 fm7 gbm7 gm7 abm7
amaj7 bbmaj7 bmaj7 cmaj7 dbmaj7 dmaj7
ebmaj7 emaj7 fmaj7 gbmaj7 gmaj7 abmaj7
_ _ _ _

Brown Eyed Girl:

g g c c g g d d7
g g c c g g d d7
g g c c g g d d7
g g c c g g d d7
g g c c g g d d7
g g c c g g d d7
c c d d  g g em em c c d d

22.7 Случай: FTP Tool

Я часто использую встроенный текстовый редактор REBOL для редактирования файлов на моем веб-сервере:

editor ftp://user:pass@site.com/path/public_html/file.ext

Всё это тематическое исследование развилось из того, что я использовал эту функцию. Я решил создать небольшой скрипт, чтобы ускорить описанный выше процесс. За счёт жёсткого кодирования всей моей информации FTP непосредственно в текстовое поле графического интерфейса всё, что мне нужно сделать для редактирования файла на моем сервере, - это запустить сценарий и изменить конкретное имя файла:

view layout [
    p: field "ftp://user:pass@site.com/path/public_html/file.ext"
    btn "Edit" [
        editor to-url p/text
    ]
]

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

  1. Добавьте в сценарий текстовый список. Вместо того, чтобы вводить URL-адрес имени файла в текстовое поле, введите папку. Когда я сейчас отправлю URL-адрес, сценарий прочитает содержимое этой папки и отобразит каждый элемент в текстовом списке.
  2. Когда я щёлкаю элемент в текстовом списке, сценарий соединяет выбранное имя файла с данной папкой и открывает редактор по этому URL-адресу. Вот код - как всегда с REBOL, он предельно прост:
view layout [
    p: field "ftp://user:pass@site.com/path/public_html/" [
        f/data: read to-url value 
        show f
    ]
    f: text-list [
        editor to-url (join p/text value)
    ]
]

Это сработало хорошо, но после того, как я использовал его несколько раз, я решил, что мне все ещё нужна возможность ввести определённое имя файла, чтобы он сразу же открылся. Вот мой мыслительный процесс:

  1. Добавьте условие "either" (либо).
  2. Если введённый URL-адрес является папкой, сделайте, как в предыдущем сценарии (я могу использовать функцию dir? Для выполнения этой проверки).
  3. В противном случае отредактируйте введённый URL напрямую.

Вот код:

view layout [
    p: field "ftp://user:pass@site.com/path/public_html/file.ext" [
        either dir? to-url value [
            f/data: read to-url value 
            show f
        ][
            editor to-url value
        ]
    ]
    f: text-list [
        editor to-url join p/text value
    ]
]

У меня иногда возникали проблемы с функцией "dir?", поэтому я изменил эту строку, чтобы читать:

either (to-string last value) = "/" [

В нынешнем виде приведённый выше сценарий представляет собой полезный редактор FTP. Чтобы создать новый файл, всё, что мне нужно сделать, это ввести его путь и имя файла в текстовое поле. Встроенный текстовый редактор REBOL автоматически создаёт файл, если он ещё не существует. Поскольку я чаще использовал этот сценарий, я хотел иметь возможность автоматически перемещаться по папкам (без необходимости вводить имена путей/файлов вообще). Вот мои мысли:

  1. Если пользователь щёлкает папку, добавьте имя папки к текущей папке, отображаемой в текстовом поле, затем перечитайте и отобразите содержимое новой папки в текстовом списке. Это эффективно меняет каталоги.
  2. Чтобы перейти вверх по папке (вернуться к предыдущей папке), добавим "../" к содержимому каталога, считываемому из папки, отображаемой в данный момент в текстовом поле, и отобразите эти данные в текстовом списке. Кроме того, отсортируем данные, чтобы "../" отображалось вверху списка.
  3. Когда пользователь щёлкает "../", удалите последнюю часть введённого в данный момент пути (все обратно до предыдущего символа косой черты), обновите текстовое поле и прочтите/отобразите файлы в этой папке в текстовом списке. Например, если в настоящее время отображается путь "ftp://user:pass@site.com/path/public_html/folder/", удалите часть "folder/", обновите текстовое поле на "ftp://user:pass@site.com/path/public_html/"и прочитать/отобразить содержимое этой папки.

Шаг 1 прост. Просто присоединитесь к текущей папке в текстовом поле со значением, выбранным из текстового списка:

p/text: rejoin [p/text value]
show p

Шаг 2 так же прост. Добавьте "../" к строке кода, которая читает и отображает файлы в текущей папке ("f/data: read to-url value"), и отсортируйте её:

f/data: sort append (read to-url p/text) "../"
show f

Для шага 3 нам нужно найти второй и последний символ "/" в текущем отображаемом пути и удалить всё, что находится после него. Для этого мы начнём поиск в обратном направлении от второго до последнего символа (чтобы исключить последний символ "/" в папке, потому что мы хотим, чтобы второй был последним символом "/"). Это просто - начните поиск в обратном направлении с ((length? p/text) - 1). Я решил использовать цикл "for", начиная с этой позиции индекса и с уменьшением на 1. Каждый раз в цикле выбирайте символ в текущей позиции индекса, и, если это "/", стирайте все символы после этой позиции индекса. (используйте функцию "clear" (очистить), чтобы удалить всё в (текущий индекс + 1)). Затем обновите текстовое поле, указав новый путь, прочтите содержимое каталога и отобразите его в текстовом списке, как в шагах 1 и 2 выше:

for i ((length? p/text) - 1) 1 -1 [
    if (to-string (pick p/text i)) = "/" [
        clear at p/text (i + 1)
        show p
        f/data: sort append read to-url p/text "../"
        show f
        break ; выйти из цикла, когда будет найден предпоследний "/"
    ]
]

Когда я посмотрел на этот код, я понял, что более простой способ сделать то же самое - использовать следующий код. Сначала очистите символ "/" в конце текста, затем очистите все, что находится после следующего символа "/" ("find /last" выполняет поиск в обратном направлении от конца):

clear at p/text (index? find/last p/text "/")
clear at p/text ((index? find/last p/text "/") + 1)
show p
f/data: sort append read to-url p/text "../"
show f

Я добавил эти изменения в текущий скрипт:

view layout [
    p: field "ftp://user:pass@site.com/path/" [
        either dir? to-url value [
            f/data: sort append (read to-url p/text) "../"
            show f
        ][
            editor to-url value
        ]
    ]
    f: text-list [
        ; если пользователь выбирает "../" запустить код из шага 3:
        either (to-string value) = "../" [
            for i ((length? p/text) - 1) 1 -1 [
                if (to-string (pick p/text i)) = "/" [
                    clear at p/text (i + 1) show p
                    f/data: sort append read to-url p/text "../" show f
                    break
                ]
            ]
        ][
            ; если пользователь выбирает папку, запустите код из 
            ; шагов 1 и 2:
            either (to-string last value) = "/" [
                p/text: rejoin [p/text value] show p
                f/data: sort append read to-url p/text "../" show f
            ][
                editor to-url rejoin [p/text value]
            ]
        ]
    ]
]

Теперь это полезный редактор FTP! Мы можем просматривать любую папку и редактировать/сохранять любой файл, просто щёлкая элементы мышью. Я, конечно, мог бы остановиться на этом, но по мере того, как я использовал программу, появлялись всё более желаемые функции. Далее я решил добавить функцию просмотра изображений. Мыслительный процесс прост: если выбранный файл является изображением (jpg, png, gif или bmp), откройте новое окно графического интерфейса пользователя и загрузите/отобразите изображение. В противном случае откройте файл в текстовом редакторе, как и раньше. Это просто:

either find [%.jpg %.png %.gif %.bmp] suffix? value [
    view/new layout [image load to-url rejoin [p/text value]]
][
    editor to-url rejoin [p/text value]
]

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

if ((request "Edit/view this file?") = true) [(делаем код выше)]

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

  1. Используем текстовый запросчик, чтобы запросить у пользователя информацию о FTP. Я сохраню его в формате URL, как одну строку, точно так же, как она введена в текстовое поле графического интерфейса. Используем текущий URL-адрес FTP, введённый в текстовое поле, в качестве текста по умолчанию в инициаторе запроса.
  2. Чтобы избежать ошибки, остановимся на этом, если пользователь откажется от запрашивающей стороны (т.е. ничего не вводит).
  3. Используем запросчик файлов, чтобы запросить у пользователя текстовый файл для сохранения информации (по умолчанию "%ftp.cfg").
  4. Добавьте ещё одну проверку ошибок, чтобы убедиться, что пользователь действительно выбрал файл.
  5. Если файл не существует, создайте его, записав строку URL-адреса FTP в новый файл.
  6. Если файл существует, добавьте строку URL-адреса FTP к существующему файлу.
  7. Предупредить пользователя о завершении операции.

Как всегда в REBOL, каждый из этих шагов предельно прост.

btn "Save URL" [
    url: request-text/title/default "URL to save:" p/text
    if url = none [break]
    config-file: to-file request-file/file/save %/c/ftp.cfg
    if (url <> none) and (config-file <> %none) [
        if not exists? config-file [
            write/lines config-file ftp://user:pass@website.com/
        ]
        write/append/lines config-file to-url url
        alert "Saved"
    ]
]

Теперь мне нужна кнопка для загрузки сохранённых URL-адресов. Вот мыслительный процесс:

  1. Используем запросчик файла, чтобы пользователь мог выбрать файл конфигурации (по умолчанию "%ftp.cfg")
  2. Используем условие "either" (либо), чтобы проверить, существует ли файл.
  3. Если файл не существует, сообщим пользователю, что ему необходимо сначала сохранить некоторые URL-адреса в файл конфигурации.
  4. Если файл существует, попросите пользователя выбрать нужную информацию FTP из файла (одна строка URL-адреса из файла). Лёгкий способ сделать это - воспользоваться функцией "request-list". Я загружу каждую строку в файл конфигурации в блок (использую цикл foreach для чтения и добавления каждой строки в файле в новый блок), а затем отображу этот список с помощью функции request-list. Когда пользователь выбирает строку из списка, я копирую выбранную строку текста в текстовый список графического интерфейса пользователя (исходное текстовое поле в этой программе, содержащее информацию FTP).

Опять же, всё это сделать очень просто:

btn "Load URL" [
    config: to-file request-file/file %/c/ftp.cfg
    either exists? config [
        if (config <> %none) [
            my-urls: copy []
            foreach item read/lines config [append my-urls item]
            if error? try [
                p/text: copy request-list "Select a URL:" my-urls
            ] [break]
        ]
    ][
        alert "First, save some URLs to that file..."
    ]
    show p 
    focus p 
]

Я добавил функцию "focus" в конец приведённого выше кода кнопки, чтобы пользователь мог просто нажать клавишу [ENTER] для подключения к серверу после выбора URL-адреса из файла конфигурации. Имеет смысл ожидать, что некоторые пользователи будут иметь кнопки "Загрузить URL", "Сохранить URL" и "Подключиться", поэтому я также решил добавить в графический интерфейс отдельную кнопку "Подключиться". Поскольку нажатие на текстовое поле и нажатие на кнопку делают одно и то же, я создал функцию "connect" (подключение), так что этот код не нужно дублировать в блоке действий каждого из этих виджетов графического интерфейса. В этой функции я добавил проверку ошибок, чтобы программа не вылетела из строя, если пользователь вводит неверную информацию FTP:

connect: does [
    either (to-string last p/text) = "/" [
    if error? try [
            f/data: sort append read to-url p/text "../" show f
        ][
            alert "Not a valid FTP address, or the connection failed."
        ]
    ][
        editor to-url p/text
    ]
]

По мере тестирования кода я понял, что было бы намного лучше увеличить размер текстового списка и текстового поля, чтобы я мог просматривать весь URL-адрес FTP и перечисленные имена файлов/папок. 600x350 пикселей работает хорошо (подходит для экранов с низким разрешением, но достаточно большой, чтобы видеть полные пути к файлам). Вот так программа выглядит сейчас:

REBOL [title: "FTP Tool"]

connect: does [
    either (to-string last p/text) = "/" [
    if error? try [
            f/data: sort append read to-url p/text "../" show f
        ][
            alert "Not a valid FTP address, or the connection failed."
        ]
    ][
        editor to-url p/text
    ]
]
view center-face layout [
    p: field 600 "ftp://user:pass@website.com/" [connect]
    across
    btn "Connect" [connect]
    btn "Load URL" [
        config: to-file request-file/file %/c/ftp.cfg
        either exists? config [
            if (config <> %none) [
                my-urls: copy []
                foreach item read/lines config [append my-urls item]
                if error? try [
                    p/text: copy request-list "Select a URL:" my-urls
                ] [break]
            ]
        ][
            alert "First, save some URLs to that file..."
        ]
        show p focus p
    ]
    btn "Save URL" [
        url: request-text/title/default "URL to save:" p/text
        if url = none [break]
        config-file: to-file request-file/file/save %/c/ftp.cfg
        if (url <> none) and (config-file <> %none) [
            if not exists? config-file [
                write/lines config-file ftp://user:pass@website.com/
            ]
            write/append/lines config-file to-url url
            alert "Saved"
        ]
    ]
    below
    f: text-list 600x350 [
        either (to-string value) = "../" [
            for i ((length? p/text) - 1) 1 -1 [
                if (to-string (pick p/text i)) = "/" [
                    clear at p/text (i + 1) show p
                    f/data: sort append read to-url p/text "../" show f
                    break
                ]
            ]
        ][
            either (to-string last value) = "/" [
                p/text: rejoin [p/text value] show p
                f/data: sort append read to-url p/text "../" show f
            ][
                if ((request "Edit/view this file?") = true) [
                    either find [%.jpg %.png %.gif %.bmp] suffix? value [
                        view/new layout [
                            image load to-url join p/text value
                        ]
                    ][
                        editor to-url rejoin [p/text value]
                    ]
                ]
            ]
        ]
    ]
]

По мере того, как я всё больше использовал программу, я узнал несколько дополнительных важных функций, которые требовались. Мне нужно было уметь: выгружать/скачивать двоичные файлы, создавать новые папки, удалять, копировать, переименовывать, изменять разрешения и получать информацию о существующих файлах. Я просто добавил кнопки в существующий графический интерфейс, чтобы реализовать каждую из этих функций. Вот мой мыслительный процесс и код, который выполняет все эти функции:

Чтобы получить размер и дату любого выбранного файла, я просто использовал функции "size?" (размер?) и "modified?" (модифицированный?), встроенные в REBOL:

btn "Get Info" [
    p-file: to-url rejoin [p/text f/picked]
    alert rejoin ["Size: " size? p-file " Date: " modified? p-file]
]

Чтобы удалить существующий файл, я просто использую встроенную в REBOL функцию "delete" (удалить). Я добавил инициирование запроса, чтобы подтвердить, что пользователь действительно хочет удалить выбранный файл. Когда операция завершена, список каталогов обновляется, и пользователь получает уведомление с помощью предупреждающего сообщения:

btn "Delete" [
    p-file: to-url request-text/title/default "File to delete:"
        join p/text f/picked
    if ((confirm: request "Are you sure?") = true) [delete p-file]
    f/data: sort append read to-url p/text "../" show f
    if confirm = true [alert "File deleted"]
]

Переименовать файл так же просто, используя функцию "rename" (переименовать). Опять же, я просто добавил запрос на подтверждение и уведомление по завершении:

btn "Rename" [
    new-name: to-file request-text/title/default "New File Name:"
        to-string f/picked
    if ((confirm: request "Are you sure?") = true) [
        rename (to-url join p/text f/picked) new-name
    ]
    f/data: sort append read to-url p/text "../" show f
    if confirm = true [alert "File renamed"]
]

Копировать файлы на FTP-сервер так же просто, как копировать файлы на локальный жёсткий диск - просто прочтите и напишите:

btn "Copy" [
    new-name: to-url request-text/title/default "New Path:"
        (join p/text f/picked)
    if ((confirm: request "Are you sure?") = true) [
        write/binary new-name read/binary to-url join p/text f/picked
    ]
    f/data: sort append read to-url p/text "../" show f
    if confirm = true [alert "File copied"]
]

Создать новый файл так же просто, как записать файл с пустой строкой (""):

btn "New File" [
    p-file: to-url request-text/title/default "New File Name:"
        join p/text "ENTER-A-FILENAME.EXT"
    if ((confirm: request "Are you sure?") = true) [
        write p-file ""
    ]
    f/data: sort append read to-url p/text "../" show f
    if confirm = true [alert "Empty file created - click to edit."]
]

Создание новой папки на FTP-сервере также выполняется так же, как создание папки на жёстком диске. Просто используйте функцию make-dir:

btn "New Dir" [
    make-dir x: to-url request-text/title/default "New folder:" p/text
    alert "Folder created"
    p/text: x show p
    f/data: sort append read to-url p/text "../" show f
]

Загрузка двоичных файлов выполняется с помощью функций "read/binary" (чтение/двоичный) и "write/binary"(запись/двоичный). Я просто добавил здесь код, чтобы найти имя файла (отделить его от полного пути к выбранному файлу), и использовал запросчик, чтобы запросить предлагаемое имя файла для сохранения:

btn "Download" [
    file: request-text/title/default "File:" (join p/text f/picked)
    l-file: next to-string (find/last (to-string file) "/")
    save-as: request-text/title/default "Save as..." to-string l-file
    write/binary (to-file save-as) (read/binary to-url file)
    alert "Download Complete"
]

Выгрузка также осуществляется с помощью функций "read/binary" и "write/binary":

btn "Upload" [
    file: to-file request-file
    r-file: request-text/title/default "Save as..." 
        join p/text (to-string to-relative-file file)
    write/binary (to-url r-file) (read/binary file)
    f/data: sort append read to-url p/text "../" show f
    alert "Upload Complete"
]

Изменение прав доступа к файлу (т.е. чтение, запись и выполнение на серверах Unix/Linux) выполняется с помощью команды "write/binary/allow":

btn "Chmod" [
    p-file: to-url request-text/default rejoin [p/text f/picked]
    chmod: to-block request-text/title/default "Permissions:"
        "read write execute"
    write/binary/allow p-file (read/binary p-file) chmod
    alert "Permissions changed"
]

Я также создал кнопку справки для отображения некоторого текста, содержащегося в переменной "instructions" (инструкции):

btn-help [inform layout [backcolor white text bold as-is instructions]]

Вот окончательный код моего полнофункционального FTP-приложения. Это далеко не "editor ftp: // ..." :) Я регулярно использую эту программу (загружаемый файл Windows .exe доступен по адресу http://musiclessonz.com/rebol_tutorial/FTP_tool.exe):

REBOL [title: "FTP Tool"]

Instructions: {

    Enter your username, password, and FTP URL in the text field, and
    hit [ENTER].

    BE SURE TO END YOUR FTP URL PATH WITH "/".  

    URLs can be saved and loaded in multiple config files for future use.

    CONFIG FILES ARE STORED AS PLAIN TEXT, SO KEEP THEM SECURE.

    Click folders to browse through any dir on your web server.  Click
    text files to open, edit and save changes back to the server.
    Click images to view.  Also upload/download any type of file,
    create new files and folders, change file names, copy and delete
    files, change permissions, etc.

}
connect: does [
    either (to-string last p/text) = "/" [
    if error? try [
            f/data: sort append read to-url p/text "../" show f
        ][
            alert "Not a valid FTP address, or the connection failed."
        ]
    ][
        editor to-url p/text
    ]
]
view center-face layout [
    p: field 600 "ftp://user:pass@website.com/" [connect]
    across
    btn "Connect" [connect]
    btn "Load URL" [
        config: to-file request-file/file %/c/ftp.cfg
        either exists? config [
            if (config <> %none) [
                my-urls: copy []
                foreach item read/lines config [append my-urls item]
                if error? try [
                    p/text: copy request-list "Select a URL:" my-urls
                ] [break]
            ]
        ][
            alert "First, save some URLs to that file..."
        ]
        show p focus p
    ]
    btn "Save URL" [
        url: request-text/title/default "URL to save:" p/text
        if url = none [break]
        config-file: to-file request-file/file/save %/c/ftp.cfg
        if (url <> none) and (config-file <> %none) [
            if not exists? config-file [
                write/lines config-file ftp://user:pass@website.com/
            ]
            write/append/lines config-file to-url url
            alert "Saved"
        ]
    ]
    below
    f: text-list 600x350 [
        either (to-string value) = "../" [
            for i ((length? p/text) - 1) 1 -1 [
                if (to-string (pick p/text i)) = "/" [
                    clear at p/text (i + 1) show p
                    f/data: sort append read to-url p/text "../" show f
                    break
                ]
            ]
        ][
            either (to-string last value) = "/" [
                p/text: rejoin [p/text value] show p
                f/data: sort append read to-url p/text "../" show f
            ][
                if ((request "Edit/view this file?") = true) [
                    either find [%.jpg %.png %.gif %.bmp] suffix? value [
                        view/new layout [
                            image load to-url join p/text value
                        ]
                    ][
                        editor to-url rejoin [p/text value]
                    ]
                ]
            ]
        ]
    ]
    across
    btn "Get Info" [
        p-file: to-url rejoin [p/text f/picked]
        alert rejoin ["Size: " size? p-file " Date: " modified? p-file]
    ]
    btn "Delete" [
        p-file: to-url request-text/title/default "File to delete:"
            join p/text f/picked
        if ((confirm: request "Are you sure?") = true) [delete p-file]
        f/data: sort append read to-url p/text "../" show f
        if confirm = true [alert "File deleted"]
    ]
    btn "Rename" [
        new-name: to-file request-text/title/default "New File Name:"
            to-string f/picked
        if ((confirm: request "Are you sure?") = true) [
            rename (to-url join p/text f/picked) new-name
        ]
        f/data: sort append read to-url p/text "../" show f
        if confirm = true [alert "File renamed"]
    ]
    btn "Copy" [
        new-name: to-url request-text/title/default "New Path:"
            (join p/text f/picked)
        if ((confirm: request "Are you sure?") = true) [
            write/binary new-name read/binary to-url join p/text f/picked
        ]
        f/data: sort append read to-url p/text "../" show f
        if confirm = true [alert "File copied"]
    ]
    btn "New File" [
        p-file: to-url request-text/title/default "New File Name:"
            join p/text "ENTER-A-FILENAME.EXT"
        if ((confirm: request "Are you sure?") = true) [
            write p-file ""
            ; editor p-file
        ]
        f/data: sort append read to-url p/text "../" show f
        if confirm = true [alert "Empty file created - click to edit."]
    ]
    btn "New Dir" [
        make-dir x: to-url request-text/title/default "New folder:" p/text
        alert "Folder created"
        p/text: x show p
        f/data: sort append read to-url p/text "../" show f
    ]
    btn "Download" [
        file: request-text/title/default "File:" (join p/text f/picked)
        l-file: next to-string (find/last (to-string file) "/")
        save-as: request-text/title/default "Save as..." to-string l-file
        write/binary (to-file save-as) (read/binary to-url file)
        alert "Download Complete"
    ]
    btn "Upload" [
        file: to-file request-file
        r-file: request-text/title/default "Save as..." 
            join p/text (to-string to-relative-file file)
        write/binary (to-url r-file) (read/binary file)
        f/data: sort append read to-url p/text "../" show f
        alert "Upload Complete"
    ]
    btn "Chmod" [
        p-file: to-url request-text/default rejoin [p/text f/picked]
        chmod: to-block request-text/title/default "Permissions:"
            "read write execute"
        write/binary/allow p-file (read/binary p-file) chmod
        alert "Permissions changed"
    ]
    btn-help [inform layout[backcolor white text bold as-is instructions]]
    do [focus p]
]

22.8 Случай: Тренировочная программа "Jeoparody"

Мой друг хотел создать программу, которая поможет обучать сотрудников на работе. Она надеялась создать игру, похожую на телешоу Jeopardy, в которую можно было бы играть с группой сотрудников, чтобы проводить с ними викторины, инструктировать и взаимодействовать с ними в приятной форме. Вместе мы разработали следующие спецификации того, как должна работать программа:

  1. Сотрудники разбиты на 2-4 команды игроков, которые соревнуются друг с другом за призы.
  2. Программа отображает 5 столбцов полей, каждое под отдельным заголовком категории. Столбцы полей разделены на строки по 5, в каждой строке отображаются значения приращения 100, 200, 300, 400 и 500 долларов.
  3. Ведущий игры управляет программой и управляет игрой. Для начала одна команда выбирает категорию и сумму в долларах для ставки, и ведущий нажимает на выбранный квадрат. При щелчке по полю отображается ответ.
  4. Первый ответивший игрок получает шанс определить правильный вопрос для данного ответа (например, "Что такое ____?"). Затем программа отображает правильный вопрос, а также некоторую образовательную информацию, относящуюся к теме (цель программы состоит в том, чтобы как тестировать, так и обучать сотрудников). Затем программа спрашивает ведущего, какой игрок ответил правильно или неправильно. Сумма ставки либо вычитается, либо добавляется к счету игрока в зависимости от правильного или неправильного ответа, а текущий общий счёт отображается в области в нижней части экрана.
  5. После заполнения каждого вопроса выбранные поля отображаются как пустые и не отвечают.
  6. Команды-победители продолжают выбирать ответы, и игра продолжается до тех пор, пока не будут выбраны все ячейки.
  7. Программа должна иметь возможность для организатора подготовить и сохранить категории, ответы и вопросы, необходимые для игры.

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

REBOL [title: "Jeoparody"] 

view center-face layout [
    backdrop effect [gradient 1x1 tan brown]
    style button button effect [
        gradient blue blue/2] 100x65 font [size: 30]
    style box box brown 100x35
    space 40x10
    across
    box 660x10 effect [gradient 1x0 brown black]  ; separator line
    return
    box "Category 1" 
    box "Category 2" 
    box "Category 3" 
    box "Category 4" 
    box "Category 5" 
    return
    box 660x10 effect [gradient 1x0 brown black] 
    return
    button "$100" []
    button "$100" []
    button "$100" []
    button "$100" []
    button "$100" []
    return
    button "$200" []
    button "$200" []
    button "$200" []
    button "$200" []
    button "$200" []
    return
    button "$300" []
    button "$300" []
    button "$300" []
    button "$300" []
    button "$300" []
    return
    button "$400" []
    button "$400" []
    button "$400" []
    button "$400" []
    button "$400" []
    return
    button "$500" []
    button "$500" []
    button "$500" []
    button "$500" []
    button "$500" []
    return 
    box 660x10 effect [gradient 1x0 brown black] 
    return tab
    box "Player 1:" effect [gradient 1x1 tan brown]
    player1: box white "$0" font [color: black] 
    box "Player 2:" effect [gradient 1x1 tan brown]
    player2: box white "$0" font [color: black]
    return tab
    box "Player 3:" effect [gradient 1x1 tan brown]
    player3: box white "$0" font [color: black]
    box "Player 4:" effect [gradient 1x1 tan brown]
    player4: box white "$0" font [color: black]
]

Это похоже на большой объем кода, но это всего лишь простые виджеты макета VID GUI. Затем я начал разрабатывать данные и логику, необходимые для работы графического интерфейса. Сначала я подумал о данных, необходимых для игры, и решил разделить все потенциальные вопросы и ответы на 2 отдельных блока строк:

answers: [
    "$100 Answer, Category 1" 
    "$100 Answer, Category 2" 
    "$100 Answer, Category 3" 
    "$100 Answer, Category 4" 
    "$100 Answer, Category 5" 
    "$200 Answer, Category 1" 
    "$200 Answer, Category 2" 
    "$200 Answer, Category 3" 
    "$200 Answer, Category 4" 
    "$200 Answer, Category 5" 
    "$300 Answer, Category 1" 
    "$300 Answer, Category 2" 
    "$300 Answer, Category 3" 
    "$300 Answer, Category 4" 
    "$300 Answer, Category 5" 
    "$400 Answer, Category 1" 
    "$400 Answer, Category 2" 
    "$400 Answer, Category 3" 
    "$400 Answer, Category 4" 
    "$400 Answer, Category 5" 
    "$500 Answer, Category 1" 
    "$500 Answer, Category 2" 
    "$500 Answer, Category 3" 
    "$500 Answer, Category 4" 
    "$500 Answer, Category 5" 
]
questions: [
    "$100 Question, Category 1" 
    "$100 Question, Category 2" 
    "$100 Question, Category 3" 
    "$100 Question, Category 4" 
    "$100 Question, Category 5" 
    "$200 Question, Category 1" 
    "$200 Question, Category 2" 
    "$200 Question, Category 3" 
    "$200 Question, Category 4" 
    "$200 Question, Category 5" 
    "$300 Question, Category 1" 
    "$300 Question, Category 2" 
    "$300 Question, Category 3" 
    "$300 Question, Category 4" 
    "$300 Question, Category 5" 
    "$400 Question, Category 1" 
    "$400 Question, Category 2" 
    "$400 Question, Category 3" 
    "$400 Question, Category 4" 
    "$400 Question, Category 5" 
    "$500 Question, Category 1" 
    "$500 Question, Category 2" 
    "$500 Question, Category 3" 
    "$500 Question, Category 4" 
    "$500 Question, Category 5" 
]

Мне также потребовались метки переменных для заголовков категорий (чтобы их мог позже редактировать ведущий без необходимости редактировать какой-либо программный код):

Category-1: "Category 1" 
Category-2: "Category 2" 
Category-3: "Category 3" 
Category-4: "Category 4" 
Category-5: "Category 5"

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

do-button: func [num] [
    ; "num" относится к параметру уникального номера, отправляемому 
    ; каждой отдельной кнопкой, каждый раз, когда эта функция 
    ; выполняется:
    alert pick answers num 
    alert pick questions num
]

Вопросы/ответы в блоках данных выше расположены так, что каждые 5 элементов увеличиваются на 100 долларов. Я добавил следующий код, чтобы присвоить выбранному вопросу сумму в долларах, в зависимости от того, какое значение "num" было передано кнопкой (вопросы 1–5 = 100 долларов США, 6–10 = 200 долларов и т.д.):

if find [1 2 3 4 5] num [val: $100]
if find [6 7 8 9 10] num [val: $200]
if find [11 12 13 14 15] num [val: $300]
if find [16 17 18 19 20] num [val: $400]
if find [21 22 23 24 25] num [val: $500]

Теперь мне просто нужно спросить ведущего, какой игрок ответил правильно или неправильно на приведённый выше ответ, и присвоить ответу переменную ("correct" (правильно)):

correct: request-list "Select:" [
    "Player 1 answered correctly" "Player 1 answered incorrectly"
    "Player 2 answered correctly" "Player 2 answered incorrectly"
    "Player 3 answered correctly" "Player 3 answered incorrectly"
    "Player 4 answered correctly" "Player 4 answered incorrectly"
]
22.8 и затем обновите поля отображения результатов в соответствии с приведённым выше ответом:
switch correct  [
    "Player 1 answered correctly" [
        player1/text: to-string ((to-money player1/text) + val)
        show player1
    ] 
    "Player 1 answered incorrectly" [
        player1/text: to-string ((to-money player1/text) - val)
        show player1
    ] 
    "Player 2 answered correctly" [
        player2/text: to-string ((to-money player2/text) + val)
        show player2
    ]
    "Player 2 answered incorrectly"[
        player2/text: to-string ((to-money player2/text) - val)
        show player2
    ]
    "Player 3 answered correctly" [
        player3/text: to-string ((to-money player3/text) + val)
        show player3
    ] 
    "Player 3 answered incorrectly" [
        player3/text: to-string ((to-money player3/text) - val)
        show player3
    ] 
    "Player 4 answered incorrectly"[
        player4/text: to-string ((to-money player4/text) - val)
        show player4
    ]
    "Player 4 answered correctly" [
        player4/text: to-string ((to-money player4/text) + val)
        show player4
    ]
]

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

button "$100" [face/feel: none face/text: "" do-button 1]
button "$100" [face/feel: none face/text: "" do-button 2]
button "$100" [face/feel: none face/text: "" do-button 3]
.
.
.

Теперь у меня есть рабочая игра по заданной схеме:

REBOL [title: "Jeoparody"] 

answers: [
    "$100 Answer, Category 1" 
    "$100 Answer, Category 2" 
    "$100 Answer, Category 3" 
    "$100 Answer, Category 4" 
    "$100 Answer, Category 5" 
    "$200 Answer, Category 1" 
    "$200 Answer, Category 2" 
    "$200 Answer, Category 3" 
    "$200 Answer, Category 4" 
    "$200 Answer, Category 5" 
    "$300 Answer, Category 1" 
    "$300 Answer, Category 2" 
    "$300 Answer, Category 3" 
    "$300 Answer, Category 4" 
    "$300 Answer, Category 5" 
    "$400 Answer, Category 1" 
    "$400 Answer, Category 2" 
    "$400 Answer, Category 3" 
    "$400 Answer, Category 4" 
    "$400 Answer, Category 5" 
    "$500 Answer, Category 1" 
    "$500 Answer, Category 2" 
    "$500 Answer, Category 3" 
    "$500 Answer, Category 4" 
    "$500 Answer, Category 5" 
]
questions: [
    "$100 Question, Category 1" 
    "$100 Question, Category 2" 
    "$100 Question, Category 3" 
    "$100 Question, Category 4" 
    "$100 Question, Category 5" 
    "$200 Question, Category 1" 
    "$200 Question, Category 2" 
    "$200 Question, Category 3" 
    "$200 Question, Category 4" 
    "$200 Question, Category 5" 
    "$300 Question, Category 1" 
    "$300 Question, Category 2" 
    "$300 Question, Category 3" 
    "$300 Question, Category 4" 
    "$300 Question, Category 5" 
    "$400 Question, Category 1" 
    "$400 Question, Category 2" 
    "$400 Question, Category 3" 
    "$400 Question, Category 4" 
    "$400 Question, Category 5" 
    "$500 Question, Category 1" 
    "$500 Question, Category 2" 
    "$500 Question, Category 3" 
    "$500 Question, Category 4" 
    "$500 Question, Category 5" 
]

do-button: func [num] [
    alert pick answers num 
    alert pick questions num
    if find [1 2 3 4 5] num [val: $100]
    if find [6 7 8 9 10] num [val: $200]
    if find [11 12 13 14 15] num [val: $300]
    if find [16 17 18 19 20] num [val: $400]
    if find [21 22 23 24 25] num [val: $500]
    correct: request-list "Select:" ["Player 1 answered correctly" 
        "Player 1 answered incorrectly" "Player 2 answered correctly"
        "Player 2 answered incorrectly" "Player 3 answered correctly" 
        "Player 3 answered incorrectly" "Player 4 answered correctly"
        "Player 4 answered incorrectly"
    ] 
    switch correct  [
        "Player 1 answered correctly" [
            player1/text: to-string ((to-money player1/text) + val)
            show player1
        ] 
        "Player 1 answered incorrectly" [
            player1/text: to-string ((to-money player1/text) - val)
            show player1
        ] 
        "Player 2 answered correctly" [
            player2/text: to-string ((to-money player2/text) + val)
            show player2
        ]
        "Player 2 answered incorrectly"[
            player2/text: to-string ((to-money player2/text) - val)
            show player2
        ]
        "Player 3 answered correctly" [
            player3/text: to-string ((to-money player3/text) + val)
            show player3
        ] 
        "Player 3 answered incorrectly" [
            player3/text: to-string ((to-money player3/text) - val)
            show player3
        ] 
        "Player 4 answered incorrectly"[
            player4/text: to-string ((to-money player4/text) - val)
            show player4
        ]
        "Player 4 answered correctly" [
            player4/text: to-string ((to-money player4/text) + val)
            show player4
        ]
    ]
]

view center-face layout [
    backdrop effect [gradient 1x1 tan brown]
    style button button effect [
        gradient blue blue/2] 100x65 font [size: 30]
    style box box brown 100x35
    space 40x10
    across
    box 660x10 effect [gradient 1x0 brown black]  ; separator line
    return
    box "Category 1" 
    box "Category 2" 
    box "Category 3" 
    box "Category 4" 
    box "Category 5" 
    return
    box 660x10 effect [gradient 1x0 brown black] 
    return
    button "$100" [face/feel: none face/text: "" do-button 1]
    button "$100" [face/feel: none face/text: "" do-button 2]
    button "$100" [face/feel: none face/text: "" do-button 3]
    button "$100" [face/feel: none face/text: "" do-button 4]
    button "$100" [face/feel: none face/text: "" do-button 5]
    return
    button "$200" [face/feel: none face/text: "" do-button 6]
    button "$200" [face/feel: none face/text: "" do-button 7]
    button "$200" [face/feel: none face/text: "" do-button 8]
    button "$200" [face/feel: none face/text: "" do-button 9]
    button "$200" [face/feel: none face/text: "" do-button 10]
    return
    button "$300" [face/feel: none face/text: "" do-button 11]
    button "$300" [face/feel: none face/text: "" do-button 12]
    button "$300" [face/feel: none face/text: "" do-button 13]
    button "$300" [face/feel: none face/text: "" do-button 14]
    button "$300" [face/feel: none face/text: "" do-button 15]
    return
    button "$400" [face/feel: none face/text: "" do-button 16]
    button "$400" [face/feel: none face/text: "" do-button 17]
    button "$400" [face/feel: none face/text: "" do-button 18]
    button "$400" [face/feel: none face/text: "" do-button 19]
    button "$400" [face/feel: none face/text: "" do-button 20]
    return
    button "$500" [face/feel: none face/text: "" do-button 21]
    button "$500" [face/feel: none face/text: "" do-button 22]
    button "$500" [face/feel: none face/text: "" do-button 23]
    button "$500" [face/feel: none face/text: "" do-button 24]
    button "$500" [face/feel: none face/text: "" do-button 25]
    return 
    box 660x10 effect [gradient 1x0 brown black] 
    return tab
    box "Player 1:" effect [gradient 1x1 tan brown]
    player1: box white "$0" font [color: black]
    box "Player 2:" effect [gradient 1x1 tan brown]
    player2: box white "$0" font [color: black]
    return tab
    box "Player 3:" effect [gradient 1x1 tan brown]
    player3: box white "$0" font [color: black]
    box "Player 4:" effect [gradient 1x1 tan brown]
    player4: box white "$0" font [color: black]
]

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

header: load to-binary decompress 64#{
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
}

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

image header 660x40

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

sizer: 5

Затем в любом месте графического интерфейса, где была пара размеров, я добавил небольшой математический расчёт для динамического определения размера. Размер изображения выше (660x40), например, был записан следующим образом:

image header (to-pair rejoin [(132 * sizer) "x" (8 * sizer)])

Размеры вкладок и площадок были установлены следующим образом:

tabs (sizer * 20)
pad (sizer * 2)

С этими вычислениями размеров, добавленными к каждому виджету, всё, что нужно было сделать ведущему - это изменить одну переменную размера, и весь размер игры в пикселях был бы скорректирован.

Затем я понял, что ведущий потенциально может делать ошибки в игре (я сделал это во время тестирования программы), поэтому мне нужен был способ вручную настраивать игровые очки. Я добавил следующий код в блок действий полей очков, который позволяет ведущему щёлкнуть поле и ввести новое значение, которое будет отображаться на лицевой стороне окна:

player1: box white "$0" font [color: black size: (sizer * 4)] [
    face/text: request-text/title/default "Enter Score:" face/text
]

Согласно последнему пункту в нашей схеме спецификации, всё, что мне сейчас нужно, это способ для ведущего редактировать и сохранять данные игры. Я начал с присвоения имени всему коду, содержащему переменные, которые ведущий должен иметь возможность редактировать. Я хотел, чтобы этот код был доступен для записи на жёсткий диск и мог быть исполненный командой "do", поэтому я сохранил его как текстовую строку и включил заголовок REBOL:

config: {

    REBOL []

    ;________________________________________________________________


    sizer: 5

    Category-1: "Category 1" 
    Category-2: "Category 2" 
    Category-3: "Category 3" 
    Category-4: "Category 4" 
    Category-5: "Category 5"

    answers: [
        "$100 Answer, Category 1" 
        "$100 Answer, Category 2" 
        "$100 Answer, Category 3" 
        "$100 Answer, Category 4" 
        "$100 Answer, Category 5" 
        "$200 Answer, Category 1" 
        "$200 Answer, Category 2" 
        "$200 Answer, Category 3" 
        "$200 Answer, Category 4" 
        "$200 Answer, Category 5" 
        "$300 Answer, Category 1" 
        "$300 Answer, Category 2" 
        "$300 Answer, Category 3" 
        "$300 Answer, Category 4" 
        "$300 Answer, Category 5" 
        "$400 Answer, Category 1" 
        "$400 Answer, Category 2" 
        "$400 Answer, Category 3" 
        "$400 Answer, Category 4" 
        "$400 Answer, Category 5" 
        "$500 Answer, Category 1" 
        "$500 Answer, Category 2" 
        "$500 Answer, Category 3" 
        "$500 Answer, Category 4" 
        "$500 Answer, Category 5" 
    ]
    questions: [
        "$100 Question, Category 1" 
        "$100 Question, Category 2" 
        "$100 Question, Category 3" 
        "$100 Question, Category 4" 
        "$100 Question, Category 5" 
        "$200 Question, Category 1" 
        "$200 Question, Category 2" 
        "$200 Question, Category 3" 
        "$200 Question, Category 4" 
        "$200 Question, Category 5" 
        "$300 Question, Category 1" 
        "$300 Question, Category 2" 
        "$300 Question, Category 3" 
        "$300 Question, Category 4" 
        "$300 Question, Category 5" 
        "$400 Question, Category 1" 
        "$400 Question, Category 2" 
        "$400 Question, Category 3" 
        "$400 Question, Category 4" 
        "$400 Question, Category 5" 
        "$500 Question, Category 1" 
        "$500 Question, Category 2" 
        "$500 Question, Category 3" 
        "$500 Question, Category 4" 
        "$500 Question, Category 5" 
    ]

    ;________________________________________________________________

}

Чтобы использовать этот код в обычной игре, я просто выполнил код, содержащийся в переменной "config":

do config

Затем я обрисовал в общих чертах некоторый псевдокод, описывающий каждый шаг, который ведущий может пройти для редактирования данных конфигурации:

  1. Предупреждаем ведущего, что эти шаги сотрут текущие данные и завершат текущую игру. Используем простой запросчик, и условие if. Выйдем из текущего блока кода, если он решат продолжить игру.
  2. Перед тем, как перейти к процессу редактирования/сохранения, спросите, не хочет ли пользователь просто загрузить ранее отредактированный файл конфигурации. Если это так, просто запустите ("do") выбранный файл конфигурации, закройте текущий графический интерфейс и повторно запустите графический интерфейс, используя новые данные конфигурации.
  3. Если пользователь не выбрал ни один из предыдущих вариантов, дадим ему несколько инструкций о том, как редактировать файл, сохранить код конфигурации по умолчанию в файл и открыть его во встроенном редакторе. После закрытия редактора запросите файл конфигурации для загрузки, затем закройте текущий графический интерфейс и перезапустите его, используя только что выбранные данные конфигурации.

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

; шаг 1

contin: request/confirm {
    This will end the current game.  Continue?}
if contin = false [break]

; шаг 2

loadoredit:  request/confirm "Load previously edited config file?"
if loadoredit = true [
    do to-file request-file/title/file {
        Choose config file to use:} "File" %default_config.txt
    unview
    view center-face layout gui
    break  ; needed so that step 3 doesn't run
]

; шаг 3

alert {Edit carefully, maintaining all quotation marks.  
    You can open a previously saved file if needed.
    When done, click SAVE-AS and then QUIT.  
    Be sure choose a filename/folder
    location that you'll be able to find later.
}
write %default_config.txt config
unview
editor %default_config.txt
alert {Now choose a config file to use (most likely the file
    you just edited).}
do to-file request-file/title/file {
    Choose config file to use:} "File" %default_config.txt
view center-face layout gui

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

REBOL [title: "Jeoparody"] 

config: {

    REBOL []

    ;________________________________________________________________

    sizer: 4

    Category-1: "Category 1" 
    Category-2: "Category 2" 
    Category-3: "Category 3" 
    Category-4: "Category 4" 
    Category-5: "Category 5"

    answers: [
        "$100 Answer, Category 1" 
        "$100 Answer, Category 2" 
        "$100 Answer, Category 3" 
        "$100 Answer, Category 4" 
        "$100 Answer, Category 5" 
        "$200 Answer, Category 1" 
        "$200 Answer, Category 2" 
        "$200 Answer, Category 3" 
        "$200 Answer, Category 4" 
        "$200 Answer, Category 5" 
        "$300 Answer, Category 1" 
        "$300 Answer, Category 2" 
        "$300 Answer, Category 3" 
        "$300 Answer, Category 4" 
        "$300 Answer, Category 5" 
        "$400 Answer, Category 1" 
        "$400 Answer, Category 2" 
        "$400 Answer, Category 3" 
        "$400 Answer, Category 4" 
        "$400 Answer, Category 5" 
        "$500 Answer, Category 1" 
        "$500 Answer, Category 2" 
        "$500 Answer, Category 3" 
        "$500 Answer, Category 4" 
        "$500 Answer, Category 5" 
    ]
    questions: [
        "$100 Question, Category 1" 
        "$100 Question, Category 2" 
        "$100 Question, Category 3" 
        "$100 Question, Category 4" 
        "$100 Question, Category 5" 
        "$200 Question, Category 1" 
        "$200 Question, Category 2" 
        "$200 Question, Category 3" 
        "$200 Question, Category 4" 
        "$200 Question, Category 5" 
        "$300 Question, Category 1" 
        "$300 Question, Category 2" 
        "$300 Question, Category 3" 
        "$300 Question, Category 4" 
        "$300 Question, Category 5" 
        "$400 Question, Category 1" 
        "$400 Question, Category 2" 
        "$400 Question, Category 3" 
        "$400 Question, Category 4" 
        "$400 Question, Category 5" 
        "$500 Question, Category 1" 
        "$500 Question, Category 2" 
        "$500 Question, Category 3" 
        "$500 Question, Category 4" 
        "$500 Question, Category 5" 
    ]

    ;________________________________________________________________

}

do config

header: load to-binary decompress 64#{
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
}

do-button: func [num] [
    alert pick answers num 
    alert pick questions num
    if find [1 2 3 4 5] num [val: $100]
    if find [6 7 8 9 10] num [val: $200]
    if find [11 12 13 14 15] num [val: $300]
    if find [16 17 18 19 20] num [val: $400]
    if find [21 22 23 24 25] num [val: $500]
    correct: request-list "Select:" ["Player 1 answered correctly" 
        "Player 1 answered incorrectly" "Player 2 answered correctly"
        "Player 2 answered incorrectly" "Player 3 answered correctly"
        "Player 3 answered incorrectly" "Player 4 answered correctly"
        "Player 4 answered incorrectly"
    ] 
    switch correct  [
        "Player 1 answered correctly" [
            player1/text: to-string ((to-money player1/text) + val)
            show player1
        ] 
        "Player 1 answered incorrectly" [
            player1/text: to-string ((to-money player1/text) - val)
            show player1
        ] 
        "Player 2 answered correctly" [
            player2/text: to-string ((to-money player2/text) + val)
            show player2
        ]
        "Player 2 answered incorrectly"[
            player2/text: to-string ((to-money player2/text) - val)
            show player2
        ]
        "Player 3 answered correctly" [
            player3/text: to-string ((to-money player3/text) + val)
            show player3
        ] 
        "Player 3 answered incorrectly" [
            player3/text: to-string ((to-money player3/text) - val)
            show player3
        ] 
        "Player 4 answered incorrectly"[
            player4/text: to-string ((to-money player4/text) - val)
            show player4
        ]
        "Player 4 answered correctly" [
            player4/text: to-string ((to-money player4/text) + val)
            show player4
        ]
    ]
]

view center-face layout gui: [
    tabs (sizer * 20)
    backdrop effect [gradient 1x1 tan brown]
    style button button effect [gradient blue blue/2] (
        to-pair rejoin [(20 * sizer) "x" (13 * sizer)]
    ) font [size: (sizer * 6)]
    style box box brown (to-pair rejoin [(20 * sizer) "x" (7 * sizer
        )]) font [size: (sizer * 3)]
    image header (to-pair rejoin [(132 * sizer) "x" (8 * sizer)]) [
        contin: request/confirm {
            This will end the current game.  Continue?}
        if contin = false [break]
        loadoredit:  request/confirm "Load previously edited config file?"
        if loadoredit = true [
            do to-file request-file/title/file {
                Choose config file to use:} "File" %default_config.txt
            unview
            view center-face layout gui
            break
        ]
        alert {Edit carefully, maintaining all quotation marks.  
            You can open a previously saved file if needed.
            When done, click SAVE-AS and then QUIT.  
            Be sure choose a filename/folder
            location that you'll be able to find later.
        }
        write %default_config.txt config
        unview
        editor %default_config.txt
        alert {Now choose a config file to use (most likely the file
            you just edited).}
        do to-file request-file/title/file {
            Choose config file to use:} "File" %default_config.txt
        view center-face layout gui
    ]
    space (to-pair rejoin [(8 * sizer) "x" (2 * sizer)])
    pad (sizer * 2)
    across
    box (to-pair rejoin [(132 * sizer) "x" (2 * sizer)]
        ) effect [gradient 1x0 brown black] 
    return
    box Category-1 
    box Category-2 
    box Category-3 
    box Category-4 
    box Category-5
    return
    box (to-pair rejoin [(132 * sizer) "x" (2 * sizer)]
        ) effect [gradient 1x0 brown black] 
    return
    button "$100" [face/feel: none face/text: "" do-button 1]
    button "$100" [face/feel: none face/text: "" do-button 2]
    button "$100" [face/feel: none face/text: "" do-button 3]
    button "$100" [face/feel: none face/text: "" do-button 4]
    button "$100" [face/feel: none face/text: "" do-button 5]
    return
    button "$200" [face/feel: none face/text: "" do-button 6]
    button "$200" [face/feel: none face/text: "" do-button 7]
    button "$200" [face/feel: none face/text: "" do-button 8]
    button "$200" [face/feel: none face/text: "" do-button 9]
    button "$200" [face/feel: none face/text: "" do-button 10]
    return
    button "$300" [face/feel: none face/text: "" do-button 11]
    button "$300" [face/feel: none face/text: "" do-button 12]
    button "$300" [face/feel: none face/text: "" do-button 13]
    button "$300" [face/feel: none face/text: "" do-button 14]
    button "$300" [face/feel: none face/text: "" do-button 15]
    return
    button "$400" [face/feel: none face/text: "" do-button 16]
    button "$400" [face/feel: none face/text: "" do-button 17]
    button "$400" [face/feel: none face/text: "" do-button 18]
    button "$400" [face/feel: none face/text: "" do-button 19]
    button "$400" [face/feel: none face/text: "" do-button 20]
    return
    button "$500" [face/feel: none face/text: "" do-button 21]
    button "$500" [face/feel: none face/text: "" do-button 22]
    button "$500" [face/feel: none face/text: "" do-button 23]
    button "$500" [face/feel: none face/text: "" do-button 24]
    button "$500" [face/feel: none face/text: "" do-button 25]
    return 
    box (to-pair rejoin [(132 * sizer) "x" (2 * sizer)]
        ) effect [gradient 1x0 brown black] 
    return tab
    box "Player 1:" effect [gradient 1x1 tan brown]
    player1: box white "$0" font [color: black size: (sizer * 4)] [
        face/text: request-text/title/default "Enter Score:" face/text
    ]
    box "Player 2:" effect [gradient 1x1 tan brown]
    player2: box white "$0" font [color: black size: (sizer * 4)] [
        face/text: request-text/title/default "Enter Score:" face/text
    ]
    return tab
    box "Player 3:" effect [gradient 1x1 tan brown]
    player3: box white "$0" font [color: black size: (sizer * 4)] [
        face/text: request-text/title/default "Enter Score:" face/text
    ]
    box "Player 4:" effect [gradient 1x1 tan brown]
    player4: box white "$0" font [color: black size: (sizer * 4)] [
        face/text: request-text/title/default "Enter Score:" face/text
    ]
]

22.9 Случай: Составление расписания учителей, часть вторая

После нескольких месяцев использования приложения для составления расписания учителей, описанного в первом тематическом исследовании, мой бизнес расширился, а преподавательский состав вырос. Учитывая то, как всё работало в моей короткой начальной программе, мне пришлось бы создать новую папку на веб-сайте и скомпилировать уникальную версию программы для каждого нового учителя. Это потребует перекомпиляции и загрузки новой версии для каждого учителя каждый раз, когда я изменяю программу. Я хотел сделать многопользовательскую версию приложения, чтобы упростить настройку и сэкономить время на обслуживание. Я также хотел добавить в существующую программу некоторую проверку ошибок и простую схему паролей. Чтобы создать новую версию приложения, вот моя концепция в форме схемы/псевдокода:

  1. Сохранить существующую структуру папок и файлов на веб-сайте (http://website.com/teacher/name, schedule.txt и index.php).
  2. Добавить на веб-сайт файл, содержащий список текущих учителей и связанные с ними пароли. Поместить его вне папки public_html, чтобы люди не могли скачать его без пароля.
  3. В приложении начать с загрузки этого файла с веб-сайта (используя ftp).
  4. Отобразить текстовый список имён учителей из загруженного файла.
  5. Когда выбрано имя учителя, запросить пароль у пользователя и убедится, что он соответствует паролю, связанному с данным учителем.
  6. Добавить имя учителя к URL-адресам http и ftp и запустить программу, как прежде.
  7. Добавить несколько процедур проверки ошибок и резервного копирования каждый раз, когда данные читаются или записываются локально или на веб-сервере. Таким образом, данные никогда не теряются.
  8. Скомпилировать программу и загрузите её на сайт. Направьте все ссылки на страницах index.php на этот единственный файл. Теперь, когда я хочу добавить нового учителя, все, что мне нужно сделать, это добавить новое имя учителя и пароль в загружаемый текстовый файл и скопировать пустые index.php и schedule.txt в новую папку на веб-сервере. Если я когда-нибудь внесу в программу дополнительные изменения, мне нужно будет только перекомпилировать и загрузить этот единственный файл программы.

Для начала я создал текстовый файл под названием "teacherlist.txt" и сохранил его вне папки public_html на веб-сервере. Он отформатирован так:

["mark" "markspassword"] ["ryan" "ryanspassword"]
["nick" "nickspassword"] ["peter" "peterspassword"]
["rudi" "rudispassword"] ["tom" "tomspassword"]

Первое, что я делаю в программе, это читаю данные:

teacherlist: load ftp://user:pass@website.com/teacherlist.txt

Далее отобразим список учителей. Первый элемент в каждом блоке Teacherlist.txt - это имя учителя. Цикл foreach считывает каждое из этих имён в новый блок, и этот блок отображается с помощью виджета текстового списка графического интерфейса пользователя:

teachers: copy []
foreach teacher teacherlist [append teachers first teacher]
view center-face layout [
    text-list data teachers [folder: value unview]    
]

Затем получите пароль от пользователя и используйте цикл foreach для просмотра списка, проверяя совпадение имён учителей и паролей, введённых пользователем (первый и второй элементы, соответственно, в каждом блоке):

pass: request-pass/only
correct: false
foreach teacher teacherlist [
    if ((first teacher) = folder) and (pass = (second teacher)) [
        correct: true
    ]
]
if correct = false [alert "Incorrect password." quit]

Я добавляю в сценарий следующую строку, которая не позволяет REBOL завершать сценарий при нажатии клавиши [Esc]. Такое поведение является стандартным в интерпретаторе REBOL и позволяет кому-то просто остановить скрипт и просмотреть список учителей. (Я не очень беспокоюсь о безопасности здесь, но я не хочу, чтобы пароли были явно доступны):

system/console/break: false

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

error-message: does [
    ans: request {Internet connection is not available.
        Would you like to see one of the recent local backups?}
    either ans = true [
        editor to-file request-file quit
    ][
        quit
    ]
]

Я заключаю все попытки подключения к Интернету в подпрограммы "error? try" и дублирую исходную процедуру резервного копирования из исходной программы, чтобы никакие данные никогда не терялись. Вот последний код:

REBOL [title: "Lesson Scheduler"] 

system/console/break: false
error-message: does [
    ans: request {Internet connection is not available.
        Would you like to see one of the recent local backups?}
    either ans = true [
        editor to-file request-file quit
    ][
        quit
    ]
]

if error? try [
    teacherlist: load ftp://user:pass@website.com/teacherlist.txt
][
    error-message
]
teachers: copy []
foreach teacher teacherlist [append teachers first teacher]
view center-face layout [
    text-list data teachers [folder: value unview]    
]

pass: request-pass/only
correct: false
foreach teacher teacherlist [
    if ((first teacher) = folder) and (pass = (second teacher)) [
        correct: true
    ]
]
if correct = false [alert "Incorrect password." quit]

url: rejoin [http://website.com/teacher/ folder]
ftp-url: rejoin [
    ftp://user:pass@website.com/public_html/teacher/ folder
]

if error? try [
    write %schedule.txt read rejoin [url "/schedule.txt"]
][
    error-message
]

; backup (before changes are made):
cur-time: to-string replace/all to-string now/time ":" "-"
; local:
write to-file rejoin [
    folder "-schedule_" now/date "_" cur-time ".txt"
] read %schedule.txt
; online:
if error? try [
    write rejoin [
        ftp-url "/" now/date "_" cur-time
    ] read %schedule.txt
][
    error-message
]

editor %schedule.txt

; backup again (after changes are made):
cur-time: to-string replace/all to-string now/time ":" "-"
write to-file rejoin [
    folder "-schedule_" now/date "_" cur-time ".txt"
] read %schedule.txt
if error? try [
    write rejoin [
        ftp-url "/" now/date "_" cur-time
    ] read %schedule.txt
][
    alert "Internet connection not available while backing up."
]

; save to web site:
if error? try [
    write rejoin [ftp-url "/schedule.txt"] read %schedule.txt
][
    alert {Internet connection not available while updating web
    site.  Your schedule has NOT been saved online.}
    quit
]    
browse url

После завершения работы над новым приложением я хотел создать дополнительное приложение cgi для веб-сайта, чтобы в совокупности отображать всё доступное время в расписании каждого учителя. Это поможет при составлении расписания, потому что и студенты, и руководство смогут мгновенно увидеть все открытые встречи "с высоты птичьего полёта" на одной веб-странице. Чтобы этот дисплей был доступен для просмотра широкой публике, я хочу, чтобы приложение cgi удаляло все личные данные, содержащиеся в расписаниях. Чтобы создать cgi, мне нужно искать в каждой строке текста расписания "----". Если строка содержит символы "----", время доступно. Вот схема псевдокода, которую я продумал, организовывая процесс:

  1. Составьте список всех страниц учителя. Храните ссылки в блоке.
  2. Используйте цикл foreach для циклического просмотра каждой страницы в списке. Прочтите данные на каждой странице в строчном формате, используя другой цикл foreach.
  3. Для каждой строки используйте функцию поиска, чтобы проверить, содержит ли строка название дня недели или символы "----". Если это так, распечатайте строку, добавив дополнительное форматирование для разделения дней в качестве заголовков. Также распечатайте ссылку на каждую страницу в виде заголовка, разделяя расписание каждого учителя в распечатке.

Сначала я создал блок ссылок:

page-list: [
    http://website.com/teacher/ryan
    http://website.com/teacher/mark
    http://website.com/teacher/nick
    http://website.com/teacher/peter
    http://website.com/teacher/tom
    http://website.com/teacher/rudi
]

На шаге 2 я создал цикл foreach для чтения каждой страницы:

foreach page page-list [
    data: read/lines page
]

Внутри этого цикла я добавил код для распечатки имени учителя и заголовков дня, а также доступного времени:

foreach page page-list [
    print newline
    print to-string page 
    print ""
    data: read/lines page
    week: ["MONDAY" "TUESDAY" "WEDNESDAY" "THURSDAY" "FRIDAY"
        "SATURDAY" "SUNDAY"]
    foreach line data [
        foreach day week [
            if find line day [print "" print line print ""]
        ]
        if find line "----" [print line]
    ]
]

Теперь у меня есть небольшое приложение командной строки, которое делает то, что мне нужно:

REBOL []
page-list: [
    http://website.com/teacher/ryan 
    http://website.com/teacher/mark 
    http://website.com/teacher/nick 
    http://website.com/teacher/peter 
    http://website.com/teacher/tom 
    http://website.com/teacher/rudi
]
foreach page page-list [
    print newline
    print to-string page 
    print ""
    data: read/lines page
    week: ["MONDAY" "TUESDAY" "WEDNESDAY" "THURSDAY" "FRIDAY"
        "SATURDAY" "SUNDAY"]
    foreach line data [
        foreach day week [
            if find line day [print "" print line print ""]
        ]
        if find line "----" [print line]
    ]
]

Затем к базовой структуре CGI, представленной ранее в этом руководстве, я просто добавил приведённый выше код. Единственными реальными изменениями, которые мне нужно было внести, были добавленные символы "<BR>" (конец строки HTML), чтобы текст правильно отображался в браузере:

#!/home/path/public_html/rebol/rebol -cs
REBOL []
print "content-type: text/html^/"
print [<HTML><HEAD><TITLE>"Available Appointment Times"</TITLE>]
print [</HEAD><BODY>]
page-list: [
    http://website.com/teacher/ryan 
    http://website.com/teacher/mark 
    http://website.com/teacher/nick 
    http://website.com/teacher/peter 
    http://website.com/teacher/tom 
    http://website.com/teacher/rudi
]
foreach page page-list [
    print [<BR><BR>]
    print to-string page 
    print [<BR>]
    data: read/lines page
    week: ["MONDAY" "TUESDAY" "WEDNESDAY" "THURSDAY" "FRIDAY"
        "SATURDAY" "SUNDAY"]
    foreach line data [
        foreach day week [
            if find line day [print [<BR>] print line print [<BR><BR>]]
        ]
        if find line "----" [print line print [<BR>]]
    ]
] 
print [</BODY></HTML>]

По мере того как в систему планирования добавлялось больше учителей, стало очевидно, что версия редактора CGI будет полезна (для использования на мобильных телефонах, на работе и в других средах, где установка исполняемого файла была проблематичной). Для этого я просто использовал защищённый паролем текстовый онлайн-редактор, найденный ранее в этом руководстве:

#!/home/path/public_html/rebol/rebol -cs
REBOL []
print "content-type: text/html^/"
print [<HTML><HEAD><TITLE>"Edit Schedule"</TITLE></HEAD><BODY>]

; submitted: decode-cgi system/options/cgi/query-string

; Мы не можем использовать указанную выше обычную строку для 
; декодирования, потому что мы используем метод post для отправки 
; данных (поскольку данные из текстового поля могут стать слишком 
; большими для метода get). Вместо этого используйте следующую 
; функцию для обработки данных из почтового метода:
read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]

submitted: decode-cgi read-cgi

; если файл schedule.txt был отредактирован и отправлен:

if ((submitted/2 = "save") or (submitted/2 = "save")) [ 
    ; save newly edited schedule:
    write to-file rejoin ["./" submitted/6 "/schedule.txt"] submitted/4
    print ["Schedule Saved."]
    print rejoin [
       {<META HTTP-EQUIV="REFRESH" CONTENT="0;
           URL=http://website.com/folder/}
       submitted/6 {">}
    ]
    quit
]

; если пользователь только открывает страницу (т.е. данные ещё не 
; отправлены), запросите пользователя / пароль:

if ((submitted/2 = none) or (submitted/4 = none)) [
    print [<strong>"W A R N I N G  -  "]
    print ["Private Server, Login Required:"</strong><BR><BR>]
    print [<FORM ACTION="./edit.cgi">]
    print [" Username: " <input type=text size="50" name="name"><BR><BR>]
    print [" Password: " <input type=text size="50" name="pass"><BR><BR>]
    print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">]
    print [</FORM>]
    quit
]

; проверьте пользователя/пароль на соответствие тем, что в 
; Teacherlist.txt, завершите, если неверно:

teacherlist: load %teacherlist.txt
folder: submitted/2 
password: submitted/4
response: false
foreach teacher teacherlist [
    if ((first teacher) = folder) and (password = (second teacher)) [
        response: true
    ]
]
if response = false [print "Incorrect Username/Password." quit]

; если пользователь/пароль верны, то
; создаём резервную копию (до изменения чего бы то не было):

cur-time: to-string replace/all to-string now/time ":" "-"
schedule_text: read to-file rejoin ["./" folder "/schedule.txt"]
write to-file rejoin [
    "./" folder "/" now/date "_" cur-time ".txt"] schedule_text

print [<strong>"Be sure to SUBMIT when done:"</strong><BR><BR>]
print [<FORM method="post" ACTION="./edit.cgi">]
print [<INPUT TYPE=hidden NAME=submit_confirm VALUE="save">]
print [<textarea cols="100" rows="15" name="contents">]
print [schedule_text]
print [</textarea><BR><BR>]
print rejoin [{<INPUT TYPE=hidden NAME=folder VALUE="} folder {">}]
print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">]
print [</FORM>]
print [</BODY></HTML>]

Теперь у всех учителей есть возможность редактировать свои расписания. Я могу добавить нового учителя в систему примерно за 5 секунд (просто создайте новый каталог на сервере и скопируйте пустые файлы schedule.txt и index.php). Любой, кто участвует в планировании, может вносить изменения онлайн, независимо от местоположения или типа компьютера, и все остаются синхронизированными. Кроме того, любой может мгновенно увидеть всё доступное время приёма "с высоты птичьего полёта" - это очень помогает при планировании новых студентов и поддержании повседневной деятельности.

22.10 Случай: CGI-программа онлайн-страницы участника

Один из моих друзей хотел создать онлайн-базу данных членов местного клуба. Он хотел, чтобы участники могли регистрироваться и добавлять свою контактную информацию, загружать фотографии и добавлять информацию о себе. Он устал вручную вносить изменения на страницы участников и хотел, чтобы пользователи могли добавлять, редактировать и удалять свою собственную информацию. Ему нужен был доступ с базовым паролем, чтобы пользователи могли редактировать только свою собственную информацию, и ему нужна была серверная утилита, которая позволяла ему вносить изменения как администратор и которая автоматически сохраняла каждое последующее изменение в базе данных, чтобы никакие данные никогда не могли быть потерянный. Он также хотел, чтобы пользователи автоматически отправляли свой пароль по электронной почте на случай, если они забудут.

Вот мой основной мыслительный процесс и план атаки:

  1. Это будет онлайн-система (веб-сайт), поэтому пользовательский интерфейс будет представлять собой набор HTML-страниц, отображающих информацию о каждом пользователе, а также набор HTML-форм для ввода информации пользователями. Мы решили, что на странице будут отображаться следующие поля: имя, адрес, телефон, электронная почта, возраст, язык, рост, дата добавления пользователя и загруженная фотография.
  2. Данные будут храниться в какой-то онлайн-базе данных. Поскольку это небольшая группа, состоящая всего из нескольких пользователей, я решил создать простую базу данных с плоскими файлами - просто текстовый файл, заполненный блоками данных REBOL, по одному блоку на пользователя, которые хранятся на веб-сервере.
  3. Страница, которая извлекает информацию из базы данных и отображает её в указанном выше HTML, будет в основном приложением REBOL CGI, которое запускает цикл foreach для печати каждой записи в указанном выше формате HTML. Страницы, на которых пользователи вводят свою информацию, будут формами, которые отправляют информацию в REBOL CGI, который добавляет её в текстовый файл базы данных. Страницы, на которых пользователи редактируют свою информацию, будут формами, которые отображают информацию, которая в данный момент находится в выбранной записи, без пароля. Когда пользователь отправляет новый пароль и обновлённую информацию, CGI проверяет, соответствует ли отправленный пароль существующему паролю для этой записи, а затем заменяет старый блок новым в текстовом файле базы данных. Здесь также будет размещён код для отправки пользователю забытого пароля по электронной почте и для автоматического резервного копирования данных.
  4. Также необходимо создать страницу загрузки/обновления изображения. Это будет форма HTML, которая принимает локальный файл изображения на компьютере пользователя, отправляет этот файл в CGI, который, в свою очередь, записывает эти двоичные данные в каталог на веб-сервере, создаёт ссылку на изображение HTML и добавляет это ссылка на соответствующую запись пользователя в текстовом файле базы данных.
  5. Серверная часть будет просто защищённым паролем текстовым редактором, рассмотренным в примере № 8, со ссылками на все текстовые файлы резервных копий для лёгкого восстановления (копирования/вставки) потерянных данных.

Вот базовый макет HTML, который я придумал для шага 1. Каждая запись в базе данных будет отображаться с использованием этого шаблона:

<HR><BR> Date/Time: 23-Mar-2008/13:11:42-7:00

<A HREF="./index.cgi?function=">edit | </A>
<A HREF="./index.cgi?function=">delete</A>

<TABLE background="" border=0 cellPadding=2 cellSpacing=2 
    width="100%"><TR>

<TD width = "600">
<BR>
<FONT FACE="Courier New, Courier, monospace">Name:          </FONT>
<STRONG>The User Name Goes Here</STRONG>
<BR>
<FONT FACE="Courier New, Courier, monospace">Address:       </FONT>
<STRONG>The Address Goes Here</STRONG>
<BR>
<FONT FACE="Courier New, Courier, monospace">Phone:         </FONT>
<STRONG>The Phone</STRONG>
<BR>
<FONT FACE="Courier New, Courier, monospace">Email:         </FONT>
<STRONG>The Email</STRONG>
<BR>
<FONT FACE="Courier New, Courier, monospace">Age:           </FONT>
<STRONG>The Age</STRONG>
<BR>
<FONT FACE="Courier New, Courier, monospace">Language:      </FONT>
<STRONG>The Language</STRONG>
<BR>
<FONT FACE="Courier New, Courier, monospace">Height:        </FONT>
<STRONG>The Height</STRONG>
<BR><BR>
</TD>

<TD width="170" valign="center">
<A HREF="./default.jpg" target=_blank><IMG align=baseline alt=""
    border=0 hspace=0 src="./default.jpg" width="160" height="120">
</A>
</TD>

</TR></TABLE>

Some Additional Notes Go Here...

<BR><BR>

Проект базы данных для шага 2 создать ещё проще. Вот пример того, как выглядит каждый блок. Обратите внимание, что каждая запись в базе данных - это просто текстовая строка, разделённая пробелами, для каждого поля информации, которое мы хотим отображать на странице участника. В блок я добавил ссылку на изображение по умолчанию, на случай, если пользователь не загрузил собственное фото. Этот файл был сохранен как %bb.db:

["Username" "19-Feb-2008/4:55:59-8:00" "1 Address St."
    "123-456-7890" "name@website.com" "40" 
    {REBOL, C, C++, Python, PHP, Basic, AutoIt, others...}
    "6'" {I'm a nobody - just a test account.} "password" 
    [
        {<a href = "./default.jpg" target=_blank>
        <IMG align=baseline alt="" border=0 hspace=0
        src="./default.jpg" width="160" height="120"></a>}
    ]
] 

["Tester McUser" "22-Feb-2008/13:14:44-8:00" "1 Way Lane"
    "234-567-8910" "tester@website.com" "35" "REBOL"
    {5' 11"} "I'm just another test account." "password"
    [
        {<a href = "./files/photo.jpg" target=_blank>
        <IMG align=baseline alt="" border=0 hspace=0
        src="./files/photo.jpg" width="160" height="120"></a>}
    ]
]

На этом этапе я мог бы начать работу на шаге 3, создав программу CGI, которая печатает HTML-страницу на шаге 1 с указанными выше данными. Вот простой сценарий CGI, который просто распечатывает дизайн HTML вместе с записями из вставленной базы данных:

#! /home/path/public_html/rebol/rebol -cs
REBOL []
print "content-type: text/html^/"
print read %header.html

bbs: load %bb.db  ; load the database info

print [<center><table border=1 cellpadding=10 width=90%><tr><td>]
print {<TABLE background="" border=0 cellPadding=0 cellSpacing=0
    height="100%" width="100%"><tr><td width = "600">}
print [<hr>]
reverse bbs
foreach bb bbs [
    print [<BR>]
    print rejoin ["Date/Time: " bb/2]
    print "                   "
    print rejoin [{<a href="./index.cgi?function=">edit | </a>}]
    print rejoin [{<a href="./index.cgi?function=">delete</a>}]
    print "         "
    print {<TABLE background="" border=0 cellPadding=2 
        cellSpacing=2 height="100%" width="100%"><tr>
        <td width = "600"><BR>}
    print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
        "Name:          </FONT><strong>" bb/1 "</strong>"]
    print [<BR>]
    print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
         "Address:       </FONT><strong>" bb/3 "</strong>"]
    print [<BR>]
    print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
        "Phone:         </FONT><strong>" bb/4 "</strong>"]
    print [<BR>]
    print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
        "Email:         </FONT><strong>" bb/5 "</strong>"]
    print [<BR>]
    print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
        "Age:           </FONT><strong>" bb/6 "</strong>"]
    print [<BR>]
    print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
         "Language:      </FONT><strong>" bb/7 "</strong>"]
    print [<BR>]
    print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
         "Height:        </FONT><strong>" bb/8 "</strong>"]
    print [<BR><BR>]
    print </td>
    print {<td width = "170" valign = "center">}
    print bb/11 ; image link
    print {</td></tr></table>}
    print bb/9  ; "other information " text
    print [<BR><BR><HR>]
]
print [</td></tr></td></tr></td></tr></table>]
print [</td></tr></table></center>]
print read %footer.html

В этом коде был ряд функций, которые, как я понял, следует добавить. Во-первых, я хотел изменить адреса электронной почты, чтобы их меньше собирали спам-боты. Эта строка кода выполняет свою работу достаточно хорошо для моих нужд. Он превращает "name@address.com" в "name at address dot com:

(replace/all (replace bb/5 "@" " at ") "." " dot ")

Я также хотел, чтобы любые ссылки http:// в разделе "другая информация" были автоматически преобразованы. Для этого я использовал синтаксический анализ для поиска "http://" и символа конечного пробела, а затем заключил эту ссылку в требуемые теги <A HREF= ...>. Вот код:

bb_temp: copy bb/9
bb_temp2: copy bb_temp
parse/all bb_temp [any [
    thru "http://" copy link to " " (replace bb_temp2 
        (rejoin [{http://} link]) (rejoin [
            { <a href="} {http://} link 
            {" target=_blank>http://} link {</a> }]))
        ] 
    to end
]

Кроме того, я хотел, чтобы окончания строк в разделе "Другая информация" автоматически преобразовывались в HTML "<br>", чтобы они правильно отображались на веб-странице. Это просто:

replace/all bb_temp newline "  <br>  "

Мой друг хотел, чтобы отображалось общее количество участников. Это тоже легко, с "length?"

print rejoin [{<font size=5> Members:  (} length? bbs {)</font></td>}]

Я также добавил ссылку "присоединиться сейчас" к странице CGI, где пользователи смогут добавлять себя в базу данных (эта страница ещё не создана):

print {<td><a href="./add.cgi">Join Now</a></td></tr></table><BR>}

Чтобы пользователи могли редактировать/удалять свою информацию позже, мне нужно было пометить каждую отображаемую запись уникальным номером, чтобы автоматически выбирать соответствующий блок из базы данных. Для этого я добавил в цикл foreach переменную счётчика и увеличивал её каждый раз в цикле (counter: counter + 1). Затем я заменил общие ссылки редактирования и удаления в приведённом выше коде. . .

print rejoin [{<a href="./index.cgi?function=">edit | </a>}]
print rejoin [{<a href="./index.cgi?function=">delete</a>}]

. . . со ссылками, которые содержат счётчик и которые могут быть расшифрованы программой CGI как "get" данные:

print rejoin [
    {<a href="./index.cgi?function=edititemnumber&messagenumber=}
    counter {&Submit=Post+Message">edit | </a>}
]
print rejoin [
    {<a href="./index.cgi?function=deleteitemnumber&messagenumber=}
    counter {&Submit=Post+Message">delete</a>}
]

Вот сценарий в его нынешнем виде:

#! /home/path/public_html/rebol/rebol -cs
REBOL []
print "content-type: text/html^/"
print read %header.html

bbs: load %bb.db

print [<center><table border=1 cellpadding=10 width=90%><tr><td>]

print-all: does [
    print {<TABLE background="" border=0 cellPadding=0 cellSpacing=0
        height="100%" width="100%"><tr><td width = "600">}
    print rejoin [{<font size=5> Members:  (} length? bbs {)</font></td>}]
    print {<td><a href="./add.cgi">Join Now</a></td></tr></table><BR>}
    print [<hr>]
    counter: 1
    reverse bbs
    foreach bb bbs [
        print [<BR>]
        if bb/1 <> "file uploaded" [
            print rejoin ["Date/Time: " bb/2]
            print "                   "
            print rejoin trim [
                {<a href=
                "./index.cgi?function=edititemnumber&messagenumber=}
                 counter 
                {&Submit=Post+Message">edit | </a>}
            ]
            print rejoin trim [
                {<a href=
                "./index.cgi?function=deleteitemnumber&messagenumber=}
                counter 
                {&Submit=Post+Message">delete</a>}
            ]
            print "         "
            print {
                    <TABLE background="" border=0 cellPadding=2 
                    cellSpacing=2 height="100%" width="100%"><tr>
                    <td width = "600"><BR>
            }
            print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
                "Name:          </FONT><strong>" bb/1 "</strong>"]
            print [<BR>]
            print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
                 "Address:       </FONT><strong>" bb/3 "</strong>"]
            print [<BR>]
            print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
                "Phone:         </FONT><strong>" bb/4 "</strong>"]
            print [<BR>]
            print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
                "Email:         </FONT><strong>"
                (replace/all (replace bb/5 "@" " at ") "." " dot ") 
                "</strong>"
            ]
            print [<BR>]
            print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
                "Age:           </FONT><strong>" bb/6 "</strong>"]
            print [<BR>]
            print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
                 "Language:      </FONT><strong>" bb/7 "</strong>"]
            print [<BR>]
            print rejoin [{<FONT FACE="Courier New, Courier, monospace">}
                 "Height:        </FONT><strong>" bb/8 "</strong>"]
            print [<BR><BR>]
        ]
        ; automatically convert line endings to HTML " <br>"
        bb_temp: copy bb/9
        replace/all bb_temp newline "  <br>  "
        bb_temp2: copy bb_temp
        ; automatically link any urls starting with http://
        append bb_temp " "
        parse/all bb_temp [any [
            thru "http://" copy link to " " (replace bb_temp2 
                (rejoin [{http://} link]) (rejoin [
                    { <a href="} {http://} link 
                    {" target=_blank>http://} link {</a> }]))
                ] 
            to end
        ]
        print </td>
        print {<td width = "170" valign = "center">}
        print bb/11 ; image link
        print {</td></tr></table>}
        print bb_temp2
        print [<BR><BR><HR>]
        counter: counter + 1    
    ]
    print [</td></tr></td></tr></td></tr></table>]
]
print-all    
print [</td></tr></table></center>]
print read %footer.html

Приведённая выше страница была сохранена как index.cgi и служит главной отображаемой страницей для сайта. Чтобы посетители всегда просматривали новую копию этой страницы, я также создал следующую страницу index.html, которая просто обновляет страницу index.cgi. Используя эту страницу index.html в качестве основной ссылки (и делая этот HTML-файл страницей по умолчанию для веб-сайта), посетители всегда автоматически видят обновлённое представление страницы участника со всеми внесёнными изменениями/обновлениями:

<html>
<head>
<title></title>
<META HTTP-EQUIV="REFRESH" CONTENT="0; URL=./index.cgi">
</head>
<body bgcolor="#FFFFFF">
</body>
</html>

Затем мне нужно было создать форму для ввода пользователями информации о участниках. Это было сохранено как add.cgi. Форма отправляет любую отправленную информацию обратно на index.cgi.

#! /home/path/public_html/rebol/rebol -cs    
REBOL []
print "content-type: text/html^/"
print read %header.html

print [<center><table border=1 cellpadding=10 width=90%><tr><td>]
print [<font size=5>" Add New Member Information:"</font>]
print "         "
print "         "
print "         "
print [<hr>]
print [<FORM method="post" ACTION="./index.cgi">]
print [<br>" Your Name: " <br><input type=text size="60"
    name="username"><BR>]
print [<br>" Password (required to edit member info later): " <br>
    <input type=text size="60" name="password"><BR>]
print [<br>" Address: " <br><input type=text size="60" name="address">
    <BR>]
print [<br>" Phone: " <br><input type=text size="60" name="phone"><BR>]
print [<br>" Email: " <br><input type=text size="60" name="email"><BR>]
print [<br>" Age: " <br><input type=text size="60" name="age"><BR>]
print [<br>" Language: " <br><input type=text size="60" name="language">
    <BR>]
print [<br>" Height: " <br><input type=text size="60" name="height"><BR>
    <BR>]
print [" Other Information/Notes: " <br>]
print [<textarea name=otherinfo rows=5 cols=50></textarea><BR><BR>]
print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Post New Member Info">]
print [</FORM>]

print [</td></tr></table></center>]
print read %footer.html

Я интегрировал следующий код в index.cgi, чтобы прочитать и добавить информацию из приведённой выше формы в базу данных:

; вот код по умолчанию, используемый для чтения любых данных из 
; HTML-формы:

read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]
submitted: decode-cgi read-cgi

; убедитесь, что были введены хотя бы имя пользователя и пароль:

if submitted/2 <> none [
    if (submitted/2 = "") or (submitted/4 = "") [
    print {
        <strong>You must include at least
         a name and password.</strong>
        <br><br>Press the [BACK] button 
        in your browser to try again.
    }
    print [</td></tr></table></center>]
    print read %footer.html
    halt
]

; теперь создайте новый блок записи для добавления в базу данных:

entry: copy []
append entry submitted/2     ; имя
; the time on the server is 3 hours different then our local time:
append entry to-string (now + 3:00)
append entry submitted/6      ; адрес
append entry submitted/8      ; телефон
append entry submitted/10     ; email
append entry submitted/12     ; возраст
append entry submitted/14     ; язык
append entry submitted/16     ; рост
append entry submitted/18     ; другая информация
append entry submitted/4      ; пароль
append/only entry [
    {<a href = "./default.jpg" target=_blank>
    <IMG align=baseline alt="" border=0 hspace=0 src="./default.jpg"
    width="160" height="120"></a>}
]

; добавим новую запись в базу данных и уведомим пользователя:

append/only tail bbs entry
save %bb.db bbs
print {<strong>New Member Added.</strong>
    Click "Edit" to upload a photo.}
print [</td></tr></table></center>]
print read %footer.html

; теперь отображать страницу участника с обновлённой информацией:

wait :00:04
refresh-me: {
    <head><title></title>
    <META HTTP-EQUIV="REFRESH" CONTENT="0; URL=./index.html"></head>
}
print refresh-me
quit

Теперь мы можем закончить оставшуюся часть работы на шаге 3 нашего плана. Псевдокод в моей схеме гласит: "Страницы, на которых пользователи редактируют свою информацию, будут формами, отображающими информацию, которая в данный момент находится в выбранной записи, без пароля. Когда пользователь отправляет новый пароль и обновлённую информацию, CGI проверяет, что отправленные пароль совпадает с существующим паролем для этой записи, а затем заменяет старый блок новым в текстовом файле базы данных". Я уже создал ссылки в index.html для ссылки на "edititemnumber" (созданный ранее с использованием переменной счётчика в цикле foreach index.cgi). И мы уже создали базовую форму ввода данных для добавления новых пользователей. Таким образом, мы можем проверить номер редактируемого элемента и заполнить форму соответствующими элементами из базы данных. Чтобы найти и заменить исходную запись в базе данных, после того, как пользователь внёс изменения, исходные значения также должны быть отправлены как дополнительные скрытые поля формы вместе с редактируемыми пользователем значениями в текстовых полях формы. Вот что я придумал:

if submitted/2 = "edititemnumber" [
    ; выберите правильную запись из базы данных, используя 
    ; переданную переменную счётчика по ссылке "edit" в index.cgi:
selected-block: pick bbs (
    (length? bbs) - (to-integer submitted/4) + 1
)
print [<font size=5>" Edit Your Existing Member Information:"</font>]
print "         "
; вот ссылка, которая нам понадобится для раздела схемы, который 
; разрешает загрузку изображений:
print rejoin [
    {<a href="./upload.cgi?name=} first selected-block 
    {">Upload Image (Add or Change)</a><hr>}
]
print "         "
print "<br><br>"
print {<strong><i>PASSWORD REQUIRED TO EDIT! </i></strong> 
    (Enter it in the field below.)}
print "<br><br>"
print [<FORM method="post" ACTION="./edit.cgi">]
print rejoin [
    {<br> Your Name:  <br>
        <input type=text size="60" name="username" value="}
     first selected-block {"><BR>}
]
print [<br> <strong> " Member Password " </strong> "(same 
    as when you created the original account): " <br>
    <input type=text size="60"     name="password"><BR>
]
print rejoin [
    {<br> Address:  <br><input type=text size="60" 
        name="address" value="} 
    pick selected-block 3 {"><BR>}
]
print rejoin [
    {<br> Phone:  <br><input type=text size="60" 
        name="phone" value="} 
    pick selected-block 4 {"><BR>}
]
print rejoin [
    {<br> Email:  <br><input type=text size="60" 
        name="email" value="} 
    pick selected-block 5 {"><BR>}
]
print rejoin [
    {<br> Age:  <br><input type=text size="60" 
        name="age" value="} 
    pick selected-block 6 {"><BR>}
]
print rejoin [
    {<br> Language:  <br><input type=text size="60"
        name="language" value="} 
    pick selected-block 7 {"><BR>}
]
print rejoin [
    {<br> Height:  <br><input type=text size="60" 
        name="height" value="} 
    pick selected-block 8 {"><BR><BR>}
]
print [" Other Information/Notes: " <br>]
print [<textarea name=otherinfo rows=5 cols=50>]
print [pick selected-block 9]
print [</textarea><BR><BR>]
print rejoin [
    {<input type="hidden" name="original_username" value="}
     pick selected-block 1 {">}
]
print rejoin [
    {<input type="hidden" name="original_date" value="}
    pick selected-block 2 {">}
]
print rejoin [
    {<input type="hidden" name="original_address" value="}
     pick selected-block 3 {">}
]
print rejoin [
    {<input type="hidden" name="original_phone" value="}
    pick selected-block 4 {">}
]
print rejoin [
    {<input type="hidden" name="original_email" value="}
    pick selected-block 5 {">}
]
print rejoin [
    {<input type="hidden" name="original_age" value="} 
    pick selected-block 6 {">}
]
print rejoin [
    {<input type="hidden" name="original_language" value="} 
    pick selected-block 7 {">}
]
print rejoin [
    {<input type="hidden" name="original_height" value="} 
    pick selected-block 8 {">}
]
print rejoin [
    {<input type="hidden" name="original_otherinfo" value="} 
    pick selected-block 9 {">}
]
print [<INPUT TYPE="SUBMIT" NAME="Submit" 
    VALUE="Update Member Information">]
print [</FORM>]
print [</td></tr></table></center>]
print read %footer.html
quit
]

Я добавил приведённый выше код в index.cgi. Обратите внимание, что приведённая выше форма указывает на edit.cgi, который фактически выполняет работу по проверке пароля и обработке изменений в базе данных. Он имеет весь стандартный заголовок и код read-cgi, а затем он использует цикл foreach для поиска записи в базе данных, которая содержит все те же данные, что и данные, представленные скрытыми элементами в форме выше, и проверяет исходный пароль в этой форме. Вход. Сравнивая исходный пароль с паролем, введённым пользователем, я также включил пароль администратора "blahblah". Я также добавил код для отправки пользователям их пароля по электронной почте на случай, если они его забыли (просто отправьте сохранённый пароль на адрес электронной почты, содержащийся в базе данных, для этой записи):

#! /home/path/public_html/rebol/rebol -cs    
REBOL []
print "content-type: text/html^/"
print read %header.html

bbs: load %bb.db

read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]
submitted: construct decode-cgi read-cgi

; получим пароль из представленной записи:

foreach message bbs [
    if all [
        find message submitted/original_username 
        find message submitted/original_date 
        find message submitted/original_address 
        find message submitted/original_phone 
        find message submitted/original_email 
        find message submitted/original_age 
        find message submitted/original_language 
        find message submitted/original_height 
        find message submitted/original_otherinfo
    ] [read-pass: message/10]
]

; сохраняем старый блок:

old-message:  to-block reduce [
    submitted/original_username 
    submitted/original_date 
    submitted/original_address 
    submitted/original_phone 
    submitted/original_email 
    submitted/original_age 
    submitted/original_language 
    submitted/original_height
    submitted/original_otherinfo 
    read-pass
]

; чтобы исходный проход не заменялся на "бла-бла":

either submitted/password = "blahblah" [
    entered-pass: read-pass
] [
    entered-pass: submitted/password
]

; создаём новую запись для базы данных:

new-message:  to-block reduce [
    submitted/username 
    submitted/original_date
    submitted/address
    submitted/phone
    submitted/email
    submitted/age
    submitted/language
    submitted/height
    submitted/otherinfo
    entered-pass
]

; проверим пароль и заменим:

if submitted/password <> "" [
    either (
        read-pass = submitted/password
    ) or (
        submitted/password = "blahblah"
    ) [
        foreach message bbs [replace message old-message new-message]
    ] [
        print {
            <strong>Forgot your member password?</strong> <br><br>
            It's being emailed to the address for this entry, right now...
            Wait for this page to refresh, then <strong>check your email!
            </strong>
        }
        print read %footer.html
        wait 3
        set-net [user@website.com smtp.website.com]
        send (to-email submitted/original_email) (to-string rejoin [
            "Forgot your member password?" newline newline 
            trim {Someone was editing an entry with this email address, 
                but the incorrect password was used.  Here is the correct
                password, in case you've forgotten:}
             newline newline read-pass
        ])
    ]
]
save %bb.db bbs

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

refresh-me: {
    <head><title></title>
    <META HTTP-EQUIV="REFRESH" CONTENT="0; URL=./index.cgi"></head>
}
print refresh-me
print read %footer.html

Здесь я решил добавить резервный код. Я создал папку для всех предыдущих версий текстового файла базы данных, которые будут сохранены в виде резервных копий. Затем я создал текстовый файл, содержащий номер текущего файла резервной копии (для начала, этот текстовый файл просто содержал номер 1). Затем я увеличил это число и сохранил его обратно в файл с номерами. И, наконец, я сохранил копию текущей базы данных в текстовый файл, добавив номер текущей резервной копии к имени файла. Этот код был выполнен прямо перед тем, как bb.db был сохранен в CGI выше:

backup-num: load %backup-num.txt
backup-num: backup-num + 1
write %backup-num.txt backup-num
filename: to-file rejoin ["./backup/bb-" (to-string backup-num) ".txt"]
save filename bbs

Следующий код в основном представляет собой более простую версию кода редактирования, приведённого выше, который позволяет пользователям удалять запись. Все, что нужно в этом случае, - это имя пользователя и пароль. Вся остальная информация передаётся в delete.cgi как скрытые поля. Этот код добавляется в index.cgi:

if submitted/2 = "deleteitemnumber" [
    selected-block: pick bbs (
        (length? bbs) - (to-integer submitted/4) + 1
    )
    print [<font size=5>" Delete An Existing Member Account:"</font><hr>]
    print [<FORM method="post" ACTION="./delete.cgi">]
    print rejoin [
        {<br> Your Name:  <br>
            <input type=text size="60" name="username" value="} 
        first selected-block {"><BR>}
    ]
    print [<br>" Member Password (
        same as when you created the original account): " 
        <br><input type=text size="60"     name="password"><BR><BR>
    ]
    print rejoin [
        {<input type="hidden" name="original_username" value="} 
        pick selected-block 1 {">}
    ]
    print rejoin [
        {<input type="hidden" name="original_date" value="} 
        pick selected-block 2 {">}
    ]
    print rejoin [
        {<input type="hidden" name="original_address" value="} 
        pick selected-block 3 {">}
    ]
    print rejoin [
        {<input type="hidden" name="original_phone" value="} 
        pick selected-block 4 {">}
    ]
    print rejoin [
        {<input type="hidden" name="original_email" value="} 
        pick selected-block 5 {">}
    ]
    print rejoin [
        {<input type="hidden" name="original_age" value="} 
        pick selected-block 6 {">}
    ]
    print rejoin [
        {<input type="hidden" name="original_language" value="} 
        pick selected-block 7 {">}
    ]
    print rejoin [
        {<input type="hidden" name="original_height" value="} 
        pick selected-block 8 {">}
    ]
    print rejoin [
        {<input type="hidden" name="original_otherinfo" value="} 
        pick selected-block 9 {">}
    ]
    print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Delete Member Info">]
    print [</FORM>]
    print [</td></tr></table></center>]
    print read %footer.html
    quit
]

Вот код для delete.cgi, на который указывает приведённая выше форма и который выполняет фактическую работу по удалению выбранного блока из базы данных (в основном это вариант сценария edit.cgi, приведённого выше):

#! /home/path/public_html/rebol/rebol -cs    
REBOL []
print "content-type: text/html^/"
print read %header.html

bbs: load %bb.db

read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]
submitted: construct decode-cgi read-cgi

foreach message bbs [
    if all [
        find message submitted/original_username 
        find message submitted/original_date 
        find message submitted/original_address 
        find message submitted/original_phone 
        find message submitted/original_email 
        find message submitted/original_age
        find message submitted/original_language 
        find message submitted/original_height 
        find message submitted/original_otherinfo
    ] [read-pass: message/10] 
]

old-message:  to-block reduce [
    submitted/original_username 
    submitted/original_date 
    submitted/original_address 
    submitted/original_phone 
    submitted/original_email 
    submitted/original_age 
    submitted/original_language 
    submitted/original_height 
    submitted/original_otherinfo 
    read-pass
]

if submitted/password <> "" [
    if (
        read-pass = submitted/password
    ) or (
        submitted/password = "blahblah"
    ) [    
        backup-num: load %backup-num.txt
        backup-num: backup-num + 1
        write %backup-num.txt backup-num
        filename: to-file rejoin [
            "./backup/bb-" (to-string backup-num) ".txt"
        ]
        save filename bbs

        foreach message bbs [replace message old-message ""]
    ]
]

remove-each message bbs [
    any [
        message = [""] 
        (all [
            message/1 = "" message/2 = "" message/3 = "" message/4 = ""
            message/5 = "" message/6 = "" message/7 = "" message/8 = ""
            message/9 = ""
            ]
        )
    ]
]

save %bb.db bbs

refresh-me: {
    <head><title></title>
    <META HTTP-EQUIV="REFRESH" CONTENT="0; URL=./index.cgi"></head>
}
print refresh-me
print read %footer.html

Создание страницы загрузки изображений для шага 4 в нашей схеме было немного сложной задачей. Это потому, что REBOL не имеет встроенного способа принимать двоичные данные из HTML-форм (в данном случае изображений), которые называются данными "form-multipart". Я просмотрел список рассылки и быстро нашёл решение на http://www.rebol.org/cgi-bin/cgiwrap/rebol/ml-display-thread.r?m=rmlKVSQ. "Decode-multipart-form-data" Андреаса Болка сделал именно то, что мне было нужно. Эта функция преобразует данные, введённые пользователем, а также файлы, которые он выбирает и загружает со своего жёсткого диска, в удобный и простой в использовании объект REBOL.

#! /home/path/public_html/rebol/rebol -cs
REBOL [Title: "HTTP File Upload"]
print "content-type: text/html^/"
print read %header.html

read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]

; вот волшебная функция Андреаса для чтения форм/составных данных:

decode-multipart-form-data: func [
    p-content-type
    p-post-data
    /local list ct bd delim-beg delim-end non-cr non-lf non-crlf mime-part
] [
    list: copy []
    if not found? find p-content-type "multipart/form-data" [return list]

    ct: copy p-content-type
    bd: join "--" copy find/tail ct "boundary="
    delim-beg: join bd crlf
    delim-end: join crlf bd

    non-cr:     complement charset reduce [ cr ]
    non-lf:     complement charset reduce [ newline ]
    non-crlf:   [ non-cr | cr non-lf ]
    mime-part:  [
        ( ct-dispo: content: none ct-type: "text/plain" )
        delim-beg ; mime-part start delimiter
        "content-disposition: " copy ct-dispo any non-crlf crlf
        opt [ "content-type: " copy ct-type any non-crlf crlf ]
        crlf ; content delimiter
        copy content
        to delim-end crlf ; mime-part end delimiter
        ( handle-mime-part ct-dispo ct-type content )
    ]

    handle-mime-part: func [
        p-ct-dispo
        p-ct-type
        p-content
        /local tmp name value val-p
    ] [
        p-ct-dispo: parse p-ct-dispo {;="}

        name: to-set-word (select p-ct-dispo "name")
        either (none? tmp: select p-ct-dispo "filename")
               and (found? find p-ct-type "text/plain") [
            value: content
        ] [
            value: make object! [
                filename: copy tmp
                type: copy p-ct-type
                content: either none? p-content [none] [copy p-content]
            ]
        ]

        either val-p: find list name
            [change/only next val-p compose [(first next val-p) (value)]]
            [ append list compose [ (to-set-word name) (value) ] ]
    ]

    use [ ct-dispo ct-type content ] [
        parse/all p-post-data [ some mime-part "--" crlf ]
    ]

    list
]

; теперь мы можем поместить загруженный двоичный файл и весь текст, 
; введённый пользователем через HTML-форму, в объект REBOL. мы можем 
; ссылаться на загруженную фотографию, используя синтаксис: 
; cgi-object/photo/content

post-data: read-cgi
cgi-object: construct decode-multipart-form-data (
    system/options/cgi/content-type copy post-data
)

; Я создал подкаталог "./files" для хранения этих изображений. 
; Теперь запишите файл на веб-сервер, используя исходное имя файла, 
; но без каких-либо символов пути Windows, и уведомите об 
; использовании:   

adjusted-filename: copy cgi-object/photo/filename
adjusted-filename: replace/all adjusted-filename "/" "-"
adjusted-filename: replace/all adjusted-filename "\" "-"
adjusted-filename: replace/all adjusted-filename " " "_"
adjusted-filename: replace/all adjusted-filename ":" "_"
adjusted-filename: to-file rejoin ["./files/" adjusted-filename]
write/binary adjusted-filename cgi-object/photo/content
print [<strong>]
print {Upload Complete.  }
print [</strong>]
print [<br><br>]

; теперь добавьте HTML-ссылку на этот файл в базу данных

bbs: load %bb.db    
entry: copy []
link-added: rejoin [
    {<a href = "} to-string adjusted-filename {" target=_blank>}
    {<IMG align=baseline alt="" border=0 hspace=0 src="} 
    to-string adjusted-filename 
    {" width="160" height="120">} </a>
]  ; display image inline
append entry link-added
foreach message bbs [
    if (all [
        cgi-object/username = message/1 
        cgi-object/password = message/10
    ]) [
        if ((length? message) < 11) [append message ""] 
        message/11: entry
    ]
]
save %bb.db bbs

; показать дополнения, обновив страницу index.cgi:

refresh-me: {
    <head><title></title>
    <META HTTP-EQUIV="REFRESH" CONTENT="0; URL=./index.cgi"></head>
}
print refresh-me
print read %footer.html

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

#! /home/path/public_html/rebol/rebol -cs    
REBOL []
print "content-type: text/html^/"
print [<HTML><HEAD><TITLE>"Edit Database!!!"</TITLE></HEAD><BODY>]

read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]

submitted: decode-cgi read-cgi

; если файл schedule.txt был отредактирован и отправлен:

if ((submitted/2 = "save") or (submitted/2 = "save")) [ 
    ; save newly edited schedule:
    write %./bb.db submitted/4
    print ["Database Saved."]
    ; print {<META HTTP-EQUIV="REFRESH" CONTENT="0; URL=./bb.db">}
    quit
]

; если пользователь только открывает страницу (т.е. данные ещё не
; отправлены), запросить имя пользователя/пароль:

if ((submitted/2 = none) or (submitted/4 = none)) [
    print [<strong>"W A R N I N G  -  Private Server, Login Required:"
        </strong><BR><BR>]
    print [<FORM ACTION="./editor.cgi">]
    print [" Username: " <input type=text size="50" name="name"><BR><BR>]
    print [" Password: " <input type=text size="50" name="pass"><BR><BR>]
    print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">]
    print [</FORM>]
    quit
]

; проверим имя пользователя/пароль, завершимся, если они неверны:

response: false
if ((submitted/2 = "username") and (submitted/4 = "password")) [
    response: true
]
if response = false [print "Incorrect Username/Password." quit]

; если пользователь/пароль в порядке, продолжим (резервное 
; копирование перед внесением изменений):

cur-time: to-string replace/all to-string now/time ":" "-"
schedule_text: read %./bb.db
write to-file rejoin [
    "./backup/" now/date "_" cur-time ".txt"
] schedule_text

; вот форма, которая позволяет пользователю редактировать текст:

print [<center>]
print [<strong>"Be sure to click [SUBMIT] when done:"</strong><BR><BR>]
print [<strong>"(This will OVERWRIGHT the current database!)"</strong>
    <BR><BR>]
print [<FORM method="post" ACTION="./editor.cgi">]
print [<INPUT TYPE=hidden NAME=submit_confirm VALUE="save">]
print [<textarea cols="100" rows="25" name="contents">]
print [schedule_text]
print [</textarea><BR><BR>]
print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">]
print [</FORM>]
print [</center>]
print {<br><br><br><br><br><br><br><br><hr>}

; вот связанный список всех файлов резервных копий, доступных для 
; копирования/вставки:

foreach file (read %./backup/) [
    print rejoin [
        {<a href="./backup/} file {" target=_blank>} file {</a> }]
    ]
print [</BODY></HTML>]

Вот и все - веб-сайт и все его функции завершены! Вы можете увидеть живую демонстрацию на http://guitarz.org/tester и загрузить полный набор скриптов для этого тематического исследования на http://guitarz.org/tester/member_board.zip.

22.11 Случай: Календарь событий CGI

Моему другу так понравилась описанная выше система, что мы адаптировали её для использования в качестве страницы онлайн-объявлений, а также в качестве списка календаря событий на том же веб-сайте. Для календаря мы просто изменили поля базы данных на: Событие, Дата/время, Местоположение, Имя контактного лица, Контактный телефон, Контактный адрес электронной почты, Требования. Ссылки и отображаемый текст, например "Присоединяйтесь сейчас", были просто изменены на "Войти в новое событие" и т.д.

Календарь использовался довольно долго и прекрасно функционировал, когда мой друг спросил, могу ли я создать страницу события, которая на самом деле выглядела бы как обычное отображение календаря, а не просто список событий. Итак, вот как я разбил основной творческий процесс:

  1. Создайте HTML-страницу, похожую на календарь. Моя руководящая мысль заключалась в том, что программа CGI, распечатывающая эту страницу, будет включать цикл, который проходит через дни текущего месяца и печатает строки и ячейки таблицы HTML для каждого пронумерованного дня, по одной строке на группу дней с воскресенья по субботу.
  2. Для каждого дня месяца, напечатанного в таблице выше, выполните поиск в базе данных дат, соответствующих текущей печатаемой ячейке таблицы, а затем распечатайте описание события (первый элемент в блоке для этого события) со ссылкой на событие. листинг страницы.

Как всегда, я начал процесс с поиска некоторого существующего кода, который может быть полезен в моем дизайне (всегда лучше не изобретать колесо). Моя работа сразу упростилась, когда я поискал "календарь" на rebol.org. Я быстро нашёл HTML-календарь Богдана Лехновски, который распечатывает HTML-календарь на текущий месяц. В нем используется конструкция таблицы, созданная циклами, как я и предполагал. Я прочитал код Богдана, сделал несколько комментариев по поводу того, что выполняет каждый раздел, и внёс некоторые изменения в дизайн таблиц, чтобы календарь растянулся на всю страницу браузера. Я также написал строку кода, чтобы визуально выделить текущий день (чтобы сегодняшняя дата всегда печаталась уникальным цветом). Вы можете увидеть исходный код по ссылке выше, и вот мои настройки и комментарии:

#! /home/path/public_html/rebol/rebol -cs    
REBOL []
print "content-type: text/html^/"
print {<HTML><HEAD><TITLE>Event Calendar</TITLE></HEAD><BODY>}

; печатаем месяц и год в заголовке:

date: now/date
html: copy rejoin [
    {<CENTER><TABLE border=1 valign=middle width=99% height=99%>
        <TR><TD colspan=7 align=center height=8%><FONT size=5>}
    pick system/locale/months date/month {  } date/year
    {</FONT></TD></TR><TR>}
]

; печатаем заголовок дней:

days: ["Sun" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat"]
foreach day days [
    append html rejoin [
        {<TD bgcolor="#206080" align=center width=10% height=5%>
        <FONT face="courier new,courier" color="FFFFFF" size="+1">}
        day 
        {</FONT></TD>}
    ]
]
append html {</TR><TR>}

; печатаем немесячные дни в начале месяца серым цветом:

sdate: date  sdate/day: 0  
loop sdate/weekday // 7 + 1 [append html {<TD bgcolor=gray></TD>}]

; печатаем через день, выделив текущий день уникальным цветом:

while [sdate/day: sdate/day + 1 sdate/month = date/month][
    append html rejoin [
        {<TD bgcolor="#}
        ; Я ДОБАВИЛ ЭТОТ КОД, ЧТОБЫ ВИЗУАЛЬНО ВЫДЕЛАТЬ ТЕКУЩИЙ ДЕНЬ:
        either date/day = sdate/day ["AA9060"]["FFFFFF"]
        {" height=14% valign=top>} sdate/day
        {</TD>}
    ]
    if sdate/weekday = 6 [append HTML {</TR><TR>}]
]

; выводим немесячные дни в конце месяца серым цветом:

loop 7 - sdate/weekday [append html rejoin [{<TD bgcolor=gray></TD>}]]

; закончили печать:

append html {</TR></TABLE></CENTER></BODY></HTML>}
print html

Сделав шаг 1 в моей схеме, я завершил второй и последний шаг, добавив приведённый ниже код. Это было действительно просто. Во-первых, я создал переменную с названием "метки событий", которая будет содержать любые события в базе данных, произошедшие в данный день. Я поместил это в цикл while Богдана, который проходил через каждый день месяца и печатал ячейки таблицы календаря для каждого отдельного дня). Я использовал цикл foreach для сравнения каждой даты, найденной в базе данных, с текущей датой, добавляемой в календарь. Если есть совпадение, "метки событий" объединяются с первым элементом в записи события (описанием события) и связываются с отображением события. Строка текста в метках событий затем печатается в таблице в ячейке текущего дня.

while [sdate/day: sdate/day + 1 sdate/month = date/month][
    event-labels: {}
    foreach entry bbs [
        date-in-entry: 1-Jan-1001
        attempt [date-in-entry: (to-date entry/3)]
        if (date-in-entry = sdate) [
            event-labels: rejoin [
                {<font size=1>}
                event-labels
                "<strong><br><br>"
                {<a href="http://website.com/path/calendar">}
                entry/1 
                {</a>}
                "</strong>"
                {</font>}
            ]
        ]
    ]

Вот и все! Вот весь сценарий:

#! /home/path/public_html/rebol/rebol -cs
REBOL []
print "content-type: text/html^/"
print {<HTML><HEAD><TITLE>Event Calendar</TITLE></HEAD><BODY>}

bbs: load %bb.db
date: now/date
html: copy rejoin [
    {<CENTER><TABLE border=1 valign=middle width=99% height=99%>
        <TR><TD colspan=7 align=center height=8%><FONT size=5>}
    pick system/locale/months date/month {  } date/year
    {</FONT></TD></TR><TR>}
]

days: ["Sun" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat"]
foreach day days [
    append html rejoin [
        {<TD bgcolor="#206080" align=center width=10% height=5%>
        <FONT face="courier new,courier" color="FFFFFF" size="+1">}
        day 
        {</FONT></TD>}
    ]
]
append html {</TR><TR>}

sdate: date  sdate/day: 0  
loop sdate/weekday // 7 + 1 [append html {<TD bgcolor=gray></TD>}]

while [sdate/day: sdate/day + 1 sdate/month = date/month][
    event-labels: {}
    foreach entry bbs [
        date-in-entry: 1-Jan-1001
        attempt [date-in-entry: (to-date entry/3)]
        if (date-in-entry = sdate) [
            event-labels: rejoin [
                {<font size=1>}
                event-labels 
                "<strong><br><br>"
                {<a href="http://website.com/path/calendar">}
                entry/1 
                {</a>}
                "</strong>"
                {</font>}
            ]
        ]
    ]
    append html rejoin [
        {<TD bgcolor="#}
        either date/day = sdate/day ["AA9060"]["FFFFFF"]
        ; HERE, THE EVENTS ARE PRINTED IN THE APPROPRIATE DAY:
        {" height=14% valign=top>} sdate/day event-labels
        {</TD>}
    ]
    if sdate/weekday = 6 [append html {</TR><TR>}]
]

loop 7 - sdate/weekday [append html rejoin [{<TD bgcolor=gray></TD>}]]

append html {</TR></TABLE></CENTER></BODY></HTML>}
print html

22.12 Случай: медиаплеер (Wave/Mp3 Jukebox)

Это тематическое исследование началось, когда читатель этого учебника прислал мне вопрос по электронной почте. У него возникли проблемы с созданием простого сценария, который загружал бы имена файлов из каталога на его жёстком диске в текстовый список графического интерфейса. Он хотел иметь возможность нажимать на файлы .wav в списке, чтобы воспроизводить звуки. Обычно у меня нет времени отвечать на подобные вопросы, но этот был быстрым. Следующий код переключается на папку Windows media, читает список каталогов и отображает имена файлов в текстовом списке графического интерфейса пользователя:

change-dir %/c/Windows/media
view layout [text-list data read %.]

Я просто добавил содержимое функции "play-sound", найденной ранее в этом руководстве, в блок действий текстового списка. Это загружает содержимое значения, выбранного в текстовом списке (имя файла), и воспроизводит звук:

change-dir %/c/Windows/media
view layout [
    vh2 "Click a File to Play:"
    text-list data read %. [
        wait 0
        sound-port: open sound://
        insert sound-port (load value)
        wait sound-port
        close sound-port
    ]
]

Это было просто.

Через несколько дней читатель прислал мне по электронной почте дополнительную помощь. В его нынешнем виде сценарий аварийно завершает работу, если пользователь выбирает что-либо, кроме файла .wav, или если во время воспроизведения файла .wav выбирается другой файл. Я написал в ответ некоторый код, чтобы текстовый список отображал только файлы .wav и заставлял программу ждать воспроизведения другого файла. Я также написал дополнительный код, позволяющий пользователям выбирать другой начальный каталог. Вот:

; Вот как использовать функцию "request-dir", чтобы пользователь мог 
; выбрать папку для переключения:

start-dir: request-dir/dir %/c/Windows/media
change-dir start-dir

; Чтобы отображать в текстовом списке только файлы с расширением 
; ".wav", создайте новый пустой блок. Используйте цикл "foreach", 
; чтобы просмотреть список каталогов, и добавьте к новому блоку 
; только имена файлов с суффиксом ".wav":
waves: []
foreach file read %. [
    if %.wav = (suffix? file) [append waves file]
]

; Теперь вы можете отображать блок данных "wav"-файлов в текстовом 
;списке графического интерфейса.

; Чтобы дождаться завершения воспроизведения звуков, прежде чем 
; можно будет выбрать другой файл, добавьте переменную "wait-flag" к 
; функции play-sound. Когда начинается воспроизведение звука, 
; установите для переменной wait-flag значение true. Когда игра 
; закончится, установите для флага ожидания значение false. Также не 
; забудьте изначально установить значение "false" в начале вашей 
; программы:

play-sound: func [sound-file] [
    wait 0
    wait-flag: true
    ring: load sound-file
    sound-port: open sound://
    insert sound-port ring
    wait sound-port
    close sound-port
    wait-flag: false
]
wait-flag: false

; Когда файл выбран из текстового списка, запускайте функцию 
; "play-sound" только в том случае, если для переменной "wait-flag" 
; не задано значение true (т.е. если звуки не воспроизводятся):

view layout [
    vh2 "Click a File to Play:"
    text-list data waves [
        if wait-flag <> true [
            play-sound value
        ]
    ]
]

Когда я протестировал приведённый выше код, я понял, что несколько различных файлов .wav в папке Windows media не воспроизводятся должным образом, и сценарий завершился с ошибкой. Я добавил следующий код для обработки ошибок:

if error? try [play-sound value] [
    alert "malformed wave"  ; Предупредить пользователя сообщением,
    close sound-port        ; закрыть порт, открытый сломанным
    wait-flag: false        ; функция воспроизведения звука и 
]                           ; установите флаг обратно в значение 
                            ; false (чтобы другие файлы могли 
                            ; воспроизводиться)

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

btn "Change Folder" [
    change-dir request-dir
    waves: copy []
    foreach file read %. [
        if %.wav = suffix? file [append waves file]
    ]
    file-list/data: waves
    show file-list
]

На данный момент у нас есть симпатичное маленькое приложение для воспроизведения .wav:

REBOL []

play-sound: func [sound-file] [
    wait 0
    wait-flag: true
    ring: load sound-file
    sound-port: open sound://
    insert sound-port ring
    wait sound-port
    close sound-port
    wait-flag: false
]
wait-flag: false
change-dir %/c/Windows/media
waves: []
foreach file read %. [
    if %.wav = suffix? file [append waves file]
]
view layout [
    vh2 "Click a File to Play:"
    file-list: text-list data waves [
        if wait-flag <> true [
            if error? try [play-sound value] [
                alert "malformed wave"
                close sound-port
                wait-flag: false
            ]
        ]
    ]
    btn "Change Folder" [
        change-dir request-dir
        waves: copy []
        foreach file read %. [
            if %.wav = suffix? file [append waves file]
        ]
        file-list/data: waves
        show file-list
    ]
]

Это было размещено в Интернете, и через несколько дней несколько читателей задали один и тот же вопрос: "Как мне заставить его воспроизводить файлы .mp3?". REBOL не может изначально воспроизводить mp3, поэтому нам нужно использовать внешний инструмент, чтобы это произошло. Ранее в учебник я включил пример .dll, который воспроизводит файлы в формате mp3, но мне нужно было решение чуть более промышленного уровня. Я решил попробовать хорошо известный mp3-кодировщик/декодер "LAME". Я загрузил скомпилированную версию LAME для Windows с http://www.rarewares.org/mp3-lame-bundle.php и сжал её .exe-версию, используя двоичный модуль для встраивания ресурсов, найденный ранее в этом руководстве. Ради экономии места в этом руководстве я загрузил сжатый встроенный код на http://musiclessonz.com/rebol_tutorial/lame.r. Следующая строка записывает программу lame.exe в текущий каталог вашего жёсткого диска:

do http://musiclessonz.com/rebol_tutorial/lame.r  ; ~250k download

Чтобы использовать нашу программу медиаплеера, не загружая ничего, просто вставьте указанный выше код lame.r прямо в свой скрипт. Установив lame.exe на жёсткий диск, вы можете использовать его для преобразования файлов .mp3 в файлы .wav, используя формат:

call/wait {lame.exe --decode your-input.mp3 your-output.wav}

Я добавил строку выше в свою существующую программу и изменил подпрограмму построения блоков "wave" foreach, включив в неё файлы .mp3:

waves: []
foreach file read %. [
    if ((%.wav = suffix? file) or
    (%.mp3 = suffix? file)) [append waves file]
]

Я также изменил процедуру воспроизведения волны (блок действий в текстовом списке графического интерфейса пользователя), так что, если выбран файл mp3, запускается lame и файл преобразуется во временный файл wav, а затем этот файл wav воспроизводится:

file-list: text-list data waves [
    either %.mp3 = suffix? value [
        call/wait rejoin ["lame.exe --decode "
            (to-local-file value) " temp.wav"]
        if wait-flag <> true [
            if error? try [play-sound %temp.wav] [
                alert "malformed wave"
                close sound-port
                wait-flag: false
            ]
        ]
    ] [
        if wait-flag <> true [
            if error? try [play-sound value] [
                alert "malformed wave"
                close sound-port
                wait-flag: false
            ]
        ]
    ]
]

С этими изменениями код теперь выглядит так:

REBOL []

do http://musiclessonz.com/rebol_tutorial/lame.r
play-sound: func [sound-file] [
    wait 0
    wait-flag: true
    ring: load sound-file
    sound-port: open sound://
    insert sound-port ring
    wait sound-port
    close sound-port
    wait-flag: false
]
wait-flag: false
change-dir %/c/Windows/media
waves: []
foreach file read %. [
    if ((%.wav = suffix? file) or
        (%.mp3 = suffix? file)) [append waves file]
]
view center-face layout [
    vh2 "Click a File to Play:"
    file-list: text-list data waves [
        either %.mp3 = suffix? value [
            message/text: "Decoding mp3..." show message
            call/wait rejoin ["lame.exe --decode "
                (to-local-file value) " temp.wav"]
            message/text: "" show message
            if wait-flag <> true [
                if error? try [play-sound %temp.wav] [
                    alert "malformed wave"
                    close sound-port
                    wait-flag: false
                ]
            ]
        ] [
            if wait-flag <> true [
                if error? try [play-sound value] [
                    alert "malformed wave"
                    close sound-port
                    wait-flag: false
                ]
            ]
        ]
    ]
    btn "Change Folder" [
        change-dir request-dir
        waves: copy []
        foreach file read %. [
            if ((%.wav = suffix? file) or
            (%.mp3 = suffix? file)) [append waves file]
        ]
        file-list/data: waves
        show file-list
    ]
    message: h2 "                   "
]

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

Другой потенциальный вариант воспроизведения mp3, который я рассматривал, был небольшой mp3-плеер командной строки под названием "madplay.exe", доступный по адресу http://www.rarewares.org/mp3-others.php. В Madplay нет графического интерфейса пользователя - вы просто запускаете его из командной строки и управляете воспроизведением с помощью нажатия клавиш. Madplay может воспроизводить многие типы мультимедиа, и нажатия клавиш могут быть отправлены на него программно с помощью Windows API или Autoit DLL. Я создал работающее приложение, используя madplay.exe и autoit.dll, но я не буду здесь документировать этот код, потому что это было похоже на ещё одно скомбинированное решение.

Чтобы найти реальное решение, я погуглил "play mp3 dll". Первым, что возникло, была "libwmp3.dll" с http://www.inet.hr/~zcindori/libwmp3/index.html. Бинго - это именно то, что я хотел. DLL поставляется с примером кода использования, написанным на Visual Basic, C, C ++ и Delphi. Из этих примеров я смог расшифровать требуемые имена функций, входные параметры и возвращаемые значения и придумал следующий код для доступа к .dll в REBOL:

lib: load/library %libwmp3.dll

Mp3_Initialize: make routine! [
    return: [integer!]
] lib "Mp3_Initialize"

Mp3_OpenFile: make routine! [
    return: [integer!] 
    class [integer!] 
    filename [string!]
    nWaveBufferLengthMs [integer!]
    nSeekFromStart [integer!] 
    nFileSize [integer!]
] lib "Mp3_OpenFile"

Mp3_Play: make routine! [
    return: [integer!] 
    initialized [integer!]
] lib "Mp3_Play"


; Следующая функция и структура не требуются для воспроизведения 
; mp3, но мы будем использовать их, чтобы определить, 
; воспроизводится ли файл в данный момент:

Mp3_GetStatus: make routine! [
    return: [integer!] 
    initialized [integer!] 
    status [struct! []]
] lib "Mp3_GetStatus"

status: make struct! [
    fPlay [integer!]
    fPause [integer!] 
    fStop [integer!] 
    fEcho [integer!]  
    nSfxMode [integer!] 
    fExternalEQ [integer!] 
    fInternalEQ [integer!] 
    fVocalCut [integer!] 
    fChannelMix [integer!] 
    fFadeIn [integer!] 
    fFadeOut [integer!] 
    fInternalVolume [integer!] 
    fLoop [integer!]
    fReverse [integer!]
] none

; Следующие функции останавливают воспроизведение и освобождают 
; память по завершении:

Mp3_Stop: make routine! [
    return: [integer!] 
    initialized [integer!]
] lib "Mp3_Stop"

Mp3_Destroy: make routine! [
    return: [integer!] 
    initialized [integer!]
] lib "Mp3_Destroy"

Теперь эти функции можно использовать в REBOL для воспроизведения любого .mp3. Это очень просто, используя 3 функции:

initialized: Mp3_Initialize
Mp3_OpenFile initialized "test.mp3" 1000 0 0
Mp3_Play initialized

; Просто измените файл "test.mp3" на любой .mp3, который хотите 
; воспроизвести. Убедитесь, что имя файла отправлено в виде строки и 
; написано в формате файла Windows (при необходимости используйте 
; функции "to-local-file" и "what-dir" для преобразования из формата 
; файла REBOL).

Это весь код, необходимый для воспроизведения mp3. Чтобы проверить, воспроизводится ли в данный момент файл mp3:

Mp3_GetStatus initialized status
if ( status/fPlay > 0 ) [print "playing"]

Чтобы остановить воспроизведение mp3:

Mp3_Stop initialized

Чтобы убрать после этого:

Mp3_Destroy initialized
free lib

Я добавил код для загрузки libwmp3.dll на жёсткий диск (конечно, файл .dll также может быть встроен непосредственно в ваш код, используя двоичный модуль для встраивания ресурсов, описанный ранее в этом руководстве):

if not exists? %libwmp3.dll [
    write/binary %libwmp3.dll
    read/binary http://musiclessonz.com/rebol_tutorial/libwmp3.dll
]

Теперь мы можем добавить в наше маленькое приложение настоящую возможность проигрывания mp3. Вот последний код:

REBOL [title: "Jukebox - Wav/Mp3 Player"]

if not exists? %libwmp3.dll [
    write/binary %libwmp3.dll
    read/binary http://musiclessonz.com/rebol_tutorial/libwmp3.dll
]

lib: load/library %libwmp3.dll

Mp3_Initialize: make routine! [
    return: [integer!]
] lib "Mp3_Initialize"

Mp3_OpenFile: make routine! [
    return: [integer!] 
    class [integer!] 
    filename [string!]
    nWaveBufferLengthMs [integer!]
    nSeekFromStart [integer!] 
    nFileSize [integer!]
] lib "Mp3_OpenFile"

Mp3_Play: make routine! [
    return: [integer!] 
    initialized [integer!]
] lib "Mp3_Play"

Mp3_Stop: make routine! [
    return: [integer!] 
    initialized [integer!]
] lib "Mp3_Stop"

Mp3_Destroy: make routine! [
    return: [integer!] 
    initialized [integer!]
] lib "Mp3_Destroy"

Mp3_GetStatus: make routine! [
    return: [integer!] 
    initialized [integer!] 
    status [struct! []]
] lib "Mp3_GetStatus"

status: make struct! [
    fPlay [integer!] 
    fPause [integer!] 
    fStop [integer!] 
    fEcho [integer!] 
    nSfxMode [integer!] 
    fExternalEQ [integer!] 
    fInternalEQ [integer!] 
    fVocalCut [integer!] 
    fChannelMix [integer!] 
    fFadeIn [integer!] 
    fFadeOut [integer!] 
    fInternalVolume [integer!] 
    fLoop [integer!] 
    fReverse [integer!] 
] none

play-sound: func [sound-file] [
    wait 0
    wait-flag: true
    ring: load sound-file
    sound-port: open sound://
    insert sound-port ring
    wait sound-port
    close sound-port
    wait-flag: false
]

wait-flag: false
change-dir %/c/Windows/media
waves: []
foreach file read %. [
    if ((%.wav = suffix? file) or
        (%.mp3 = suffix? file)) [append waves file]
]

initialized: Mp3_Initialize

view center-face layout [
    vh2 "Click a File to Play:"
    file-list: text-list data waves [
        Mp3_GetStatus initialized status
        either %.mp3 = suffix? value [
            if (wait-flag <> true) and (status/fPlay = 0) [
                file: rejoin [to-local-file what-dir "\" value]
                Mp3_OpenFile initialized file 1000 0 0
                Mp3_Play initialized
            ]
        ] [
            if (wait-flag <> true) and (status/fPlay = 0) [
                if error? try [play-sound value] [
                    alert "malformed wave"
                    close sound-port
                    wait-flag: false
                ]
            ]
        ]
    ]
    across
    btn "Change Folder" [
        change-dir request-dir
        waves: copy []
        foreach file read %. [
            if ((%.wav = suffix? file) or
            (%.mp3 = suffix? file)) [append waves file]
        ]
        file-list/data: waves
        show file-list
    ]
    btn "Stop" [
         close sound-port
         wait-flag: false
         if (status/fPlay > 0) [Mp3_Stop initialized]
    ]
]

Mp3_Destroy initialized
free lib

Я написал более длинный пример, который показывает, как использовать другие функции libwmp3.dll: паузу/возобновление, регулировку громкости, быструю перемотку вперёд/назад, зацикливание, обратное воспроизведение и удаление вокала. Он доступен по адресу http://www.rebol.org/view-script.r?script=mp3-player-libwmp.r. Прототипы функций в этом примере демонстрируют, как использовать все другие функции в библиотеке: настройки эквалайзера, воспроизведение потока, получение поля идентификатора и информации о записанных данных, применение эффекта (эхо, реверберация и т.д.) И многое другое. В приведённом ниже примере показано, как прикрепить параметры громкости, воспроизведения цикла и поиска к элементам управления слайдом графического интерфейса пользователя (этот пример работает только с файлами MP3):

REBOL [Title: "Jukebox"]

if not exists? %libwmp3.dll [
    write/binary %libwmp3.dll
    read/binary http://musiclessonz.com/rebol_tutorial/libwmp3.dll
]
lib: load/library %libwmp3.dll
Mp3_Initialize: make routine! [
    return: [integer!]
] lib "Mp3_Initialize"
Mp3_OpenFile: make routine! [
    return: [integer!] 
    class [integer!] 
    filename [string!]
    nWaveBufferLengthMs [integer!]
    nSeekFromStart [integer!] 
    nFileSize [integer!]
] lib "Mp3_OpenFile"
Mp3_Play: make routine! [
    return: [integer!] 
    initialized [integer!]
] lib "Mp3_Play"
Mp3_Stop: make routine! [
    return: [integer!] 
    initialized [integer!]
] lib "Mp3_Stop"
Mp3_Destroy: make routine! [
    return: [integer!] 
    initialized [integer!]
] lib "Mp3_Destroy"
Mp3_GetStatus: make routine! [
    return: [integer!] 
    initialized [integer!] 
    status [struct! []]
] lib "Mp3_GetStatus"
status: make struct! [
    fPlay [integer!] 
    fPause [integer!] 
    fStop [integer!] 
    fEcho [integer!] 
    nSfxMode [integer!] 
    fExternalEQ [integer!] 
    fInternalEQ [integer!] 
    fVocalCut [integer!] 
    fChannelMix [integer!] 
    fFadeIn [integer!] 
    fFadeOut [integer!] 
    fInternalVolume [integer!] 
    fLoop [integer!] 
    fReverse [integer!] 
] none
Mp3_Time: make struct! [
    ms [integer!] 
    sec [integer!]
    bytes [integer!] 
    frames [integer!] 
    hms_hour [integer!] 
    hms_minute [integer!] 
    hms_second [integer!] 
    hms_millisecond [integer!] 
] none
TIME_FORMAT_SEC: 2
SONG_BEGIN: 1
SONG_CURRENT_FORWARD: 4
Mp3_Seek: make routine! [
    return: [integer!] 
    initialized [integer!]
    fFormat [integer!]
    pTime [struct! []]
    nMoveMethod [integer!]
] lib "Mp3_Seek"
Mp3_PlayLoop: make routine! [
    return: [integer!] 
    initialized [integer!]
    fFormatStartTime [integer!]
    pStartTime [struct! []]
    fFormatEndTime [integer!] 
    pEndTime [struct! []]
    nNumOfRepeat [integer!] 
] lib "Mp3_PlayLoop"
Mp3_GetSongLength: make routine! [
    return: [integer!]
    initialized [integer!]
    pLength [struct! []]
] lib "Mp3_GetSongLength"
Mp3_GetPosition: make routine! [
    return: [integer!] 
    initialized [integer!]
    pTime [struct! []]
] lib "Mp3_GetPosition"
Mp3_SetVolume: make routine! [
    return: [integer!] 
    initialized [integer!]
    nLeftVolume [integer!]
    nRightVolume [integer!]
] lib "Mp3_SetVolume"
Mp3_GetVolume: [
    initialized [integer!]
    pnLeftVolume [integer!]
    pnRightVolume [integer!]
    return: [integer!]
] lib "Mp3_GetVolume"
Mp3_VocalCut: make routine! [
    return: [integer!] 
    initialized [integer!]
    fEnable [integer!]
] lib "Mp3_VocalCut"
Mp3_ReverseMode: make routine! [
    return: [integer!] 
    initialized [integer!]
    fEnable [integer!]
] lib "Mp3_ReverseMode"
Mp3_Close: make routine! [
    return: [integer!] 
    initialized [integer!]
] lib "Mp3_Close"
waves: []
foreach file read %. [
    if (%.mp3 = suffix? file) [append waves file]
]
append waves "(CHANGE FOLDER...)"
initialized: Mp3_Initialize
view center-face layout [
    vh2 "Click a File to Play:"
    file-list: text-list data waves [
        if value = "(CHANGE FOLDER...)" [
            new-dir: request-dir
            if new-dir = none [break]
            change-dir new-dir
            waves: copy []
            foreach file read %. [
                if (%.mp3 = suffix? file) [append waves file]
            ]
            append waves "(CHANGE FOLDER...)"
            file-list/data: waves
            show file-list
            break
        ]
        Mp3_GetStatus initialized status
        if (status/fPlay = 0) [
            file: rejoin [to-local-file what-dir "\" value]
            Mp3_OpenFile initialized file 1000 0 0
            Mp3_Play initialized
        ]
    ]
    across
    tabs 40
    text "Seek:   " 
    tab slider 140x15 [
        plength: make struct! Mp3_Time compose [0 0 0 0 0 0 0 0]
        Mp3_GetSongLength initialized plength
        location: to-integer (value * plength/sec)
        ptime: make struct! Mp3_Time compose [0 (location) 0 0 0 0 0 0]
        Mp3_Seek initialized TIME_FORMAT_SEC ptime SONG_BEGIN
        Mp3_Play initialized
    ]
    return
    text "Volume: " 
    tab slider 140x15 [
        volume: to-integer value * 100
        Mp3_SetVolume initialized volume volume
    ]
    return
    btn "Reverse" [
        Mp3_GetStatus initialized status
        either (status/fReverse > 0) [
            Mp3_ReverseMode initialized 0
        ] [
            Mp3_ReverseMode initialized 1
        ]
    ]
    btn "Vocal-Cut" [
        Mp3_GetStatus initialized status
        either (status/fVocalCut > 0) [
            Mp3_VocalCut initialized 0
        ] [
            Mp3_VocalCut initialized 1
        ]
    ]
    return
    tabs 50
    text "Loop Start:" 
    tab start-slider: slider 120x15 []
    return
    text "Loop End:  " 
    tab end-slider: slider 120x15 []
    return
    btn "Play Loop" [
        plength: make struct! Mp3_Time compose [0 0 0 0 0 0 0 0]
        Mp3_GetSongLength initialized plength
        s-loc: to-integer (start-slider/data * plength/sec)
        pStartTime: make struct! Mp3_Time compose [0 (s-loc) 0 0 0 0 0 0]
        end-loc: to-integer (end-slider/data * plength/sec)
        pEndTime: make struct! Mp3_Time compose [0 (end-loc) 0 0 0 0 0 0]
        ; TIME_FORMAT_SEC: 2
        Mp3_PlayLoop initialized 2 pStartTime 2 pEndTime 1000  ; 1000x
    ]
    btn 58 "Stop" [
        Mp3_GetStatus initialized status
        if (status/fPlay > 0) [Mp3_Stop initialized]
    ]
]
Mp3_Destroy initialized
free lib

Libwmp3.dll - очень мощное и простое решение для воспроизведения файлов mp3 на любом языке программирования Windows. Если вы заинтересованы в проигрывании mp3-файлов в REBOL, это просто необходимо.

22.13 Случай: Принтер для гитарных аккордов

Мой бизнес, связанный с уроками музыки, поставил меня перед огромным количеством задач по программированию. Эта программа была написана, чтобы помочь ученикам старшеклассников в джаз-бэнде быстро сыграть все распространённые расширенные, изменённые и сложные типы аккордов. Он создаёт и мгновенно распечатывает диаграммы гитарных аккордов для песен. Хотя он был написан для обучения сложным джазовым аккордам, его также можно использовать для создания диаграмм аккордов для любого другого типа музыки (с более простыми аккордами): фолк, рок, блюз, поп и т.д.

Когда я задался целью создать эту программу, вот что я задумал:

  1. Пользователи должны иметь возможность выбирать из списка основных нот (A, Bb, C # и т.д.) и звучности (мажор, минор, 7 (# 5b9) и т.д.) для каждого аккорда в песне.
  2. Список выбранных аккордов должен отображаться в текстовой области.
  3. Когда пользователь добавил все аккорды в песню, он должен иметь возможность щёлкнуть кнопку, чтобы просмотреть аккорды в своем браузере.
  4. Отображая в браузере, пользователь может настроить параметры принтера для печати диаграмм разных размеров.

Я хотел, чтобы пользователь мог сохранять и загружать списки аккордов в текстовый файл, а также иметь возможность создавать zip-файл со всеми визуализированными аккордами в песне (HTML и все изображения, используемые для отображения песни в браузере. ). Чтобы понять, как работает эта программа, важно понимать некоторые основы формирования аккордов на грифе гитары. Каждая метка аккорда в нашей музыкальной системе состоит из двух частей: основной ноты (название буквы) и звучности (типа/характерного звука). Традиционный способ обучения теории аккордов на гитаре - использовать два образца аппликатуры грифа: 1 с основной нотой аккорда на 6-й струне, а другой с основной нотой на 5-й струне. Каждая фигура представляет собой схему "интервалов" (нот из основной гаммы, "do re mi fa so la ti do"), которые объединяются в аккорды. Каждый тип аккорда состоит из определённой формулы интервалов, и эта уникальная комбинация интервалов создаёт предсказуемый характерный звук. Формы можно сдвигать вверх или вниз по грифу, так что основная нота (интервал номер 1) на диаграмме помещается на заданную корневую ноту данного аккорда.

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

Формы интервалов 6:             Формы интервалов 5: 
___________                     ___________
| | | | 4 |                     | | | | | |
| 3 6 9 | 7                     | | | | | |
1 | | | 5 1                     | | | | 1 4
| | 7 3 | |                     | | 3 6 | |
| 5 1 4 6 9                     5 1 4 | 9 5
| | | | | |                     | | | 7 | |
| | 9 | 7 |                     | | 5 1 3 6

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

ТИП АККОРДА:           ИНТЕРВАЛЫ:           СИМВОЛЫ:

Power Chord           1    5                5
Major Triad           1    3    5           none  (just a root noot)
Minor Triad           1   b3    5           m, min, mi, -
Dominant 7            1    3   (5)  b7      7
Major 7               1    3   (5)   7      maj7, M7, (triangle) 7
Minor 7               1   b3   (5)  b7      m7, min7, mi7, -7
Half Diminished 7     1   b3   b5   b7      m7b5, (circle with line) 7
Diminished 7          1   b3   b5  bb7 (6)  dim7, (circle) 7
Augmented 7           1    3   #5   b7      7aug, 7(#5), 7(+5)

Добавьте эти интервалы к указанным выше 7-м аккордам, чтобы создать 
расширенные аккорды.

9 (такой же как 2)   11 (такой же как 4)   13 (такой же как 6)   

Примеры:               9          =    1   3  (5)  b7    9
                       min9       =    1  b3  (5)  b7    9
                       13         =    1   3   5   b7   13
                       9(+5)      =    1   3  #5   b7    9
                       maj9(#11)  =    1   3  (5)   7    9  #11

Вот ещё несколько распространённых типов аккордов:

"sus"       =  измените 3 на 4
"sus2"      =  измените 3 на 2
"add9"      =  1 3 5 9  (такой же как "add2", нет 7 в "add" аккордах)
"6,  maj6"  =  1 3 5 6
"m6, min6"  =  1 b3 5 6
"6/9"       =  1 3 5 6 9
11          =  1 b7 9 11
"/"         =  Басист играет ноту после косой черты
ПРИМЕЧАНИЕ. При игре сложных аккордов (джазовых аккордов) в группе 
гитаристы, как правило, НЕ ДОЛЖНЫ ИГРАТЬ ОСНОВНУЮ НОТУ аккорда (её 
играет басист или клавишник). На диаграммах, созданных этой 
программой, ненужные примечания будут обозначены светлыми кружками, 
а необходимые примечания - тёмными кружками.

Вот расположение основных нот на 6-й и 5-й струнах:

Шестая струна:                  Пятая струна

0  1  3  5  7  8  10  12        0  2  3  5  7  8  10  12
E  F  G  A  B  C  D   E         A  B  C  D  E  F  G   A

Символ диеза ("#") перемещает ноты ВВЕРХ на один лад.
Плоский символ ("b") перемещает ноты ВНИЗ на один лад.

Вот мой план для создания программы:

  1. Используем виджет текстового списка, чтобы отобразить интерактивный список всех возможных корневых заметок.
  2. Используем виджет текстового списка для отображения интерактивного списка всех возможных типов аккордов.
  3. Используем виджет области для отображения аккордов, выбранных пользователем (основная нота + тип аккорда). Каждый раз, когда пользователь щёлкает новый аккорд, соединяйте существующий текст с новым выбранным текстом аккорда. Ставим каждую новую метку аккорда на новую строку.
  4. Добавим кнопку, позволяющую пользователю выбирать, когда визуализировать и отображать изображения всех аккордов в браузере.
  5. Для создания изображений я буду использовать встроенный в REBOL диалект рисования. Я также создам простую HTML-страницу для отображения визуализированных изображений и сохраню все эти файлы во вновь созданном подкаталоге. Затем запустим браузер, чтобы просмотреть созданную страницу.
  6. Чтобы нарисовать изображения, сначала я нарисую сетку из линий, каждые 20 пикселей друг от друга. Вертикальные линии представляют струны, горизонтальные линии - лады.
  7. Я нанесу круги на приведённую выше сетку, где требуемые интервалы расположены в каждом выбранном типе аккорда. Для этого я создам карту всех возможных типов аккордов, сообщая программе, какие номера интервалов требуются для создания каждого выбранного типа аккорда. Я также создам карту всех возможных номеров интервалов, сообщив программе, где построить каждый требуемый интервал (координата XxY), указанный на карте типа аккорда. Наконец, я создам карту ладов, на которой расположены все возможные корневые ноты. Программа просто прочитает метки аккордов, введённые пользователем, построит требуемые круги с соответствующими координатами для всех интервалов, требуемых в каждом аккорде. Я также напечатаю соответствующий номер лада для данной основной ноты и метку аккорда непосредственно в каждом изображении.
  8. Мне нужно будет создать отдельные карты типов аккордов, координат и корневых нот как для 6-й, так и для 5-й струны. Мне нужно дважды выполнить процесс рендеринга для каждого аккорда (один раз для формы 6-й струны и один раз для формы 5-й струны).

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

  1. Метка аккорда (символ), используемая для обозначения каждого конкретного типа аккорда.
  2. Список различных других имён и меток, используемых для представления каждого типа аккорда (все они содержатся в строке).
  3. Блок интервалов, используемый для создания каждого типа аккорда. Поскольку несколько интервалов можно найти в нескольких местах на каждой диаграмме, я включил некоторые числа несколько раз, используя несколько меток (т.е. основная нота отображается как 1, 11 и 111 в различных аккордах).

Вот все карты root 6:

root6-shapes: [
    "." "major triad, no symbol (just a root note)" [1 3 5 11 55 111]
    "m" "minor triad, min, mi, m, -" [1 b3 5 11 55 111]
    "aug" "augmented triad, aug, #5, +5" [1 3 b6 11 111]
    "dim" "diminished triad, dim, b5, -5" [1 b3 b5 11]
    "5" "power chord, 5" [1 55]
    "sus4" "sus4, sus" [1 4 5 11 55 111]
    "sus2" "sus2, 2" [1 99 5 11]
    "6" "major 6, maj6, ma6, 6" [1 3 5 6 11]
    "m6" "minor 6, min6, mi6, m6" [1 b3 5 6 11]
    "69" "major 6/9, 6/9, add6/9" [1 111 3 13 9]
    "maj7" "major 7, maj7, ma7, M7, (triangle) 7" [1 3 5 7 11 55]
    "7" "dominant 7, 7" [1 3 5 b7 11 55]
    "m7" "minor 7, min7, mi7, m7, -7" [1 b3 5 b7 11 55]
    "m7(b5)" "half diminished, min7(b5), (circle w/ line), m7(-5), -7(b5)"
        [1 b3 b5 b7 11]
    "dim7" "diminished 7, dim7, (circle) 7" [1 b3 b5 6 11]
    "7sus4" "dominant 7 sus4 (7sus4)" [1 4 5 b7 55 11]
    "7sus2" "dominant 7 sus2 (7sus2)" [1 b7 99 5 11]
    "7(b5)" "dominant 7 flat 5, 7(b5), 7(-5)" [1 3 b5 b7 11]
    "7(+5)" "augmented 7, 7(#5), 7(+5)" [1 3 b6 b7 11]
    "7(b9)" "dominant 7 flat 9, 7(b9), 7(-9)" [1 3 5 b7 b9]
    "7(+9)" "dominant 7 sharp 9, 7(#9), 7(+9)" [1 111 3 b77 b33]
    "7(b5b9)" "dominant 7 b5 b9, 7(b5b9), 7(-5-9)" [1 3 b5 b7 b9]
    "7(b5+9)" "dominant 7 b5 #9, 7(b5#9), 7(-5+9)" [1 3 b5 b7 b33]
    "7(+5b9)" "augmented 7 flat 9, aug7(b9), 7(#5b9)" [1 3 b6 b7 b9]
    "7(+5+9)" "augmented 7 sharp 9, aug7(#9), 7(#5#9)" [1 3 b6 b7 b33]
    "add9" "add9, add2" [1 3 5 999 55 11]
    "madd9" "minor add9, min add9, m add9, m add2" [1 b3 5 999 55 11]
    "maj9" "major 9, maj9, ma9, M9, (triangle) 9" [1 3 5 7 9]
    "maj9(+11)" "major 9 sharp 11, maj9(#11), M9(+11)" [1 3 7 9 b5]
    "9" "dominant 9, 9" [1 3 5 b7 9 55]
    "9sus" "dominant 9 sus4, 9sus4, 9sus" [1 4 5 b7 9 55]
    "9(+11)" "dominant 9 sharp 11, 9(#11), 9(+11)" [1 3 b7 9 b5]
    "m9" "minor 9, min9, mi9, m9, -9" [1 b3 5 b7 9 55]
    "11" "dominant 11, 11" [1 b7 99 44 11]
    "maj13" "major 13, maj13, ma13, M13, (triangle) 13" [1 3 55 7 11 13]
    "13" "dominant 13, 13" [1 3 55 b7 11 13]
    "m13" "minor 13, min13, mi13, m13, -13" [1 b3 55 b7 11 13]
]
root6-map:  [
    1 20x70 11 120x70 111 60x110 3 80x90 33 40x50 b3 80x70 5 100x70
    55 40x110 b5 100x50 7 60x90 b7 60x70 9 120x110 99 80x50 6 60x50
    13 100x110 4 80x110 44 100x30 999 60x150 b77 100x130 b33 120x130
    b9 120x90 b6 100x90 b55 40x90
]
root6-notes:  [
    "e" {12} "f" {1} "f#" {2} "gb" {2} "g" {3} "g#" {4} "ab" {4}
    "a" {5} "a#" {6} "bb" {6} "b" {7} "c" {8} "c#" {9} "db" {9} "d" {10}
    "d#" {11} "eb" {11}
]

Карты корневых 5 просто отражают приведённые выше списки, со значениями, изменёнными для 5-й строки:

root5-shapes: [
    "." "major triad, no symbol (just a root note)" [1 3 5 11 55]
    "m" "minor triad, min, mi, m, -" [1 b3 5 11 55]
    "aug" "augmented triad, aug, #5, +5" [1 3 b6 11 b66]
    "dim" "diminished triad, dim, b5, -5" [1 b3 b5 11]
    "5" "power chord, 5" [1 55]
    "sus4" "sus4, sus" [1 4 5 11 55]
    "sus2" "sus2, 2" [1 9 5 11 55]
    "6" "major 6, maj6, ma6, 6" [1 3 55 13 11]
    "m6" "minor 6, min6, mi6, m6" [1 b3 55 13 11]
    "69" "major 6/9, 6/9, add6/9" [1 33 6 9 5]
    "maj7" "major 7, maj7, ma7, M7, (triangle) 7" [1 3 5 7 55]
    "7" "dominant 7, 7" [1 3 5 b7 55]
    "m7" "minor 7, min7, mi7, m7, -7" [1 b3 5 b7 55]
    "m7(b5)" "half diminished, min7(b5), (circle w/ line), m7(-5), -7(b5)"
        [1 b3 b5 b7 b55]
    "dim7" "diminished 7, dim7, (circle) 7" [1 b33 b5 6 111]
    "7sus4" "dominant 7 sus4, 7sus4" [1 4 5 b7 55]
    "7sus2" "dominant 7 sus2, 7sus2" [1 9 5 b7 55]
    "7(b5)" "dominant 7 flat 5, 7(b5), 7(-5)" [1 33 b5 b7 111]
    "7(+5)" "augmented 7, 7(#5), 7(+5)" [1 33 b6 b7 111]
    "7(b9)" "dominant 7 flat 9, 7(b9), 7(-9)" [1 33 5 b7 b9]
    "7(+9)" "dominant 7 sharp 9, 7(#9), 7(+9)" [1 33 b7 b3]
    "7(b5b9)" "dominant 7 b5 b9, 7(b5b9), 7(-5-9)" [1 33 b5 b7 b9]
    "7(b5+9)" "dominant 7 b5 #9, 7(b5#9), 7(-5+9)" [1 33 b5 b7 b3]
    "7(+5b9)" "augmented 7 flat 9, aug7(b9), 7(#5b9)" [1 33 b6 b7 b9]
    "7(+5+9)" "augmented 7 sharp 9, aug7(#9), 7(#5#9)" [1 33 b7 b3 b6]
    "add9" "major add9, add9, add2" [1 3 5 99 55]
    "madd9" "minor add9, min add9, m add9, m add2" [1 b3 5 99 55]
    "maj9" "major 7, maj9, ma9, M9, (triangle) 9" [1 33 5 7 9]
    "maj9(+11)" "major 9 sharp 11, maj9(#11), M9(+11)" [1 33 b5 7 9]
    "9" "dominant 9, 9" [1 33 5 b7 9]
    "9sus" "dominant 9 sus4, 9sus4, 9sus" [1 44 5 b7 9]
    "9(+11)" "dominant 9 sharp 11, 9(#11), 9(+11)" [1 33 b5 b7 9]
    "m9" "minor 9, min9, mi9, m9, -9" [1 b33 5 b7 9]
    "11" "dominant 11, 11" [1 b7 9 44 444]
    "maj13" "major 13, maj13, ma13, M13, (triangle) 13" [1 3 55 7 13]
    "13" "dominant 13, 13" [1 3 55 b7 13]
    "m13" "minor 13, min13, mi13, m13, -13" [1 b3 55 b7 13]
]
root5-map:  [
    1 40x70 11 80x110 111 100x30 3 100x110 33 60x50 b33 60x30 5 120x70
    55 60x110 b5 120x50 7 80x90 b7 80x70 9 100x70 6 80x50 13 120x110
    4 100x130 44 60x70 444 120x30 99 80x150 b3 100x90 b9 100x50 b6 120x90
    b66 60x130 b55 60x90
]
root5-notes: [
    "a" {12} "a#" {1} "bb" {1} "b" {2} "c" {3} "c#" {4} "db" {4}
    "d" {5} "d#" {6} "eb" {6} "e" {7} "f" {8} "f#" {9} "gb" {9} "g" {10}
    "g#" {11} "ab" {11}
]

Затем я написал код для рисования изображения сетки, на котором будут нанесены все круги. Это просто вопрос создания 2 блоков начальных/конечных точек линии (по одному блоку для вертикальных и горизонтальных линий), рисования этих линий на дисплее и последующего сохранения этого дисплея как изображения:

f: copy []
for n 20 160 20 [append f reduce ['line (as-pair 20 n) (as-pair 120 n)]]
for n 20 120 20 [append f reduce ['line (as-pair n 20) (as-pair n 160)]]
fretboard: to-image layout/tight [box white 150x180 effect [draw f]]

Чтобы начать с кода действия, я создал скелетный код графического интерфейса пользователя для программы. Вот макет, основанный на спецификациях, которые я придумал ранее (я также добавил кнопку для справки):

view center-face layout [
    across
    t1: text-list 60x270 data [
        "E" "F" "F#" "Gb" "G" "G#" "Ab" "A" "A#" "Bb" "B" "C" "C#" "Db"
        "D" "D#" "Eb"
    ]
    ; Список меток аккордов просто извлекается из блока выше:
    t2: text-list 330x270 data extract/index root6-shapes 3 2 []
    return
    a: area
    return
    btn "Create Chart" []
    btn "Save" []
    btn "Load" []
    btn "Create Zip" []
    btn "Help" [editor help] ; справка будет просто длинной строкой текста
]

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

t2: text-list 330x270 data extract/index root6-shapes 3 2 [

    ; Когда выбран тип аккорда, сделаем следующее:

    either empty? a/text [

        ; Если текстовая область пуста, вставьте корневую ноту и тип 
        ; аккорда в текстовую область:

        a/text: rejoin [
            copy t1/picked " "
            pick root6-shapes ((index? find root6-shapes value) - 1)
        ]
    ] [

        ; Если текстовая область не пуста, воссоедините существующий 
        ; текст, новую строку, корневую ноту и тип аккорда.

        a/text: rejoin [
            a/text newline copy t1/picked " " 
            pick root6-shapes ((index? find root6-shapes value) - 1)
        ]
    ]

    ; Отобразите добавленный аккорд:

    show a

]

Теперь мне нужно написать код для создания диаграмм для каждого аккорда в списке. Я поместил этот код прямо в блок действий кнопки "Создать диаграмму":

btn "Create Chart" [if error? try [

    ; Создаём подкаталог chords, если он не существует:

    make-dir %chords

    ; Каждый раз стираем временное содержимое;

    delete/any %chords/*.*

    ; Приступаем к созданию HTML-макета:

    html: copy "<html><body bgcolor=#ffffffff>"

    ; Проcматриваем каждый аккорд в текстовом списке:

    foreach [root spacer1 spacer2 type] (parse/all form a/text " ") [

        ; Начинаем создавать блок рисования, который содержит 
        ; изображение грифа, которое мы создали ранее:

        diagram: copy [image fretboard]
        diagram2: copy [image fretboard]

        ; Это цикл, который отображает каждый из интервалов в блоке 
        ; формулы аккорда:

        root1: copy root
        foreach itvl (third find root6-shapes type) [
            either find [1 55] itvl [

                ; Нарисуем белый кружок для интервалов 1 и 55:

                append diagram reduce [
                    'fill-pen white 'circle (select root6-map itvl) 5
                ]
            ] [

                ; Нарисуем чёрный кружок для всех остальных интервалов:

                append diagram reduce [
                    'fill-pen black 'circle (select root6-map itvl) 5
                ]
            ]
        ]

        ; Нанесём основной тон и текст номера лада на изображение аккорда:

        append diagram reduce ['text (trim/all join root1 type) 20x0]
        append diagram reduce [
            'text 
            trim/all to-string (
                select root6-notes trim/all to-string root1
            )
            130x65
        ]

        ; Визуализируем собранный блок отрисовки и сохраните
        ; созданное изображение в файл:

        save/png
            to-file trim/all rejoin [
                %./chords/ (replace/all root1 {#} {sharp}) type ".png"
            ]
            to-image layout/tight [
            box white 150x180 effect [draw diagram]
        ]

        ; Добавим код в HTML-строку для отображения созданного изображения:

        append html rejoin [
            {<img src="./} 
            trim/all rejoin [
                replace/all copy root1 {#} {sharp} type ".png"
            ]
            {">}
        ]

        ; Повторите весь процесс, описанный выше, чтобы создать образ 5:

        foreach itvl (third find root5-shapes type) [
            either find [1] itvl [
                append diagram2 reduce [
                    'fill-pen white 'circle (select root5-map itvl) 5
                ]
            ] [
                append diagram2 reduce [
                    'fill-pen black 'circle (select root5-map itvl) 5
                ]
            ]
        ]
        append diagram2 reduce ['text (trim/all join root type) 20x0]
        append diagram2 reduce [
            'text 
            trim/all to-string (
                select root5-notes trim/all to-string root
            )
            130x65
        ]
        save/png 
            to-file trim/all rejoin [
                %./chords/ (replace/all root {#} {sharp}) 
                type "5th.png"
            ]
            to-image layout/tight [
            box white 150x180 effect [draw diagram2]
        ]
        append html rejoin [
            {<img src="./} (trim/all rejoin [
                replace/all root {#} {sharp} type "5th.png"
            ]) {">}
        ]
    ]

    ; Завершим HTML-код, сохраним его в файл и отобразите в 
    ; браузере пользователя:
append html [</body></html>]
write %./chords/chords.html trim/auto html
browse %./chords/chords.html 
] [

; Если где-то в процессе рендеринга произошла ошибка (из-за 
; того, что пользователь неправильно редактировал метки 
; аккордов), предупредим его, чтобы он внёс изменения:

alert "Error - please remove improper chord labels."

]]
btn "Save" [

; Сохраним выбранные аккорды в текстовый файл с проверкой 
; ошибок, чтобы избежать случайной перезаписи существующих файлов:

savefile: to-file request-file/file/save %/c/mysong.txt
if exists? savefile [
    alert "Please choose a file name that does not already exist."
    return
]
if error? try [save savefile a/text] [alert "File not saved"]
]
btn "Load" [

; Загрузим сохранённый список аккордов и отобразим его в 
; текстовой области. Проверка ошибок есть в случае, если 
; пользователь нажмёт кнопку "отменить":

if error? try [
    a/text: load to-file request-file/file %/c/mysong.txt
    show a
] []
]

btn "Create Zip" [

; Эта процедура создаёт zip-файл созданного файла HTML и каждого 
; визуализированного изображения в папке ./chords. Он использует 
; скрипт rebzip Винсента Экюера:
if not exists? %chords/ [alert "Create A Chart First" return]
do to-string to-binary decompress 64#{

    ; Вставьте сюда сжатый zebzip-код Винсента Экюера, который 
    ; можно найти по адресу:
    ; http://www.rebol.org/view-script.r?script=rebzip.r

}
zipfile: to-file request-file/file/save %/c/mysong.zip
if exists? zipfile [
    alert "Please choose a file name that does not already exist."
    return
]
zip/deep zipfile %chords/
]
btn "Help" [editor help]

Вот окончательная программа с текстом справки и кодом архивирования:

REBOL [title: "Guitar Chords"]

help: {
    This program creates guitar chord diagram charts for songs.  It was
    written to help students in high school jazz band quickly play all of
    the common extended, altered, and complex chord types.  It can also
    be used to create chord charts for any other type of music (with
    simpler chords):  folk, rock, blues, pop, etc.

    To select chords for your song, click the root note (letter name:  A,
    Bb, C#, etc.), and then the sonority (major, minor, 7(#5b9), etc.) of
    each chord.  The list of chords you've selected will be shown in the 
    text area below.  When you've added all the chords needed to play your
    song, click the "Create Chart" button.  Your browser will open, with a
    complete graphic rendering of all chords in your song.  You can use
    your browser's page settings to print charts at different sizes.

    Two versions of each chord are presented:  1 with the root note on the
    6th string, and another with the root note on the 5th string.  Chord
    lists can be saved and reloaded with the "Save" and "Load" buttons.
    The rendered images and the HTML that displays them are all saved to
    the "./chords" folder (a subfolder of wherever this script is run).
    You can create a zip file of all the contents of that folder to play
    your song later, upload it to a web server to share with the world,
    etc.


    -- THEORY --

    Here are the formulas and fingering patterns used to create chords in
    this program:


    6th string notes:               5th string notes:

    0  1  3  5  7  8  10  12        0  2  3  5  7  8  10  12
    E  F  G  A  B  C  D   E         A  B  C  D  E  F  G   A


    The sharp symbol ("#") moves notes UP    one fret
    The flat  symbol ("b") moves notes DOWN  one fret


    Root 6 interval shapes:         Root 5 interval shapes:     
    ___________                     ___________
    | | | | 4 |                     | | | | | |
    | 3 6 9 | 7                     | | | | | |
    1 | | | 5 1                     | | | | 1 4
    | | 7 3 | |                     | | 3 6 | |
    | 5 1 4 6 9                     5 1 4 | 9 5
    | | | | | |                     | | | 7 | |
    | | 9 | 7 |                     | | 5 1 3 6


    To create any chord, slide either shape up the fretboard until the
    number "1" is on the correct root note (i.e., for a "G" chord, slide
    the root 6 shape up to the 3rd fret, or the root 5 shape up to the
    10th fret).  Then pick out the required intervals:


    CHORD TYPE:           INTERVALS:           SYMBOLS:

    Power Chord           1    5                5
    Major Triad           1    3    5           none  (just a root noot)
    Minor Triad           1   b3    5           m, min, mi, -
    Dominant 7            1    3   (5)  b7      7
    Major 7               1    3   (5)   7      maj7, M7, (triangle) 7
    Minor 7               1   b3   (5)  b7      m7, min7, mi7, -7
    Half Diminished 7     1   b3   b5   b7      m7b5, (circle with line) 7
    Diminished 7          1   b3   b5  bb7 (6)  dim7, (circle) 7
    Augmented 7           1    3   #5   b7      7aug, 7(#5), 7(+5)


    Add these intervals to the above 7th chords to create extended chords:

    9 (is same as 2)   11 (is same as 4)   13 (is same as 6)   

    Examples:              9          =    1   3  (5)  b7    9
                           min9       =    1  b3  (5)  b7    9
                           13         =    1   3   5   b7   13
                           9(+5)      =    1   3  #5   b7    9
                           maj9(#11)  =    1   3  (5)   7    9  #11


    Here are some more common chord types:

    "sus"       =  change 3 to 4
    "sus2"      =  change 3 to 2
    "add9"      =  1 3 5 9  (same as "add2", there's no 7 in "add" chords)
    "6,  maj6"  =  1 3 5 6
    "m6, min6"  =  1 b3 5 6
    "6/9"       =  1 3 5 6 9
    11          =  1 b7 9 11
    "/"         =  Bassist plays the note after the slash


    NOTE:  When playing complex chords (jazz chords) in a band setting,
    guitarists typically SHOULD NOT PLAY THE ROOT NOTE of the chord
    (the bassist or keyboardist will play it).  In diagrams created by 
    this program, unnecessary notes are indicated by light circles, and
    required notes are indicated by dark circles.
}

root6-shapes: [
    "." "major triad, no symbol (just a root note)" [1 3 5 11 55 111]
    "m" "minor triad, min, mi, m, -" [1 b3 5 11 55 111]
    "aug" "augmented triad, aug, #5, +5" [1 3 b6 11 111]
    "dim" "diminished triad, dim, b5, -5" [1 b3 b5 11]
    "5" "power chord, 5" [1 55]
    "sus4" "sus4, sus" [1 4 5 11 55 111]
    "sus2" "sus2, 2" [1 99 5 11]
    "6" "major 6, maj6, ma6, 6" [1 3 5 6 11]
    "m6" "minor 6, min6, mi6, m6" [1 b3 5 6 11]
    "69" "major 6/9, 6/9, add6/9" [1 111 3 13 9]
    "maj7" "major 7, maj7, ma7, M7, (triangle) 7" [1 3 5 7 11 55]
    "7" "dominant 7, 7" [1 3 5 b7 11 55]
    "m7" "minor 7, min7, mi7, m7, -7" [1 b3 5 b7 11 55]
    "m7(b5)" "half diminished, min7(b5), (circle w/ line), m7(-5), -7(b5)"
        [1 b3 b5 b7 11]
    "dim7" "diminished 7, dim7, (circle) 7" [1 b3 b5 6 11]
    "7sus4" "dominant 7 sus4 (7sus4)" [1 4 5 b7 55 11]
    "7sus2" "dominant 7 sus2 (7sus2)" [1 b7 99 5 11]
    "7(b5)" "dominant 7 flat 5, 7(b5), 7(-5)" [1 3 b5 b7 11]
    "7(+5)" "augmented 7, 7(#5), 7(+5)" [1 3 b6 b7 11]
    "7(b9)" "dominant 7 flat 9, 7(b9), 7(-9)" [1 3 5 b7 b9]
    "7(+9)" "dominant 7 sharp 9, 7(#9), 7(+9)" [1 111 3 b77 b33]
    "7(b5b9)" "dominant 7 b5 b9, 7(b5b9), 7(-5-9)" [1 3 b5 b7 b9]
    "7(b5+9)" "dominant 7 b5 #9, 7(b5#9), 7(-5+9)" [1 3 b5 b7 b33]
    "7(+5b9)" "augmented 7 flat 9, aug7(b9), 7(#5b9)" [1 3 b6 b7 b9]
    "7(+5+9)" "augmented 7 sharp 9, aug7(#9), 7(#5#9)" [1 3 b6 b7 b33]
    "add9" "add9, add2" [1 3 5 999 55 11]
    "madd9" "minor add9, min add9, m add9, m add2" [1 b3 5 999 55 11]
    "maj9" "major 9, maj9, ma9, M9, (triangle) 9" [1 3 5 7 9]
    "maj9(+11)" "major 9 sharp 11, maj9(#11), M9(+11)" [1 3 7 9 b5]
    "9" "dominant 9, 9" [1 3 5 b7 9 55]
    "9sus" "dominant 9 sus4, 9sus4, 9sus" [1 4 5 b7 9 55]
    "9(+11)" "dominant 9 sharp 11, 9(#11), 9(+11)" [1 3 b7 9 b5]
    "m9" "minor 9, min9, mi9, m9, -9" [1 b3 5 b7 9 55]
    "11" "dominant 11, 11" [1 b7 99 44 11]
    "maj13" "major 13, maj13, ma13, M13, (triangle) 13" [1 3 55 7 11 13]
    "13" "dominant 13, 13" [1 3 55 b7 11 13]
    "m13" "minor 13, min13, mi13, m13, -13" [1 b3 55 b7 11 13]
]
root6-map:  [
    1 20x70 11 120x70 111 60x110 3 80x90 33 40x50 b3 80x70 5 100x70
    55 40x110 b5 100x50 7 60x90 b7 60x70 9 120x110 99 80x50 6 60x50
    13 100x110 4 80x110 44 100x30 999 60x150 b77 100x130 b33 120x130
    b9 120x90 b6 100x90 b55 40x90
]
root5-shapes: [
    "." "major triad, no symbol (just a root note)" [1 3 5 11 55]
    "m" "minor triad, min, mi, m, -" [1 b3 5 11 55]
    "aug" "augmented triad, aug, #5, +5" [1 3 b6 11 b66]
    "dim" "diminished triad, dim, b5, -5" [1 b3 b5 11]
    "5" "power chord, 5" [1 55]
    "sus4" "sus4, sus" [1 4 5 11 55]
    "sus2" "sus2, 2" [1 9 5 11 55]
    "6" "major 6, maj6, ma6, 6" [1 3 55 13 11]
    "m6" "minor 6, min6, mi6, m6" [1 b3 55 13 11]
    "69" "major 6/9, 6/9, add6/9" [1 33 6 9 5]
    "maj7" "major 7, maj7, ma7, M7, (triangle) 7" [1 3 5 7 55]
    "7" "dominant 7, 7" [1 3 5 b7 55]
    "m7" "minor 7, min7, mi7, m7, -7" [1 b3 5 b7 55]
    "m7(b5)" "half diminished, min7(b5), (circle w/ line), m7(-5), -7(b5)"
        [1 b3 b5 b7 b55]
    "dim7" "diminished 7, dim7, (circle) 7" [1 b33 b5 6 111]
    "7sus4" "dominant 7 sus4, 7sus4" [1 4 5 b7 55]
    "7sus2" "dominant 7 sus2, 7sus2" [1 9 5 b7 55]
    "7(b5)" "dominant 7 flat 5, 7(b5), 7(-5)" [1 33 b5 b7 111]
    "7(+5)" "augmented 7, 7(#5), 7(+5)" [1 33 b6 b7 111]
    "7(b9)" "dominant 7 flat 9, 7(b9), 7(-9)" [1 33 5 b7 b9]
    "7(+9)" "dominant 7 sharp 9, 7(#9), 7(+9)" [1 33 b7 b3]
    "7(b5b9)" "dominant 7 b5 b9, 7(b5b9), 7(-5-9)" [1 33 b5 b7 b9]
    "7(b5+9)" "dominant 7 b5 #9, 7(b5#9), 7(-5+9)" [1 33 b5 b7 b3]
    "7(+5b9)" "augmented 7 flat 9, aug7(b9), 7(#5b9)" [1 33 b6 b7 b9]
    "7(+5+9)" "augmented 7 sharp 9, aug7(#9), 7(#5#9)" [1 33 b7 b3 b6]
    "add9" "major add9, add9, add2" [1 3 5 99 55]
    "madd9" "minor add9, min add9, m add9, m add2" [1 b3 5 99 55]
    "maj9" "major 7, maj9, ma9, M9, (triangle) 9" [1 33 5 7 9]
    "maj9(+11)" "major 9 sharp 11, maj9(#11), M9(+11)" [1 33 b5 7 9]
    "9" "dominant 9, 9" [1 33 5 b7 9]
    "9sus" "dominant 9 sus4, 9sus4, 9sus" [1 44 5 b7 9]
    "9(+11)" "dominant 9 sharp 11, 9(#11), 9(+11)" [1 33 b5 b7 9]
    "m9" "minor 9, min9, mi9, m9, -9" [1 b33 5 b7 9]
    "11" "dominant 11, 11" [1 b7 9 44 444]
    "maj13" "major 13, maj13, ma13, M13, (triangle) 13" [1 3 55 7 13]
    "13" "dominant 13, 13" [1 3 55 b7 13]
    "m13" "minor 13, min13, mi13, m13, -13" [1 b3 55 b7 13]
]
root5-map:  [
    1 40x70 11 80x110 111 100x30 3 100x110 33 60x50 b33 60x30 5 120x70
    55 60x110 b5 120x50 7 80x90 b7 80x70 9 100x70 6 80x50 13 120x110
    4 100x130 44 60x70 444 120x30 99 80x150 b3 100x90 b9 100x50 b6 120x90
    b66 60x130 b55 60x90
]
root6-notes:  [
    "e" {12} "f" {1} "f#" {2} "gb" {2} "g" {3} "g#" {4} "ab" {4}
    "a" {5} "a#" {6} "bb" {6} "b" {7} "c" {8} "c#" {9} "db" {9} "d" {10}
    "d#" {11} "eb" {11}
]
root5-notes: [
    "a" {12} "a#" {1} "bb" {1} "b" {2} "c" {3} "c#" {4} "db" {4}
    "d" {5} "d#" {6} "eb" {6} "e" {7} "f" {8} "f#" {9} "gb" {9} "g" {10}
    "g#" {11} "ab" {11}
]

f: copy []
for n 20 160 20 [append f reduce ['line (as-pair 20 n) (as-pair 120 n)]]
for n 20 120 20 [append f reduce ['line (as-pair n 20) (as-pair n 160)]]
fretboard: to-image layout/tight [box white 150x180 effect [draw f]]
; spacer: to-image layout/tight [box white 20x20]

view center-face layout [
    across
    t1: text-list 60x270 data [
        "E" "F" "F#" "Gb" "G" "G#" "Ab" "A" "A#" "Bb" "B" "C" "C#" "Db"
        "D" "D#" "Eb"
    ]
    t2: text-list 330x270 data extract/index root6-shapes 3 2 [
        either empty? a/text [
            a/text: rejoin [
                copy t1/picked " "
                pick root6-shapes ((index? find root6-shapes value) - 1)
            ]
        ] [
            a/text: rejoin [
                a/text newline copy t1/picked " " 
                pick root6-shapes ((index? find root6-shapes value) - 1)
            ]
        ]
        show a
    ]
    return
    a: area
    return
    btn "Create Chart" [if error? try [
        make-dir %chords
        delete/any %chords/*.*
        ; save/bmp %./chords/spacer.bmp spacer
        html: copy "<html><body bgcolor=#ffffffff>"
        foreach [root spacer1 spacer2 type] (parse/all form a/text " ") [
            diagram: copy [image fretboard]
            diagram2: copy [image fretboard]
            root1: copy root
            foreach itvl (third find root6-shapes type) [
                either find [1 55] itvl [
                    append diagram reduce [
                        'fill-pen white 'circle (select root6-map itvl) 5
                    ]
                ] [
                    append diagram reduce [
                        'fill-pen black 'circle (select root6-map itvl) 5
                    ]
                ]
            ]
            append diagram reduce ['text (trim/all join root1 type) 20x0]
            append diagram reduce [
                'text 
                trim/all to-string (
                    select root6-notes trim/all to-string root1
                )
                130x65
            ]
            save/png
                to-file trim/all rejoin [
                    %./chords/ (replace/all root1 {#} {sharp}) type ".png"
                ]
                to-image layout/tight [
                box white 150x180 effect [draw diagram]
            ]
            append html rejoin [
                {<img src="./} 
                trim/all rejoin [
                    replace/all copy root1 {#} {sharp} type ".png"
                ]
                {">}
            ]

            foreach itvl (third find root5-shapes type) [
                either find [1] itvl [
                    append diagram2 reduce [
                        'fill-pen white 'circle (select root5-map itvl) 5
                    ]
                ] [
                    append diagram2 reduce [
                        'fill-pen black 'circle (select root5-map itvl) 5
                    ]
                ]
            ]
            append diagram2 reduce ['text (trim/all join root type) 20x0]
            append diagram2 reduce [
                'text 
                trim/all to-string (
                    select root5-notes trim/all to-string root
                )
                130x65
            ]
            save/png 
                to-file trim/all rejoin [
                    %./chords/ (replace/all root {#} {sharp}) 
                    type "5th.png"
                ]
                to-image layout/tight [
                box white 150x180 effect [draw diagram2]
            ]
            append html rejoin [
                {<img src="./} (trim/all rejoin [
                    replace/all root {#} {sharp} type "5th.png"
                ]) {">}
                ; {<img src="./spacer.bmp">}
            ]
        ]
        append html [</body></html>]
        write %./chords/chords.html trim/auto html
        browse %./chords/chords.html 
    ] [alert "Error - please remove improper chord labels."]]
    btn "Save" [
        savefile: to-file request-file/file/save %/c/mysong.txt
        if exists? savefile [
            alert "Please choose a file name that does not already exist."
            return
        ]
        if error? try [save savefile a/text] [alert "File not saved"]
    ]
    btn "Load" [
        if error? try [
            a/text: load to-file request-file/file %/c/mysong.txt
            show a
        ] []
    ]
    btn "Create Zip" [
        if not exists? %chords/ [alert "Create A Chart First" return]
        ; rebzip by Vincent Ecuyer:
        do to-string to-binary decompress 64#{
        eJztW+uP20hy/+6/oldGsJ7bcEU2X00Zd4bX9iILXO4AY5N8EOYAjkTNMNaQOomy
        PTbmf8+vqptkU3xIM4cgARICHmvYVdX1fnRrPn745a9/FsvrFy9W1VfnW75biFVZ
        VNnXSixfCDyr/crZlsXtwvzeeVz885IkSsJEJYEQju96bhCHrhKOF8s4CGToSgKS
        QeQHnh/jo1KRG8aRFzb0HD9O/CCMPA+fvciPwziUEX4RMkhkpEAIH90gAGFf0j6h
        lCqUbhTRPkEcY3dftvx5kUwCMBYSCU+GIO1KIh64sR+5oAo8FUWhBDnwJIJEhX4U
        Bgq4IJbIOIqilj/phknoRrSMz1GcuJGrSC5wBCRFvHoKLISQzSchVOwr0PMJHjyr
        JArjlj8CVH6SsP4iBQFiLyR6TuCrIJRhABJJ7LsBRCQg3w8j3w8U6S+KPGI1avUn
        HNggCsKA9ZdIpUJgJ1hIkhgSu0lEJlBeEvpgFkDYzvO8JKJ9wtiVkCgMGnqe60ZK
        KS8I2XQkVBzR3k4cuxArimLCk6RJ6I32gd2TOJaQgei6RKCl50Bb0JRL8jqeD39x
        vTCCMgWwYdYgJP3BYSRkc0mIEDYIvdCnfQJP+lC+77XyRqEXwwqK9efHWPahENID
        VOdJ2A94xHMko4T2CaMwdGNJvEKqIJJebNFzlAv7h2HC+oOWwFWSkFxR7IGnhPzP
        C8mllMv+l4BXP/LZ/+BCkZcElrwiTkLlh6Ek1/IC2N93pQyJJwgC5cAXHUgVKC8G
        HxxDgUdUJHuH54VQTEPPIWbJA2lrOBR4ChLaG3huHICviA0FVv1YerRPhF29wA3I
        F32yrSv91v+kC6XFiBGXfwtJ867PsYQHvkN2dPA/wGQYUQz5McweRz4LRL8pz2/5
        g/IilYRxzC4sOSEkCcUERE1CWJhihewRKNIZ4ilCRMBTSR4J51CBp1r/U1BO7MKY
        gmMM1pCkJ0d6HqJAQgO8qx968EyyaUwBqRLONT4JLmPX0h+CN4CTUIoiwYIkSOKI
        bBrBt0PF9AQ0CgvAiwnBlyQF0Yb34iUiuPUXz4e/+WHCoSkSFSSkb9JfGMR4i1RF
        sSIhYBKHimMoAo8wNwUXtAd2PK/lLyQ4KJi3iwOIgqDlmIDh4bBuRLEC0jHUyj6n
        wlj6yqd9EBmsY7flDyaCjwQR2QMxBu1CZxxLgfQofkOOMWDCIsQTlBr6fkIJDsGF
        gEx87Nky6LnIXyCpONmQTZFWKdgD5B0w5VKyAZ4HHn1yQGTDIPDBIUUXIgRJTXmi
        5RCxhtiCY3K4wEt8BY/kaFIhJQ+OZuI2Voqjzw0SlA6XRIopuCjFWhyCHx+lIuTs
        66K6yMAnYJQkpCvgUkpAMqCADtlukfTJb8g8cF3lIUdJYdUkbAG2Ql2TAs6Yih0S
        kQXnTEhvKDsKviBp10C5LmKdc4JykbvAsUVQIANQRfRdjkCsJ+QJhIjcmniUvYEo
        FZiXigMNsuMfBbFEdkXSQ9RaHEqsgRUv4qqEihdHiUfUkQ6hfeQ7iowIe8HzyFNg
        fqRBN+RIgpeBhcAyigigGiklhx1kRe2OFZcRBEQEhmOXPikZIf0Q50IidFA3JaWi
        GC4ETwttHcLACAJPG4WqgkpYocJHeUKyIYdHrZYI5Yh6A4QatQYB9w+hQlFBeCuL
        QxSwGLHic0/hB1iOeHeH0lmAHEE6pOB2kd042FDIoE1FwQS3jVD/wsTiMIopMwfs
        bjC2knBu9nKUeKgAlYKIJFAg0iRnxQAMIF592gh5TmFjyygCFHyJxKkzMrIkJSny
        IfBBbQqXJo96IRmw90ewODZyyQ996BaeEUUWh/BYdFcel1+PyiYeyeEWI4uiDHFN
        g02gXS/k2gRXcWPuIZDEAAAci8PYRz1CeQ04DkPqtJKQnNbF5hQSFL+UPShwyFho
        xqjIUGNDXurD16SVvOC9iuwScdQgUSJsdXFC4kQajbmoUeeFxEuGjdDt+DCdx1GG
        foBSXJsaoDwXJLmYQJ9YhqOSXWNoB4mUTOJQQGMXlxtDMIi2hPIRVSrIg+xnpQbl
        I/BhK86uCFgoyKVWE0EVkOZZSPg1jImmh2IJFoQpKO84ktw+QuWykleM9EgFjgVD
        oUYUwru5PCnkyDgMOakjn8Nu3BrCi9F3UkoXAedMFNOWQ9RjlCBASM0wQiDi2ujF
        HrWHIduKOl4F0Ti/oByhpYkSTn2o2S6ylcUhmm1k/5gbYAHTkBlc0mGETgcGcqkT
        g1DcuLtc0UARfazkIKPONbECz6HyFlAAkQQOJXzEBHsekij6IF2t0PpT4eUCAGFg
        4iCmCg/fRGsCh7I6TPS/KI+xrqA+Eheyp8spCnUT/knujnQCD+Yy5lGCg4dziUQd
        hVfDjnaFgmIQooorFNIMFVD2Qm4GYBKSDGWBLBLSRtQ6QHpu19F9INTdxLdUGEgq
        skFAoSYirsKBz30bumvEleIeE70cQjIgFYZU0PyYUomA3WDAGA2bVaGgnASxy85B
        WQfr3PyFQEMB5pQBRqkwetysYSoj1H1+e1c5h7t8UzlqITbHYmUNY7OP7fJBVHeZ
        +Jxuj5m4eRBK3OR4lxZrsc+q4744iLz6edagasBljnnvNtv/cC1mvzfoVSmYpIa+
        bvfLcuyxF0V2m1b55+xNTaUzHTqe+FruzRLt71AfORf0kxawTm8pFVDH2uBen5Ah
        eALUhBi/hX3R/uQfx906rTIHE+uAkoz8pB9ApTTXWprgV8u0eHAO1T4vbkkX7+kd
        1LC6y1afDsf7Fhq4Ha39VuRVnm41lz2Fbcp9lq7uQCfdm426kzQz/KpjZXp3xZra
        5atPzRDOO5M+UGp5lWn+JLyeUprZ3ZfjykjFu4/vUEtrCZ+lkL5rZPe76uGNIfHy
        u2uexxPjbst0Lf6zzAsxe/l9JpZV6dxlX2vXaY2pCeH17HF2PSzo60N+W6QQKju8
        0KRX6dbZ5NvMwcoCTBsxxMvvCONfXEydj1pFWVHtp2ERqxo2K9ZOuXFqlGFoNK8a
        mrh21tlhtc93VbkfAcdQ8GhkWJXF52x/yMuCDaYlAUKuj19OjfiOwTm8hfFFMk4q
        tnlVQRhwm2OJkCdD/t/rcNf79wMeLNzkRbp/QBJhc3XNiGX2wjbW4ZyDECSLYfRV
        C42WU7lXJ7F9Ho2zZKQxUQn9S3HnddKJrgYzCWEc7sp99WyNM/b/PpWfS6EAfaKj
        3eS3/7CXrcuBFKDRKdw1a7dZdYFRhszAjDZs9zm0EpzYAX6KVUuXd1lKJZXCNbOM
        tSp3D/Ndim01eWmxf063A2H7P8t80LjF/WFdHpwqv88mBGCPYEhBkH1u6e2lzq+N
        bUJ2flce9+IPdACsrjpeXtZRPb/Pi2OVAciXfZDTIJgfMuy9pli46gaAlpQKz2WS
        EmRfUnr7RElptvyDeHWffhWuYfIhQ/w6NJe4o1KXRXWnhRZ1tzVfpw+WUOR7F9lv
        sy/vz1jwKf7WxV1YIazf2Dqg3eCQ6+PqtI3E0ICGvtMAwgk6IJidui2ifQ8g6DTQ
        WoUTDea+VktnbN/R0rD1/7u0RLuNaCmkQ2lbB+ROP7HrdAAD5V6qqvEu+1u+c6gD
        emjU9P1deb9D63XIKI9RJ9UZOSxmuS/TEJSFMupdVzXymhdaYNNnafB1vs9WaKMe
        BO+toa4fG+gihQ8tCZT0/Rf6rdwwaqentaLzX8t1vslXGGLQcPHSEMJ4E2zY1tBz
        lqwzINxn1V25tsRzmBxU1r455N8ycfL7qWu8RgHYlEIziaKb0TzRIIF5e9dFN7vr
        AYARG6geA4umyRTbrLit7nTr/mKyre84jhZ1IX48wEKW63ShXotv2/xGQJVZet9Z
        OdHRonnR5dxoY2MLD+VkG5gxh1v8s+D92xkvLw4V1NEhYIR5ZYt6Jf7Uvjhh5mrg
        3rNm0hTM3jo9h0/5rmd8OQ5apfm2B++oHkKj63W22cJrOwDXfV5HbNOofktl5mTf
        Lk0rFYw5Zbqpsv2wTz7R1wYT3Gs7cVjxbx5uIF9+fxywVHcQ7C3r4fQRG5jJawIC
        2r499NaNO5lg/6NR89BdeYdYAz9slQE7Gnx1gj/kBUyh96bTyHHCm9OncTjOiQy3
        HtqBchw9r02W6QMM5DX76SWiIVZMNaydhdP8a3YE/qjfT1gt+4oassmz7XoMlumw
        HDXVQd1rkKK0KA7mBaOStqhNBJSBNecJKHHP8PDTA4zzPi4O6KdXfSn7gOiq/z8g
        /s8FhGVhUAVL93Cx88Dr/PCpON7fwPiHaqgqtqA0je0LJPS0Ql91g8mt70fWuaHm
        +okYpr8sN5tD1uflSUE/EfEtkNHTaOnUP/k/aihp2zfjJ7O/f/y3D3Rw3HbTx705
        Xz78PLNHjephh3bWNI14oZvg+lQeWObjtdkcLrRLqztmoLf/92bQAeIP1OZyR23a
        +fvyMzp8UiwdYM7ms7b9NgxpaMK9ZOjJdb89fIOAxZfYAhG9yfeH+lBiaQYl6r1E
        Qd9S08J1UPXgMTjHNLwukChW5TpzGr0265xs/2k2O2GoPosAL+Crs8Rq1+ukWzGb
        XY+tIzIw5HUgOr4BZxU/YsRqZqtfjvl2TXMVvUz3q7v8c1aPoewccBJMBWh0PtUD
        zOFnTbt2JhOVWKVakcOGENCQ+rm14Rfk7Y4NhT4Z+kG0089/MAxsekNsibxqFaGr
        ShefuCK0X4krQsuL1fa4ziwGWgLzdZbt6DKFQQ5aFII8HG/q8Q/MWwjwqpsSY87s
        z/mhqhG+3JFWml60uB2c0DgBcL3maiuKG6dWjiZTIOvDP9o0Y5VpM4kyoJ7hymMl
        mqJguTje10GmHbWuj00OqNXecZglJhdEjFk7cfGTWrhkoIWYRLFjrre1IVDusmKu
        pZtry8+/7HOUPF63mv9W7oWlt4Vwx3VlIhYtzIsTh9HDHllx+Uq/ubq2XJJsuSzK
        iuejNwbnRANky4VZmnvdJWPFhY5BTofa9Plef+pGKotmgHvJPt1uxbJBNNvid3IA
        frOn2ZuJLrtkT1u+/KARUkGi8WDdnnC8GZpX9d4UIA3GG83t9QCnr0W6gzXX+ru8
        RVVncrFFoAwWsfqSkqEaOUbaNnqMu/HYakKfMOaL7iHOoAIGujnbj6xYtK8260cf
        0RXllxcDcpNmqSyUhQlvU0OJtzdDpDDItyEpltrNmISRqg0X42FypBk1ZEzV2qYo
        FFqHteuP4LE2N2zX2l0n9N5qgA8tGZq3+fHeOtByBjvU+un3wvVDljfBL0a7oj76
        iGS6ctdxbin1gu5cR5fdrQwzBM3VZWC5Q5mqBqKantekr9PDRvjX4MGi9ZijzubU
        s6kcWX+y0/vopKlrM3efVrXrzwR3aXGb6SMgpj+XIpDtKUmbawd2Ose7HaP98mW2
        G6Db15LupnugVPY0mX6Y2lXCqpQ/NaPLEOJpomSfQcvUeT3lUC0B4m1kbu7poptx
        e9f8neXuxEPN1dRyfYiNrTpg7SRnZTvIe9P0aIT+NJQedOecbVrocw7XisXNqWmx
        piZDevrQwy0J3ZM07chqS8Fsmo4aqBW51y0fC7tffp+t7NsIu2vmi7vW/9vG2fTL
        fy22D+JLuf+EJhK5vHPMrM8XDvVxBg1E9Qms1ULTo6Xo9MFUQupe2Oqh1w2rnUaa
        Bes3091riLetTC2ZLo2p9rjFoVHuFSRJj9vq6oTA3485zSrvy+LHiluHCSKDXTYf
        EQ3chHAmGD/uoBTraLfS6badwOvXXNubg5sGs0OTYaxY4dmMnZEPr7P9vtwfTjt2
        /dZuZgl4pImnpkwriaq30fdpf16gFxnv3qlmma9AzJ7VtpsG9CReCIG4O8Xi7vEr
        mdK8OeHnPv2UUYrQwxhDnGOn9tU6Lds9hOnXB8KYNNy83qX7QzYnhmtiJ8lp6iD/
        UA42qtXd/nju/P/VeNt51QOWukqP3xVwn6ddXsMO5sRXptf7lu3LNxp8br4NKZbm
        6GKTbuFHfRZ4BxNOU1vUlz7WfbJ+NUKSA8nI90p/UaD7xQGGGEHmXqhGHuTH6le7
        B6mj0HPNxOB5bEuR6Tg0Csy/Ich6kH2Gg9qGo0e0u4deogomNN2732q+7HNKZkR7
        /fvgqe0GLm/bDXuLI1va6XXKiyy4jitZ70d26OfrqX160J3deqsTUnVEm3DIZrgY
        nyvo4TIxuNrnoS/yoMDd++3a5IOgw6wf0Jqs7uamYtfJYHxedM/Mkr2KuZgEp6f9
        ppj+JvWZSwv7YY3O/uYI50+i/ISaQd3U9d9OzlPtZ3xWVeen5Ha419m2H2zTJGpp
        +eBqCmhitK+fM9/pHHteflf014sfgviRmvD37lvvrfuWGvHmO88X0jGXI+8J97d/
        ef9xqnE/fWaEMAMiXTcsxsbBoQfzcL5GNPyxo3xx1lPqx3wt9YLLsaGnkdrTd0L0
        HXuw4qGbHagXIxQU4d7kcPdsd6G+aN9HnqS3GDNIadj1dp8+HNCPXM76ozWMt1PI
        k9CR4Sr7PrYYKpRTBIpS39dt04FzmzHE9h6uaP9e4EJ/eYJb1s5B33aLOt8lu9xF
        Zr+9f/t769qDhzpDz8vvsUrePdZfa3pSSLhsiNo2dM8Hvyie4pOuoD+OFL9++PVX
        AUVfhPYP5G175+fbtov9pPTz4S/vWxtlxeUx8Dxux6uOXp1crr8vR5PkG0Fnhktd
        i/gvXujjBeXCrpSbFG3Kms7/+cBhsl42TJg5Vv8/eJB++vRbgfPZ4gJJntFh0EOT
        aHMD+fTsv892WVqJ/Fk1nx778LQnwkUU6Kn/EIP/jIt/6G8mXmCOaR/ULHa7qUv9
        Y5zy8MrUxQWxsDzxU1NsZnXVmRFH45s+wVWf4qLDG1r3r3wkN3wBaz9D96uT3xgZ
        QrBPjCZtc3p9NJ1p9D3g5e55rU+jlj2MJxv+tQ7Q9jg15b+oQClr7z/GLT52VTY9
        PGz6x1fzxQWI9AwcbGnc50bK9JZ0Mr1sDhyvxWG3zSu+yzpv1WE5GfeZchLuM+U0
        DPUPVs+zwo5QnwReqvH6eXrKPTT3oU/zDHp6d6eL8fMp+znXJzxtpf+2e87QXa/K
        TiNknc6anHxSBfS3cObvc+TYY2EuRdYLpGnr5LOP1V6o0ARS5+pZna07CC0Pet62
        j9hfoGv6L0cgfyluTAAA
    }
    zipfile: to-file request-file/file/save %/c/mysong.zip
    if exists? zipfile [
        alert "Please choose a file name that does not already exist." 
        return
    ]
    zip/deep zipfile %chords/
    ]
    btn "Help" [editor help]
]

22.14 Случай: система управления контентом веб-сайта (CMS), Sitebuilder.cgi

В течение многих лет я использовал простой менеджер веб-сайтов CGI под названием Chico WebTool. Вместе с HTML-редактором Javascript WYSIWYG сценарий WebTool предоставил пользователям чрезвычайно простой способ добавлять, редактировать и управлять содержимым страниц на веб-сайтах без установки какого-либо программного обеспечения на клиентский ПК (т.е. без Dreamweaver, Frontpage и т.д.). Новые страницы можно было добавлять прямо в браузере с любого компьютера, и они были автоматически связаны в древовидной структуре подстраниц на веб-сайте. Пользователи выбирали из списка HTML-шаблонов (которые дизайнерам было очень просто создавать), чтобы придать всему сайту единообразный внешний вид. Добавив сторонний редактор javascript, пользователи могут легко редактировать содержимое страницы, даже не зная HTML. WebTool работает на веб-серверах в любой операционной системе, если на них установлены CGI и PERL. WebTool создавал статические HTML-страницы и не требовал никакой базы данных SQL. Это была универсальная установка, которая абсолютно интуитивно работала как для новичков, так и для опытных пользователей, задолго до того, как какой-либо из современных пакетов CMS стал популярным.

Одна из проблем с WebTool заключалась в том, что установка и настройка на каждом веб-сервере занимали много времени. Также был ряд функций, которые я хотел добавить к нему, и некоторые существенные изменения, которые я хотел внести в рабочий процесс. Веб-сайт, на котором распространялся Web Tool, больше не поддерживается его автором. Поэтому я решил создать аналогичную систему в REBOL, более точно соответствующую моим потребностям. Вот мои мысли о том, как это сделать:

  1. Начнём с простого редактора текстовых полей HTML для создания и редактирования страниц. Защищённый паролем редактор, представленный ранее в этом руководстве, является хорошим началом.
  2. Создадим интерфейс для добавления новых страниц и выбора существующих страниц для редактирования и удаления. Всё, что нужно, - это текстовое поле для ввода имён новых страниц и список ссылок на существующие страницы. Содержимое любой существующей страницы будет прочитано и отправлено в редактор текстового поля, а отредактированный текст будет сохранен обратно в тот же файл после отправки пользователем. Если вводится новое имя файла, новый файл нужно просто создать с пустой строкой, а затем отправить в редактор.
  3. Интегрируем код для редактора Javascript WYSIWYG, чтобы автоматически включить визуальное создание/редактирование страниц в указанном выше редакторе. Я решил использовать редактор openwysiwyg с http://openwebware.com, потому что он стабильный, небольшой, работает практически во всех браузерах и предоставляет множество важных функций. Установка openwysiwyg состоит из множества файлов в нескольких папках, поэтому я решил сжать её с помощью архиватора рипов Карла Сассенрата. Это позволило бы мне встраивать и извлекать весь пакет в любой операционной системе прямо из скрипта веб-редактора.
  4. Создадим интерфейс для загрузки изображений и других файлов, используемых на веб-сайте. Функция Андреаса Болка decode-multipart-form-data, описанная ранее, отлично подойдёт для обработки загрузки файлов.
  5. Добавим интерфейс для запуска консольных команд ОС и REBOL, для управления файлами и папками, для создания резервных копий, для загрузки файлов с других FTP-серверов и т.д. целый сценарий, который выполняет всё это, уже был представлен ранее в этом тексте.
  6. Создадим систему шаблонов, чтобы обернуть все страницы контента, созданные пользователями, в единый общий дизайн страницы. Созданный пользователем контент будет просто вставлен в область таблицы в любом HTML-макете шаблона. Это придаёт всему сайту единообразный вид, без какой-либо работы или общего дизайна со стороны пользователей. Всё, что нужно сделать пользователям, - это ввести текст, добавить изображения и/или другой базовый контент, отправить его, и страница, созданная этим скриптом, будет выглядеть завершённой. Чтобы связать страницы вместе и создать структуру навигации на сайте, система шаблонов должна создать несколько областей меню на каждой странице с автоматически сгенерированными ссылками на другие страницы сайта. Я хотел, чтобы пользователи могли добавлять новые страницы в качестве подстраниц любой другой существующей страницы на сайте. Домашняя страница должна иметь возможность иметь ссылки на столько подстраниц, сколько нужно, и каждая из этих страниц должна иметь столько подстраниц, сколько желательно, и так далее, на столько уровней, сколько нужно, чтобы создать простая древовидная структура с домашней страницей в качестве отправной точки. Система должна автоматически выбирать между двумя базовыми шаблонами дизайна: один с областью меню ссылок (для страниц, имеющих подстраницы), а другой без области меню ссылок (для страниц без подстраниц). Я также хотел, чтобы каждая страница содержала отдельную область меню со ссылками, возвращающимися в древовидной структуре просматриваемой в данный момент подстраницы, на каждой странице. Это упростит навигацию по всему сайту как вниз по подстраницам, так и обратно к домашней странице. Это организует каждую область сайта на чётко разделённые подразделы и позволяет посетителям мгновенно узнать, где они находятся, и как перемещаться вверх и вниз по древовидной структуре. Карта сайта и ссылки меню должны создаваться сценарием автоматически, но должны редактироваться вручную непосредственно в сценарии с использованием простого синтаксиса. Страницы макета шаблона должны легко редактироваться веб-дизайнером или даже пользователями, которые знают основы HTML, чтобы изменить весь внешний вид сайта и легко изменить статические элементы, найденные на каждой странице (графика логотипа, цветовые макеты, информация об авторских правах, так далее.).

Шаги 1, 4 и 5 уже были описаны в сценариях, описанных ранее в этом руководстве. Мне просто нужно было бы включить их в новый скрипт с шагами 2, 3 и 6 выше. Я начал с этого большого количества кода, который я уже написал для других ситуаций и рассмотрел ранее в этом тексте:

#!./rebol276 -cs
REBOL []
print "content-type: text/html^/"
print [<HTML><HEAD><TITLE>"Sitebuilder"</TITLE></HEAD><BODY>]

; Читаем отправленные данные GET или POST (стандартный код описанный ранее)

read-cgi: func [/local data buffer][
    switch system/options/cgi/request-method [
        "POST" [
            data: make string! 1020
            buffer: make string! 16380
            while [positive? read-io system/ports/input buffer 16380][
                append data buffer
                clear buffer
            ]
        ]
        "GET" [data: system/options/cgi/query-string]
    ]
    data
]

submitted: decode-cgi submitted-bin: read-cgi

; Если данные не были отправлены, запросить имя пользователя и 
; пароль (как показано в нескольких предыдущих сценариях):

if ((submitted/2 = none) or (submitted/4 = none)) [
    print [<strong>"W A R N I N G  -  "]
    print ["Private Server, Login Required:"</strong><BR><BR>]
    print [<FORM METHOD="post" ACTION="./sitebuilder.cgi">]
    print [" Username: " <input type=text size="50" name="name"><BR><BR>]
    print [" Password: " <input type=text size="50" name="pass"><BR><BR>]
    print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="submit">]
    print [</FORM>]
    print {</BODY></HTML>} quit
]

; Проверим имя/пароль и завершим программу, если они неверны:

username: submitted/2 password: submitted/4 
either ((username = "username") and (password = "password")) [
    ; если имя/пароль в порядке, продолжаем
][
    print "Incorrect Username/Password." 
    print {</BODY></HTML>} quit
]

; Вот функция Андреаса Болка decode-multipart-form-data, заключённая 
; в некоторый стандартный код CGI (все это было рассмотрено ранее в 
; этом тексте). Он даётся здесь в сжатом виде и записывается на 
; сервер, чтобы избежать некоторых проблем, которые могут возникнуть 
; при расшифровке различных типов почтовых данных. Загруженные 
; двоичные данные просто отправляются в совершенно отдельный 
; сценарий (созданный ниже), а затем возвращаются в этот сценарий:
if not exists? %upload.cgi [
    write/binary/allow %upload.cgi to-binary decompress 64#{
eJyFV21v2zYQ/m7A/+GqoYUDTFHaAV2h2A7aNVsHpEixZhgGwxsoibbZSqJKUs2M
LP99d3yRZNnFBFgv5N3xubvnjvR3T84TxTNZvvjxJcS5nk5+u35zewOr9XTSKFEb
eMhlbXhtYrNveAqG/2OSnanKv5LHILKav7t7f7Ocv7t+/XY5v/v17uZ6GX0Uhmet
KAuuonniBueJE3lz+/bPJa4wnSjOijjfihQ2bZ3DKillzkoomGGQtZsNV+vVdAJ4
6Xth8h3ovTa8SmRjhKx1gqqI/0vLtYkrbnayAC9PV/Th9uNdNByhi4ynULHPHLRB
B7ZP4PnFi4tDIbf4WOzlD69Gcvc7UXJYNVILI77yK7AeCRmANlIZnYi6aY236ays
R6DoYk3D62Lo/LFMXnKmTs6u+8/Ba/TLNUXA+XwieBg6tY+dg17NP0hlOrFZKngu
Cx5XbWlEw5SJN1JVsbPp8uZUmnhIljCGsTGxs0YDPsWl0AZyDAo6zEtRxRnf+jcK
Qi3rOFf2UW78F75UouIxQUBkYVWylEIum72lLQ0JUjGwkW1dXMFGoMFDbBB1ziSd
MxgnTJ9pVe3QWd9t0IP9Uw5mRQqfpKghiuPIidGCiWGiJAejjFAwtV9EPrDBXa+H
ESDfhpMYAT9pvc6KgMQFInVUkFVT8grRQL5jSnOD6Is2RzqiWkiji+D/a9T8vhQ1
H6rR2qS4Ctn4F/qUeLkuISg4oPQMPY8LoRtJkbMhS0mT07jrJJFtJU3JRB3BWa/a
s+Gyt44lSHc7h41lQP0oZMSuRlUoazTuEhFAAKv3PYn6aNOF1YAeRodtrte3bDlS
H5aY/b4Mbp7C6Gy5+X7UyAHfvZHeYdsJjk3NYMfqosRi7CQ7JwPagOSsq2f3MtY8
LF5fwN7YaKyn+0Gd90O+qk3VQM0qDl9Z2dp73HgUp9dJoSEiDkbg4XIRPQbMloto
EHceGSNh43upCphpXnKsrYFWRFLRgEdcmB122xmx7oqAYf871tpg+x5r+gujBbNR
CwndY8Dds/H+Yl1Pj/O9/oag3WJk9gmhPRmL0BUg+h6EnhzLONaGHjXOV7i6SvSx
caHpsukKnRPQUbsb0t26crDXHMXcpj11QbOdlOAfWlhRB6q3PJF1ibWF4XRatkdJ
TR1pthGKdLu5M5jZkJ0hwvXYnN883bbS2xiyhlCcsNF50FqVb5bTIYOJtQkry+EG
h9paIvn72nR7wqBhdIsRUL+9dvophPPQdIK32JHCUgl36Dan5vKtnfjU5n6w47mM
9nvxdHIJjZIZznRLYQci30u212B9wyU0yA1gWkG3GTYjwwvolve0JVN/KGxUlqvU
2DRXX5EIrcZjhVWWSmxFjR0isPl7W1+4S4vN3kpg9FVKsPAjJqkUSobJRDjCYDTN
jlqAXcD10w510uykkUmwPJ3cE5Ykw/WUte21xgpdhfqT7jzHL66WcwY7xTeL6DzR
/Sn2HPWvyP6CkNLLs4ZpvaAb0euZi8/CPaLlG5Z/plAMDsLzhC3nmVo+9ksaliG0
e1GY3eLVxVPI0BJXi+cHMgjJFF7o+cXFU2skoO0FHbHmSBVZb5e/f7i5ff0Wfrp9
/+Hm+u56nvhxq4u/Q+mfMUIa8lYptIoViUcPsxMaj1AEPD2hjev6SSS9Mpa68PSc
RvEVj+o26E6kOyC6/wuK27PN6mEQ6Ucn/xAB7vRbbhZ/ZyWrPy/9eBRiF63XtmxC
cAhPYgq62TeKJz7tXwz8v0F/TBDpl1aY/wDPpuhm7AwAAA==
} [read write execute read write execute read write execute]

    ; Я добавил небольшую проверку ошибок, чтобы убедиться, что 
    ; разрешения для сценария загрузки установлены правильно:

    if error? try [call {chmod 755 ./upload.cgi}] [
        print {
            <center><table border="1" width=80% cellpadding="10"><tr><td>
            <strong>./upload.cgi</strong> has been created, but there was
            apparently a problem setting permissions for it.  Please be
            sure that upload.cgi is chmod to 755.<br><br><center>
            <a href="./sitebuilder.cgi?name=username&pass=password&submit
            =submit">Continue</a></center></td></tr></table></center>
            </BODY></HTML>
        } quit
    ]
]

; Если в сценарий были отправлены имя пользователя и пароль, но нет 
; других данных, распечатаем главную стартовую страницу:

if submitted/6 = "submit" [

    ; Распечатаем текущий рабочий путь (по умолчанию, где установлен 
    ; этот скрипт) - просто чтобы пользователь знал, в какой папке 
    ; он работает на веб-сервере: 

    print rejoin [
        "<center>Path: " what-dir 
        {<br><table border="1" width=80% cellpadding="10"><tr><td>}
    ]

    ; Вот форма для загрузки данных в скрипт upload.cgi:

    print rejoin [
        {<br>
        <FORM ACTION="./upload.cgi" METHOD="post" 
        ENCTYPE="multipart/form-data">
        Upload File: <INPUT TYPE="file" size="50" NAME="photo">
        <INPUT TYPE="submit" NAME="Submit" VALUE="Upload">

89140701704 450

; Я добавил здесь новый код, чтобы пользователи могли 
; щёлкнуть ссылку, чтобы увидеть существующие файлы на 
; сервере. Эта ссылка отправляет некоторые данные GET 
; обратно в сценарий. ВАЖНО: *** Данные после 
; вопросительного знака в URL-адресе отображаются так же, 
; как если бы они были отправлены с помощью HTML-формы ***. 
; Когда сценарий видит эти отправленные данные, он запускает 
; соответствующий код в разделе "listfiles" ниже (см. 
; "if submitted/6 = listfiles"). Этот метод используется в 
; этом сценарии для запуска "подпрограмм" в коде CGI для 
; выполнения различных действий. Это позволяет нам 
; использовать один сценарий CGI вместо множества отдельных 
; файлов (аналогично сценарию управления веб-сервером, 
; описанному ранее в этом тексте).

<a href="./sitebuilder.cgi?name=username&pass=password&
subroutine=listfiles">Files</a>  
</FORM>

; Вот форма, которая отправляет данные, предупреждающие 
; скрипт о создании нового имени файла. После отправки 
; сценарий запустит подпрограмму редактирования, используя 
; имя файла, введённое ниже:

<FORM method="post" ACTION="./sitebuilder.cgi"> 
<INPUT TYPE=hidden NAME=username VALUE="} submitted/2 {">
<INPUT TYPE=hidden NAME=password VALUE="} submitted/4 {">
<INPUT TYPE=hidden NAME=subroutine VALUE="edit">
Create New Page: 
<INPUT TYPE=text size="50" name="file" value="">
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
</FORM>}
]

;Эта ссылка запускает "консольную" подпрограмму:

print {<a href="./sitebuilder.cgi?name=username&pass=password&
subroutine=console">Console</a>       }

]

; Если была отправлена созданная ссылка редактирования, запустим 
; эту подпрограмму редактирования. Этот код был представлен ранее в 
; руководстве:

if submitted/6 = "edit" [

; Создаём новый файл, если он не существует:

write/append to-file rejoin [what-dir submitted/8] ""

; Резервное копирование (до внесения изменений):

cur-time: to-string replace/all to-string now/time ":" "-"
document_text: read to-file rejoin [what-dir submitted/8]
make-dir %edit_history
write to-file rejoin [
what-dir "edit_history/" 
to-string (second split-path to-file submitted/8) 
"--" now/date "_" cur-time ".txt"
] document_text

; Распечатаем текстовое поле HTML, в котором пользователь может 
; редактировать текст. Эти данные отправляются обратно в этот 
; сценарий, и когда сценарий видит значение "save" 
; (submitted/6), он запускает подпрограмму "save" (сохранить), 
; используя отправленные текстовые данные. Закрывающие теги 
; textarea заменяются в редактируемом тексте, чтобы они не 
; нарушали фактическое текстовое поле, в котором они отображаются:

prin rejoin [
{<center><strong>Be sure to SUBMIT when done:</strong><BR><BR>
<FORM method="post" ACTION="./sitebuilder.cgi"> 
<INPUT TYPE=hidden NAME=username VALUE="} submitted/2 {">
<INPUT TYPE=hidden NAME=password VALUE="} submitted/4 {">
<INPUT TYPE=hidden NAME=subroutine VALUE="save">
<INPUT TYPE=hidden NAME=path VALUE="} submitted/8 {">
<textarea id="textarea1" name="test1" cols="100" rows="15"
  name="contents">}
replace/all document_text "</textarea>" "<\/textarea>"
{</textarea>
<a href="./sitebuilder.cgi?name=username&pass=password&
    subroutine=listfiles-popup" target=_blank>
<FONT size=1>Files</FONT></a><BR><BR>
<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
</FORM></center></BODY></HTML>}
]
print {</BODY></HTML>} quit
]

; Следующая подпрограмма сохраняет текст отредактированного файла, 
; который был отправлен процедурой редактирования выше:

if submitted/6 = "save" [ 

; Сохраним недавно отредактированный документ (теги textarea, 
; заменённые в процессе редактирования, здесь возвращаются в 
; нормальное состояние и сохраняются в том виде, в каком они 
; должны быть):

write (to-file rejoin [what-dir submitted/8])
(replace/all submitted/10 "<\/textarea>" "</textarea>")
print {</BODY></HTML>} quit
]

; Запустим консоль REBOL (для работы с файлами и ОС). Весь этот 
; сценарий был представлен ранее в руководстве:

if submitted/6 = "console" [
if not exists? %rebol276 [
print "<center>REBOL version 276 required!</center><br>"
]
print {<center><a href="./sitebuilder.cgi?name=username&
pass=password&submit=submit">Back to Sitebuilder</a></center>}
entry-form: [
print {
    <CENTER><FORM METHOD="post" ACTION="./sitebuilder.cgi"> 
    <INPUT TYPE=hidden NAME=username VALUE="username">
    <INPUT TYPE=hidden NAME=password VALUE="password">
    <INPUT TYPE=hidden NAME=subroutine VALUE="console">
    <INPUT TYPE=hidden NAME=submit_confirm 
        VALUE="command-submitted">
    <TEXTAREA COLS="100" ROWS="10" NAME="contents"></TEXTAREA>
    <BR><BR>
    <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
    </FORM></CENTER></BODY></HTML>
}
]
if submitted/8 = "command-submitted" [
write %commands.txt join "REBOL[]^/" submitted/10
; Для функции "call" требуется REBOL версии 2.76:
call/output/error 
    "./rebol276 -qs commands.txt"
    %conso.txt %conse.txt
do entry-form
print rejoin [
    {<CENTER>Output: <BR><BR>}
    {<TABLE WIDTH=80% BORDER="1" CELLPADDING="10"><TR><TD><PRE>}
    read %conso.txt
    {</PRE></TD></TR></TABLE><BR><BR>}
    {Errors: <BR><BR>}
    read %conse.txt
    {</CENTER>}
]
quit
]
do entry-form
]

; Перечислим существующие файлы:

if submitted/6 = "listfiles" [

; Распечатаем ссылку, чтобы вернуться на главную страницу 
; конструктора сайтов:

print {<center><a href="./sitebuilder.cgi?name=username&
pass=password&submit=submit">Back to Sitebuilder</a><br>}
print {<table width=60% border=1 cellpadding="10">}
print {<tr><td width=100%><br>}

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

folder: sort read %.
foreach file folder [
print [rejoin [
    {    <a href="./sitebuilder.cgi?name=username&
        pass=password&subroutine=cleanedit&file=}
    file {">(edit)</a>    }
]] 
print [rejoin [
    {<a href="./} file {" target=_blank>} file {</a><br>}
]]
]
print {<br></td></tr></table></center></BODY></HTML>}
quit
]

Это всё для шагов 1, 4 и 5 в схеме. Шаг 2 очень простой. О новой программе создания файлов уже позаботились в приведённом выше коде. Чтобы создать список существующих страниц, которые можно редактировать, я просто построил ссылки на каждый из файлов с вызовом подпрограммы "edit" в отправленных данных (submit/6 = "edit"). Я хочу, чтобы в этом списке отображались только файлы страниц содержимого, поэтому я удалил все остальные типы файлов из списка:

pages: sort read %.
dont-show-suffixs: [
    %.html %.jpg %.gif %.png %.bmp %.rip %.exe %.pdf %.cgi %.php
    %.zip %.txt %.tpl %.r %.tgz
]

; Удалим с дисплея типы файлов, перечисленные выше:

remove-each page pages [find dont-show-suffixs (suffix? page)]

; Также не показываем каталоги:

remove-each page pages [find to-string page "/"]

; И ещё несколько необычных файлов:

dont-show-files: [%rebol276 %sitemap %.ftpquota]
remove-each page pages [find dont-show-files page]

print "<hr><br>Edit Existing Pages:<br><br>"
foreach page pages [
    print rejoin [
        {<a href="./sitebuilder.cgi?name=username&pass=password&
        subroutine=edit&file=}
        to-string page {">} to-string page {</a>    
        } ; <br>}
    ]
]
print {<br><br><hr>}

Шаг 3 тоже очень прост. Я использовал rip.r с сайта rebol.org для упаковки редактора openwsiwyg - это заняло всего несколько секунд. Результаты находятся на http://re-bol.com/openwysiwyg.rip. Просто "сделайте" ("do") этот файл, используя REBOL на любой платформе, и все содержимое будет распаковано. Вот код, который распаковывает и запускает его, вместе с приятным уведомлением для пользователя:

if not exists? %./openwysiwyg/scripts/wysiwyg.js [
    write/binary %./openwysiwyg.rip to-binary decompress 64#{

        (compressed/embedded rip file data)

    }
    print {
        <center><table border="1" width=80% cellpadding="10"><tr><td>
        <center><strong>INITIAL SETUP: PLEASE RELOAD THIS PAGE AFTER
        FILES HAVE BEEN UNPACKED...</strong>
        </center></td></tr></table></center></BODY></HTML>
    }
    do %openwysiwyg.rip
    print {
        <center><table border="1" width=80% cellpadding="10"><tr><td>
        <center><strong>FILES HAVE BEEN UNPACKED: PLEASE RELOAD
        THIS PAGE NOW</strong>
        </center></td></tr></table></center></BODY></HTML>
    }
]

Вот код javascript, который интегрирует редактор WYSIWYG в наш существующий редактор текстовых полей. Я только что распечатал это на той же странице, что и сценарий редактора текстового поля:

{<script type="text/javascript" 
src="openwysiwyg/scripts/wysiwyg.js"></script>
<script type="text/javascript">
    var full = new WYSIWYG.Settings();
    full.ImagesDir = "openwysiwyg/images/";
    full.PopupsDir = "openwysiwyg/popups/";
    full.CSSFile = "openwysiwyg/styles/wysiwyg.css";
    full.Width = "85%"; 
    full.Height = "250px";
    WYSIWYG.attach('all', full);
</script>}

Я решил, что хочу включить отдельный редактор, не содержащий никакого кода WYSIWYG. Это было бы полезно для редактирования шаблонов, файлов кода и любого другого текста, который не требует визуального редактирования HTML. Я просто включил наш исходный код текстового редактора в отдельную "подпрограмму", которую можно вызвать, установив для третьего значения GET (submitted/6) значение "cleanedit":

; non-wysiwyg edit:

if submitted/6 = "cleanedit" [
    write/append to-file rejoin [what-dir submitted/8] "" 
    ; резервное копирование (до внесения изменений):
    cur-time: to-string replace/all to-string now/time ":" "-"
    document_text: read to-file rejoin [what-dir submitted/8]
    make-dir %edit_history
    write to-file rejoin [
        what-dir "edit_history/" 
        to-string (second split-path to-file submitted/8) 
        "--" now/date "_" cur-time ".txt"
    ] document_text

    prin rejoin [
        {<center><strong>Be sure to SUBMIT when done:</strong><BR><BR>
        <FORM method="post" ACTION="./sitebuilder.cgi"> 
        <INPUT TYPE=hidden NAME=username VALUE="} submitted/2 {">
        <INPUT TYPE=hidden NAME=password VALUE="} submitted/4 {">
        <INPUT TYPE=hidden NAME=subroutine VALUE="save">
        <INPUT TYPE=hidden NAME=path VALUE="} submitted/8 {">
        <textarea id="textarea12" name="test2" cols="100" rows="15"
            name="contents">}
        replace/all document_text "</textarea>" "<\/textarea>"
        {</textarea><br>
        <a href="./sitebuilder.cgi?name=username&pass=password&
           subroutine=listfiles-popup" target=_blank>
        <FONT size=1>Files</FONT></a><BR><BR>
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
        </FORM></center></BODY></HTML>}
    ]
    print {</BODY></HTML>} quit
]

Шаг 5 - основная работа этого проекта. Первым делом я сделал ссылку на главной странице, которая запускала бы подпрограмму "buildsite":

print {<a href="./sitebuilder.cgi?name=username&pass=password&
    subroutine=buildsite">Build Site</a>

Чтобы начать создание системы шаблонов, я создал несколько базовых HTML-шаблонов, сжал их и попросил сценарий записать их в файлы на сервере:

; Создаём сайт:

if submitted/6 = "buildsite" [
    if not exists? %menu.tpl [
        write %menu.tpl decompress #{
        789CB556DB4EDB40107DE72B0623A456AA63C7691E00AFA5247649A484A46129
        E2A9F265B15D8C9DAE9D00ADFA41FDCBCEAEED90847015355292DD999D3D73E6
        CC1873D71EF7E8C5C4813E1D0D6172D61D0E7AA0A89A76DEEA699A4DEDD2F0B9
        A103E56E9AC7459CA56EA269CE8962ED98C268997DA7635B261DD0A163EDE063
        EEAA2AA02BF3E6711230FEBD888B8481AA5A20CD5AE96A8E1CDA81A828662AFB
        398F17A497A5054B0B95DECD18F8E5822805BB2DB4A8B84E8EC08F5C9EB382DC
        C46990DDE46AD3681B02860CD4A774A23A5FCF06DF88327143A63A18802BD01B
        9F50E7841265CA16CC4D641A1FC09E7357E4428C4FF78991A6F151C43BA51743
        07042FD5F57E9EE3BE7B18650BC6E137824B327E085E326747203CD480F95919
        F110E629269DC429DA3CD7BF0A79863B87B0F7E5C0B11D1BFE2003F206BC4993
        DCED98DDB17D015ED81371C99E2E1FDCA69D2E027193384C89CF443E2B1189A2
        809771BC8BE8E0B32499B84110A72131E4EA74E6FAE52A62711821934D5DDF57
        E0260E8A882807ED7D912A15378BEFA9F8B037C35788121120E4EC0E161D09A6
        C8664B78EF85482EDE00A93A6EB4CAD3DB3055676E2214658DB0B986B0A9BF1C
        220868880C6F9BAC15072D5F506DA53C48374110D807BF1869A1E5944EC727C7
        F8C38588B34BA2A088D96D43285BB1FAD9353335D712CAA8DC34114A7C4F7017
        2FC30F418826132CB79EA0C79397574934756B13A96CC41A49435BC5620E46C7
        95B3E7E64C0819974885D0AA3385C1A873ECAC143ACA9134863F72EE8B581173
        D1D0F8310B3116E654F6FC7A16756D9F97D35A3EFF5B5CD551437F58DAD5CA96
        3A929535ACBAB02396CE97E5ABAAB72D6DC1C6EB1AED0194AE909F24ABCC0E7B
        D9326BE3E6F0C5FA5DE562F89A5AEDB2D45085AE1ECB5321B792A1DA074C8F57
        4E0F51BFB87A7B97F279DFFA9945F02C8BF7055DF273CF4211BCB81ACB38CBF1
        29E0949AA83AFCD102CCDC22DAE0FF125F6E2B00DE21918DBE5A0F87AA457A53
        D25AE7F63513BCD97E6B976D657855DD9B7C556FFE923231E56A716E6AF3BEB3
        1EB73CF71AD90B5AE2AF4EA7BD6DA6D76DF25498F295FD449847E647D3EA65B3
        3B2E4EC15F3074FD006814E770CE3C3845AF06749204A43D07CE72C6172C686C
        1D2F2FA747125A954893FFC3EDFC039C3D1A760A0A0000
        }
    ]
    if not exists? %nomenu.tpl [
        write %nomenu.tpl decompress #{
        789CAD556D6FDA3010FECEAFB8A6AAB4490B09B47C288D235192162468197557
        F5D3E4246E92CD4B9863A0DDB41FB47FB973121874636DA7FA83E397E3B9E79E
        3B1FCE9E77D9A7B7131F06743C82C9F5E968D807C3B4AC9BC3BE6579D4AB2E8E
        9A3650C9B22255699E316159FE85E1361C7DE93A03BFE7B90E1DD291EF420387
        B3679A80B63C98A722E2F2A34A95E0609AF5B555D9369CB14F7B90283533F9D7
        79BA20FD3C533C53267D987108AB0D3114BF5756A2BE88130813260BAEC832CD
        A27C5998AD76A76DAC8006944E4CFFFDF5F00331262CE6A68F00D280FEE505F5
        2F2831A67CC19928037903DE5C321D0D69BFFB1D1A691DBDD57857F476E48356
        A6761F16059EB36E922FB884EF484EE4B20B8198F313D01666C4C3BC42ECC23C
        C3A8459AE15DC0C2CFB1CCF1A40BFB67C7BEE77BF00325283DA027AB54AFE19C
        5E7AB710C47D8D4BF6ED72E031ED9D221126D2382321D7F16C2012C3802097E8
        8BD8107221262C8AD22C26ED7277356361B54B781A27A864CBB60F0C58A6914A
        8871DC39D0A152ED597FA77AF21EC3D78C840688257F8045AF24A3F2D99ADE6B
        312A37DB9476330A041EAF705AB6EB4CB66472CB4A6390487E478CA68505C3EF
        9BBA8A0CD7198ECF6BE380155C270AB7C846E7C29FC270DC3BF73702490AE4CD
        7151C89018096778DCFC348B11C96295236B826BEAE969BA53CCA7A55BE5FFAE
        1CAF2B253C2BBD355A1B05AD8574ACF5424565B42F016AD9EB7AD38CD6A08F5B
        C48CA94477880D6F28E74BBDEDA6FDCFC4E06B463D3372B82DE64B0ABCD5F9CF
        B4FC3DC28D52F943AABA31566AD52295E1E154E2E257D3DEA8C8DD374F3DFCBA
        15ADC2E9A0EDA387E69C617FAD1A225926C813C97EE3A4E5F6F3D983D4BF829F
        D0B6ED63A0495AC00D0FE00AAD9A003D21A0342840F282CB058F9A8EA5E1DCED
        07F5FCC050885A5AABFC736AFC02A1651F4AE3060000
        }
    ]
]

Приведённые выше документы представляют собой простые общие HTML-страницы со вставленными 4 кодами:

sitebuilder_title   ; Заголовок страницы 
                    ; ** По умолчанию совпадает с именем исходного 
                    ; файла ** 
sitebuilder_links   ; Меню ссылок, созданное этим скриптом (как 
                    ; определено в карте сайта, которую мы создадим)
sitebuilder_path    ; Ссылки возвращаются через иерархию подстраниц 
                    ; на домашнюю страницу, которую мы также 
                    ; создадим. 
sitebuilder_content ; Все данные, содержащиеся в исходном файле 
                    ; каждой страницы с контентом.

Где бы ни были найдены эти коды, сценарий заменит их соответствующими данными, а затем сохранит созданные файлы как веб-страницы для просмотра на сайте.

Я сохраню список ссылок, которые должны отображаться на каждой странице, вместе со всей древовидной структурой подстраниц сайта в файле с именем sitemap.r. При первом запуске скрипт создаёт файл sitemap.r и пустой файл содержимого домашней страницы:

if not exists? %sitemap.r [
    write %sitemap.r {%Home []} 
    write %Home {}
]

Начиная с домашней страницы, каждая запись на карте сайта будет просто состоять из блока, содержащего 2 элемента: имя исходного файла и блок ссылок на подстраницы. На карте сайта должна быть одна и только одна "домашняя" страница. Это может быть любое имя файла, но по умолчанию мы будем использовать "Home" (обратите внимание на созданное выше имя файла %Home - которое может быть заменено пользователем на карте сайта, но это рекомендуется по умолчанию). Мы также автоматически создадим страницу index.html, которая будет перенаправлять на домашнюю страницу, если index.html не существует. Вот пример того, как карта сайта будет выглядеть только с одной страницей на веб-сайте, помеченной "Home.html":

%Home []

Имя файла (%Home выше) содержит имя исходного файла для обработки (файл содержимого, который пользователь может загрузить или создать с помощью встроенного редактора). Следующий за ним блок (пустой выше) содержит имена всех подстраниц, которые будут обработаны и автоматически связаны с ними (в приведённом выше случае нет).

Ниже приведён пример того, как карта сайта будет выглядеть для сайта, состоящего из домашней страницы и двух подстраниц. Home.html, Page_One.html и Page_Two.html будут созданы из перечисленных исходных файлов, а строка меню будет автоматически сгенерирована и помещена на Home.html со ссылкой на две другие страницы. Ни Page_One.html, ни Page_Two.html не будут содержать никаких панелей меню со ссылками, потому что они не содержат никаких подстраниц:

%Home [             ; ваша домашняя страница (index.html пересылает 
                    ; на неё)
    [%Page_One []]  ; Page_One.html отображается в строке 
                    ; меню Home.html
    [%Page_Two []]  ; Page_One.html отображается в строке 
                    ; меню Home.html
]

Следующий пример карты сайта ниже содержит домашнюю страницу с 5 подстраницами, 3-я из которых содержит 2 подстраницы, а 2-ая из которых содержит 3 подстраницы. На сгенерированных страницах .html меню ссылок размещаются только на тех страницах, которые имеют подстраницы (т.е. только Home.html, Page_Three.html и Page_Three_B.html ниже будут содержать меню ссылок):

%Home [                      ; ваша домашняя страница
    [%Page_1 []]             ; Page_1.html строка меню Home.html
    [%Page_2 []]             ; Page_2.html строка меню Home.html
    [%Page_3 [               ; Page_3.html строка меню Home.html
        [%Page_3_A []]       ; Page_3_A.html строка меню Page_3_A.html
        [%Page_3_B [         ; Page_3_B.html строка меню Page_3_B.html
            [%Page_3_B_1 []] ; Page_3_B_1.html меню Page_3_B.html
            [%Page_3_B_2 []] ; Page_3_B_2.html меню Page_3_B.html
            [%Page_3_B_3 []] ; Page_3_B_3.html меню Page_3_B.html
        ]]
    ]]
    [%Page_4 []]             ; Page_4.html строка меню Home.html
    [%Page_5 []]             ; Page_5.html строка меню Home.html
]

Ключом к пониманию идеи карты сайта является то, что любые имена исходных файлов, за которыми следует блок ссылок, будут содержать автоматически сгенерированное меню ссылок на эти подстраницы в файлах .html, созданных этим сценарием. Страницы без блоков ссылок не будут содержать ссылок на подстраницы. Их просто заворачивают в шаблон. Конечно, пользователи могут вручную ссылаться на любую созданную ими страницу, если они не хотят, чтобы на сайте появлялись автоматически сгенерированные меню ссылок или дизайн шаблона. Они могут использовать этот сценарий для простой загрузки содержимого или для создания/редактирования файлов HTML сценариев. В этом случае им вообще не нужно создавать карту сайта.

Чтобы обработать страницы и построить сайт, я задумал создать функцию, которая считывает каждый элемент карты сайта и выполняет следующие действия:

  1. Если элемент не содержит подстраниц, используйте шаблон nomenu.tpl. Если он содержит подстраницы, используйте шаблон menu.tpl.
  2. Прочтите содержимое указанного файла и замените код sitebuilder_content в шаблоне содержимым файла.
  3. Замените код sitebuilder_title в шаблоне именем указанного файла.
  4. Если текущий элемент является подстраницей другой страницы, замените код sitebuilder_path в шаблоне ссылками на содержащиеся страницы, чтобы создать путь вверх по древовидной структуре вплоть до домашней страницы.
  5. Если элемент содержит подстраницы, замените код sitebuilder_links в шаблоне ссылками на каждую из перечисленных подстраниц.
  6. Сохраните файл, используя указанное имя файла плюс ".html".
  7. Вызовите эту функцию рекурсивно для каждой подстраницы, содержащейся в текущем элементе. Это будет работать на всей карте сайта.

Вот код, который я придумал:

; Сначала загрузим карту сайта и получите имя домашней страницы:

homepage: to-string first load %sitemap.r
current-path: rejoin [
    {<a href="./} homepage {.html">} homepage {</a>}
]

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

begin-recurse: true

; Вот основная функция. Требуется имя страницы для чтения из карты 
; сайта и переменная текущего пути, чтобы отслеживать, где мы в 
; данный момент находимся в дереве карты сайта:

recurse: func [page current-path][

    ; Установите текущий путь (где мы находимся на карте сайта):

    either begin-recurse = true [
        print-path: (to-string page/1)
    ] [
        print-path: rejoin [current-path { : } (to-string page/1)]
    ]
    begin-recurse: false

    ; ШАГ 1

    ; Выбераем, использовать ли шаблон menu.tpl или nomenu.tpl 
    ; (page/ 2 относится к блоку ссылок на текущей странице - если 
    ; он пуст, используем шаблон без меню):

    either (page/2 = []) [

        ; ШАГ 2 (для страниц без меню):

        constructed: replace (
            read %nomenu.tpl
        ) {<!-- sitebuilder_content -->} (read to-file page/1)

        ; ШАГ 3 (для страниц без меню):

        constructed: replace constructed {<!-- sitebuilder_title -->}
            (to-string page/1)

        ; ШАГ 4 (для страниц без меню):

        constructed: replace constructed {<!-- sitebuilder_path -->}
            print-path

    ] [

        ; ШАГ 2 (для страниц с меню):

        constructed: replace (read %menu.tpl)
            {<!-- sitebuilder_content -->}(read to-file page/1)

        ; ШАГ 5 (для страниц с меню).  Этот код создаёт HTML-ссылку 
        ; с красивым эффектом смены цвета для каждой подстраницы, 
        ; указанной на текущей странице (блок page/2):
link-list: copy {}
foreach item page/2 [
    link-list: rejoin [
        link-list
        {<TR><TD style="border: solid" }
        {onmouseOver="this.bgColor='#FFFFFF'"; }
        {onmouseOut="this.bgColor='#D3D3D3'";> }
        {<CENTER><FONT face="Arial, Verdana, 
            MS Sans Serif" size=1>}
        {<A HREF="./} (to-string item/1) {.html">}
            (to-string item/1) {</A>}
        {</FONT></CENTER></TD></TR>}
        newline
    ]
]

; Теперь добавим его в область меню в шаблоне:

constructed: replace constructed {<!-- sitebuilder_links -->}
    link-list

; ШАГ 3 (для страниц с меню):

constructed: replace constructed {<!-- sitebuilder_title -->}
    (to-string page/1)

; ШАГ 4 (для страниц с меню):

constructed: replace constructed {<!-- sitebuilder_path -->}
    print-path

]

; Шаг 6:

write (to-file join page/1 ".html") constructed
print page/1 print { ... DONE<br>}

; Шаг 7:

; Этот код создаёт путь конструктора сайтов и вызывает функцию 
; рекурсии на каждой странице в списке ссылок:

if not (page/2 = []) [
if (to-string page/1) <> homepage [
    current-path: rejoin [
        current-path
        { : <a href="./} (to-string page/1) {.html">} 
        (to-string page/1) {</a>}
    ]
]
foreach block page/2 [recurse block current-path]
]
]
print {<center><table border="1" width=80% cellpadding="10"><tr><td>}

; Запустите весь процесс сборки, вызвав функцию рекурсии в карте 
; сайта. Это начнётся с домашней страницы и будет двигаться вниз по 
; древовидной структуре:

recurse mymap: load %sitemap.r current-path
print {</td></tr></table><br><a href="./sitebuilder.cgi?name=username&
pass=password&submit=submit">Back to Sitebuilder</a></center>}

; Напишем файл index.html, который будет перенаправлять на домашнюю 
; страницу:

if not exists? %index.html [
write %index.html rejoin [{
<html>
<head>
<title></title>
<META HTTP-EQUIV="REFRESH" CONTENT="0; URL=./} 
(to-string mymap/1) {.html">
</head>
<body bgcolor="#FFFFFF"><div id="divId">
</div>
</body>
</html>
}]
]

Я добавил следующую ссылку на домашнюю страницу конструктора сайтов, чтобы я мог вручную редактировать файл sitemap.r с помощью подпрограммы редактора (без WYSIWYG):

print {<a href="./sitebuilder.cgi?name=username&pass=password&
    subroutine=cleanedit&file=sitemap.r">Edit Site Map</a>       }

На этом этапе я могу создать и отредактировать свой собственный файл sitemap.r и фактически построить сайт. Теперь мне нужно написать код, который автоматически строит карту сайта. Я добавил следующий код в подпрограмму "save", созданную ранее, чтобы пользователям никогда не приходилось понимать формат карты сайта или когда-либо вручную создавать/редактировать файл sitemap.r:

either (submitted/8 <> "sitemap.r") and (
    submitted/8 <> (to-string first load %sitemap.r)
) [
    print {<center><strong>Document Saved</strong><br><br>}

    ; Этот код рекурсивно просматривает существующую карту сайта и 
    ; перечисляет все существующие страницы:

    recurse-sitemap: func [page] [
        append sitemap-pages page/1
        if not (page/2 = []) [
            foreach block page/2 [recurse-sitemap block]
        ]
    ] 
    sitemap-pages: copy []
    recurse-sitemap load %sitemap.r
    prin {<table border="1" width=80% cellpadding="10"><tr><td>
        <center>Now ADD this page as a SUB-PAGE of another in
        your site map:<br><br>}

    ; Для каждой страницы, находящейся в настоящее время на карте 
    ; сайта, распечатайте обратную ссылку на скрипт sitebuilder, 
    ; который запустит подпрограмму, которая фактически добавляет 
    ; новую страницу в выбранное место в дереве сайта:

    foreach page sitemap-pages [
        prin rejoin [
            {<a href="./sitebuilder.cgi?name=username&pass=password&
                subroutine=addsitemap&newpage=}
            submitted/8 {&existingpage=} page {">} page {</a>       }
        ]
    ]

    ; Дайте пользователю возможность не добавлять текущую страницу 
    ; на карту сайта:

    print {
        <br><br>If you've ALREADY added this page to your site map,
        or if you do not want it in your site map
        <a href="./sitebuilder.cgi?name=username&pass=password&
            submit=submit"><strong>click here</strong></a>
        </center><br></td></tr></table></center>
    } 
] [
    print {<html><head><META HTTP-EQUIV="REFRESH" CONTENT="0; 
        URL=./sitebuilder.cgi?name=username&pass=password&
        submit=submit"></head>}
]

Это подпрограмма, вызываемая при запуске и отправке приведённого выше кода. Он повторно просматривает существующее дерево сайта, находит страницу, которую выбрал пользователь, и добавляет информацию о новой подстранице, а затем сохраняет данные в файл sitemap.r:

if submitted/6 = "addsitemap" [
    recurse-add-sitemap: func [page] [
        if page/1 = (to-file submitted/10) [
            new-block: copy []
            append new-block (to-file submitted/8)
            append/only new-block []
            insert/only page/2 new-block
        ]
        if not (page/2 = []) [foreach block page/2 [
            recurse-add-sitemap block
        ]]
    ] 
    recurse-add-sitemap new-site-map: load %sitemap.r
    save %sitemap.r new-site-map
    print {
        <html><head><META HTTP-EQUIV="REFRESH" CONTENT="0;
        URL=./sitebuilder.cgi?name=username&pass=password&
        submit=submit"></head>
    }
]

На данный момент скрипт полностью работоспособен. Я добавил ещё одну подпрограмму для отображения текста справки:

;  Печать инструкции:

if submitted/6 = "instructions" [
    print {<pre>}
    print instructions: {

    ; ... ТЕКСТ ПОМОЩИ И ИНСТРУКЦИИ ТУТ ...

    }

    print {<pre>}
    quit
]

В завершение я добавил следующие ссылки на главную страницу для отображения справки и просмотра сгенерированных страниц веб-сайта:

print rejoin [
    {<a href="./} (to-string first load %sitemap.r)
    {.html" target=_blank>View Home Page</a>       }
]
print {<a href="./sitebuilder.cgi?nameword&
    subroutine=instructions">Instructions</a>}
print {<br></td></tr></table></center></BODY></HTML>} quit
]

Следующий код представляет собой полный сценарий CGI веб-сборки. Это самая длинная программа в этом руководстве, поэтому она аккуратно сжата, чтобы поместиться на этой веб-странице. Распакуйте его и загрузите сценарий sitebuilder.cgi на свой веб-сервер вместе с копией REBOL версии 2.76+, и вы можете использовать его для визуального создания веб-сайтов WYSIWYG прямо в браузере. Загружайте и редактируйте файлы любого типа, автоматически создавайте меню навигационных ссылок, оборачивайте страницы в шаблоны дизайна (2 включены для начала), запускайте консольные скрипты и всё остальное, необходимое для создания полных сайтов и управления ими. Установка занимает всего несколько секунд, и новички могут сразу же использовать её.

REBOL [title: "Sitebuilder CGI"]
write %sitebuilder.cgi to-binary decompress 64#{
    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
    hI55M8U/gP7vhgF/Hgc8NHh8fLv6VP6ffvPot5Dvl+4/Yb/vRO0vPxGutjlU9ivk
    /EXP3yR96vn7rtf+wXaG0e/HA0Cn8cd36vUr2sU/IfFPk/8FzH6Hez/R0t8gya9f
    P59+J8af+s5/xoy/wa19XLTHkL8h4j8rWPXH7JO/LMcu/SHK+49fmvz128b8z1ji
    335DX/+T8f962vHvhO5XCvertXz9Hc3jnux3mlRP1ZgfVjqCv1CAr78KNL8J+Ljl
    1cFXTvQqOW59/Q05+06zuuxIpL/pf+ry80b8Q0b2faTPrv/3t/8+/ucttm+nw0rj
    H6tj8z6nedDFzwUMn1b78eu+/ET3fqe876WW/wIb/viHi8zyKDrs9NsSf+FsPy3x
    b2ce9p+o6bcSfmF2/0AC+l+S8Ks6fpYRR5+e/EtH9lvk+njGy0HD02OX/6HIf8jE
    v5vA7FfT8eU/2/T/Vxr+09c//X5PToZ/zGo4aHHbj99zxz/98Xs954hFfxiydvnD
    MCVJvh5t/vxPf/ws3R0tii49PtMjGPzTH7vm8zqou+Ozzz8/j7zzeT/6fPoZSY/r
    7PP+/u3peCz1+Ow+5fSfV+n+fSZ9XLdz/IfYD7Pv4ebb1D7+/I1i/910Pgs9nxf/
    8a3Zv/4XRBxE83vZ6vujr+DXv3xGpEPy/3Eo/xD9ccSVg1y3fR4Pv9PBNzv/1MAv
    tc6fI9uxgmTs3lM7+t/EfZYM/I/k2O/2W22hjaKPb70//r8s8nuPz2e/DU9ff8i+
    5yDuMLIP7jN6fi5H/baFP+enr986HNHm78b4XU7/u2D3PeD9L8eFTw/4588F/Pi3
    k+jf6f9wr7/93b1vMeSfm2Do/v2nPycRfzsU/LnGXwX/5e9i+F9/VsSnsv52evC/
    vLTPSmrz6/p+SW5fv+/IZ9X6Q/a7b6v4Hmf/N0/gW5fPvl//xHxefhvyPxnu5/39
    7bB/+/iXX1We5P0wfnzLQL/m6X/9+Os3L//6ceT9NB5//L+Cym/KP1n5YdLfyj6f
    5vbbIf/yv1nHB0hrj8j3iYY+L/7/U2beHIqYwm+17a9/En7z7XPMv/2dTf1Xsddv
    y5hHiPxc0DfJcfTxaTsfVd6U/6DW/Q/R1bd0ciqk/YSsjm38tMFfdvkXYPSrAPIv
    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
    0JLKjHWaW0qqN8dJfzvRLcsaORVOFLgVrkIcI7NiZ37SJLtxicdu3KOuT1+Xttqt
    nN75vDfk0Fgmx9kw7k77Y9sbjrrreNBzzGlj28DHdR4sgaketRWLx40qESiTxtjA
    92X/jBZYuhULbHy0KirdYKRuRWYZw6I1rkKL54rEuZPmXL/2N4bWUUYn5ug2ieow
    YXSSUFW6nFrFyXPS4NuLRn29EJaXQ7VW9UbN4XpXlyAaqUNsuImVa2PRaVkdlhwo
    sb5oJM36YL2MVztuV3ZDB06y3s/NmcJRjaRrtntTAJ2ZfZpoHjdRxkmjO6nZp+3S
    31Z9Hvdk8hxLbWlL8lRfulzd8vYnbR1u5322xiYn+6xGQle9nuLeZLm/tncn7cIc
    NkDxniQtqZagmDW6Oxa6s5npJIRxWnvVcgp/7qw39UHfWdk252/IxoLvVSZii/DX
    W9pgmaVaqQ2C1nm4XA6M1q7BLGnd6YW0Jg8Xijetlq2TLc+qlUF9Z1y8aofrypWF
    AW5xtPW9SX1mrI+iznhD2vKsrVodd/3ZWhjbo+bVq8YBTVTjcjqab240RdBXzMWR
    vfaZDPc7sx/Mtb67q68tb1FnTvth5ert18cNce32JsR8yS0c4Ro41ejUfrru+dQT
    FqttrbsfB/toJzZGicuMq+OR0Zka5GRgVzdqdzEeervzdm00+Etyji+zylhdyVrd
    fNrnyLYu3mg0PgzEPXviuNqkYw81rtveH6L+dT3QuYEa+c0B3pkvq7XRaTY5u4d6
    axiS7mG8oJ6+qWUYHt2Z3DivB4bbqg6Xuz4nLdtsfDrO5IAnnPph5LoBbW6smUSQ
    4+5JlunAncpjQb9W2kF5TTisn7bkrhMNvJhtD4h4siKD+ah7Ydr9yzlcj/f0aSA5
    wWiBq42dflyM6dbkutgkk4Z5rS03ZYtux4f+gJyZhy1uNuWx4u27s3NzUx8PRp7b
    aWjaNXKj6aE6PWwsvMlNrZ1ypNa7q0nLjUmv+nRXBFllGW/AnZc14hhRRNhfUB2F
    HXOXPd/vKMcmvx73lRj3prEcdWu25x/GZizi3cp5xOv18orYpr7y955wCJP2eh+3
    CL13xuN43AlbU47b9tnG2hqH26DTb0U6u5hK9Ixd9PazDrMcd8fjXnmLBYtvom6w
    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
}

editor %sitebuilder.cgi

Вот небольшой скрипт, который я использую для быстрого создания изображений заголовков для запуска общих сайтов. Чтобы использовать в стандартных шаблонах sitebuilder.cgi, преобразуйте изображение в .jpg или переименуйте ссылку на изображение header.jpg в шаблонах на header.png:

REBOL []

save/png %rebol.png to-image layout/tight [
    origin 0x0 space 0x0
    image logo.gif
    image logo.gif
    image logo.gif
]

save/png %header.png to-image layout/tight [
    origin 0x0 space 0x0 across

    ; Загрузите сюда своё собственное изображение:
image  load %rebol.png

; Используйте здесь свой собственный текст:

box black 400x72 font-color white "Sitebuilder Demo" effect [
    gradient 1x0 tan brown
]
box black 50x72 effect [
    gradient 1x0 brown black
]
]

view layout [image %header.png]

22.14.1 Sitebuilder.cgi вместе с Makedoc

Вот версия скрипта конструктора сайтов, который заменяет компонент JavaScript WYSIWYG форматированием Makedoc. Этот сценарий полезен для создания веб-сайтов с документацией, на которых все отдельные страницы вводятся с использованием простого синтаксиса разметки Makedoc. Всё содержимое страницы преобразуется в аккуратно отформатированный HTML-код с помощью встроенного скрипта Makedoc каждый раз при создании сайта. По сути, это мощная вики-система, которая обеспечивает согласованный макет, быстрый и простой ввод содержимого страницы (без какого-либо HTML-кодирования) и только абсолютные минимальные требования к браузеру. Javascript даже не требуется в браузере веб-мастера, поэтому сложные сайты можно создавать и редактировать практически с любого устройства, подключённого к Интернету.

REBOL [title: "Sitebuilder Makedoc"]
editor decompress #{
789CED9DFF72DC4696A5FFD753606AC26D71961481C46F5952470248B4B5634B
1E89EE0E8757EB28B240B15A6415BBAA689AE3D0BBECA3EE7733011400526D77
B727663676D46D89AC02129937EF3DF79C9B5959FFFC4F4F9E1C6F9AD3F5A54A
13EFE86CFBE88D295E7FE57DFFEED1F566B9DA79B3B3F56AD7AC7647BBBBEBE6
A9B76B7EDA1D5FECAE2EFFF7F1ACBDE2FB675F9E7CFDD58B675F1A5DBD7876F2
F2E42BF362F676B96B4E6F96978B66337B76EC5E7C76EC2E295E57DFBD78F768
D3CC174767EF974FBDF39BD599F7FDF1E5FA6C7EE92DE6BBB9777A737EDE6CDE
7DFFC8E3CFF676B93BBBF0B677DB5D7375BCBEDE2DD7ABED3177D2F1BFDC34DB
DDD155B3BB582F3C77B9FC997DF3FAEDC96CF082FC91969F7A57F30F8DB7DDD1
F5F7FFE405BEF247D7B8074FAF4AC26C7CD9EDC5F2B2F1BEBF5E6F97BBE58FCD
EF3D3B98E5BAEBE4F57AB3DB1E2F57D737BBB649D7C8BB718FE4CFFCFABA592D
86C3BE77C9D96533DF3CF4E6BB47F77F9AFDC1C8C8DD601FB01916DBDC1DB9A1
B9BBDCDF72C3A3778F1E6D6F4EAF96BB5DB378EA2D9AB3F5A29139F2FA578F4E
97ABA75E37778F1E7DE12DCFBDD5DAF5FF62BEF54E9B66B5BFFCD06BE7C8BBD9
369BE3EBF976FBF4D1236E79FCB8BFE65879CF6962D51C78EB8D37783DEA5E3F
6867B273387ABF5EBD7F31FB93A7BD37DE2BEF25FFFDC1F38E18FDBBE185B36F
36CB1FE7BBC67BDB6C7E6C3687DE57EBF7CB95F7862E2D378C10D76C9B7A56BC
B1FF8D6E7F56BF7EF3B5F7B539F9F275F57CC664EF669E2E4F5EBE7EF57CF6E4
78BB77F127986236BE77E67DCB8057F32B8266E63D73AE2031F45C42C8DB2EFF
BD793E8BFD9927973C9FC9DFB3877B31F3BEC16AB7EBCDE257B52426FE444BCF
5EBEFAE6DB13EFE4BB6FCCF3D9DB6F8BAF5FE229AFF4D7F29B35FACCFBA3FEEA
5B7E75733019D2B363B1C7E8B59F9F1DDB6826B405033E7A1876275EF4857776
D19C7DD8CFBAD7300177DE6E79D5304FE2F1D79BF5FBCDFC4AFC67B93A5B6F36
CDD90EDF7874B3B7DBD915EE2B56F7AEF726E85EB4DD787475B7BF7EE851DED5
DDFE9E814B3D6A96BB0BC2F1F1E3EE3E9C6CDFC88137A76B8FBB5BED7BDD2F07
CE3F1F9F2FB964F8AC9FCB1623ABE5D641C29A18395F6FAE8E242CBEF878E03D
7BD17A780B00366CF6C6596EBDF58743EFFDDA5BAF1E7520D102F0CBCE38BD47
1D770EF16436BCF21393E1D9785BAD775EF3D372BBDBFEDEFB4C5CF76A7EFD64
D376E776C30BC3977FFEECCB3596F9FEDD476F78817DF1E78F32C134B9374182
9D3A9F699BFCC2DD73DCC2DBA0EDD94CDE75007BB369BC65D7B1815F6D9A3FAF
09D43D5AFEFCEC74F3E2D919466EF877373F057E4FB140B3793E0B66DEED72B1
BB789EF99F7967CDE5E5F57CB100DD78C727127672C3E28534D037E722DBA58D
5F11D9DEFEC641085D2C170BB0CE0650EF4C6D007D1C3BC8ECC52FB6D0BBDC03
2D44BFAA05AEDFAC6F76CB55DF8B662141DCDF5802DB4CE3ABE61648794FC03C
D8E483A8724EBE9B793FCE2F6FF8E5137DF9EB90D2FE3AB8D5C1C9C7410EBAA6
575BC295EC69538CF7D913979B88AFA3EDC5FAF6684B125CFEC435DF7FF64468
0857FCF9FA3D7FBFC7213F7B72BD929F4FAFAEF97BB394BF9B9F1A797D21EF4A
22E3E70B79FDDFEDBB3B86CADFD7D2CE467E7AFFEFF2B7BDFF6AE13AB569AED6
3F3647CD1C0E221D74BDF4BEB73070AF6792BFE487DFDBCB0E7E4513BB759B8D
DD5BB3E3D93B09105AFE9C79A0696FB194F05F6F96CD76620E99166B8C9EC275
81C600CE77D77FB959EFE6B63981B5B977CED4AF2DFEAD170BCFDEEDFD2D8374
77C87BC32C307B76B1B1F165F037CF4830CB70BEB1B3690397FF1C54018AF79E
D17BC42762DFC53FEC62D39C3F109EBFB71EFAD1EB43F0E7DF492CF14A1F523F
FF6E1F1BCF25287E270379FE71F488C93C10711FEFBDF6EC78FEC2FBDDEA747B
FD45FBCFA8898F185AC6BA6F78C8B03A946E0D2246FBF800E6FD864315E2B8DA
8FB7C7E0999B2921E9DED7F3EBC1A8FECFBD7F3FBEFB8FEDA3BD55DA98BD28E4
47DBAB7FAC471FBDC7FB893B5F6E609F97EBF930091D783F5BF89879BBF9E67D
B37BFEC3E9E57CF5E1C51F970488CD72E2BCFFA97659AE18C0CD9925EEB3172F
07BF49B73EDE77A9634972C73BFB93E447FEEDF2E527491AA089BE734D370B4F
5CC5BB5CAE3E3CC0E49F3E94F36D821992882EDF637F71B9DE30B717F3DD1138
3668207BD7528133979656985EB8E00EBC69B6027E9616B46CE2747EF6E1E6DA
7B7CDA0884C02EE72B018F393F5FCD17CDC1537BDDD9CDE64828E6D341E46E9A
EBCBF9193DBBBC1CBCBA5ADF1E5B323A7B3AF36647B31658CF6EAEB0D90F9203
9DCAF97523B1770BA5B1EF7C2666F9E182CEAF377703FE346DAA4789BECDD9F0
CEE3D93E41EF3BFE788B2A13027A7DB9DC1D5DCF77177DBB831E1DEC6F9D1D1D
CDEC701762E5D90FB3DE4ADE4C12A01BFABBF1E01FB5668736D2EF0BE20141DD
1226E8BA7D49BCC9B2DCA78F7A677C88B9755ED84AADA2E57DBBB5E70803C3C7
D14832CDD37B7AECFF37C6B69DFFD8FCAA4733ED0F3C361B3F56669208997B4B
6CD6FD12749C4E5414BF9CAD2FB74295E17A9BF5ADFC187757B4B51FF0679FD1
86E134721968C071F78C17C4D4B3FF35F875E00EFB57EF4FF23FCC263F85790F
A4E14F822242EDE8F66EBBBCBD7B6F11F17EC1C2625F9F5C67AD681C4E83E8A1
3EDBF6F58BDF0222ED3DFF8D93FF8D93FF8D93FFC138A98640A9FE53817254BB
F8CD28E625F16315DDD1F5FAFAE67A4A8671A55727AE1210BCA8E53A01D95727
2F8480FE17C56E8051A0A171E2D62E57FC5A2E6B5DCAFBDE6B235A7E15C0BDBC
EB5AEC26F1E900AA1EFF2AD83BF01E0FBD61FF56E04FA67FEC1B07F6495DC572
E8BECF5E8C12CCFD04C405BF28820EEE69EF7B2054B563F6DE628EC51E765A0D
3BF476D072DB1CB5AD778B3AB65430D1F26DFE6BAF3C725500F9FB38185DD756
2D1FDBB7648DE07B0CF97D574138BD5C9F7DF0DAF7BE9F3CDEBDFB6EB03EB207
BAD1739F12D5D777B2DCF589814C8D363218F6FABB4A91AD915FAD6F3D5D5524
89A5338087A7CE05ED8FBED17F30DEFA9C7975E51A9E75B7BED9D8CE7B62DFFB
33302AAD8C8D3BB6FFC3F9678F3BBF19BE30F6B61FBF2390A42B93928B9D8E21
28FFAE698B47EEE24125E65EFDE50145FE8B6B6313B13EBABE33E8CB73B1F4E7
04BFFEEA8DD1D5771EC320FAF79344421ECDC5A12C0B2CED6D6084F5D9DB39CF
81A14DA76DFCC47FD8D018EE79B754D345ECD9E592B8C0679A7DB462B5F1937B
A4FDEB8583FEA6B6DCF0EE93C5BA9F9F4939E5C5B30BA8E08B675F9B130DED39
F9E6C8FCDBB72FFFF87CF6C6D46FCCDB2F675E49FE30AF4E9ECFFC2FBC6FDF7C
F5FCB719FAB17DECC7878A6D9FCC14CCB39DCD3E3BB4D3BCEE27CBFB7139B7D5
9007D3C5DEB9BB0248871CBCF34B30B83C6F018F961EDFA7A5817F30894CE2E7
C862DA7DC0923F2DA4F6573DD46876F0C02DC7EB15096E7FDFA4DDE58A69D8B9
8B5AA8EDAF7D20BEFE1EC41E586B84DAEFBC4F19D576417E3EB2167E08A06DF2
1EAC390DEF78B06CF75FC27BAD5FBA22A8B4FBA0DBEDCBA5AD874CD7F6445DC1
54861DDB0C25945BC97BF02A59EFBFBADE34DBADF7CF63704CB3BCCCE3B0488A
34346191E54155D7B92A0ABF8EA2CA24516E8A2428EB248AD22AD1619D844951
C4719AE67155EA78D45892846999FB6960749A69A55490E5611A44659EFB5554
66B149D2DAA445ADFD20F37D3FCAD33C494C5CA828F655558D1A8BFC34D56115
A8BAF4833A35B52E6BFE5445E86761E507791DD745182679EA1726A86B15D746
45751D472A499432A3C67C5507A688824CFBB51FD5615404459026795CEAD457
858E4C94C679111945F75468CA24A91446C9EBCAA7E97163A650892AB2228BF3
8C3ED44595727B5485759E547114E695E12E556565A9B158A9741E46611C1886
1A9A3C1C3596557946038C8CB7B2A8D66952A779C69023632A1D68197D54A645
569A3256751A546189713353157E58A7A3C66A93972A4899AFCC447E5CC65A25
B45BE43AD6751D44455AF9213F2561ED573415055198E6CC795AEA24CCC7B3C9
5BB841120651CD0099FD2CAB3148AA354EA1B4CAFD3AD61821CA18A6CAE33A55
748909C8ABA42A0A359E80C0F8A92AD23A8C5591875808F3AB34553A5041A093
34088A2855AA2CD32452659065858FF142E3E726648E468D152A2D8CAF8DCA98
FA1C670C824A1765AAA308974C7552C6B9D67594976511574161CA5AA5595907
0C318EAB62D4982A23FC22CD4DACF90F37D32AF6F340A57E8A83AB38C5A94D1C
073ED358A60AE317356E5DE28978AD1E371627659686B9F183300BF1CA24F22B
A2AB52BE4F0499CC4FA300D3A701B352D32BE652F9695D1A9DC88EA5683CCCC4
8F5498263133542B7C4865756CA4ADBAA637651527DAC4254F0AEAB2F4555807
8A20AB711E1FDB8C6733C4A593AAF2CBCC8F921C57087D7AA99324C0E940003F
AB79852EC70677CCCAD4AF99F9524555968A29C6A85195959F156954ABA02A13
4D48D5461B5D6A66B552B98A52823B0443B2285655ACFC98C057455CFB404158
8F1AD34121C30A8352F3AFC9555E2438659E8ABB67551163CFB2C0948974B820
78932CCA02AC1304B956E3C694D18ABEA54916A43E9842D864819F62A740453A
8D7C53FB090E136ADFE89C48571126604679DB8F82896BC47E56452A1780CCA3
B036992EE244F959ACA350279A4E9A803857BA88309A4F206401014F30330B65
3E8E001D63AD0CFFCF545CF979A14A457CEA92008A194DA5F0BDBA602263E6BD
4CD3C44F8B92F1A7750EE295E3D98CB31A8C2B4D01EE319FA136197E4228FAE2
6544AE4E8D5F67491594855F96511DA56518E0BA05D8925713D750694C70F859
9E6529D0A88AC044A14F00C6384B00CAD6BE06DA1550981114607799A8806888
0C2E3A760DB0176C0C8933AC22FE5906B1EF875919FB65A0FC10B02042492B06
CFCDF2A82E7C13548121CC19C93802B04286B3975598A755159B3C4DD218CC00
5EFD84D8F171B82A232DA5CA1401B811614D00CE8FC33AAA82710428B108B782
4405A30257990A7ECF13C2B9AE749696824F3A4C82342CE23C01F74D982725F0
A8827C9C03F2B2E0E939781BD455402E0C535215AE5599308A990152514676AA
4C5CE746C7CA48C40509A6512A0CC77896076916047825B09556755428838533
9586F202468C485F55CD4BCC05310ABE06F43F232FE0B3E97802B28C449714CC
5212031E599D310558BAC87400848745146620711C1795292546A28C9EEBB852
442A683E6E4CD51A4BE0978AFF639222D4DA8013C08B0E089D0408CB12322BBE
2DF9BECAC2C417FCADF3BCAA27E0181574D75459A4AB089C080D2E1B034325F0
552564AAAC76C058D6601DCE1AC4451C9145D30A041C3BAD627492F4416F2309
29801128310F664905EAC1D9A2020CA234655A63131204A9C9C0AB90E4391926
CFE389898962C240F24F58457086D02F243B717D926B6C07122AA231F70D38EE
678AA674A1AA72425C1252274C0382845B93FCFD1C502D4B93629E3C24692A26
A18C63C5DF15A84B52E6F9519E63117F8C670061059F08540835A8E80C8118F8
A05B1EE478430AD49631C859F13C92A002235360B9CA201FBC36EE19EE9F0A2F
81EE04922A419C288225F83AE1152C18D16CC15486421488BB8C5733489C6F32
063B765ADA374C5D20548C70326596E152210F4F4C12E409DE4E27881F6801E9
8D440883C11EC472A0B22819A7BA94A90304E3AC1256A923ED33D890E41B1971
323FD6898D333F2617405FF2382713063C57A0454DF26698C66427986316E54A
F955813D147912E81522AB014AC2218B40616005FAE8930EF0243847914DC011
FE9B0525A626BFC646A93803E37555C0BF8A92FC820170AD384F31619A1363CC
A1C62C1AE7098A89CD0499486E51A6F34CFE357912910E027A95553842A6207B
B52441E1BAB130D38810C59E44663E6E2C08E1CF381299032F2F70777C22F14B
F025F241C23C0C60A798BDA22318348B41DD50E186290925CAC61004E6909CB3
248852A63A2DE99A8D719CB706D5E21082AC7C533003351201A641B4073509BF
82458E7B9624D823AA034179688BD855824B91C893183FD5E0535EA551296FE5
51C1F590FB5C928ED2261F170B7930FA20D2B01C7E22188B22AAE32C65F6813A
B8518D76810FE5404A4A8379A2D0195946800246753219664102F18B2CC92039
9132E223752EC0436242B790B922D825F19B4355CB2A4F723F82A1102ECCDC38
A190FE8860B25B96FA49129B94BFF0F5D04409E4282F4AD824C4027647EA015E
511C8544066E02DDC8F4A8B18A4949C32C2F62EC5F3089840B5C343735AC0918
2EC90E10C4843E31823CAB21F2B89B21BACA222AC7A222AC5011CA2F527C1122
95AB84A4E49784565197415EA44989C9924CA48A1F630E98765585CACF091A42
6B2C7712110D103B8220533E841D1CF3F15E1C34419EC1D4610E70352DF9D887
0842C7117F29E985C81843105116A30EE3921C9B97094C8F5154489E429792B3
4877F84590DA49A7E782CA3AC9A0068150BA71128E0218A0C411710D470F8971
B80D4E9B544C018F267F94051049B3D02FD208B311973E9042DE3613D7D0A2C4
B4F285F58669482E841CE69911C18318CC4B249C246A3279A9E0EE498933477E
98247E85281E47401EE369390E0EDB825998CCE06D01F10A8FC506E01A0E81B1
6179CA84426E80A30A56423E35C558D589AF1A12120415FE2AF11E92DC49BA29
302662045D47E0026415C988B155E457A617314AE04D9C3614161AA431830A12
F403610EB9CA72B27F44E24B1816D988275609B99AB916818E6B0A9C407EC7B3
1980C6293E9387E83F482D9430CCE22C29A3AA12E200A921E718900AF69F302E
1C511738AD5FE89C091DF74CA525E141F246EF94F44361C0402399E1A501AE49
9CE1CE581222A42149118E4B94071074C2601C01C84BE85389A80B092BB29170
5BA22997F4EF8B10C6B1321FD6E7E35ACCB129AB28C425EBA08CB2E930494E91
944442459E238260DE6515F836D3090BB73FD3B9324EE298C823F5126D113661
FE27AA4ED2770882C6883126BE068A2B68478193886645BBA7759042ED249786
800EDDD68110DA30CCA209DB16DA846D35040696480EA8C8A0712ECC446BD828
9204690D530B21C831FAA4CC6BB814142D8F101FE370020EC84344710C420B89
8981085824BFA70C90D950053901288138FB892AF3A8A255A4AED417CA316C47
1282F859CC3B26A42B4599E460385984194911AA3061E684A0E0FD58BCBA008A
A032F00D94D93836E34CA86C09358C8903E42D609341BA49BE704A121F4E4BE2
243C54CA6809A400FBC752240808FEF13083A0261041C3B880A130D0A2CA35BC
2A826FC77929521C4AC4E402F9C4565594211283B00AA55435E667151949A047
6A0BA83126B40812A00CCA5CA1336D88E6A8391C312228405D8D978750DD0ABB
6593C2520611106B947959A2754804115165D0761076F41BBC08C0817714E01E
C89D29B2A8D6A5847CE28F21880C039C407F806C9C1054AB33281D2189AFA11B
733446441842D6C99D105A4424616C7C8528CD8A680241C81BE13F6887083902
CE1ADA40E148F5A80040C80159081CD15D8838818772D605A90F015E8C299578
13530C438D82CA1037958ED22A24C39A38CE6C3E2D42520DE43D2C48A1C025EC
B6E2DF08A5E7FB9384E2E382058A8D9186127F28604847AC6BD2103EE6E38818
2B8871695498C8FDA030E014BF2067C70439CDF17DF84E16F164F806E0850646
B58686948C201024C6CDF2145190124AA5E4629809316126750D296C4243C8C4
64F5A04661C624E420F56996CB43BF346871262F242A2BB22B6441DAF285CA17
93621C49DB804B10A72A216B8B2A8619AB2A27A1202B91429978824991D71078
A9DDA28343E43F7A05A137B699CFCD68359D55107B082919857B4AF0D2401D73
2950A4252A4CF9A930455340FE134D2E51269816E398C098342EC5C84225B580
1F976710173F8229275028894BF1330DB94963A97130D9602FACBEAA27195D4B
2D061542C2204C6362D297526B9601E3C02263E5FF6865F2206CB28AD33285BC
3096148C1FCFA6C850C934262FC2B0409ED6842F7E0AF216688412F64677203C
F87D09D94ED117C02E38C27F553AC63380A448491335ECA9AC083D1118E8E03A
627211E799513E218986817CD4A812C87CCDD4223AFCA29C50F72465BE10CEE0
2DB41E158C628176C641E62B7030AB91F09A044A3E26E1C102E9AD2FB386300D
4D38E6673876104ACD8D304F997B2810A197936618180826623C2C74E04B4EC0
2DF05AA9AAC7647BF06A52D76014A412D2AFD81C67F37D61D799405B2505CC98
4C03812F0D3F1B34852F25307E159C0DFC740C41F81D941F6EC7E3A088CC6D02
E040FB91AB703932484C0A4E35385ED404661A55C0233E9E61BC6AE267618131
192619A22CA59815329E9CC8AED0BF702D5FE7356901E206ED0031A2943C8F60
872A0470B6710E206DA67E2AC5C414859E00B0558D326078B8B316C92205509F
945AD595A00AE32D9130C41D2C71821A46B02A89602C358F2D24FFD77E2A9953
500DEA2C3AD32F8B9A6C5F48C4A2BFA5FA0A094069EBB10E084C0111210874A0
7127A98C1564EE3405BA91A7BEEF4330D0DD0028041B1A11930D19BB02DD30E4
1869019238A843E83BDD2089D29EC9213CCC2A44B2465426C8469CB5222AE23A
945A5C12DB5487EB4C343A8442458C151501162742BC24CB711F2A0834933442
6247BF20178985023208CA214F50EED1E4E3B862AD0C860D9746C964752AE8A2
4D65C81EA54FB62318C9F08544945F964099F88EAFF18D0CFF9DC81DAB0D8A32
4276090DAA740E57E7FF4404D1C95C430FFC18E924BD01BB0B83DA257956B914
AFC6482BC57AC418720B61820A2497ABAA0EC01BF4363F64FC6B3509485B9146
2AC837AA4AC419196BE2B43028344D5469CD5D215EA01358A782AB63C23C0419
A3000B0875635C44782A7402C7C013B3B81E27614C91C9D24BA4650DA52C1935
DAB4347441DC94EC49722F918F10A4DC474A43F3606C264E229C319F64744453
5417A14F7E4C311C38894BF8B2085007C2D6A586428CF82964193642EED2B1C1
69425C88989B94098102C2868089E95194612AC42192097F2840B2AAA077157C
51F01B660F41C5C3B568D0180899F0B35AA45A20048A8623A6A9C813A68C44CA
CB01362B712BA2C2673EC9E5051859021F21E39C222DD847C044B2F203394733
49951CA25DC4B0EA84D1326FF0875401AC451C1546C414A3C62B8A645AC04CB9
B708A03439A91655216143EFF0765CA5424E1B4D5283B8413B0949419D804802
AD8CA9A16AE3098044A1A2B17CE64B8D29E717B801088DAD41FE5AD8202CD595
6F64258BCC2A1A52D62E6068E3C622C0546A57558D20885421C5071F21471266
2C3AB6D51785CA86155625FE418A455715C4298C6A2211B5A49C3CAAEB028E06
22FA2A960A9E89512668421F760B0E68A45989F8912537B282810CF94444594F
F859C55BF0BDA202D2143A0BCA01F180B5D2C38840523AD5708C18DE6C64ED0D
BFC381A15C7E00A71983A38F3E14859F87B92FC902EA9FC1008066C867902336
7180D0802D049686E365759C3119889038992CFC41F60B457E4CA1E9552A0B02
C280E995AE0105D48E0CA6081561282ABD80B1A009844D7221EA7ADC1811C754
332AD11F46D63D82BA4818536C0A21C3F81D198C2E63AABC824FF1C88C6041F7
25713459C422826AA430919862B604650C072D93A486F7F8A52F8C8C8043182B
D2540E621160E85D846582D41B8323045057320749CC68007E920BE09D0B8B24
8356F83D911B88E880A7088F894B8D7121CF64E809DBC63AA8223F0EA5D29862
259527167660E6B2181F4050C8B732569D5409710BBD876BF95267CA27A801C7
2563E8CC8451621D35173A9AD14D9D050AA60FC113590C37936845DAF9C8A104
8C548AEC370EA734115C6470606852E722744C50113D253AAE963527A49ECA51
D105FE0E61D1B25461D0C70A0799AC56478C3F0C70995A8A52912C2754B22580
5947E2244959291504924E25EA21F3402963866F135F6138CEE8515A903E040E
B5142E8C0C97A1891E8670AADC871CF30FAA8921CB33901EB1521513020C5663
3F53B05E59EBD304B192522FFD0364839A09418FC0210121428E7E27358E1884
242CF01A92C6B4E931D98B4557421E138014EB20484D5CA704818861E317A435
B02694F791AF31BABA9222425556798DEC50131654C98AAF1F8A92D12438E47D
0D8F039E4571A52800A93E67FC5B905B71E6284078403401AA6092D199B980A4
036B256390080CA108B7C864A3485613B5A2A6B16426B431C07D2B21B74033C9
3AACFD7142A965353F4E544A1CC11DD18A393E9212F24A6656475064D9E180D2
C9751986CA8408C714089465E9496C1AD8B1CFEB3081081C0EE31A3F87F58BFE
F063B4BEAC7C983A51D09702BF8D85622406462DF0A4C6FC8CE880D3108221C6
333CD048250261A97596A4E29E2828922D0245971A94CDA536A7233AAF63D2FB
7898515A06BCCBE80468EB1C6707DA605A6454051F422D26E4405226E29F2982
E8A06C615796498CE58ED479C891C871217A982711C196D3705449795D893256
41416AA9C830846E58C26864E45A85E118B6F10504A140561027F8184E5BA595
2CF2A56920F501E81ABE8B8448984DE419BA388921A6225193496955CA65A432
6EC2E7258100E22AC81829591845C504D708625315A1488A40C5222CC8EDB16C
F899B806ACA4B61AD3C07603301EAA06FB4735079057F259887231455C612360
C54046000CAE482A45D04FB59314975329DEFBF0FF0ABF83F721F9A318115705
00AE2C05A2217D65722BAA64C78962E0B1AA827104E85A6016456221CA270533
4659A993C7CABF588FE85068BE5AC46892A7518DCF3143B930F0C95A5D20E394
353E7C5DB63084314957AA719606400F8924594B15B68D472445013D444EC26D
9374CC35B0179107798124D780569C2A85D74A83A0A1C905D5755E44D83CCD61
2D6400DCA854241F38D1A48019222C61860C3414E515CBC26B9449393C953A54
5493C349AA6827208429402FC052D144AA4A70E0093DC88B022828D0961A41E8
97B24986648B1B57D824B13F45188EA67272930AAB5A16AD039C045A39E16751
C954C14C509AC89818E3C1961260C26832640E6D32CC63091D56891039D02E4E
646D16A9A5268BF281AC6216B40341417B96E29012501961186020304C3369B9
210FE611920D4FD54247450A1613ED94C30D21E25246CEA486A5C116787F095E
E3A011F30A0413ED6918A21A610805B2AD0A508AA4B46C9237F1CA322A60EEB0
6B80078D04470C0168E22F0A11E7AA527020A937C5852C7A13B1064107250D4C
3A59DDC94D98A6119E8824672E2A526D18930BF0735F760C41CF6B29C18525F6
8A4039020E118229F34216D12610944B2CE6D006D9F5141137A98123CA868D08
EC353064A286C113EDA4E54CC9DE151ACF02E4503D212EA444183D4333B28701
14045D8164A60E519502E23E4C95D42702CB573503914A98902E38D864C34C4E
5B4A169E6BE87382FF54D203D9A723DBCB545290A565479DA8BC3AABA5140771
2799270A2408C6A80191806BE048F05920AFAAA4701D8857205C33E02C8B756D
EA1055178BE690E25A207B4A125FD4EC181C6B1F5004873338AC6F77C7653510
9EE942C3D570285D26650EB621B5CBB8D6A126BB56626818456226E51B245520
650059DF20E36B910FC6C8BE47A947098C24C4554DDF193168267BA8E42DE04C
D6BAC63DB3C5CB0864E1F1C407A92493253ED003B307D828561982A0A822D948
901761922A9386509A4A6C3D267B05F0C0E3C06512A2C6AF2117A8701043E1C8
787A9E87017EE1CBE606888431C27753DC37035726D50353323335FC9EF88611
0512E055161538708882AD99CF0055823370A9EC425339098D870781460B4D2A
2ECC1DB728A488445F463A282364357034E0D2C38F3EDDDBE7DBAC6EECC925F7
F7F676EFFCF5FDBCD00EB40E8C9FB0A6E395C1563E6007854BA4105192070348
97AD30CA52640ECA011B24C0C9CA2F34BB56490C5F432BE49536393EAAAB1A8A
5FE37B52F683B1F804101C013DA572A9A910B1004738595F2507654C671A4818
6725C946B80DFC0EFA840F24FC244BAD0824686F5249592EA7631511A1646FC3
D8CC012A0F510B5F036C906AFCAC893AC425B04E9FB212DA93E7A9AD0BA3F368
1EED6579810138C7C3440C28A96E15B52421B2013CD6C89E91AC909D86085D8D
FC2227C15D912B410970071A7199205E27358290880B4885792A843B2AE8795C
012D0181880E84EFC212C95E45A4A5759319D9C805D7271166D3AD44789B6C9D
AE0C3407C002D86423852C2D6A701630D3715DCA5638B01B721BC8AE448229CB
180BC479DCB31C4621243760464B4C815FCA1A2F0880F225D2D09690C5AC44F7
E34570170404384E06222DA7D1D8663EF8ADEA2A16454A742159D22290B54274
07A2438A504867D42A04268402483536A4DF992FFB0B8209418797A035016349
46441732BC4235F96471D42FA941541D92DA9752BB5D1B20B1D4C00FF9239F6C
A8AE4A99FE4A5800262073E066290001AE1BF15ADC4A0045AAFDB2A00A318588
4621D2219095C7C9663A1F9D19C05E614908224038CF65D139A06B0A6D6590FA
6904E420F8954662D006D01742925108F118027968E29B4C6799B036244D8530
91A55D48167C3C979D14615A3211D07ED282F2A5EAEC9B406223D79370C2F964
DDDFD4B2035B895B43AF987F03C84A21B8908A27D4027E49528401892B6649C1
5510E4B19F41586180B80DA2119E2E3B3609492617D7922A0F2428954157F4BB
901228E86DA079742F5528B309D297B2094076D2902F20FD19BA03CE8EA4280A
9186B07D140F9A02FD85D041BAD1361E2D1BF4FC60DC984282FA28456C064752
52E837C8E83477759B5CB625A1274359C72C71E6288F08F75272134133E9591E
4336E110112147864825D354769B791697043498553278A2087E17489937AC65
91072107474CA73B0C2ABF96E5BA42328D91224E524BF527A1470864B007EE03
DDA863B81E1C46F6DA43D52BF810CD4FCA4750C61C0485E96A625C769D9A5C16
8D0B2D89BB929A1D815504E46DD96F0712919195ACB0E220D365979C882B0C58
56FB92E4C11FEC51DB557A235B444444F36340DE8E436577119281B50F9DCBA6
4E1B19BF4624D40421F91FC64FEA86D5487D3046C7C86EF814478348A7A1283A
587259808E2570264A6C3C9BF0401432931EEA5A7650A8B2AC64AF13AC4EF694
21A68943788D50B30097C502185976E0202FE2C90428C640AA95523F821273C1
2DC81C89AC4088882C52D13A752D5B5CA0B41841A73253B29800DA4E975DA4FF
69807BFA085F525F2DB15D607A881CCDCA466BB05D3EE88044C9C9793812734C
9E8A278DC1D12472D15CA12F9B317DD9B0199934F5112F04872C4EA318A25CF9
C2D62B3F2DC9D1A5E4C132B9B7F984F487DA0641B18FCE09F63CAC890A5381DB
615E8615CC32F1B55D1BFA750C63B5FE34C718BCF7D759066E8C9F27B56C740C
C89790019C44C330907ED8104A1FA5A522FA9188B237375191EC5B4DE38987D5
C83FD12D6473A9700B48859AA4279B8A69A26652E1C7520B4E422623ADF11E43
F89374485B93F5CBB090FD76BC0EB8327BA9DD078CA9C33253888F980C9E098A
952109582A9EB842222B3D84923293FD18428B6354806C2D60B0A49050D69ED1
3064B540AA9D90AB5C4A64857CAC29978DEC95825B1107A883F13085D0E61A4E
0E01839F239A0D138BE0604E8D5450B400058E9B23FE4A844C2D2DAA4CC0B2CE
278B07086B28490D3FE55DFE23C392F484DF021FC889202A08641C5E15B68604
DE852275B429210AE5649510088F732951A452C6AA7499F9B2412832917C9481
3C564B391EDEA04A232B5FFC2A850754A8EC069DECB2AFE9B27CA007BE97E74A
3E8B90FB61259F535196EC917DEA822C4E1E46AAA3F489D6544AEC862C134FD8
34F92BA883C48F851E8983A85AF65418F960893111CA18AC212AD129B2B484B2
8E98513884294B35F9CC4489C4862B827B221B640B5F1194652D7B9D80373A9C
1606BF2964B00A311282882AD5451CA1BF827CDC580C57C5BBFC1208C8705A59
194845A704462A0E92BD659F5EA664D4294328954FBA0B6166500A33AEA86470
4D883A7789899907A5A0AFD2600A2D93A2BE2C4DA5A02AFD4147A1FA125FB03D
132346E372330A47AAA1B9D05D546A1583D9B0455DCBDEB5503EDA9345B2AF4B
61F5CC1728228D40A6E0EDBA3293621B93AE654F936CAE0D4320260A0873005F
179668C8D65D408B584A644526930FC26550AC0CC700A2C6A5D3BC287446F226
F1E7D0DE5A725406458B4B95FA58938E2506C840C0F2026320B584A90FE7F235
AE38F9089270D32C970F12E6499AC222C0C4282BAC1604A455981499A44121ED
440B2CA6D4E490989C42FA1A07BA7CF448C96250A944F4C5B18669C7BA92AAA2
2E65AB04D2292E60BC381919826401C5CACA2AD142DEC7362BE50371440F9DCA
F077E2C1A48849848CEC61A79BA45459880CAB30449941FA4924B2F31C9250E7
93E5B3226258A4C8AC928D2A3013184118C409BD4064831C81EC6C82DA2AD96C
5019CDF45676177E487E28267B0B1852A9104D3E7D019F0C4043D2911A9406E1
7259930F4027A9EFE9083B25B4921308B23CCDB48CD32F48662A816A44BC2E81
0400413E3324256C225E218EE5236B885F59EEC59E590DFEA1AD414938FA444E
0032F2D127D8620C4D917DCA6948A62E7DA84922D5392DBB083F99E22E485BD7
F6E8D35F381BC35E7D76B3D9C8B1BE728ACCD3874EEC191E39D835DD9D2CF862
F8923DB26FD08FD3E6FD7275D47EB699CE6C6E1AFB7AFFCAFE63E3A35E0CCE0F
6F4F0519B5E43DB74D3D70DE433788C7E3132D8F83FD87C2A7C7740C6FEBC63E
EC8BF7B3F7D4FBF8408B0F9DBC3019F0F9FC72DBF46F2ED6C7F3CDFBED273E95
DC7F947DDAE1EE5C94F1A7CD4763181C6EF8B43B9047CE6291B9DE53980326E8
9F8E8EBCC1737F684FF4F18E8E9848778735C1B833DEECC9D56276F0CBCF1C9E
B278FF61BBE58E26DDA33E3D417F67E376AE6CDBFB19FDE4A4FF3583F5E6FAEB
D6FA9B8C25C72B1CC93944EDB9063F8F0FF1E88E0E90B0EC4F0E185D3169E493
C79A8CAE7BF0DD9F9F9DBC79F1ECA4F2B6BBBBCBE6F9CC9DE922A7115F2E1733
EFFEC925F6A6F5EA6A7DB36D5EFF28C7BFC819214F4EDF97EBCBF5E6F9E7FF5C
DB3F9FCFBEF8A59B6F76F7EE05A5F91FF7BEF8D4CDCF4AF3EAC4BC698F693A67
A29ECFF46639BF3CF4FED86C16F3D5FCD0FBFAADF776BEDACAF9F7CBF3597796
D3A71AD4DE976F4C7DEF0455313E7E3800B687DE7C76AC3FD970776C54D7E3E3
934AFE7AF3891BE4D4A5E5AAB9F7DEF86C8A77FF6860883B6C5D643CEC19FF6F
45F244A2F5B06903A28F403B8507C307F5F7B93331DA2BDBF353BC274F9E78D5
EB57667CD6D0C3077D8CC6274726DEB3801C4BD567C5FB41FA0B09F753973EEC
7564A74F9D09DCF5E6418FEEDFDC27ED4FBBDDBB07E1EAC1934EDA5747E97C32
7BE3A36BFE91D3EE3F0EE9847775F7D04929F76D383D15E5FEC940725ED06F7A
6C5131C72672C4E1BE297BBC5C7704D1C7876B13CBD5A2F9C99D007FAF363178
EF13473DD9D35E262FC9192CE3976C2CCBD0EDBFE3F7FE96336246DE65A762E8
7BE3768F1FE8C7E97A71E79DBE3FB39961D66615A67AB1FCD11E58C8BF2F17F7
1AE2D5E94BD2D0BDE78D4C313A42697F5692F78D758CE191D40F1E4E333AB37A
F49D2D3F3FBBDE34A3E3CE478D796E82DC770FFDC914DEDB9727C62BBE7DF955
65DEB4C7669EC8015CDBB3CDF21A3F58893B6EED915B384F33DF2E2FEFDA5352
0FED797D87EEC4FBCD46CE3F754770BAB3D0D6ED915CB7CDA96D567CF889B4DE
B44A60BB6BAEE50B3968B73D76752EE7246DBC9B6B1B408E943BC2D3DE288EBB
3BC2D16C8B7F7AFBDDCB3F7DF707F750E90BF7CE2F2FD7B77D7F2FE7FCB06B3B
F4E3727BC3DB7787C4F5EE425EBF98FF28AEC285D6A36DA3F315E35B2FA4AB2F
771EB144EAFAF38D0897E5077AE8D9E8BADEACCF9AED76BDC1063F9D35D7F60C
B2CD0D04C07D8500366A4F24732A448EB194AFC5D92CDF5FEC8696E9ACF21DFD
3D9BCBD958F659E78C7A7BE8594FDCB616BEBCF47824B68079D856E5A4D3F94E
0E713B6EC7D97EFDD0A8BD856711855696576286439B83DB46DD9977CD652347
0F6E0F9D0178506720318B58686094D77292B980DD6177141B0E27DFBBD459D1
8D6D7DBB72F322B7D976991C619EF23D2C5C74BE595FB58FDF72FD9974ECAEED
74FB3D54E2B8F395D8F2B4D9DDCAC9616E066DCFFB667F5C36B787620B4FAAA3
97CDCEF9CC66EDAE6B1B914ECA95533FB289BBF537E73AB6D1D67DBA3718BD3B
5B76690DE9626360511E7EBA5CCD3777ED373DD8F304EFECD76B389BDAD13A23
5E5DDF00B76D3F4810F011BAECBE3FCA9BDFECD6CCE9F24CFCD4734750792207
DCA4499FACCD5C78BB87DF6CED0CE149327C3B680005B0EF8F576BBD637731DF
D9916DAF9BB3E5F99D73013938F77DB36A36F38EA2B86891838CAD2B6CE40033
77C66EDBDBEDBE2999AED64A3203368C8FEDE9F67D20AC573DC77CACDCA39667
83B6E441CBD5D9E54D7B2ADCFBA6EDE56EBEA14B0798AA68CEE6E43A7734E0ED
C5FAB233984BBE1277DD01807DA8DD0D22A0B394EB683B8B3278DB536BA71667
5C0EEC1DAF9F37F9CA3489ADDEDCBCF6FAED1E49EC2D9797DD5BDBF5F9EE9681
3D79E420B57C63F4C9CB577FF0F4ABCA33D54BFBB31C3DF9B6C3DC3D06DAC3A7
2DF689538F80E2D04DF29DFD562A19F2DC1E8B6B2F947974475B4A74C8D984B6
61797936F93E9A99F5A6D605DB986AE1F376C994AFAF9B95B34F67445B55B0D7
304257B2E971B9839AC1D1D9CE83ACF18895B904F338FA3B193B40CBDDBAEF6F
DBA5365EA5C776985BF992969BCB85F7CAFCD1BCB1F95FBF7CE5BDFD4697E6AD
275F33E5DDC04836DB33D8E1D64E0939FEA045353908D5DD2EFCE642CEAD9399
B200D0FC446FB62D74BEDC7DCE936EDE33849D3D8E1257776AE36C2EE4B211DA
B5109BF7D1D276EFB1FBEA2DA688C017D0B1B9E26C7EBDC4375084F8723FDBAD
2F7608D2438AA430BAE590B13353176E7258A633F3A24F22EECBFCDA3971E0D2
BB893BA2D2B9C0C57ABD65E64F6F763B6E6D23DF9D9B3DFBD676A67BF389E77A
D94DAC0D12E9943DE4D74EE4A291E7CFEDE00FE5B86C7776AA9CC7B9ED7BE822
451EE020A0F5B1C32E1B3813C857D79DDE8D7ADCDD66E7C63A38CE377BE03B6F
664FAC2D25C4E72B17BA57EDC1A15DDFEE59C25DB13CEB8CD046D2F8792D80DB
6B5BB7DCCF40D76F1EAEAD2238EC22C3054D9B6AA497EDB40ACBC1301D426FFB
61F62E2FE9E5F844ECEBE6C0B5B4B7609FB0DAC6FBA9F24A4BDF1DF0480E5BBE
5F4D62F4D01D4E6F4BB0BC628B13E272527DD9B6C9D4F10C3B7372DDE9CDA564
D28527527D40405C7FEFAE2541094C6C2C286E6D15557E1A30257A7D27A7B4D2
67EF6ABDDD4D889344DAFF948BB7B08FB3167741F4ED1EBFC4C5EC8C8986B673
EAC891737B9BD3BA7095BED9D883A0DEB5797F7763BF32D2B91B91B9B230393F
DDAE2F09120769E4A26D87D1275F1AC786BFD6DFB4A8FC278910C140C951721E
7C37D1C7CE07C4545D8CB63946AEB6207A2A13FD4112A76349F6A45A3B884537
EB57E20ADD21C46FC5E95CD88FF973CB10F53E9F8B378D88429FBFF758777D73
CADC5D346DF2B3DD9C4BC678F8464B3224B72EC16A47FAADBADCEE59BF6D7A05
637E6FD3D4667DE312C466C8ECFB3EBAACD19DE8BD970E9D50B0B0E948CD7AD5
71AE4D9BE54EE75B87703BDBA48D1A14D091BBA99B79BDBA1B6984FDC1B2BB41
4F9C67898B6CD793D19F6F8875CB6C564B043EEEDA6C091F69A36327879691E0
46AE7BC2BE376DDBF32EC6B6C4883CFE72BDFE606D7FDE34972DD2BF1C4FBE85
528B885263901977B3333DBED8E581C5E2D87D5DD830A1F6A2C2996A00F1A3AF
9AEA73FC5BA151E2A84255EC8552176AD1D1252C3ABF190B96A1ABB5083AF78A
AF5E97FFDA3992B4A85CC4CAD7DBD9BBDEBEFEF64D69BCFAE557C61E48DF4651
AF29651606203B6869348D0F64BBC3D6333B5BF6ED4A40414FE7F6E06E5CCE55
236C7318D7FA813CCA81FA38A0DAEF92DC77A8E31783B5148B7FF27D4D3FCD2D
4D996F1DAEDD361B77CFC27D57440F9C7D5A6B81FBEAB0BFC7F5735FD973DEDD
7E13D4D642D96AE4B98716D69C3A1AF6F2EB6FDF9E7885F1BE7923479D98CA2B
BEF3F60B8BDF983752FDF5DE7EF7B568FCC7B3CF661DEDE8CF3BFFEAE5AB7F7D
FBD4DB4F8FE9CF331FF9C2DE0FAE04784F85698A4862D8A77736C52EAD136FFF
72230879BA999F7D6876DB2783696FB6FBD7F720D496E8B875AC6BF6D0D719E2
7032C57DD3F6DBB3AC346AAF6E8B6CB689168D7A4F7704B57B3A92CA92F41651
206EB69D51D3DB03B9AEF3B736038E6EFA6B4DB6C31AB6D0B76E99A87CFFA853
ADDDAD97C4E2E516086AAE87F731929E8D4DE7C39258C94A56CAAFACD6020764
D4B3CE0C2F771D14F76CD706838B2F07612D5F3BEA323B0DCDBEB48D3C9EB7DF
45EABE0C679A77BA3B6F1D93DC978D964E46BB3A0FFAECE0C9B8ACF4503392F9
BD4149D11D012F58809910530B71937DA618A098FB62E4C1ADAE7AD991838DEC
9B6286171D9B17DB7E103B8F1B6ABB7CD945B6B3411B3B5F12F19FDB2E820536
21E1BCF29D91E338B9B50AC3668236EFDB799199B419ADE94FB61FC0C1B41E74
488E3B8516B55D70F5CB164CBBEF8BED4A7543D6FAD8BD393F25651C74BE69D1
67CF6A47784237708C9693F138A67B481CDB8B06706CD9E7FA66CB903A16BCD7
FD8B7D8AE9E9AAA3FC074FFADEBAC07730E296B5904D57D7BBBB4F76BB2FA7EC
9992ED92B0AC472D4CEF8760E374E45D128A8E16C8B3E4CB813BECB57ACE3DB6
9DE5A2A15BD6CFEFCDF310991F98E67686E7DD350BD109B6E7B6E13D1259D171
BBDE03096EDACFF2A1D5353FBC5EB95FEDC5F69593DBB57BC53D796E09E6287A
2D39B09DDC4FF0B6CD5F2D7C39B83C05495D2B020A0FD3C13D8D94D5324BC506
7D148BB6755379A01A6237A379D5EE17180F65D5EAE58786D3C328B3DCF571EB
BCA92D569EF61520AB2AE4CBB8063739CED219741C29DEA7FE7C31097FEFF100
41069083DBEC1714BFFFAC1B1531F86EDAE064F26C846F3B6FEB8D8F53F4C69C
368C613ED9706FB45FDDF0002556A22D3B97EEFDF8D4FA7B1F72F38135ACF563
B16A276DE459211A6E7D6E5B758CACBF570D2FED0A8B4A12D3B98BD7FECA707F
654F7A6C8B7B35B2CF006DBDBAD527C232DC7724F47EE992B1EB8C4D8883B4BE
7CD23C69CB0D72D334CC4E2E36CDBD4093D77E28DCABCE3C631FDDF7E6E0D738
DAD4CB7ED993FE63DCE8EFF6A161AB629BD140BFB86FC8BFA5DD69DB3FE8B6D7
C3767FD0BFD8F2BD8B3FFD84A2EDFF170F4CF7AF7B4271FF0983A7843F143F04
7614ED13EC0BBF6DFB6ADABEFA6DDB0FA7ED87FF60FBC3EF877A37F5AA5AE263
E4ACED93E58DBFDF596B11ED0F36CB1B7F3F8E7E20FFC8CA902D34EF808DAE42
3794CC16EFDC3AC054BC8D05946DD6228AE345B672B44F6C363D1F8D8B3423D1
B4BB586F8788B7DC8369C70BDA7C460FDA5AFAB65FB4D83F78DB7D9FD230138F
C54C4FE1EE2C0AB75581C1EAD0BCAF9A8C5709BB350454CE4D4FC82C036EEB37
F70AACBDE8EFBFEB49F2BDADA1C83D13A30CD2832C83B47DB08DBA7ACE40414E
4A005D85BB5F5B6AE589FD6E223BBEB64CDF5552BBD275BF64EDD6BA6CF5B6BD
D7F2AE27B6F22363FA7CDB53CDC3C160568DA3A4AE1547B77A176AC9E8EBD559
A7D36875E5AA797DA561C8D1DB8CEB16355C816DE10A8CB2AE3268F9B0ADEE4C
5707F65F08DED78D3ADBD855025B721EAD14F662054776FE36ACDCCFC6DFEFDD
36DA165A5F9F7C69DE78B5D127DFBEE917C0DA42596797EB6623CBDB7BD18AD1
5FBF1D949C4795AF9217D697F7FADEAD0E76B74941DEADB38A20C462DB41857D
2F49DCBA7AFF4D82DE6CB1DCCC1C1F72CB7EDD628B34713028720989F67EE6EA
8FE305145B7A6CD9AB2D18AE9DF692AE498B6E37465743DF1E77AB068FCFDBD5
20594896B83F95A5B4BD4C1D5AC3AECD6CF1F1B38BB66AB958DFAEFAF5CE63B7
D2B6DE2CBB3AA2E3ECF5C9377616B9A5D99D3DE9859A48A005F669A0EDB275D2
BB06B4367D6DD55A7130F24D6305E667EBCB85459A1D9DF96CD5DC76BFF4172E
1ABB42FFD9CDCA09A61FEE5DD16DEDB223745B5CCF77D74F8F8F6F9E7E767DE4
BE5CEA08E31F1F78DFBBFD06EDD74272E163BB09E96267AFB7312E17DA0B069B
A51F77F3D23BD9A55DA0104BCB57122F6541657E268B077B136380ED762965F1
365BB8496B67F3C099A2BAB10B31375B5BC396EF076ED7D11FAC9AB4B2D676DF
F157ACDE16CC7BBF740A772CA7BDC7F2B55BFDD7DE7EF6E478F82DBD82DB3833
F9E9E0500A4DFD42F8E5EDFC6EDBD5E1CF973FD9A6AFB869FEA1B1D8B99172B0
45BF4EE7CBA28A5D1775B5035757DD973B86CB2FF8DFA6B9922AFADB975F7FF3
95E925E4BF7DFBB2FC57ABA3447BD8EFB8746B5B8DD58AB7ED3A8374F112FF5D
0D3CCCA515D9E4E28AE557CBD50DBEEAF612B8CB48573FD12FB9C22DF0B42B34
0FE4E4563A00F4F6EDB96CC0FBCBCD72632B4FF2FAFAE6FDC5AED536F3DD762D
E6E8B48C45F2F63BDCA4D4209B3D262B3BB2A9DEFBA1FD22E81F46EB106ECDE2
A043C097AFDE9EE8AFBED2F26DC1FB0D00ED1E8243B7886677690F9392AB404A
26DBACAF374B5984713E68FD9519DBB900DD97929A8D0CC059AB9D484F3D4993
FF21F668C7BE68B7CE3850DA238AF84EB39310C1439C0B74A28E3C74CA18BD7E
5B8755787D470F5DCAB0DB373CB765F2B89B14C75286DFEBD8AFBE199C469FB8
D583B72DF42D7E9CAFCE6C34D0FB0E6945DB74BB485C316DB495C4C68ACB7BA3
4E9D4278F4ABEFDC469E7EDF4D571B9595A3DE636FB683F5F56E71ABBBC3D555
061468BF33A8E57D673BF7F5A233BBBE34B306060B4795B5FD061AB76BA2AD9C
F5FD7DD252AD7BCB689BF9A28311EB687646A41075E8C8A4AC3D399F6DC90F93
C34F58DFC1D89E0E6E07C9FC74E46ADD2E85915DF7F1DD2EC8F5EC65D2D716DE
8EBB2FF31DEF0372C8D6AF7B4F90ED70B82FA0DFA123246F2EDEDDA1E2D04C80
CDEB3727FAD5C95359D259B413D45684C054597EE907D2EE00D95B749FFFB9C2
6E719232498BB2765256EB6E3FCA9FBE7B2BDB0DBB0D253FCE97976EC3F064DB
C2CC7E85B3232387AEF0D262C164A345B77271356F378E7791E47A3F9C8B57AF
4FCCD3C9DEB5FDB0DCB7474AF9862C281B31EC1E87B95B6ADCAD6D1DD22DF5B9
42D767EEBA277FBE7633712A4B9FE7F39BCBDDC1937B6B961D71B9FFDCC18EBD
115BEEFA31D991E128F8277BB51C079CDD1CD04647BFA3D03676E8BE7572E9A0
9AF99DED8733EB0278DCE56EE7D7FC6C79B9DCDD0DF6F62D7763CCEC96B205EF
5DFBE4B6213345280D36AC4A4AA253128F37ABE55F6E9A2EEC24E817CBF3F3C6
AE49598AB537EDC261937324596FA345BB20619767FBC1B4B6EB899C6CDCF0B6
F2B4013F07C26FCEDC70ED3D7FC1556588D8C1ADD8DF5C9DBAECE97679F47676
8D77C8DCEF586BD7F12FE6D7ED82C1C8A7F601249B3C5ADD42360048BC236262
BD24561BBB29554C2B7BF976EBB56CA46F9C6E62E436C7ADDD7621D1BBC23FA4
582E6F5E49C8E2F94DD3B2F746F6A6914B76E3F55BEF71ECFB3F05BEEF5D2F7F
927533BBB6FC7EBDA66F3263D7F3CD0767AF6E6F5C9717F6EE7B700FE73AF51B
C938375620BBE507BBE3C962F67225F82B882DDFFC3B2DB85BFC38B6570FD692
4F9B41BDD20E6188C88392E9BECDFDEAE6DEE2F76A9ECBF6D30423A1DE0AFA89
BAEDBEEE79B8417BEFC67B24E91773FA0D65C2D7DD37D7B6AEBD7534CB06EE30
645A026465A76C9BBE97D3FACD580EAEFA1C20E6DA76B5566B813DFC5C0CB605
0C55FC3E75DD33654B13EE7F5848FEB81A50BB9F4FB482C886DDFCBDE7FDCBBF
78450F836ED7805BB9BF57C4E1D27B4F701519F784AFBAB97BBC3DF8648EF51E
CF65B1175DEF88FC6819F1E05EFBF6134983F6A5671BE1AB0E32963C03E57767
17E3BB6A90D3203DBC8D4BD00F7DA8501AD7B2FFD3ED9C58CC77F32E24F66A63
680D01F079BB08D02DBDF7A532D13DE2CDB76B97E064A79D0DA6DEDB9E7A6D01
CB7A4ABFF0D351EEEE1DB7DB679FFA5C5AD907F2F020AEE59697EEB6DD5A575F
2B6B33ED65732E60BCE87787B87C6807E104BE8D60073D5210F851B6C783E267
BB83B60FFB87756BE2DD76D061C5AF8D30410C5B9579FDCA78AFDF785FBF7E63
EC04D927C845E223AD65F7D13BDDA53E3C05A41FF5F0C97B40F0E0D127DF4DEA
8A4334E8B720DDCBE75D29A9C7001BF423BEDCD60E3A7E22E508B7C78094830B
4A1A746AD24A98396CF41619817B2C1E829B9686DA264700D55B473A6E7E9A5F
09717C18C46DADAE69EC12EDB2FBBCC6113F7EBE753B02B7BBCD5C3E75D1AEE8
B902C75B0744DD4E88CDA0BDDED0E2847BBBB7AAA6650D16E45A52E7B47607ED
CE3456794D57C8C6BCD55E38FC9CCEF0933BDD2782E4DFFF0B8956716B3A9C00
00
}

22.15 Случай: загрузка каталогов - приложение для работы с сервером

В какой-то момент мой ноутбук был отключён из-за серьёзной атаки рекламного ПО. При попытке стереть проблемные файлы машина не могла загрузить операционную систему. Мне нужно было скопировать большое количество последних файлов данных, для которых ещё не было выполнено резервное копирование. Для выполнения такой работы было доступно несколько вариантов, не требующих написания кода. Я мог бы удалить жёсткий диск и вставить его на другую машину, а затем скопировать файлы прямо с одного жёсткого диска на другой. У меня не было аппаратного разъёма для установки привода ноутбука, и мне не хотелось разбирать машину. Я мог бы попытаться переустановить ОС и отправить файлы по сети для резервного копирования на другой компьютер. Без системного диска, доступного для восстановления операционной системы, это было неудобно. Я мог бы также потенциально использовать автономное приложение для локальной передачи файлов (тип "laplink"), но без каких-либо последовательных/параллельных портов и без доступа к ОС для обеспечения поддержки USB, у меня не было приложения, которое бы такой вариант возможен. Вместо этого у меня оказался компакт-диск Knoppix, с которого я смог загрузить ноутбук (http://www.knoppix.org/ предоставляет полную версию операционной системы Linux на одном бесплатном компакт-диске - для этого не требуется жёсткий диск или любая установка для запуска). Я загрузил компьютер в Knoppix, он нашёл мою сеть, и я запустил веб-сервер Aprelium (http://aprelium.com/) на ноутбуке. Тада! Используя другой компьютер в сети, я смог получить доступ ко всем своим файлам через веб-сервер. На тот момент у меня был доступ к файлам, но поскольку у меня были буквально тысячи файлов в сотнях каталогов на ноутбуке, я не мог загрузить каждый из них вручную. Вместо этого я написал небольшое приложение на REBOL, которое мгновенно выполнило свою работу.

Чтобы создать программу на естественном языке, я подумал о процессе, через который мне пришлось бы пройти, и о том, как бы я щёлкнул по структуре каталогов, если бы мне пришлось вручную загружать каждый файл:

  1. Создать новую папку назначения на клиентском компьютере для хранения переданных файлов.
  2. Начать с текущего подкаталога на портативном компьютере (начиная с папки, в которой хранились мои данные) и загрузите все файлы в нем в новый целевой каталог на клиентском компьютере.
  3. Создать подкаталоги в целевом каталоге на клиенте для зеркалирования каждой папки в текущем каталоге на портативном компьютере.
  4. Перейти в каждый из подкаталогов на портативном компьютере и на клиенте и повторите шаги 2–4 для каждого подкаталога.

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

  1. Получите начальный удалённый URL-адрес от пользователя. Для этого используйте встроенную функцию "request-text". Затем создайте локальную папку для её зеркального отображения с красиво оформленным именем (только допустимые символы в имени файла Windows). Используйте функцию "replace", чтобы заменить неиспользуемые символы, и функцию "make-dir", чтобы создать папку назначения с очищенными символами.
  2. Поскольку списки файлов и каталогов доступны через веб-сервер, мне нужно будет проанализировать веб-страницу на предмет имён файлов для загрузки. Это просто - веб-сервер помещает символы "/" в конец списка всех папок, поэтому всё, что не содержит "/" в конце, является файлом. Создайте блок имён файлов и используйте цикл foreach для просмотра списка файлов, используя функции "read/binary" (чтения/двоичного) кода и "write/binary" (записи/двоичного) файла для загрузки фактических файлов в папку назначения.
  3. Мне также нужно будет проанализировать веб-страницу на предмет имён папок, которые нужно создать. Используем другой цикл "foreach" для работы с блоком имён папок и функцию "make-dir" для создания локальных каталогов с этими именами.
  4. Создадим функцию, которая меняет каталоги как на локальных, так и на удалённых машинах. Чтобы работать с правильными папками, мне нужно создать некоторые переменные, чтобы отслеживать каталог, в котором я начал, текущую локальную папку, в которую я пишу, и текущую удалённую папку, которую я читаю. Когда я переключаюсь в каждый каталог и выхожу из него, я буду использовать функции повторного присоединения и замены для объединения и удаления текущих имён папок в локальный каталог и переменные удалённого URL-адреса и из них. Поскольку мне нужно создать функцию, которая повторяет как предыдущие шаги, так и ЭТОТ ТЕКУЩИЙ шаг в каждом подкаталоге, мне нужно будет заключить все три этих шага в функцию и вызвать эту функцию изнутри себя. (Вы видели этот рекурсивный процесс создания функции, которая вызывает саму себя, в примере с "простым поиском". Здесь необходимо проделать то же самое с каждой папкой, углубляясь до тех пор, пока больше не останется вложенных папок.

Первый шаг был прост. Вот код, который я придумал:

; Получим исходный удалённый URL-адрес и создадим локальную папку 
; для его зеркалирования с красиво оформленным именем (только 
; допустимые символы в имени файла Windows).

    initial-pageurl: to-url request-text/default trim {
        http://192.168.1.4:8001/4/}
    initial-local-dir: copy initial-pageurl
    replace initial-local-dir "http://" ""
    replace/all initial-local-dir "/" "_"
    replace/all initial-local-dir "\" "__"
    replace/all initial-local-dir ":" "--"
    lrf: to-file rejoin [initial-local-dir "/"]
    if not exists? lrf [make-dir lrf]
    change-dir lrf
    clf: lrf

Поскольку шаги 2–4 выше будут заключены в одну функцию, я решил, что должен назначить несколько переменных слов, которые будут относиться к папкам, к которым я буду обращаться: "lrf" = local-root-folder, "clf" = current-local-folder и "crfu" = URL-адрес текущей удалённой папки.

Чтобы начать шаг 2, я написал небольшой код для синтаксического анализа имён файлов и папок в текущем списке каталогов веб-страницы. Я объединил требования к синтаксическому анализу из шагов 2 и 3 выше и решил использовать переменные слова "files" (файлы) и "folders" (папки) для обозначения блоков, которые будут содержать проанализированные результаты. Вот код, который я придумал для чтения и разбора содержимого текущей страницы на пригодные для использования блоки. Он ищет любую ссылку (всё, что начинается с href=" и заканчивается кавычкой (")) и добавляет её в блок folders, если он содержит символ "/". Все, что не содержит символа "/", добавляется к блоку files:

page-data: read crfu
files: copy []
folders: copy []
parse page-data [
    any [
        thru {href="} copy temp to {"} (
            last-char: to-string last to-string temp
            either last-char = "/" [
                ; don't go upwards through the folder structure:
                if not temp = "../" [
                    append folders temp
                ]
            ][
                append files temp
            ]
        )
    ] to end
]

Чтобы завершить шаг 2, вот цикл foreach, который я придумал для загрузки всех файлов, содержащихся в файловом блоке. Он содержит трюк с replace/rejoin соединением, чтобы убедиться, что имя файла правильно конкатенируется с текущим URL-адресом (без дополнительных "/"):

foreach file files [
    print rejoin ["Getting: " file]
    new-page: rejoin [crfu "//" file]
    replace new-page "///" "/"
    write/binary to-file file read/binary to-url new-page
]

Я столкнулся с некоторыми проблемами с определёнными ссылками на веб-странице, которые на самом деле не были списками файлов или папок или которые не загружались должным образом. Я использовал несколько условных комбинаций "if" и "error? try", чтобы устранить эти проблемы. Я записал ошибки в текстовый файл, чтобы потом проверить их и при необходимости загрузить вручную. Вот исправленная версия приведённого выше кода с процедурами обработки ошибок:

foreach file files [
    if not file = "http://www.aprelium.com" [
    ; Бесплатный сервер aprelium помещает эту ссылку на все 
    ; страницы, которые он обслуживает. Я не хотел проверять все 
    ; содержимое их веб-страницы.
        print rejoin ["Getting: " file]
        new-page: rejoin [crfu "//" file]
        replace new-page "///" "/"
        if not exists? to-file file [
            either error? try [read/binary to-url new-page] [
                write/append %/c/errors.txt rejoin [
                    "There was an error reading:  "    new-page
                    newline]
            ] [
            if error? try [
            write/binary to-file file read/binary to-url new-page
            ][
                write/append %/c/errors.txt rejoin [
                "error writing: " crfu newline]]
            ]
        ]
    ]
]

Я хотел выполнить шаг 3, но понял, что именно здесь должен возникнуть шаблон рекурсии - для каждой копируемой папки я хотел заглянуть внутрь этой папки и создать все содержащиеся в ней папки, а затем внутри этих папок и т.д.. Я определил шаблон рекурсии для перехода в текущие локальные и удалённые папки и для запуска функции, в которой содержались все шаги 2–4. Я решил обозначить всю включающую функцию "copy-current-dir" - ей будут переданы параметры "lrf", "clf" и "crfu". Эта функция содержит рекурсивную функцию, которая вызывает охватывающую функцию copy-current-dir, которая сама содержит рекурсивную функцию и т.д. Эффект этой рекурсии заключается в том, что вводится каждая подпапка каждой папки. Вот рекурсивная функция:

recurse: func [folder-name] [
    change-dir to-file folder-name
    crfu: rejoin [crfu folder-name]
    clf: rejoin [clf folder-name]
    ; ТЕПЕРЬ РЕКУРСИЯ - вызовите функцию, в которой эта функция 
    ; содержится:
    copy-current-dir crfu clf lrf
    ; Когда закончите, вернитесь к папке на локальном и удалённом 
    ; машинах. Действия замены удаляют текст текущей папки из конца 
    ; строк текущей папки.
    change-dir %..
    replace clf folder-name ""
    replace crfu folder-name ""
]

Наконец, я выполнил шаги 3 и 4, создав локальные папки для зеркалирования каждого каталога в текущей удалённой папке, а затем вызвал рекурсивную функцию, чтобы пройти через них. Я использовал цикл foreach для работы с каждым каталогом в текущем списке подкаталогов. Поскольку этот цикл содержит рекурсивную функцию, которая, в свою очередь, запускает copy-current-dir, который, в свою очередь, содержит этот цикл, каждый подкаталог каждого подкаталога обрабатывается до тех пор, пока задание не будет завершено:

foreach folder-name folders [
    make-dir to-file folder-name
    recurse folder-name
]

Я заключил разделы синтаксического анализа, цикла/чтения и рекурсии в функцию copy-current-dir, чтобы их можно было вызывать рекурсивно. Затем я добавил несколько процедур обработки ошибок, пока играл с рабочим кодом. Я включил блок URL-адресов, которых следует избегать, и некоторый код в последний цикл foreach, чтобы проверить, что эти URL-адреса ещё не были загружены (на случай, если я ранее запускал программу в том же каталоге). Вот финальный сценарий:

REBOL [title: "Directory Downloader"]

avoid-urls: [
    "/4/DownLoad/en_wikibooks_org/skins-1_5/common/&"
    "DownLoad/groups_yahoo_com/group/Join%20This%20Group!/"
    "DownLoad/pythonide_stani_be/ads/"
    "Nick%20Antonaccio/Desktop/programming/api/ewe/"
]

copy-current-dir: func [
{
    Download the files from the current remote directory
    to the current local directory and create local subfolders
    for each remote subfolder.  Then recursively do the same
    thing inside each sub-folder.
} 
    crfu  ; current-remote-folder-url
    clf   ; current-local-folder
    lrf   ; local-root-folder
] [
    ; Убедитесь, что URL-адрес, который будет проанализирован, 
    ; отсутствует в приведённом выше списке исключения. Это даёт 
    ; возможность при необходимости пропустить указанные папки:

    foreach avoid-url avoid-urls [
        if find crfu avoid-url [return "avoid"]
    ]

    ; Сначала проанализируйте удалённую папку на предмет имён файлов 
    ; и папок. Учитывая URL-адрес удалённой страницы, создайте 2 
    ; переменных списка. файлы: удалённые файлы для загрузки (в 
    ; текущем каталоге) папки: удалённые подкаталоги для рекурсии. 
    ; Есть проверка ошибок, если страница не читается:
if error? try [page-data: read crfu] [
    write/append %/c/errors.txt rejoin [
        "error reading (target read error): " 
        crfu newline]
    return "index.html"
]

; Если веб-сервер находит файл index.html в папке, он будет 
; обслуживать его содержимое, а не отображать структуру 
; каталогов. Затем он попытается выполнить сканирование 
; HTML-страницы. Следующее предотвратит возникновение этой 
; ошибки. ПРИМЕЧАНИЕ: эта ошибка была более эффективно устранена 
; путём редактирования имён индексных страниц на вебсервере Abyss:
if not find page-data {Powered by <b><i>Abyss Web Server} [
    ; </i></b>
    write/append %/c/errors.txt rejoin [
        "error reading (.html read error): " 
        crfu newline]
    return "index.html"
]
files: copy []
folders: copy []
parse page-data [
    any [
        thru {href="} copy temp to {"} (
            last-char: to-string last to-string temp
            either last-char = "/" [
            ; don't go upwards through the folder structure:
                if not temp = "../" [
                    append folders temp
                ]
            ][
                append files temp
            ]
        )
    ] to end
]

; Затем загрузите файлы из текущей удалённой папки в текущую 
; локальную папку:

foreach file files [
    if not file = "http://www.aprelium.com" [
        print rejoin ["Getting: " file]
        new-page: rejoin [crfu "//" file]
        replace new-page "///" "/"
        if not exists? to-file file [
            either error? try [read/binary to-url new-page][
                write/append %/c/errors.txt rejoin [
                    "There was an error reading:  "    new-page
                    newline]
            ] [
            if error? try [
    write/binary to-file file read/binary to-url new-page
            ][
                write/append %/c/errors.txt rejoin [
                "error writing: " 
                crfu newline]]
            ]
        ]
    ]
]

; Проверьте, нет ли больше вложенных папок. Если это так, 
; выйдите из функции copy-current-dir.

if folders = [] [return none]

; Определите шаблон рекурсии. Это изменяет текущую локальную 
; папку и запускает функцию copy-current-dir (текущая функция, в 
; которой мы находимся), которая сама содержит рекурсивную 
; функцию, которая сама вызовет copy-current-dir и т. Д. Эффект 
; этого Рекурсия заключается в том, что вводятся все подпапки 
; каждой папки. Вот что позволяет паукам:
recurse: func [folder-name] [
    change-dir to-file folder-name
    crfu: rejoin [crfu
        folder-name]
    clf: rejoin [clf
        folder-name]
    copy-current-dir crfu clf lrf
    ; Когда закончите, вернитесь к папке на локальном и 
    ; удалённом машинах. Действия замены удаляют текст текущей 
    ; папки из конца строк текущей папки.
change-dir %..
replace clf folder-name ""
replace crfu folder-name ""
]

; В-третьих, создайте локальные папки для зеркалирования каждого 
; каталога в текущей удалённой папке, а затем пролистайте их, 
; используя функцию рекурсии, чтобы загрузить все файлы и 
; подкаталоги, включённые в каждую папку:
foreach folder-name folders [
;    foreach avoid-url avoid-urls [
;        if not find folder-name avoid-url [
            make-dir to-file folder-name
            recurse folder-name
;        ]
;    ]
]
]

; Теперь получите исходный удалённый URL-адрес и создайте локальную 
; папку для его зеркалирования с красиво оформленным именем (только 
; допустимые символы имени файла Windows).

initial-pageurl: to-url request-text/default trim {
    http://192.168.1.4:8001/4/}
initial-local-dir: copy initial-pageurl
replace initial-local-dir "http://" ""
replace/all initial-local-dir "/" "_"
replace/all initial-local-dir "\" "__"
replace/all initial-local-dir ":" "--"
lrf: to-file rejoin [initial-local-dir "/"]
if not exists? lrf [make-dir lrf]
change-dir lrf
clf: lrf

; Запустите процесс, запустив функцию copy-current-dir:

copy-current-dir initial-pageurl clf lrf

print "DONE" halt

22.16 Случай: овощеводство

Моя мама - разработчик Microsoft Access на пенсии, которая в свободное время любит заниматься садоводством. Фермерство стало для неё занятием неполный рабочий день и бизнесом, и её внимание всегда уделялось обучению повышению урожайности. Она собрала обширные знания о том, как определённые растения лучше выживают, если их посадить рядом друг с другом, и она хотела создать программу, которая поможет упорядочить эту информацию. Она хотела создать автономную версию, которую она могла бы использовать на своём домашнем компьютере и раздавать друзьям. Она также хотела опубликовать его в сети как динамическую базу данных. Кроме того, она ожидала создания версии, которую можно было бы носить в саду на карманном компьютере. Я предложил использовать REBOL, потому что он может удовлетворить все её потребности. Она несколько дней работала со своими инструментами разработки, и я сказал ей, что могу сделать всё в тот же вечер, используя REBOL. Вот набросок, который я создал:

  1. Создадим структуру базы данных для хранения информации о совместимости овощей и другой связанной информации.
  2. Напишем версию сценария для командной строки, которая позволяет пользователям отображать всю информацию для любого выбранного овоща (это можно запустить в любой операционной системе, поддерживающей версию REBOL для командной строки, включая карманный компьютер).
  3. Создадим CGI-версию вышеуказанного скрипта, которая работает на веб-сайте.
  4. Создадим красивую версию с графическим интерфейсом для использования на домашнем ПК.
  5. Напишем отдельный графический интерфейс для управления административным добавлением данных в базу данных.
  6. Обеспечим способ обновления файлов данных на веб-сайте.

Для начала я использовал пример сетки данных listview из этого руководства, чтобы предоставить интерфейс для файлов данных о овощах. Это обеспечило структуру данных, подходящую для проекта, и мгновенное решение для создания внешнего интерфейса GUI. Шаги 1 и 5 были выполнены мгновенно (этот пример базы данных очень полезен - большое спасибо Хенрику Микаэлю Кристенсену за создание модуля listview!).

Я создал несколько начальных строк данных для работы. Вот рабочий файл database.db, который я создал:

["basil" "" "tomato" "basil protects tomatoes." "" ""]
["beans" "onion" "cabbage carrot radish" "" "" ""] 
["cabbage" "celery" "tomato" "" "" ""] 
["carrot" "" "tomato" 
    "Carrots strengthen the roots of tomatoes."
    "Carrots love tomatoes." ""] 
["radish" "cabbage" "beans carrot tomato" "" "" ""] 
["tomato" "cabbage" "basil carrot" "" "" ""]

Каждый блок содержит 6 частей информации о каждом возможном овоще:

  1. название овоща
  2. список овощей, совместимых с данным овощем (т.е. которые хорошо себя чувствуют, если их посадить рядом с данным овощем).
  3. список несовместимых овощей
  4. 3 поля для общих замечаний о данном овоще

Я решил добавить кнопку "upload@ (загрузить) в графический интерфейс listview, чтобы выполнить шаг № 6 в моей схеме программы. Здесь имело смысл добавить эту функцию, потому что рабочий процесс пользователя обычно включает добавление/изменение данных в базе данных (с использованием списка), а затем обновление онлайн-базы данных для соответствия. Вот код загрузки, который я придумал. Он включает в себя некоторую проверку ошибок, чтобы приложение не вылетало из строя при проблемах с подключением к Интернету. Я добавил кнопку в графический интерфейс списка и поместил следующий код в блок действий:

btn "upload to web" [
    uurl: ftp://user:pass@website.com/public_html/path/
    if error? try [
        ; first, backup former data file:
        write rejoin [uurl "database_backup.db"] read rejoin [
            uurl "database.db"]
        write rejoin [uurl "database.db"] read %database.db
        alert "Update complete."
    ] [alert "Error - check your Internet connection."]
]

Затем я понял, что добавление и удаление новых овощей в базу данных и из неё требует особого внимания. В итоге это была самая большая часть этого проекта по кодированию. Я мог бы использовать встроенные возможности модуля listview, чтобы просто добавить новый овощ в базу данных, но с этим возникла проблема. Каждый раз, когда новый овощ добавляется в базу данных, он создаёт список совместимости. Помимо простого добавления нового блока в базу данных с полями, перечисляющими совместимость и несовместимость, этот новый овощ необходимо добавить в список совместимости каждого другого овоща, с которым он совместим. Его также необходимо добавить в список несовместимых овощей, с которыми он несовместим. Редактирование этих блоков вручную потребует больших усилий и повысит вероятность ошибок пользователя, особенно по мере роста базы данных. Вместо этого я решил создать небольшой скрипт, чтобы делать это автоматически. Вот процесс мышления псевдокода для этого сценария:

  1. Составить список имеющихся овощей. Это можно сделать, прочитав существующую базу данных, перебрав каждый блок и выбрав первый элемент в каждом блоке (название овоща).
  2. Создать небольшой новый графический интерфейс для ввода новой информации о овощах. Он должен включать поле ввода для нового названия овоща, 2 текстовых списка, показывающих возможные совместимые и несовместимые овощи (считанные из существующего списка овощей в базе данных), и 3 поля для примечаний.
  3. Используйте цикл foreach для просмотра списков совместимых и несовместимых овощей. Пусть цикл автоматически добавит новый овощ в соответствующие списки совместимости других овощей.

Я создал код графического интерфейса и поместил цикл foreach внутри блока действий кнопки, используемой для добавления нового овоща. Вот код, который я сохранил как "add_veggie.r":

REBOL [title: "Add Veggie"]

; читаем текущую базу данных
veggies: copy load %database.db
; создаём список овощей (1-й пункт в каждом блоке):
veggie-list: copy []
foreach veggie veggies [append veggie-list veggie/1]

; создать окно с соответствующими полями и текстовыми списками:
view/new center-face add-gui: layout [
    across
    text "new vegetable:" 88x24 right new-veg: field
    return
    text "compatible:" 88x24 right 
    new-compat: text-list data veggie-list
    return
    text "incompatible:" 88x24 right 
    new-incompat: text-list data veggie-list
    return
    text "note 1:" 88x24 right new-note1: field
    return
    text "note 2:" 88x24 right new-note2: field
    return
    text "note 3:" 88x24 right new-note3: field
    return

    ; теперь добавьте кнопку для запуска циклов foreach:

    tabs 273 tab btn "Done" [
        ; Сначала добавьте новый блок вегетарианских данных к 
        ; существующему блоку базы данных. Создайте новый блок из 
        ; текста, введённого в каждое поле, и из элементов, 
        ; выбранных в каждом из приведённых выше списков ("reduce" 
        ; оценивает перечисленные элементы, а не включает 
        ; фактический текст. То есть вы хотите добавить текст, 
        ; введённый в поле new-veg, а не фактический текст 
        ; "new-veg/text"). "append/only" добавляет новый блок в базу 
        ; данных как блок, а не как набор отдельных элементов:
        append/only veggies new-block: reduce [
            new-veg/text
            ; "reform" создаёт строку в кавычках из блока выбранных 
            ; элементов в текстовых списках text-lists:
            reform new-compat/picked
            reform new-incompat/picked
            new-note1/text
            new-note2/text new-note3/text
        ]
        ; Теперь прокрутим список совместимости нового овоща и 
        ; добавим его в списки совместимости всех других совместимых 
        ; овощей. Я ставлю пробел, если в списке уже были другие 
        ; овощи:
foreach onecompat new-compat/picked [
    foreach veggie veggies [
        if find veggie/1 onecompat [
            either veggie/2 = "" [spacer: ""] [
                spacer: " "]
            append veggie/2 rejoin [spacer 
            new-veg/text]
        ]
    ]
]
; Теперь проделайте то же самое со списком несовместимости:
foreach oneincompat new-incompat/picked [
    foreach veggie veggies [
        if find veggie/1 oneincompat [
            either veggie/3 = "" [spacer: ""] [
                spacer: " "]
            append veggie/3 rejoin [spacer 
            new-veg/text]
        ]
    ]
]
save %database.db veggies
; когда закончим, снова запустим редактор данных:
launch %veggie_data_editor.r
unview add-gui
]
]
focus new-veg
do-events

Поскольку сценарий add_veggie.r всегда будет запускаться из программы veggie_data_editor.r, я добавил следующий код в блок действий для кнопки "добавить овощи" в редакторе данных. Он запускает указанную выше программу add_veggie и закрывает список:

btn "add veggie" [launch %add_veggie.r quit]

Когда пользователь закрывает программу add_veggie, код "launch %veggie_data_editor.r" в конце программы перезапускает редактор данных. Это обрабатывает переключение между двумя экранами. При перезапуске редактора данных всё новые данные автоматически обновляются и отображаются, поэтому мне не нужно вручную обновлять любую отображаемую информацию. Поигравшись с системой, я понял, что перед закрытием редактора данных лучше сохранить изменения, внесённые в базу данных. Поэтому я скорректировал приведённый выше код следующим образом:

btn "add veggie" [
    launch %add_veggie.r
    backup-file: to-file rejoin ["backup_" now/date]
    write backup-file read %database.db
    save %database.db theview/data
    quit
]

Затем я использовал приведённый выше код для создания аналогичной программы remove_veggie.r. Вместо того, чтобы создавать для него графический интерфейс, я просто добавил код к кнопке "удалить овощи" в редакторе данных, чтобы сохранить имя текущего выбранного овоща в файл (veggie2remove.r). Я также скопировал процедуру резервного копирования из приведённого выше кода, чтобы убедиться, что все изменения в списке сохранены, прежде чем продолжить:

btn "remove veggie" [
    if (to-string request-list "Are you sure?" 
            [yes no]) = "yes" [
        ; получить название из текущей выбранной строки в списке:
        first-veg: copy first theview/get-row
        theview/remove-row
        write %veggie2remove.r first-veg
        launch %remove_veggie.r
        backup-file: to-file rejoin ["backup_" now/date]
        write backup-file read %database.db
        save %database.db theview/data
        quit
    ]
]

Сценарий remove_veggie.r просто считывает имя овоща из файла veggie2remove.r, созданного выше, и выполняет несколько циклов foreach, чтобы удалить этот овощ из списков совместимости других овощей:

REBOL [title: "Remove Veggie"]

veggies: copy load %database.db
remove-veggie: read %veggie2remove.r

; удалить выбранный овощ из совместимых списков (второе поле в 
; каждом блоке). Это делается заменой любого вхождения remove-veggie 
; пустой строкой (""). Это эффективно стирает все вхождения овощей:

foreach veggie veggies [
    replace veggie/2 remove-veggie ""
]

; сделайте то же самое с несовместимыми списками всех других овощей 
; (поле 3 в каждом блоке):

foreach veggie veggies [
    replace veggie/3 remove-veggie ""
]

save %database.db veggies
; когда закончим, снова запустим редактор данных:
launch %veggie_data_editor.r

Теперь редактор данных listview и все его вспомогательные скрипты готовы. Поскольку listview обычно запускается из GUI-версии основной программы ("veggie_gui.r" - ещё не написано), я добавил следующий код в существующую процедуру закрытия listview:

launch "veggie_gui.r"

Когда я разрабатываю основную программу veggie_gui, я добавляю кнопку для запуска списка. Когда я закрываю представление списка, приведённый выше код перезапускает программу с графическим интерфейсом пользователя, чтобы обрабатывать переключение между этими двумя экранами. Вот окончательный код сетки данных listview со всеми описанными изменениями и дополнениями:

REBOL [title: "Veggie Data Editor"]

evt-close: func [face event] [
    either event/type = 'close [
        inform layout [
            across
            btn "Save Changes" [
                ; при нажатии кнопки сохранения автоматически 
                ; создаётся файл данных резервной копии:
                backup-file: to-file rejoin ["backup_" now/date]
                write backup-file read %database.db
                save %database.db theview/data
                launch "veggie_gui.r"
                quit
            ]
            btn "Lose Changes" [
                launch "veggie_gui.r"
                quit
            ]
            btn "CANCEL" [hide-popup]
        ] none ] [ 
        event
    ]
]
insert-event-func :evt-close

if not exists? %list-view.r [write %list-view.r read
    http://www.hmkdesign.dk/rebol/list-view/list-view.r
]
do %list-view.r

if not exists? %database.db [write %database.db {[][]}]
database: load %database.db

view center-face gui: layout [
    h3 {To enter data, double-click any row, and type directly
        into the listview.  Click column headers to sort:}
    theview: list-view 775x200 with [
        data-columns: [Vegetable Yes No Note1 Note2
            Note3]
        data: copy database
        tri-state-sort: false
        editable?: true
    ]
    across
    btn "add veggie" [
        launch %add_veggie.r
        backup-file: to-file rejoin ["backup_" now/date]
        write backup-file read %database.db
        save %database.db theview/data
        quit
    ]
    btn "remove veggie" [
        if (to-string request-list "Are you sure?" 
                [yes no]) = "yes" [
            first-veg: copy first theview/get-row
            theview/remove-row
            write %veggie2remove.r first-veg
            launch %remove_veggie.r
            backup-file: to-file rejoin ["backup_" now/date]
            write backup-file read %database.db
            save %database.db theview/data
            quit
        ]
    ]
    btn "filter veggies" [
        filter-text: request-text/title trim {
            Filter Text (leave blank to refresh all data):}
        theview/filter-string: filter-text
        theview/update
    ]
    btn "upload to web" [
        uurl: ftp://user:pass@website.com/public_html/path/
        if error? try [
            ; first, backup former data file:
            write rejoin [
                uurl "database_backup.db"] read rejoin [
                uurl "database.db"]
            write rejoin [uurl "database.db"] read %database.db
            alert "Update complete."
        ] [alert "Error - check your Internet connection."]
    ]
]

Затем я создал версию программы для командной строки. Пример "Циклическое сканирование данных", представленный ранее в этом руководстве, послужил идеальной моделью. Я просто изменил некоторые метки переменных и загрузил данные из существующего файла database.db. Вот код:

REBOL [title: "Veggies"]

veggies: load %database.db

a-line: copy [] loop 65 [append a-line "-"]
a-line: trim to-string a-line

print-all: does [
    foreach veggie veggies [
        print a-line
        print rejoin ["Veggie:   " veggie/1]
        print a-line
        print rejoin ["Matches:  " veggie/3]
        print rejoin ["No-nos:   " veggie/2]
        print rejoin ["Note 1:   " veggie/4]
        print rejoin ["Note 2:   " veggie/5]
        print rejoin ["Note 3:   " veggie/6]
        print newline
    ]
] 
forever [
    prin "^(1B)[J"
    print "Here are the current foods in the database:^/"
    print a-line
    foreach veggie veggies [prin rejoin [veggie/1 "  "]]
    print "" print a-line
    print "Type a vegetable name below.^/"
    print "Type 'all' for a complete database listing."
    print "Press [Enter] to quit.^/"
    answer: ask {What food would you like info about?  }
    print newline
    switch/default answer [
        "all"     [print-all]
        ""         [ask "^/Goodbye!  Press [Enter] to end." quit]
        ][
        found: false
        foreach veggie veggies [
            if find veggie/1 answer [
                print a-line
                print rejoin ["Veggie:   " veggie/1]
                print a-line
                print rejoin ["Matches:  " veggie/3]
                print rejoin ["No-nos:   " veggie/2]
                print rejoin ["Note 1:   " veggie/4]
                print rejoin ["Note 2:   " veggie/5]
                print rejoin ["Note 3:   " veggie/6]
                print newline
                found: true
            ]
        ]
        if found <> true [
            print "That vegetable is not in the database!^/"
        ]
    ]
    ask "Press [ENTER] to continue"
]    
halt

Это было просто! Просто сравните его с оригинальным примером - он практически идентичен. Опять же, этот обобщённый пример был представлен в этом руководстве, чтобы предоставить модель для использования во многих различных ситуациях. С его помощью мне даже не пришлось писать псевдокод.

Теперь я расширил приведённый выше пример командной строки, чтобы создать приложение CGI. Для начала я использовал последний пример CGI, представленный ранее в этом руководстве, в качестве модели. К нему я добавил код, который я создал для приведённого выше примера командной строки. Единственными реальными изменениями, которые мне нужно было внести, были некоторые дополнительные теги форматирования HTML, необходимые для правильного отображения страницы в браузере (в основном, новая строка "<BR>"). Опять же, просто смесь нескольких существующих примеров. Никакого псевдокода не требуется - мне просто нужно было подумать о том, как организовать существующий код командной строки, чтобы он вписался в общую схему CGI. Вот код:

#! /home/path/public_html/rebol/rebol -cs
REBOL [title: "Veggies"]
print "content-type: text/html^/"
print [<HTML><HEAD><TITLE>"Veggies"</TITLE></HEAD><BODY>]

veggies: load %database.db

a-line: copy [] loop 65 [append a-line "-"]
a-line: trim to-string a-line

print-all: does [
    foreach veggie veggies [
        print a-line
        print [<BR>]
        print rejoin ["Veggie:   " veggie/1]
        print [<BR>]
        print a-line
        print [<BR>]
        print rejoin ["Matches:  " veggie/3]
        print [<BR>]
        print rejoin ["No-nos:   " veggie/2]
        print [<BR>]
        print rejoin ["Note 1:   " veggie/4]
        print [<BR>]
        print rejoin ["Note 2:   " veggie/5]
        print [<BR>]
        print rejoin ["Note 3:   " veggie/6]
        print [<BR>]
    ]
]

print "Here are the current foods in the database:^/"
print [<BR>]
print a-line
print [<BR><strong>]
foreach veggie veggies [prin rejoin [veggie/1 "  "]]
print ""
print [</strong><BR>]
print a-line
print [<BR>]

submitted: decode-cgi system/options/cgi/query-string
if submitted/2 <> none [
    switch/default submitted/2 [
        "all"     [print-all]
        ][
        found: false
        foreach veggie veggies [
            if find veggie/1 submitted/2 [
                print a-line
                print [<BR>]
                print rejoin ["Veggie:   " veggie/1]
                print [<BR>]
                print a-line
                print [<BR>]
                print rejoin ["Matches:  " veggie/3]
                print [<BR>]
                print rejoin ["No-nos:   " veggie/2]
                print [<BR>]
                print rejoin ["Note 1:   " veggie/4]
                print [<BR>]
                print rejoin ["Note 2:   " veggie/5]
                print [<BR>]
                print rejoin ["Note 3:   " veggie/6]
                found: true
            ]
        ]
        if found <> true [
            print [<BR>]
            print "That vegetable is not in the database!"]
            print [<BR>]
    ]
]

print [<FORM ACTION="http://website.com/rebol/veggie.cgi">]
print [<BR><HR><BR>"Enter a veggie you'd like info about:"<BR>]
print ["Veggie: "<INPUT TYPE="TEXT" NAME="username" SIZE="25">]
print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">]
print [</FORM>]
print [</BODY></HTML>]

Мне не нравилось, что CGI требует, чтобы пользователь вводил название перечисленного овоща. Вместо этого я избавился от распечатки списка и добавил его в выпадающий список с возможностью выбора. Вот последний пример cgi с раскрывающимся списком HTML:

#! /home/path/public_html/rebol/rebol -cs
REBOL [title: "Veggies"]
print "content-type: text/html^/"
print [<HTML><HEAD><TITLE>"Veggies"</TITLE></HEAD><BODY>]

veggies: load %database.db

a-line: copy [] loop 65 [append a-line "-"]
a-line: trim to-string a-line

print-all: does [
    foreach veggie veggies [
        print a-line
        print [<BR>]
        print rejoin ["Veggie:   " veggie/1]
        print [<BR>]
        print a-line
        print [<BR>]
        print rejoin ["Matches:  " veggie/3]
        print [<BR>]
        print rejoin ["No-nos:   " veggie/2]
        print [<BR>]
        print rejoin ["Note 1:   " veggie/4]
        print [<BR>]
        print rejoin ["Note 2:   " veggie/5]
        print [<BR>]
        print rejoin ["Note 3:   " veggie/6]
        print [<BR>]
    ]
]

submitted: decode-cgi system/options/cgi/query-string
if submitted/2 <> none [
    switch/default submitted/2 [
        "all"     [print-all]
        ][
        found: false
        foreach veggie veggies [
            if find veggie/1 submitted/2 [
                print a-line
                print [<BR>]
                print rejoin ["Veggie:   " veggie/1]
                print [<BR>]
                print a-line
                print [<BR>]
                print rejoin ["Matches:  " veggie/3]
                print [<BR>]
                print rejoin ["No-nos:   " veggie/2]
                print [<BR>]
                print rejoin ["Note 1:   " veggie/4]
                print [<BR>]
                print rejoin ["Note 2:   " veggie/5]
                print [<BR>]
                print rejoin ["Note 3:   " veggie/6]
                found: true
            ]
        ]
        if found <> true [
            print [<BR>]
            print "That vegetable is not in the database!"]
            print [<BR>]
    ]
]

print [<FORM ACTION="http://website.com/rebol/veggie.cgi">]
print [<BR>"Please select a veggie you'd like info about:"<BR>]
print ["Veggie: "<select NAME="username"><option>"all"]
foreach veggie veggies [prin rejoin ["<option>" veggie/1]]
print [</option>]
print [<INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">]
print [</FORM>]
print [</BODY></HTML>]

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

  1. Отобразить полный список овощей в базе данных (создадим список, используя цикл foreach, аналогичный тем, которые используются в программе командной строки, и отобразите этот блок в виджете текстового списка).
  2. Отображение информации о любом овоще, выбранном из виджета текстового списка (когда элемент выбран, собрать всю информацию о выбранном овоще и отобразить её в красивом формате в отдельном виджете текстовой области).
  3. Добавить кнопку для запуска созданного ранее редактора списка.

Сначала я позаимствовал код из примера add_veggies.r, чтобы создать список всех овощей в базе данных. Он использует цикл foreach для циклического перебора каждого блока в базе данных и создаёт список первого элемента в каждом блоке (имя каждого овоща). Затем он сортирует список по алфавиту. Это должно быть выполнено до отображения графического интерфейса:

load-data: does [
    veggies: copy load %database.db
    veggie-list: copy []
    foreach veggie veggies [append veggie-list veggie/1]
    veggie-list: sort veggie-list
]

Я решил использовать виджет текстового списка для отображения блока названий овощей. Чтобы отобразить информацию о каждом овоще, я использовал простой текстовый дисплей. Вот код макета REBOL для этого:

list-veggies: text-list 200x400 data veggie-list
display: area "" 300x400

В блок действий этого виджета с текстовым списком я добавил код для отображения информации о выбранном овоще (она оценивается/выполняется всякий раз, когда пользователь выбирает элемент из списка):

; Сначала создадим блок текста со всей информацией о выбранном 
; овоще, красиво отформатированный с помощью новой строки и 
; заглавных заголовков разделов:

current-info: []
foreach veggie veggies [
    if find veggie/1 value [
        current-info: rejoin [
            "COMPATIBLE:     " veggie/3 newline newline
            "INCOMPATIBLE:   " veggie/2 newline newline
            "NOTE 1:   " veggie/4 newline newline
            "NOTE 2:   " veggie/5 newline newline
            "NOTE 3:   " veggie/6
        ]
    ]
]

; Теперь отобразим и обновите этот текст в виджете текстовой области:

display/text: current-info
show display show list-veggies

Наконец, добавьте кнопку для запуска редактора данных listview:

btn "Edit Tables" [do %veggie_data_editor.r]

Вот в основном и всё. Вот финальная версия:

REBOL [title: "Veggie Matches"]

load-data: does [
    veggies: copy load %database.db
    veggie-list: copy []
    foreach veggie veggies [append veggie-list veggie/1]
    veggie-list: sort veggie-list
]

load-data

view display-gui: layout [
    h2 "Click a veggie name to display matches and other info:"
    across
    list-veggies: text-list 200x400 data veggie-list [
        current-info: []
        foreach veggie veggies [
            if find veggie/1 value [
                current-info: rejoin [
                    "COMPATIBLE:     " veggie/3 newline newline
                    "INCOMPATIBLE:   " veggie/2 newline newline
                    "NOTE 1:   " veggie/4 newline newline
                    "NOTE 2:   " veggie/5 newline newline
                    "NOTE 3:   " veggie/6
                ]
            ]
        ]
        display/text: current-info
        show display show list-veggies
    ]
    display: area "" 300x400 wrap
    return
    btn "Edit Tables" [
        do %veggie_data_editor.r
        ; launch "veggie_data_editor.r"
        ; load-data 
        ; show list-veggies
        ; show display
    ]
]

Есть 5 полных локальных файлов сценариев, которые составляют законченную программу: veggie_data_editor.r, add_veggie.r, remove_veggie.r, veggie_command_line.r, veggie_gui.r. Как правило, основные настольные приложения запускаются с помощью скрипта veggie_gui.r. Veggie_data_editor.r также может быть запущен сам по себе (помните, что он запускает программу veggie_gui.r, когда закрывается). Чтобы veggie_data_editor работал, файл listview.r должен быть включён в тот же каталог. Созданный файл database.db также должен храниться в том же каталоге. Я упаковал все эти файлы в исполняемый файл с помощью XpackerX и отправил его маме. Шестой файл сценария, veggie.cgi, был загружен на сайт. Файл database.db также был загружен вручную, но моя мама предпочитает использовать кнопку загрузки в veggie_data_editor для обновления базы данных на веб-сайте. Veggie2remove.r и файлы резервных копий базы данных создаются автоматически при использовании программы - они находятся в той же папке, что и файлы сценариев.

22.17 Дополнительный проект автоматизации учителей

Теперь, когда система группового планирования завершена, я хочу автоматизировать нашу ежедневную процедуру оформления заказа. Каждый день нашим учителям платят непосредственно их ученики. В свою очередь, они платят нам арендную плату/реферальную плату за использование помещения и ресурсов. Это наш основной источник дохода. В конце дня учителя складывают всех учеников, которых они видели, и платят определённую плату за каждое завершённое получасовое занятие. Некоторые студенты вносят предоплату своим учителям, а учителя, в свою очередь, вносят предоплату нам, чтобы им не приходилось управлять арендной платой за предоплаченные встречи в будущем.

Чтобы вручную рассчитать ежедневные сборы, требуется много времени, и при ручном расчёте этот процесс подвержен ошибкам. Я хочу автоматизировать расчёты платежей на основе существующей информации о расписании онлайн, и я хочу создать интегрированную систему учёта, чтобы упростить отслеживание предоплаты. Учителя должны отслеживать пропущенные/перенесённые платежи за приём, чтобы учащиеся получали должное вознаграждение за перенесённое время приёма. Кроме того, помимо ежедневных уроков по месту жительства, некоторые из наших инструкторов проводят онлайн-уроки, за которые _мы__ непосредственно учащиеся. Для этих уроков мы вычитаем арендную плату за комнату из общей суммы, уплаченной нам учителями. Нам нужно решение, чтобы легко управлять и отслеживать все эти ежедневные вычисления для всех учителей. Цель состоит в том, чтобы постоянно подсчитывать, сколько денег каждый учитель должен каждый вечер, и сколько денег ученики должны учителям. Чтобы создать схему программного обеспечения, я подумал о том, что я делаю вручную каждый день, чтобы рассчитать комиссию за оформление заказа для одного человека. Этот мыслительный процесс послужит схемой для разработки автоматизированной системы учёта:

  1. Каждый день в расчётный час складывается общее количество уроков для учителя.
  2. Учитель должен нам определённую сумму за уроки, которые проходили в тот день в местной студии.
  3. Мы должны учителю вычитаться за каждый урок, который они провели онлайн в тот день.
  4. Любые уроки, которые ранее были оплачены учителем, вычитаются из общей суммы задолженности.
  5. Учитель вносит предоплату за любые будущие уроки, которые были оплачены студентами в тот день, и записи обновляются для отслеживания текущих предоплаченных сумм.
  6. Иногда из причитающейся нам суммы делаются другие вычеты (иногда учитель предоставляет бесплатный урок по разным причинам, или мы предоставляем дополнительное время учителю/ученику и т.д.). Эти суммы вычитаются из общей суммы задолженности.

Основываясь на приведённых выше рекомендациях, вот как я систематизировал свои мысли о том, что должна делать автоматизированная многопользовательская система:

  1. Многопользовательские требования приложения аналогичны требованиям приложения для планирования из предыдущего раздела. Я могу использовать код из приложения для планирования, чтобы предоставить текущий список учителей, простую защиту паролем, загрузку/сохранение/резервное копирование необходимых файлов данных для выбранного учителя и т.д.
  2. Чтобы выполнять ежедневные вычисления для одного преподавателя, я хочу предоставить динамически создаваемый список ежедневных студентов, и я хочу получить текущие записи о предоплате для данного учителя. Эти данные будут храниться на веб-сайте, любые изменения будут копироваться локально и на веб-сайте. Мне нужно придумать структуру данных для хранения записей о предоплате. Вся остальная информация (случайные отчисления, бесплатные уроки и т.д.) будет предоставляться пользователем ежедневно. Обычный ежедневный список студентов и записи о предоплате можно загрузить и отобразить в текстовых списках. Другие случайные вычеты и добавления можно вводить вручную в поля ввода текста и отображать в текстовых списках.
  3. По умолчанию каждый учитель должен заданную сумму за каждого ученика, выбранного из ежедневного списка (количество_студентов X половина_часа). Добавьте к этому любую плату за дополнительных студентов, не включённых в ежедневный список (перенесённые уроки, случайные дополнительные встречи и т.д.)
  4. Для каждого онлайн-урока вычитаем 1 студента из общего числа студентов, обученных в этот день, и вычитаем соответствующую сумму из общей суммы, причитающейся.
  5. Вычитаем все предыдущие предоплаты из общей суммы задолженности. Каждый раз, когда это происходит, вносим поправки в учётную запись учителя о предоплате.

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

Основная работа по созданию приложения выполняется на шаге 2. Необходимые расчёты производятся на шагах 3-5. Вот более структурированный план с псевдокодом, который поможет писать программный код:

  1. Прочтите текущий список ежедневных учеников из файла schedule.txt выбранного учителя на веб-сервере. Сохраните эту информацию в блоке и отобразите ее в виджете текстового списка графического интерфейса. Сохраните список местных студентов, выбранных из приведённого выше виджета, в отдельном блоке.
  2. Снова отобразите сегодняшних учеников во втором виджете текстового списка, чтобы пользователь мог выбрать тех, кто брал уроки онлайн. Сохраните выбранные данные в другом блоке.
  3. Предоставьте поле для ввода текста, чтобы можно было добавлять любых учащихся, не включённых в ежедневный список. Отображение виджета текстового списка, содержащего учащихся, введённых в текстовое поле. Обновляйте отображение текстового списка каждый раз, когда добавляется ученик. Чтобы удалить неправильные записи из этого списка, блок действий текстового списка должен содержать код для удаления любых студентов, выбранных пользователем.
  4. Предоставьте другое текстовое поле и текстовый список для ввода вычетов с тем же макетом и удалите код.
  5. Предоставьте кнопку для управления записями и расчётами предоплаты. Чтобы справиться с этим процессом, создайте отдельный сценарий, который будет описан ниже.
  6. Предоставьте кнопку "Рассчитать общую комиссию". Блок действий этой кнопки должен добавлять и вычитать общее количество элементов во всех текстовых списках в соответствии с правилами, определёнными в шагах 3-5 общей программы, описанной выше. Предоставьте краткое содержание HTML, которое учитель может распечатывать и отправлять каждый день.

Вот код, который я придумал для всего этого:

; Переменная url, приведённая ниже, взята из многопользовательской 
; среды, заимствованной из приложения-планировщика:
students: read/lines rejoin [url "/schedule.txt"]
; Инициализировать некоторые другие переменные:
other-additions: []    other-deductions: [] prepays: [] 
pay-for: copy [] online: copy []

view center-face layout [
    h2 "Local Students:"
    ; "face/selected" относится к текущим выбранным элементам в 
    ; текстовом списке (используйте [Ctrl] + щелчок мышью, чтобы 
    ; выбрать несколько элементов, и назначьте это переменной 
    ; "pay-for":
    text-list data copy students [pay-for: copy face/picked]
    h2 "Other Additions:" 
    field [
        ; добавить введённую информацию в текстовый список и 
        ; обновить отображение:
        append other-additions copy face/text
        show other-additions-list
    ]
    other-additions-list: text-list 200x100 data other-additions [
        ; удалить любую запись, выбранную пользователем, и обновить 
        ; отображение
        remove-each item other-additions [item = value]
        show other-additions-list
    ]
    at 250x20
    h2 "Online Students:"
    text-list data copy students [online: face/picked]
    h2 "Other Deductions:"
    field [
        append other-deductions copy face/text
        show other-deductions-list
    ]
    other-deductions-list: text-list 200x100 data other-deductions [
        remove-each item other-deductions [item = value]
        show other-deductions-list
    ]
    at 480x20
    h2 "Prepaid Lessons:"
    prepay-list: text-list data prepays [
        remove-each item prepays [item = value]
        show prepay-list
    ]
    ; I still need to create the prepay.r program:
    btn 200x30 "Calculate Prepaid Lessons" [
        save %prepay.txt load rejoin [url "/prepay.txt"]
        do %prepay.r
    ]
    at 480x320
    btn 200x100 font-size 17 "Calculate Total Fees" [
        total-students: (
            (length? pay-for) - (length? online) + 
            (length? other-additions) - (length? other-deductions) -
            (length? prepays)        
        )
        ; Я хочу создать HTML-вывод для этого раздела:
        alert rejoin ["Total: " to-string total-students]
    ]
]

Теперь осталось создать отдельную программу для управления информацией о предоплате. Вот схема, описывающая мои намерения:

  1. Создайте и загрузите файл данных "preday.txt" для хранения информации о предоплате для каждого учителя. Он должен содержать отдельный блок для каждого студента, который вносит предоплату, с полями для имени студента, вложенным блоком для сумм и дат каждой предоплаты и вложенным блоком для дат каждого посещённого урока и суммы, вычитаемой из предоплаты для каждого урок.
  2. Создайте графический интерфейс с текстовым списком, отображающим каждого студента, который внёс предоплату. Просмотрите данные в файле preday.txt, чтобы получить имена учащихся (первый элемент в каждом блоке). Каждый раз, когда имя выбирается пользователем, отображайте имя ученика, даты и суммы предоплаты, а также даты уроков в отдельных текстовых списках. Отобразите общий баланс предоплаты для выбранного учащегося в текстовом поле.
  3. Должна быть кнопка "Добавить" и несколько текстовых полей для ввода новых предоплат. Должны быть поля для имени студента, суммы и даты предоплаты. Если из списка выбран существующий ученик, эти поля должны быть автоматически заполнены сегодняшней датой и именем существующего ученика. Блок действий кнопки добавления должен добавлять информацию в соответствующие блоки в файле preday.txt.
  4. Должна быть кнопка "Применить сегодня", чтобы выбрать предоплату, которая будет применяться к сегодняшнему балансу. Сохраните имена выбранных учащихся в блоке, сохраните этот блок для чтения и использования в основном приложении и добавьте информацию о дате в соответствующие блоки в файле preday.txt.
  5. В графическом интерфейсе со списком должна быть кнопка "Готово", позволяющая изменять и сохранять информацию. Всякий раз, когда учащийся выбирается из списка, его записи о предоплате должны отображаться в редактируемом виде списка (импортируйте модуль просмотра списка и используйте в качестве модели пример базы данных из ранее в этом руководстве). Должны быть поля для сумм и дат предоплаты, а также дат и сумм уроков.
  6. Когда основное приложение предоплаты закрыто, необходимо создать резервную копию файла preday.txt и сохранить его на веб-сайте.

Для шага 1 вот пример блочной структуры, которую я придумал для хранения данных в файле preday.txt:

[
    ; имя:
    "John Smith"

    ; суммы и сроки предоплаты:
    [ [$100 4-April-2006] [$100 5-May-06] ]

    ; даты занятий:
    [
        [$20 4-April-06] [$20 11-April-06] [$20 18-April-06]
        [$20 25-April-06] [$20 5-May-06]
    ]
]

[
    "Paul Brown" 

    [ [$100 4-April-2006] ]

    [
        [$20 4-April-06] [$20 25-April-06]
    ]
]

[
    "Bill Thompson"

    [ [$200 22-March-2006] ]

    [
        [$20 22-March-06] [$20 29-March-06] [$20 5-April-06]
        [$20 12-April-06] [$20 19-April-06]    [$20 26-April-06]
        [$20 3-May-06]
    ]
]

Вот код, который я создал для выполнения моих требований к схеме:

REBOL [title: "Prepayment Calculator"]

prepays: load rejoin [url "/prepay.txt"]
names: copy []
prepay-history: []
lesson-history: []
display-todays-bal: does [
    ; рассчитать и отобразить текущий баланс для выбранного 
    ; студента:    
    todays-balance: $0
    foreach payment prepay-history [
        todays-balance: todays-balance + (
            first (to-block payment)
        )
    ]
    foreach lesson-event lesson-history [
        todays-balance: todays-balance - (
            first (to-block lesson-event)
        )
    ]
    ; обновление и отображение текущего баланса для каждого студента
    today-bal/text: to-string todays-balance
    show today-bal
]
foreach block prepays [append names first block]
view center-face gui: layout [
    across
    text bold "New Prepayment:"
    text right "Name:" new-name: field
    text right "Date:" new-date: field 125 to-string now/date
    text right "Amount:" new-amount: field 75 "$"
    btn "Add" [
        create-new-block: true
        foreach block prepays [
            if (first block) = new-name/text [
                create-new-block: false
                append (second block) to-string rejoin [
                    new-amount/text " " new-date/text
                ]
            ]
        ]
        if create-new-block = true [
            new-prepay: copy []
            append new-prepay to-string new-name/text
            append new-prepay to-string rejoin [
                new-amount/text " " new-date/text
            ]
            append prepays new-prepay
            names: copy []
            foreach block prepays [append names first block]
        ]
        display-todays-bal
        show existing show pre-his show les-his show today-bal
    ]
    return 
    text bold underline "Edit Data Manually" [
        view/new center-face layout [
            new-prepays: area 500x300 mold prepays
            btn "Save Changes" [
                prepays: copy new-prepays/text 
                unview
            ]
        ]
        names: copy []
        foreach block prepays [append names first block]
        show gui
        show existing show pre-his show les-his show today-bal
    ] 
    return
    text "Existing Prepayments:"  pad 75
    text "Prepayment History:"  pad 85
    text "Lesson History:" pad 100
    text "Balance:"
    return
    existing: text-list data names [
        ; Когда имя выбрано из этого текстового списка, обновите 
        ; другие поля на экране:
        new-name/text: value
        show new-name
        foreach block prepays [
            if (first block) = value [
                ; обновите другие текстовые списки, чтобы отобразить 
                ; предоплату и историю уроков выбранного учащегося:
                prepay-history: pre-his/data: second block
                show pre-his
                lesson-history: les-his/data: third block
                show les-his
            ]
        ]
        display-todays-bal
        ; получить список выбранных студентов
        prepaid-today: copy face/picked
    ]
    pre-his: text-list data prepay-history
    les-his: text-list data lesson-history
    today-bal: field 85
    return
    btn "Apply Selected Prepayments Today" [
        save %prepaid.txt prepaid-today

        unview
    ]
]

В исходной схеме расписания я заменяю все ссылки в коде на "schedule.txt" на "preday.txt":

REBOL [title: "Payment Calculator"] 

error-message: does [
    ans: request {Internet connection is not available.
        Would you like to see one of the recent local backups?}
    either ans = true [
        editor to-file request-file quit
    ][
        quit
    ]
]

if error? try [
    teacherlist: load ftp://user:pass@website.com/teacherlist.txt
][
    error-message
]
teachers: copy []
foreach teacher teacherlist [append teachers first teacher]
view center-face layout [
    text-list data teachers [folder: value unview]    
]

pass: request-pass/only
correct: false
foreach teacher teacherlist [
    if ((first teacher) = folder) and (pass = (second teacher)) [
        correct: true
    ]
]
if correct = false [alert "Incorrect password." quit]

url: rejoin [http://website.com/teacher/ folder]
ftp-url: rejoin [
    ftp://user:pass@website.com/public_html/teacher/ folder
]

if error? try [
    write %prepay.txt read rejoin [url "/prepay.txt"]
][
    error-message
]

; резервное копирование (до внесения изменений):
cur-time: to-string replace/all to-string now/time ":" "-"
; локально:
write to-file rejoin [
    folder "-prepay_" now/date "_" cur-time ".txt"
] read %prepay.txt
; в сети:
if error? try [
    write rejoin [
        ftp-url "/" now/date "_" cur-time
    ] read %prepay.txt
][
    error-message
]

editor %prepay.txt

; резервное копирование ещё раз (после внесения изменений):
cur-time: to-string replace/all to-string now/time ":" "-"
write to-file rejoin [
    folder "-prepay_" now/date "_" cur-time ".txt"
] read %prepay.txt
if error? try [
    write rejoin [
        ftp-url "/" now/date "_" cur-time
    ] read %prepay.txt
][
    alert "Internet connection not available while backing up."
]

; сохраняем на веб сайте:
if error? try [
    write rejoin [ftp-url "/prepay.txt"] read %prepay.txt
][
    alert {Internet connection not available while updating web
    site.  Your schedule has NOT been saved online.}
    quit
]    
browse url

Мне также нужно заменить строку "editor %preday.txt" новым кодом, который выполняет работу по расчёту ежедневных сборов и отслеживанию предоплат.

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

Следует отметить, что меня не беспокоит безопасность данных в этом приложении. Важно, чтобы учителя имели удобный доступ к этой информации из любого места. Также важно делать локальные резервные копии. Автоматическое резервное копирование файлов обеспечивает исторический контрольный журнал транзакций и изменений в записях, что является важной проблемой, поскольку эта программа управляет доходами. Сделать эти записи общедоступными - не проблема, поэтому я использую ftp и общедоступный веб-сайт для хранения и извлечения данных. Однако написание безопасных приложений является важным требованием в большинстве ситуаций, связанных с финансовыми транзакциями. Вы должны знать, что безопасность данных является первоочередной задачей, если вы собираетесь заниматься программированием, связанным с типичными бизнес-транзакциями, но эта тема выходит за рамки данного руководства. Это тематическое исследование было предоставлено в качестве дополнительного примера того, как можно организовать мысли о кодировании, чтобы вы могли перейти от концептуальных этапов к конечному продукту. Однако этот конкретный код не следует эмулировать для проектов, требующих безопасных транзакций с данными.

23. Программирование игр для улучшения алгоритмического мышления и графических навыков.

Небольшое изучение программирования игр может быть полезным для развития способности придумывать алгоритмические решения абстрактных вычислительных задач. Графические возможности, почерпнутые из игрового кода, также могут быть полезны при создании анимированных и всё более интерактивных аудио/визуальных бизнес-презентаций. Вы уже видели, как игра "Jeoparody" работала как действительно функциональное решение реальной проблемы бизнес-обучения, повышая эффективность подхода к обучению. Творческие подходы к другим игровым моделям ограничиваются только творчеством. В противном случае тематические исследования игр в этом разделе не являются существенными для понимания основных принципов бизнес-программирования, но они могут быть полезны для повышения общих навыков программирования, мотивации и получения удовольствия от искусства.

23.1 Случай: Подробнее о творческой алгоритмической мысли: клон тетриса

Одна из моих любимых игр - Тетрис. Мне особенно нравится клон "Rebtris", написанный на REBOL Фрэнком Зивертсеном. Недавно, играя в Ребтрис, я подумал, что написание моего собственного клона тетриса было бы полезным упражнением для этого урока. Придумывая, как сделать его немного отличным от всех других бесконечных вариаций тетриса, я подумал: "Почему бы не попробовать текстовую версию?". Создать его может быть проще, чем версию с графическим интерфейсом, и он будет работать на машинах, на которых версии REBOL с графическим интерфейсом недоступны. Написание текстовой версии тетриса также заставило бы меня организовать набор методов отображения, которые могли бы быть полезны при компоновке других текстовых приложений. Похоже на забавный проект с некоторыми полезными побочными эффектами.

ПРИМЕЧАНИЕ. Я никогда не думал о том, как создать клон Тетриса, и я пишу этот раздел по мере того, как проектирую, экспериментирую и пишу код для программы. Так что, читая это, вы будете следовать моему точному ходу мыслей при составлении программы, и я не буду пытаться искусственно очистить процесс. Цель этого руководства - продемонстрировать, как именно создавать все типы программ самостоятельно. Я надеюсь, что следование моим стопам в точности - несовершенства и все такое - должно быть полезным опытом. Давай посмотрим что происходит...

Вместо того, чтобы начинать всё с нуля, я вспомнил, как бегло просмотрел учебник о том, как создать диалект позиционирования текста под названием "TUI". Я снова нашёл его на http://www.rebolforces.com/articles/tui-dialect/ и искал какой-то повторно используемый код ...

Вот диалект TUI, созданный Инго Хоманом (я переименовал его функцию "cursor2" в "tui"):

tui: func [
    {Cursor positioning dialect (iho)}
    [catch]
    commands [block!]
    /local screen-size string arg cnt cmd c err
][
    screen-size: (
        c: open/binary/no-wait [scheme: 'console]
        prin "^(1B)[7n"
        arg: next next to-string copy c
        close c
        arg: parse/all arg ";R"
        forall arg [change arg to-integer first arg]
        arg: to-pair head arg
    )
    string: copy ""
    cmd: func [s][join "^(1B)[" s]
    if error? set/any 'err try [
        commands: compose bind commands 'screen-size ][
        throw err
    ]
    arg: parse commands [
        any [
            'direct set arg string! (append string arg) |
            'home  (append string cmd "H") |
            'kill  (append string cmd "K") |
            'clear (append string cmd "J") |
            'up    set arg integer! (append string cmd [
                arg "A"]) |
            'down  set arg integer! (append string cmd [
                arg "B"]) |
            'right set arg integer! (append string cmd [
                arg "C"]) |
            'left  set arg integer! (append string cmd [
                arg "D"]) |
            'at   set arg pair! (append string cmd [
                arg/x ";" arg/y "H" ]) |
            'del   set arg integer! (append string cmd [
                arg "P"]) |
            'space set arg integer! (append string cmd [
                arg "@"]) |
            'move  set arg pair! (append string cmd [
                arg/x ";" arg/y "H" ]) |
            set cnt integer! set arg string! (
                append string head insert/dup copy "" arg cnt
            ) |
            set arg string! (append string arg)
        ]
        end
    ]
    if not arg [throw make error! "Unable to parse block"]
    string
]

Я прочитал руководство и немного поиграл с кодом. Конечный продукт - это не только отличный учебник, но и полезный инструмент для форматирования вывода в консольных приложениях. В функцию tui передаётся блок параметров, включая текст для печати, ключевые слова направления для перемещения курсора по экрану ("home" (домой), "up" (вверх), "down" (вниз), "left" (влево), "right" (вправо), "at" (в) определённое место) и несколько других команд, чтобы получить размер экрана, очистить экран, удалить текст, повторить текст и вставить пробелы.

Я попробовал несколько команд, чтобы ознакомиться с синтаксисом:

prin tui [ clear ]
print tui [ 50  "-" ]
print tui [ right 10 down 7 50  "x" ]
prin tui [ clear right 10 down 10 50  "x" ]
print tui [ clear home "message1"]
print tui [ home space 20 "message2"]
print tui [ at 20x20 "message3" kill "message4"]
print tui [ at 20x20  del 10]
print tui [ move 10x10]
prin tui [ clear (screen-size/y * screen-size/x - 4)  "x" ]

Мне было больше удовольствия от игры с диалектом TUI, чем с тетрисом :) По сути, TUI оборачивает некоторые встроенные в REBOL коды управления печатью в красивом чистом формате, который устраняет все нечётные символы, используемые в собственных кодах. Он содержит всё необходимое для перемещения игровых элементов по экрану, поэтому я начну придумывать некоторые требования для сборки игры. Вот схема, охватывающая основные цели дизайна, которые я ставлю на данный момент:

  1. Нарисуем статичное игровое поле (неизменный графический фон, который отображается на экране всё время, пока идёт игра). Это будет представлять левую, правую и нижнюю вертикальные границы, которые не могут выходить за пределы игровых координат.
  2. В игре используется 7 форм блоков. Создать текстовые версии каждой графической формы в каждом из 4 возможных положений поворота. Придумать код для печати и удаления каждой из графических фигур. Диалект TUI позволяет мне печатать и удалять символы в любом месте экрана. Я буду использовать инструкции направления для печати необходимых символов, начиная с любой заданной координаты. Поместите все эти подпрограммы фигур в блок для упрощения именования и повторного использования.
  3. Написать непрерывный цикл, чтобы поместить одну фигуру на экран, заставить её падать с заданной скоростью и позволить пользователю вращать её и перемещать влево-вправо.
  4. Если фигура касается нижней части игрового поля, заставьте её зафиксироваться в сетке других фигур, которые уже упали. Если нижний ряд завершён, удалите его и заставьте все ряды над ним упасть вниз, чтобы занять его место. Если фигура касается потолка, завершите игру.

Первая часть схемы проста. Я буду использовать код "напечатать строку", созданный в примере "циклы и условия - простое приложение для хранения данных" ранее в этом руководстве. Вот простой небольшой фон, который печатается на экране:

a-line: copy [] loop 28 [append a-line " "] 
a-line: rejoin ["   |"  to-string a-line "|"]
loop 30 [print a-line] prin "   " loop 30 [prin "+"] print ""

Для второй части мне нужно создать код для печати 7 форм блоков. Они выглядят так:

;   1   ####    2   ###     3   ###    4    ###
;                    #          #             #
;
;   5   ##      6    ##     7   ##
;        ##         ##          ##

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

;                   #
;   1   ####    2   #
;                   #
;                   #
;
;   3   ###     4    #      5    #      6   #
;        #          ##          ###         ##
;                    #                      #
;
;   7   ###     8   ##      9     #     10  #
;       #            #          ###         #
;                    #                      ##
;
;   11  ###     12   #      13  #       14  ##
;         #          #          ###         #
;                   ##                      #
;
;   15  ##      16   #
;        ##         ##
;                   #
;
;   17   ##     18  #
;       ##          ##
;                    #
;
;   19  ##
;       ##

Чтобы напечатать любую часть, я могу начать с верхней левой координаты в форме и переместить соответствующее количество пробелов вправо, влево и/или вниз, чтобы напечатать другие символы в каждой части. Например, начиная с первого символа в форме 2, я бы переместился следующим образом: "#", вниз на 1 влево 1, "#", на 1 вниз влево 1, "#", на 1 вниз влево 1, "#". Форма 3 будет перемещаться следующим образом: "###", вниз 1 влево 2, "#". Вот блок под названием "shape", состоящий из отдельных блоков, которые можно передать в tui для печати всех вышеперечисленных фигур:

shape: [
    ["####"]
    ["#" down 1 left 1 "#" down 1 left 1 "#" down 1 left 1 "#"]
    ["###" down 1 left 2 "#"]
    [right 1 "#" down 1 left 2 "##" down 1 left 1 "#"]
    [right 1 "#" down 1 left 2 "###"]
    ["#" down 1 left 1 "##" down 1 left 2 "#"]
    ["###" down 1 left 3 "#"]
    ["##" down 1 left 1 "#" down 1 left 1 "#"]
    [right 2 "#" down 1 left 3 "###"]
    ["#" down 1 left 1 "#" down 1 left 1 "##"]
    ["###" down 1 left 1 "#"]
    [right 1 "#" down 1 left 1 "#" down 1 left 2 "##"]
    ["#" down 1 left 1 "###"]
    ["##" down 1 left 2 "#" down 1 left 1 "#"]
    ["##" down 1 left 1 "##"]
    [right 1 "#" down 1 left 2 "##" down 1 left 2 "#"]
    [right 1 "##" down 1 left 3 "##"]
    ["#" down 1 left 1 "##" down 1 left 1 "#"]
    ["##" down 1 left 2 "##"]
]

Теперь я могу использовать формат "print tui shape/number" для печати любой формы. Например:

prin tui shape/3

то же самое, что писать:

prin tui [right 1 "#" down 1 left 2 "##" down 1 left 1 "#"]

Я придумал следующий код, чтобы распечатать каждую форму, проверить наличие ошибок и привыкнуть к использованию указанного выше формата. Обратите внимание на использование функции "compose":

for i 1 19 1 [
    print tui [clear] 
    print rejoin ["shape " i ":"]
    do compose [print tui shape/(i)]
    ask ""
]

Чтобы стереть формы, я решил расширить блок, используя дубликаты каждой формы для печати пробелов вместо "#". Теперь все, что мне нужно сделать, это добавить 19 к порядковому номеру любой фигуры, и я могу распечатать фигуру, состоящую из пробелов, которая стирает исходную фигуру, состоящую из "#". Вот последний блок формы:

shape: [
    ["####"]
    ["#" down 1 left 1 "#" down 1 left 1 "#" down 1 left 1 "#"]
    ["###" down 1 left 2 "#"]
    [right 1 "#" down 1 left 2 "##" down 1 left 1 "#"]
    [right 1 "#" down 1 left 2 "###"]
    ["#" down 1 left 1 "##" down 1 left 2 "#"]
    ["###" down 1 left 3 "#"]
    ["##" down 1 left 1 "#" down 1 left 1 "#"]
    [right 2 "#" down 1 left 3 "###"]
    ["#" down 1 left 1 "#" down 1 left 1 "##"]
    ["###" down 1 left 1 "#"]
    [right 1 "#" down 1 left 1 "#" down 1 left 2 "##"]
    ["#" down 1 left 1 "###"]
    ["##" down 1 left 2 "#" down 1 left 1 "#"]
    ["##" down 1 left 1 "##"]
    [right 1 "#" down 1 left 2 "##" down 1 left 2 "#"]
    [right 1 "##" down 1 left 3 "##"]
    ["#" down 1 left 1 "##" down 1 left 1 "#"]
    ["##" down 1 left 2 "##"]

    ; Here are the same shapes, with spaces instead of "#"s:

    ["    "]
    [" " down 1 left 1 " " down 1 left 1 " " down 1 left 1 " "]
    ["   " down 1 left 2 " "]
    [right 1 " " down 1 left 2 "  " down 1 left 1 " "]
    [right 1 " " down 1 left 2 "   "]
    [" " down 1 left 1 "  " down 1 left 2 " "]
    ["   " down 1 left 3 " "]
    ["  " down 1 left 1 " " down 1 left 1 " "]
    [right 2 " " down 1 left 3 "   "]
    [" " down 1 left 1 " " down 1 left 1 "  "]
    ["   " down 1 left 1 " "]
    [right 1 " " down 1 left 1 " " down 1 left 2 "  "]
    [" " down 1 left 1 "   "]
    ["  " down 1 left 2 " " down 1 left 1 " "]
    ["  " down 1 left 1 "  "]
    [right 1 " " down 1 left 2 "  " down 1 left 2 " "]
    [right 1 "  " down 1 left 3 "  "]
    [" " down 1 left 1 "  " down 1 left 1 " "]
    ["  " down 1 left 2 "  "]
]

Я написал ещё один быстрый сценарий, чтобы проверить это. Обратите внимание на "i + 19", используемое для стирания существующей формы:

for i 1 19 1 [
    print tui [clear] 
    print rejoin ["shape " i ":"]
    do compose [prin tui [move 10x10] print tui shape/(i)]
    ask ""
    do compose [prin tui [move 10x10] print tui shape/(i + 19)]
    print rejoin ["shape " i " has been erased."]
    ask ""
]

Красота. Шаги 1 и 2 завершены. Теперь я могу работать над последней частью схемы (вещами, связанными с тем, как на самом деле играет игра, перемещением фигур и ответом на ввод пользователя). Сначала я заставлю фигуры падать на экран. Это будет сделано путём печати, стирания, а затем перерисовки части на один ряд ниже в непрерывном цикле. Вот схема организации этого мыслительного процесса:

  1. Начнём с очистки экрана.
  2. Фрагменты появляются в случайном порядке, поэтому придумаем случайное число, чтобы представить порядковый номер некоторой случайной формы.
  3. Используя цикл for для увеличения вертикального положения детали: для каждой строки выведите случайный номер детали в текущей горизонтальной позиции (изначально установленной на 15) и в вертикальной позиции, представленной текущей переменной "for".
  4. Подождём, затем сотрите кусок (используя номер фигуры + 19). Затем увеличьте номер строки и начнём снова.
  5. Когда кусок дойдёт до последней строки, распечатайте его там, не стирая.
  6. Обернём всё это бесконечным циклом, чтобы оно продолжалось бесконечно.

Давайте начнём:

prin tui [clear]

forever [
    random/seed now
    r: random 19      ; номер случайной фигуры
    xpos: 18          ; исходное горизонтальная позиция
    for i 1 25 1 [
        pos: to-pair rejoin [i "x" xpos]
        ; напечатаем фигуру, представленную буквой "r" в координате 
        ; "pos":
        do compose/deep [prin tui [at (pos)] print tui shape/(r)]
        ; Время ожидания может быть переменной, управляемой 
        ; пользователем, или его можно увеличивать по мере 
        ; увеличения уровня сложности:
        wait :00:00.30
        ; сотрём фигуру, затем продолжите цикл:
        do compose/deep [
            prin tui [at (pos)] print tui shape/(r + 19)]
    ]
    ; перепечатаем форму на месте её окончательной остановки
    do compose/deep [prin tui [move (pos)] print tui shape/(r)]
]

ПРИМЕЧАНИЕ. При написании приведённого выше кода меня поразило, что функция TUI фактически принимает все свои координаты в необычном порядке. Обычно в координатах формы XxY "X" - это горизонтальное положение, а "Y" - вертикальное положение. TUI использует формат YxX, где Y - вертикальное положение, измеренное в строках от верхнего края экрана. X - горизонтальное положение, измеряемое в столбцах с левой стороны экрана. Имейте в виду, что порядок координат X и Y противоположен нормальному ожиданию.

Теперь мне нужно придумать способ, позволяющий пользователю управлять горизонтальным положением формы. Вот какой-то псевдокод, который поможет мне подумать, как это сделать:

  1. Следим за нажатием клавиш пользователем.
  2. Если пользователь нажимает клавишу "l", добавьте 1 к текущему положению фигуры по горизонтали (сохраняется в переменной "xpos"). Если пользователь нажимает клавишу "k", вычтите 1 из xpos.

Во-первых, мне нужен способ получить ввод с помощью нажатия клавиш, не блокируя поток программы (т.е. мне нужно дождаться подтверждения нажатия клавиши, когда это произойдёт, но я не могу просто остановить нормальный поток программы, чтобы дождаться нажатия клавиш. игра продолжается, циклы "for" и "forever" не могут быть прерваны. Поэтому я поискал в Google по запросу "нажатие клавиши REBOL" ("REBOL key stroke") и нашёл следующий код на http://www.rebol.org/cgi-bin/cgiwrap/rebol/ml-display-thread.r?m=rmlSCRQ (в архиве списка рассылки REBOL):

c: open/binary/no-wait [scheme: 'console]
; установим следование на всё, что вы хотите, намеренно замедленное 
; на 2 секунды, чтобы вы могли "увидеть" эффект
wait-duration: :0:2
d: 0
forever [
    if not none? wait/all [c wait-duration] [
        print to-char to-integer copy c
    ]
    d: d + 1 ; давай займёмся другими делами
    print d
]

Этот небольшой фрагмент кода делает именно то, что мне нужно. Для моих нужд требуются следующие детали:

c: open/binary/no-wait [scheme: 'console]
forever [
    if not none? wait/all [c wait-duration] [
        print to-char to-integer copy c
    ]
]

Я скорректировал имена переменных, проверил нажатия клавиш "k" или "l" и использовал приведённый ниже код, чтобы проверить, работает ли он так, как я хотел:

keys: open/binary/no-wait [scheme: 'console]
forever [
    if not none? wait/all [keys :00:00.01] [
        switch to-string to-char to-integer copy keys [
            "k" [print "you pressed k"]
            "l" [print "you pressed l"]
        ]
    ]
; print "nothing pressed" ; make sure it's working
]

Затем я интегрировал приведённый выше код в созданный ранее цикл, чтобы опустить фигуру вниз по экрану. Обратите внимание, что я добавил условное "if", которое будет выполняться при нажатии клавиш "k" или "l". Проверяет, чтобы горизонтальные границы не выходили за пределы 5-30 позиций. Это сохраняет формы в пределах горизонтальных границ игрового поля. Также обратите внимание, что переменная old-xpos используется для хранения позиции фигуры, которую нужно стереть:

keys: open/binary/no-wait [scheme: 'console]
forever [
    random/seed now
    r: random 19
    xpos: 18
    for i 1 25 1 [
        pos: to-pair rejoin [i "x" xpos]
        do compose/deep [prin tui [at (pos)] print tui shape/(r)]
        old-xpos: xpos 
        if not none? wait/all [keys :00:00.30] [
            switch to-string to-char to-integer copy keys [
                "k" [if (xpos > 5) [xpos: xpos - 1]]
                "l" [if (xpos < 30) [xpos: xpos + 1]]
            ]
        ]
        pos: to-pair rejoin [i "x" old-xpos]
        do compose/deep [
            prin tui [at (pos)] print tui shape/(r + 19)]
    ]
    do compose/deep [prin tui [move (pos)] print tui shape/(r)]
]

Все идёт хорошо :) Теперь мне нужно уметь вращать фигуры. Вот какой-то псевдокод, чтобы упорядочить мои мысли:

  1. Следите за нажатием клавиши "O". Это будет ключевой код для запуска кода вращения формы.
  2. Создайте набор условных операторов для циклического просмотра списка повёрнутых фигур, связанных с текущей формой. Например, если текущая форма (переменная "r") - номер 12, то повёрнутые версии этой формы - номера 11-14. При каждом нажатии клавиши "O" заменяйте переменную r следующей фигурой в этом списке. Эта логика должна "закручиваться" (т.е. следующая фигура после 14 должна быть 11). Вместо того, чтобы использовать для этого блок-список фигур, я решил использовать структуру переключателя, чтобы индивидуально сопоставить каждую фигуру с той, на которую она должна вращаться (что-то вроде "если фигура r теперь №14, превратите фигуру r в №11") - сделать это явно для каждой формы).

У меня уже есть код для отслеживания нажатий клавиш, поэтому сначала я попробую последнюю часть вышеприведённого плана:

switch to-string r [
    "1" [r: 2]
    "2" [r: 1]
    "3" [r: 4]
    "4" [r: 5]
    "5" [r: 6]
    "6" [r: 3]
    "7" [r: 8]
    "8" [r: 9]
    "9" [r: 10]
    "10" [r: 7]
    "11" [r: 12]
    "12" [r: 13]
    "13" [r: 14]
    "14" [r: 11]
    "15" [r: 16]
    "16" [r: 15]
    "17" [r: 18]
    "18" [r: 17]
    "19" [r: 19]
]

Подождите секунду - это заставит фигуры вращаться по часовой стрелке (от # 11 до # 12, от # 14 до # 11 и т.д.) я предпочитаю, чтобы они вращались против часовой стрелки (от # 11 до # 14, # 14 до # 13 и т.д.). Вот исправленный код:

switch to-string r [
    "1" [r: 2]
    "2" [r: 1]
    "3" [r: 6]
    "4" [r: 3]
    "5" [r: 4]
    "6" [r: 5]
    "7" [r: 10]
    "8" [r: 7]
    "9" [r: 8]
    "10" [r: 9]
    "11" [r: 14]
    "12" [r: 11]
    "13" [r: 12]
    "14" [r: 13]
    "15" [r: 16]
    "16" [r: 15]
    "17" [r: 18]
    "18" [r: 17]
    "19" [r: 19]
]

Теперь добавьте букву "O" в список клавиш, за которыми нужно следить, и запустите приведённый выше код при её нажатии. Также создайте переменную "old-r", чтобы сохранить номер формы, которую нужно стереть. (Поскольку пользователь меняет формы после того, как текущая была напечатана, нам нужно отслеживать, какую из них удалить):

keys: open/binary/no-wait [scheme: 'console]
forever [
    random/seed now
    r: random 19
    xpos: 18
    for i 1 25 1 [
        pos: to-pair rejoin [i "x" xpos]
        do compose/deep [prin tui [at (pos)] print tui shape/(r)]
        old-xpos: xpos 
        old-r: r
        if not none? wait/all [keys :00:00.30] [
            keystroke: to-string to-char to-integer copy keys
            switch keystroke [
                "k" [if (xpos > 5) [xpos: xpos - 1]]
                "l" [if (xpos < 30) [xpos: xpos + 1]]
                "o" [switch to-string r [
                    "1" [r: 2]
                    "2" [r: 1]
                    "3" [r: 6]
                    "4" [r: 3]
                    "5" [r: 4]
                    "6" [r: 5]
                    "7" [r: 10]
                    "8" [r: 7]
                    "9" [r: 8]
                    "10" [r: 9]
                    "11" [r: 14]
                    "12" [r: 11]
                    "13" [r: 12]
                    "14" [r: 13]
                    "15" [r: 16]
                    "16" [r: 15]
                    "17" [r: 18]
                    "18" [r: 17]
                    "19" [r: 19]
                ]]            
            ]
        ]
        do compose/deep [
            prin tui [at (pos)] print tui shape/(old-r + 19)
        ]
    ]
    do compose/deep [prin tui [at (pos)] print tui shape/(r)]
]

Теперь фигуры двигаются правильно, но предстоит ещё много работы. Первая строка последнего раздела общего плана игры гласит: "Если фигура касается нижней части игрового поля, заставьте её зафиксироваться в сетке других фигур, которые уже упали". Сейчас все фишки просто падают в разные точки остановки на игровом поле (в зависимости от их высоты), и они не складываются друг на друга. Вот какой-то псевдокод, чтобы исправить это:

  1. Мне нужно знать высшую координату в каждом столбце игрового поля. Когда игра начинается, наивысшая координата в каждом столбце игрового поля - это строка 30 (плоская нижняя линия, составляющая игровое поле). Я сохраню каждую из этих координат в блоке под названием "floor" (пол).
  2. Мне также нужно знать самую низкую координату в каждом столбце падающей в данный момент формы. Я сделаю блок под названием "edge" (край), чтобы удерживать эти координаты (относящиеся к нижним краям формы). Эти координаты будут определять положение каждой из самых низких точек в текущей падающей форме по отношению к её верхней левой точке (координата "pos").
  3. Каждый раз, когда фигура опускается на одну позицию вниз по экрану, добавьте каждую из координат края к координате pos. Если какая-либо из этих координат на одну позицию выше, чем координата пола в том же столбце, прекратите перемещать эту фигуру (выйдите из цикла "for", из-за которого фигура упадёт). Используйте цикл foreach для циклического перебора текущих координат в соответствующих столбцах каждого блока, выполняя сравнительную проверку координат пола и края в каждом столбце.
  4. Когда фигура завершает своё раскрытие экрана, вычислите новую наивысшую позицию в столбцах, которые она занимает (координаты верхнего символа в каждом столбце), и внесите эти изменения в блок, который содержит информацию о высшей точке. Для этого мне нужно создать "top" (верхний) блок для хранения относительных позиций высших координат в форме и добавить их к высоте текущих координат в соответствующих столбцах.

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

floor:     [30x1 30x2 30x3 30x4 30x5 30x6 30x7 30x8 30x9 30x10 30x11
    30x12 30x13 30x14 30x15 30x16 30x17 30x18 30x19 30x20 30x21
    30x22 30x23 30x24 30x25 30x26 30x27 30x28 30x29 30x30 30x31
    30x32 30x33 30x34 30x35]

Затем я определю набор нижних координат для каждой формы и сохраню их во вложенной блочной структуре, подобной более раннему блоку "shape". "0x0" относится к той же координате, что и "pos" (0 позиций вправо и 0 позиций вниз от "pos"). "0x10" - на одну позицию вправо, а "1x0" - на одну позицию вниз. Я снова смотрю на визуальные представления фигур, чтобы составить список:

;                   #
;   1   ####    2   #
;                   #
;                   #
;
;   3   ###     4    #      5    #      6   #
;        #          ##          ###         ##
;                    #                      #
;
;   7   ###     8   ##      9     #     10  #
;       #            #          ###         #
;                    #                      ##
;
;   11  ###     12   #      13  #       14  ##
;         #          #          ###         #
;                   ##                      #
;
;   15  ##      16   #
;        ##         ##
;                   #
;
;   17   ##     18  #
;       ##          ##
;                    #
;
;   19  ##
;       ##

Вот полный набор определений нижней точки для каждой формы:

edge: [ [0x0 0x1 0x2 0x3] [3x0] [0x0 1x1 0x2] [1x0 2x1] 
    [1x0 1x1 1x2] [2x0 1x1] [1x0 0x1 0x2] [0x0 2x1] [1x0 1x1 1x2]
    [2x0 2x1] [0x0 0x1 1x2] [2x0 2x1] [1x0 1x1 1x2] [2x0 0x1]
    [0x0 1x1 1x2] [2x0 1x1] [1x0 1x1 0x2] [1x0 2x1] [1x0 1x1] ]

Так, относительные координаты нижних точек в фигуре 3, например, обозначаются как edge/3. Вот пример кода, демонстрирующий, как теперь я могу ссылаться на нижние точки любой формы с помощью цикла foreach. Код "pos + position" относится к нижнему краю в каждом столбце:

pos: 5x5
r: 6
foreach position compose edge/(r) [print pos + position]

Чтобы проверить, касается ли какой-либо из этих краёв пола, используйте цикл foreach для циклического просмотра текущих координат в соответствующих столбцах каждого блока, выполняя сравнительную проверку координат пола и края в каждом столбце. Вот пример кода для конкретизации и проверки этой идеи:

pos: 30x10
for r 1 19 1 [
    print tui [clear]
    prin "Piece: " print r
    foreach po compose edge/(r) [
        print pos + po
        foreach coord floor [
            floor-y: to-integer first coord
            floor-x: to-integer second coord
            edge-y: to-integer first pos + to-integer first po
            edge-x: to-integer second pos + to-integer second po
            print rejoin [
                "edge: " edge-y "x" edge-x " "
                "floor: "floor-y "x" floor-x 
            ]
            if (edge-y >= floor-y) and (floor-x = edge-x) [
                print rejoin [ 
                    "You're touching or beyond the floor at: "
                    pos + po
                ]
            ]
        ]
    ]
    ask ""
]

Теперь давайте интегрируем эту технику в существующий код. Мы будем использовать новую переменную stop, чтобы выйти из цикла, в котором фигура опускается, когда текущая фигура касается пола:

keys: open/binary/no-wait [scheme: 'console]
forever [
    random/seed now
    r: random 19
    xpos: 18
    for i 1 32 1 [
        pos: to-pair rejoin [i "x" xpos]
        do compose/deep [prin tui [at (pos)] print tui shape/(r)]
        old-r: r
        old-xpos: xpos 
        if not none? wait/all [keys :00:00.30] [
            keystroke: to-string to-char to-integer copy keys
            switch keystroke [
                "k" [if (xpos > 5) [xpos: xpos - 1]]
                "l" [if (xpos < 30) [xpos: xpos + 1]]
                "o" [switch to-string r [
                    "1" [r: 2]
                    "2" [r: 1]
                    "3" [r: 6]
                    "4" [r: 3]
                    "5" [r: 4]
                    "6" [r: 5]
                    "7" [r: 10]
                    "8" [r: 7]
                    "9" [r: 8]
                    "10" [r: 9]
                    "11" [r: 14]
                    "12" [r: 11]
                    "13" [r: 12]
                    "14" [r: 13]
                    "15" [r: 16]
                    "16" [r: 15]
                    "17" [r: 18]
                    "18" [r: 17]
                    "19" [r: 19]
                ]]            
            ]
        ]
        do compose/deep [
            prin tui [at (pos)] print tui shape/(old-r + 19)
        ]
        stop: false
        foreach po compose edge/(r) [
            foreach coord floor [
                floor-y: to-integer first coord
                floor-x: to-integer second coord
                edge-y:  i + to-integer first po
                edge-x:  xpos + to-integer second po
                if (edge-y >= floor-y) and (floor-x = edge-x) [
                    stop: true
                    break
                ]
            ]
        ]
        if stop = true [break]
    ]
    do compose/deep [prin tui [at (pos)] print tui shape/(old-r)]
]

Это работает, но есть ошибка. Если кусок вращается (используя клавишу "O"), новый цикл foreach не сможет остановить падение части. Это потому, что цикл foreach только перебирает координаты блока "edge/r". Если пользователь переворачивает фигуру, значение "r" изменяется перед запуском этого кода. Самый простой способ решить эту проблему - просто повторить цикл foreach с использованием блока "edge/old-r". Это неэффективный быстрый приём, но я пишу его поздно ночью - и есть некоторая ценность в указании на плохую практику программирования - поэтому я решил использовать это решение. Я обещаю себе позже придумать более элегантное решение ... (Примечание для себя: как только решение для кодирования реализовано, вносить изменения становится труднее, и плохой код обычно остаётся постоянным ... Мне нужно быть осторожно при использовании быстрых хаков). Вот текущий код:

keys: open/binary/no-wait [scheme: 'console]
forever [
    random/seed now
    r: random 19
    xpos: 18
    for i 1 32 1 [
        pos: to-pair rejoin [i "x" xpos]
        do compose/deep [prin tui [at (pos)] print tui shape/(r)]
        old-r: r
        old-xpos: xpos 
        if not none? wait/all [keys :00:00.30] [
            keystroke: to-string to-char to-integer copy keys
            switch keystroke [
                "k" [if (xpos > 5) [xpos: xpos - 1]]
                "l" [if (xpos < 30) [xpos: xpos + 1]]
                "o" [switch to-string r [
                    "1" [r: 2]
                    "2" [r: 1]
                    "3" [r: 6]
                    "4" [r: 3]
                    "5" [r: 4]
                    "6" [r: 5]
                    "7" [r: 10]
                    "8" [r: 7]
                    "9" [r: 8]
                    "10" [r: 9]
                    "11" [r: 14]
                    "12" [r: 11]
                    "13" [r: 12]
                    "14" [r: 13]
                    "15" [r: 16]
                    "16" [r: 15]
                    "17" [r: 18]
                    "18" [r: 17]
                    "19" [r: 19]
                ]]            
            ]
        ]
        do compose/deep [
            prin tui [at (pos)] print tui shape/(old-r + 19)
        ]
        stop: false
        foreach po compose edge/(r) [
            foreach coord floor [
                floor-y: to-integer first coord
                floor-x: to-integer second coord
                edge-y:  i + to-integer first po
                edge-x:  xpos + to-integer second po
                if (edge-y = floor-y) and (floor-x = edge-x) [
                    stop: true
                    break
                ]
            ]
        ]
        foreach po compose edge/(old-r) [
            foreach coord floor [
                floor-y: to-integer first coord
                floor-x: to-integer second coord
                edge-y:  i + to-integer first po
                edge-x:  old-xpos + to-integer second po
                if (edge-y = floor-y) and (floor-x = edge-x) [
                    stop: true
                    break
                ]
            ]
        ]
        if stop = true [break]
    ]
    do compose/deep [prin tui [at (pos)] print tui shape/(old-r)]
]

Затем я решаю проверить существующую программу на наличие других ошибок. Я храню отдельные текстовые файлы, содержащие все изменения кода, которые я делаю по мере продвижения. Каждый раз, когда я создаю, тестирую и изменяю кусок кода, я сохраняю новую пробную версию с новым именем файла и номером версии. Я сохраняю каждую версию, чтобы не стирать старый код навсегда при каждом изменении - это может быть потенциально полезно. Моя текущая рабочая версия - №19.

Во время этого сеанса отладки я заметил, что фигура 1 всё ещё пробивает правую сторону стены. Я мог бы изменить это, настроив условное выражение "(xpos <30)", которое появляется при нажатии клавиши "L". Но это решение не позволит другим формам плотно прилегать к стене. Фактически, эта дополнительная проблема возникает сейчас с фигурами шириной всего 2 символа - я не замечал до сих пор. Чтобы справиться с этими проблемами, я создаю блок значений под названием "ширина", в котором перечислены ширины всех 19 фигур, которые можно использовать в существующем условном выражении:

width: [4 1 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 2]

Теперь я могу проверить, находится ли фигура на правой границе, используя исправленный код ниже:

[if (xpos < (33 - compose width/(r))) [
    xpos: xpos + 1]
]

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

Игра действительно начинает складываться! Теперь нам нужно сделать так, чтобы фигуры накладывались друг на друга. Ранее я написал следующие общие мысли: "когда фигура завершает своё выпадение на экране, вычислить новую наивысшую позицию в столбцах, которые она занимает (координаты верхнего символа в каждом столбце), и внести эти изменения в блок, который содержит информация о высших точках. Для этого мне нужно создать блок "top", чтобы удерживать относительные положения высших координат в фигуре, и добавить их к высоте текущих координат в соответствующих столбцах ". Похоже, мне нужно пройти через несколько столбцов, чтобы внести изменения в пол.

Чтобы создать "верхний" блок, я еще раз смотрю на визуальные представления каждой формы и придумываю список координат, представляющий верхние точки формы относительно верхней левой координаты. Он похож на блок "edge" (край):

top: [ [0x0 0x1 0x2 0x3] [0x0] [0x0 0x1 0x2] [1x0 0x1] 
    [1x0 0x1 1x2] [0x0 1x1] [0x0 0x1 0x2] [0x0 0x1] [1x0 1x1 0x2]
    [0x0 2x1] [0x0 0x1 0x2] [2x0 0x1] [0x0 1x1 1x2] [0x0 0x1]
    [0x0 0x1 1x2] [1x0 0x1] [1x0 0x1 0x2] [0x0 1x1] [0x0 0x1] ]

Форма завершает своё выпадение на экране во время предыдущих циклов foreach, которые мы создали, поэтому для расчёта новых высших позиций в столбцах, занимаемых фигурой, мне сначала нужно определить, какая фигура была последней на экране ("r" или "old-r"). Быстрый приём, который я сделал ранее, теперь возвращается, чтобы немного укусить меня - теперь мне нужно сделать дубликаты любых изменений, которые происходят в обоих циклах foreach:

stop-shape-num: r  
; (или stop-shape-num: old-r, в зависимости от цикла foreach)
stop: true
break

Теперь, чтобы внести изменения в блок "floor" (этаж), я перебираю столбцы, занятые частью, устанавливая каждый из верхних символов в форме как верхние координаты в соответствующих столбцах этажа. Функция "poke" позволяет мне заменить исходные координаты в блоке этажа новыми координатами. Эти изменения вносятся непосредственно перед выходом из цикла, в котором фигура опускается:

if stop = true [
    ; получить крайний левый столбец (left-most), который занимает 
    ; последняя фигура:
    left-col: second pos 
    ; получить количество столбцов, которые занимает фигура:
    width-of-shape: length? compose top/(stop-shape-num)
    ; получить самый правый столбец, который занимает фигура:
    right-col: left-col + width-of-shape - 1
    ; Прокрутите каждый столбец, занимаемый фигурой, заменяя каждую 
    ; координату в текущем столбце этажа новой высокой координатой:
    counter: 1
    for current-column left-col right-col 1 [
        add-coord: compose top/(stop-shape-num)/(counter)
        new-floor-coord: (pos + add-coord + -1x0)
        poke floor current-column new-floor-coord
        counter: counter + 1
    ]
    break
]

Новый код стекирования работает, но есть недостаток дизайна. Если я перемещаю фигуру в незанятое пространство прямо под любой высокой точкой пола, не касаясь сначала высокой точки в этой колонне, фигура не останавливается. Кроме того, если это произойдёт, он изменит новую высшую точку на нижнюю часть столбца, который занимает текущая форма. Здесь я понимаю, что мне нужно отметить не только высокие точки на полу, но и все дополнительные координаты на экране, содержащие персонажа. Это так же легко сделать. Вместо изменения текущих координат в блоке этажа (с помощью функции "poke"):

poke floor current-column new-floor-coord

просто добавьте новые координаты в список (используя "append" (добавить)). Это будет отслеживать все точки, в которых символ печатается на экране:

append floor new-floor-coord

Это решает проблему, описанную выше, но я также понял, что если я перемещаю фигуру боком в открытое положение на полу, символы иногда все равно перекрываются ненадлежащим образом. Это потому, что блоки "top" (верх) и "edge" (край) отмечают только самые высокие и самые низкие точки в каждой форме. Теперь мне кажется, что я мог просто объединить эти два блока в один, отметив все координаты, занятые фигурой. Вот новый блок - я называю его "oc", сокращённо от "occupied" (занято):

oc:  [ 
    [0x0 0x1 0x2 0x3] [0x0 1x0 2x0 3x0] [0x0 0x1 0x2 1x1]
    [0x1 1x0 1x1 2x1] [0x1 1x0 1x1 1x2] [0x0 1x0 1x1 2x0]
    [0x0 0x1 0x2 1x0] [0x0 0x1 1x1 2x1] [0x2 1x0 1x1 1x2]
    [0x0 1x0 2x0 2x1] [0x0 0x1 0x2 1x2] [0x1 1x1 2x0 2x1]
    [0x0 1x0 1x1 1x2] [0x0 0x1 1x0 2x0] [0x0 0x1 1x1 1x2]
    [0x1 1x0 1x1 2x0] [0x1 0x2 1x0 1x1] [0x0 1x0 1x1 2x1]
    [0x0 0x1 1x0 1x1] 
]

Я удаляю блоки "top" (верх) и "edge" (край) и заменяю все ссылки кода на них на "oc".

Теперь мне нужно исправить ещё одну ошибку. Иногда, когда я нажимаю клавишу, возникает следующая ошибка:

** Script Error: Invalid argument: [45
** Where: to-integer            |
** Near: forall arg [change arg to-integer first arg]
arg: to-pair

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

if not none? wait/all [keys :00:00.30] [...]

Я оборачиваю всё это проверкой ошибок:

if not error? try [if not none? wait/all [keys :00:00.30] [...]]

И, хммм ... это не работает. Поэтому вместо того, чтобы гадать, я методично проверяю каждый из других основных разделов программы. Каждый раздел оборачивается процедурой "error? try", и я также добавляю условную структуру "if" (если) для вывода пронумерованного сообщения об ошибке всякий раз, когда возникает ошибка. Я обнаружил, что ошибка впервые возникает здесь:

do compose/deep [prin tui [at (pos)] print tui shape/(r)]

Этот раздел, заключённый в тест на ошибку, выглядит так:

if error? try [
    do compose/deep [
        prin tui [at (pos)] print tui shape/(r)
    ]
] [print "er1"]

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

if error? try [
    do compose/deep [
        prin tui [at (pos)] print tui shape/(r)
    ]
] [print rejoin [pos " " r]]

Кажется, всё в порядке. Каждый раз, когда возникает ошибка, переменные показывают правильные координаты и номер формы. Итак, пока я просто оставлю проверку ошибок на месте, удалив распечатку. Это будет держать игру в движении всякий раз, когда возникает призрачная ошибка. Мне нужно отправить сообщение в список рассылки REBOL, чтобы узнать, знает ли кто-нибудь, почему возникает ошибка. На данный момент следующий обработчик ошибок устраняет проблему:

if error? try [
    do compose/deep [
        prin tui [at (pos)] print tui shape/(r)
    ]
] []

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

if error? try [
    do compose/deep [
        prin tui [at (pos)] print tui shape/(old-r + 19)
    ]
] []

if error? try [
    do compose/deep [
        prin tui [at (pos)] print tui shape/(old-r)
    ]
] []

Когда все известные ошибки устранены, я могу перейти к реализации последних частей игрового дизайна. Нам нужно проверить, достигнут ли верхний ряд игровой площадки. Если какая-либо фигура перестаёт двигаться в этом ряду потолка, завершите игру. Это нужно делать каждый раз, когда кусок достигает места своего последнего упокоения, поэтому я помещаю его сразу после основного цикла for в схеме программы (чтобы он оценивался сразу после выполнения кода остановки):

if (first pos) < 2 [
    prin tui [at 35x0]
    print "Game Over"
    halt
]

Наконец, чтобы стирать нижнюю линию фигур каждый раз, когда строка заполняется по горизонтали, нам придётся полностью перерисовывать игровое поле. Блок "floor" содержит всю информацию, необходимую для восстановления текущего состояния игрового поля (все позиции, на которых в данный момент печатается персонаж). Вот схема и некоторый псевдокод, чтобы продумать, что нужно сделать:

  1. Каждый раз, когда фигура перестаёт двигаться, проверяйте, не заполнена ли какая-либо строка пола (т.е. в каждом столбце печатается по одному символу). Я могу использовать цикл for и функцию поиска, чтобы выполнить эту проверку на блоке пола. (Я начну с проверки нижнего ряда).
  2. Если какая-либо строка заполнена (пока только нижняя строка), удалите эту строку символов из блока пола. Используйте цикл remove-each, чтобы удалить из блока пола все координаты, которые имеют позиции y в соответствующей строке.
  3. Переместите все остальные символы над соответствующей строкой на одну строку вниз. Добавьте одну позицию y ко всем другим координатам в блоке пола, которые находятся над соответствующей строкой. Используйте цикл foreach, чтобы пройти по каждой координате в блоке и добавить 1x0. Чтобы заменить старый блок этажа новым, сначала создайте временный блок, состоящий из координат нового блока этажа, а затем скопируйте его обратно в блок этажа после его завершения.
  4. Сотрите текущий экран, распечатайте статический фон, а затем повторно напечатайте новое игровое поле, используя обновлённый блок координат этажа. Мы можем легко сделать это, используя цикл foreach и TUI для печати символов в каждой координате в списке.
; #1:

line-is-full: true
for colmn 5 32 1 [
    each-coord: to-pair rejoin [29 "x" colmn]
    if not find floor each-coord [
        line-is-full: false
        break
    ]
]

; #2:

if line-is-full = true [
    remove-each cor floor [(first cor) = 29]

; #3:

    new-floor: copy []
    foreach cords floor [
        append new-floor (cords + 1x0)
    ]
    floor: copy new-floor

; #4:

    prin tui [clear]
    a-line: copy [] loop 28 [append a-line " "] 
    a-line: rejoin ["   |"  to-string a-line "|"]
    loop 30 [print a-line] 
    prin "   " loop 30 [prin "+"] print ""
    foreach was-here floor [
        if not ((first was-here) = 30) [
            prin tui compose [at (was-here)]
            prin "#"
        ]
    ] 
]

На этом этапе я понимаю, что допустил некоторые логические ошибки в структуре блока пола и процедуры остановки. В его нынешнем виде, когда экран обновляется, нижнюю строку блока (строка 30) необходимо стереть, чтобы все символы в строке 29 могли упасть на одну позицию. Но если стереть 30-й ряд, то нижняя часть этажа исчезнет. Оказывается, строку 31 следует рассматривать как нижнюю строку, и все символы должны останавливаться на позиции 1x0 выше, чем любой персонаж на полу.

Я вношу необходимые изменения в координаты в блоке пола (меняю все позиции y с 30 на 31). Я также изменяю переменную "new-floor -ordin" в подпрограмме остановки и корректирую приведённый выше код так, чтобы символы ниже строки 30 не печатались. Кроме того, весь приведённый выше раздел заключён в цикл "for", чтобы проверить, заполнена ли каждая строка 1-30. В приведённом выше коде я проверял только, заполнена ли нижняя строка - число 29 относится к строке. Я заменяю это число на переменную "row", созданную в цикле for. Таким образом, последние требования моего первоначального плана игры выполнены, и начальная версия "Textris" находится в рабочем состоянии. Вот код:

REBOL [Title: "Textris"]

tui: func [
    {Cursor positioning dialect (iho)}
    [catch]
    commands [block!]
    /local screen-size string arg cnt cmd c err
][
    screen-size: (
        c: open/binary/no-wait [scheme: 'console]
        prin "^(1B)[7n"
        arg: next next to-string copy c
        close c
        arg: parse/all arg ";R"
        forall arg [change arg to-integer first arg]
        arg: to-pair head arg
    )
    string: copy ""
    cmd: func [s][join "^(1B)[" s]
    if error? set/any 'err try [
        commands: compose bind commands 'screen-size ][
        throw err
    ]
    arg: parse commands [
        any [
            'direct set arg string! (append string arg) |
            'home  (append string cmd "H") |
            'kill  (append string cmd "K") |
            'clear (append string cmd "J") |
            'up    set arg integer! (append string cmd [
                arg "A"]) |
            'down  set arg integer! (append string cmd [
                arg "B"]) |
            'right set arg integer! (append string cmd [
                arg "C"]) |
            'left  set arg integer! (append string cmd [
                arg "D"]) |
            'at   set arg pair! (append string cmd [
                arg/x ";" arg/y "H" ]) |
            'del   set arg integer! (append string cmd [
                arg "P"]) |
            'space set arg integer! (append string cmd [
                arg "@"]) |
            'move  set arg pair! (append string cmd [
                arg/x ";" arg/y "H" ]) |
            set cnt integer! set arg string! (
                append string head insert/dup copy "" arg cnt
            ) |
            set arg string! (append string arg)
        ]
        end
    ]
    if not arg [throw make error! "Unable to parse block"]
    string
]

shape: [
    ["####"]
    ["#" down 1 left 1 "#" down 1 left 1 "#" down 1 left 1 "#"]
    ["###" down 1 left 2 "#"]
    [right 1 "#" down 1 left 2 "##" down 1 left 1 "#"]
    [right 1 "#" down 1 left 2 "###"]
    ["#" down 1 left 1 "##" down 1 left 2 "#"]
    ["###" down 1 left 3 "#"]
    ["##" down 1 left 1 "#" down 1 left 1 "#"]
    [right 2 "#" down 1 left 3 "###"]
    ["#" down 1 left 1 "#" down 1 left 1 "##"]
    ["###" down 1 left 1 "#"]
    [right 1 "#" down 1 left 1 "#" down 1 left 2 "##"]
    ["#" down 1 left 1 "###"]
    ["##" down 1 left 2 "#" down 1 left 1 "#"]
    ["##" down 1 left 1 "##"]
    [right 1 "#" down 1 left 2 "##" down 1 left 2 "#"]
    [right 1 "##" down 1 left 3 "##"]
    ["#" down 1 left 1 "##" down 1 left 1 "#"]
    ["##" down 1 left 2 "##"]
    ;
    ["    "]
    [" " down 1 left 1 " " down 1 left 1 " " down 1 left 1 " "]
    ["   " down 1 left 2 " "]
    [right 1 " " down 1 left 2 "  " down 1 left 1 " "]
    [right 1 " " down 1 left 2 "   "]
    [" " down 1 left 1 "  " down 1 left 2 " "]
    ["   " down 1 left 3 " "]
    ["  " down 1 left 1 " " down 1 left 1 " "]
    [right 2 " " down 1 left 3 "   "]
    [" " down 1 left 1 " " down 1 left 1 "  "]
    ["   " down 1 left 1 " "]
    [right 1 " " down 1 left 1 " " down 1 left 2 "  "]
    [" " down 1 left 1 "   "]
    ["  " down 1 left 2 " " down 1 left 1 " "]
    ["  " down 1 left 1 "  "]
    [right 1 " " down 1 left 2 "  " down 1 left 2 " "]
    [right 1 "  " down 1 left 3 "  "]
    [" " down 1 left 1 "  " down 1 left 1 " "]
    ["  " down 1 left 2 "  "]
]

floor:  [
    31x5 31x6 31x7 31x8 31x9 31x10 31x11 31x12 31x13 31x14 31x15
    31x16 31x17 31x18 31x19 31x20 31x21 31x22 31x23 31x24 31x25
    31x26 31x27 31x28 31x29 31x30 31x31 31x32
]

oc:  [ 
    [0x0 0x1 0x2 0x3] [0x0 1x0 2x0 3x0] [0x0 0x1 0x2 1x1]
    [0x1 1x0 1x1 2x1] [0x1 1x0 1x1 1x2] [0x0 1x0 1x1 2x0]
    [0x0 0x1 0x2 1x0] [0x0 0x1 1x1 2x1] [0x2 1x0 1x1 1x2]
    [0x0 1x0 2x0 2x1] [0x0 0x1 0x2 1x2] [0x1 1x1 2x0 2x1]
    [0x0 1x0 1x1 1x2] [0x0 0x1 1x0 2x0] [0x0 0x1 1x1 1x2]
    [0x1 1x0 1x1 2x0] [0x1 0x2 1x0 1x1] [0x0 1x0 1x1 2x1]
    [0x0 0x1 1x0 1x1] 
]

width: [4 1 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 2]

a-line: copy [] loop 28 [append a-line " "] 
a-line: rejoin ["   |"  to-string a-line "|"]
loop 30 [print a-line] prin "   " loop 30 [prin "+"] print ""

keys: open/binary/no-wait [scheme: 'console]
forever [
    random/seed now
    r: random 19
    xpos: 18
    for i 1 30 1 [
        pos: to-pair rejoin [i "x" xpos]
        if error? try [
            do compose/deep [
                prin tui [at (pos)] print tui shape/(r)
            ]
        ] []
        old-r: r
        old-xpos: xpos 
        if not none? wait/all [keys :00:00.30] [
            keystroke: to-string to-char to-integer copy keys
            switch/default keystroke [
                "k" [if (xpos > 5) [
                        xpos: xpos - 1
                    ]]
                "l" [if (xpos < (33 - compose width/(r))) [
                        xpos: xpos + 1
                    ]]
                "o" [if (xpos < (33 - compose width/(r)))  [
                        switch to-string r [
                            "1" [r: 2]
                            "2" [r: 1]
                            "3" [r: 6]
                            "4" [r: 3]
                            "5" [r: 4]
                            "6" [r: 5]
                            "7" [r: 10]
                            "8" [r: 7]
                            "9" [r: 8]
                            "10" [r: 9]
                            "11" [r: 14]
                            "12" [r: 11]
                            "13" [r: 12]
                            "14" [r: 13]
                            "15" [r: 16]
                            "16" [r: 15]
                            "17" [r: 18]
                            "18" [r: 17]
                            "19" [r: 19]
                        ]
                    ]
                ]           
            ] []
        ]
        if error? try [
            do compose/deep [
                prin tui [at (pos)] print tui shape/(old-r + 19)
            ]
        ] []
        stop: false
        foreach po compose oc/(r) [
            foreach coord floor [
                floor-y: to-integer first coord
                floor-x: to-integer second coord
                oc-y:  i + to-integer first po
                oc-x:  xpos + to-integer second po
                if (oc-y = (floor-y - 1)) and (floor-x = oc-x) [
                    stop-shape-num: r
                    stop: true
                    break
                ]
            ]
        ]
        foreach po compose oc/(old-r) [
            foreach coord floor [
                floor-y: to-integer first coord
                floor-x: to-integer second coord
                oc-y:  i + to-integer first po
                oc-x:  old-xpos + to-integer second po
                if (oc-y = (floor-y - 1)) and (floor-x = oc-x) [
                    stop-shape-num: old-r
                    stop: true
                    break
                ]
            ]
        ]
        if stop = true [
            left-col: second pos 
            width-of-shape: length? compose oc/(stop-shape-num)
            right-col: left-col + width-of-shape - 1
            counter: 1
            for current-column left-col right-col 1 [
                add-coord: compose oc/(stop-shape-num)/(counter)
                new-floor-coord: (pos + add-coord)
                append floor new-floor-coord
                counter: counter + 1
            ]
            break
        ]
    ]
    if (first pos) < 2 [
        prin tui [at 33x0]
        print "GAME OVER!!!"
        halt
    ]
    if error? try [
        do compose/deep [
            prin tui [at (pos)] print tui shape/(old-r)
        ]
    ] []
    for row 1 30 1 [
        line-is-full: true
        for colmn 5 32 1 [
            each-coord: to-pair rejoin [row "x" colmn]
            if not find floor each-coord [
                line-is-full: false
                break
            ]
        ]
        if line-is-full = true [
            remove-each cor floor [(first cor) = row]
            new-floor: copy [
                31x5 31x6 31x7 31x8 31x9 31x10 31x11 31x12 31x13
                31x14 31x15 31x16 31x17 31x18 31x19 31x20 31x21
                31x22 31x23 31x24 31x25 31x26 31x27 31x28 31x29
                31x30 31x31 31x32
            ]
            foreach cords floor [
                either ((first cords) < row) [
                    append new-floor (cords + 1x0)
                ][
                    append new-floor cords
                ]
            ]
            floor: copy unique new-floor
            prin tui [clear]
            a-line: copy [] loop 28 [append a-line " "] 
            a-line: rejoin ["   |"  to-string a-line "|"]
            loop 30 [print a-line] 
            prin "   " loop 30 [prin "+"] print ""
            foreach was-here floor [
                if not ((first was-here) = 31) [
                    prin tui compose [at (was-here)]
                    prin "#"
                ]
            ]
        ]
    ]
]

Теперь, когда программа работает в соответствии с моими исходными спецификациями, я хочу, чтобы она выглядела немного изящнее. Во-первых, игровая площадка выглядит слишком широкой и высокой. Я проверяю Rebtris, он всего 10 столбцов в ширину и 20 строк в высоту. Мне нравится этот внешний вид, поэтому я настраиваю блок пола, код, который рисует статический фон, и все вычисления, связанные с правильными границами игрового поля и количеством строк, чтобы отразить это изменение.

Я также хочу распечатать заголовок "Textris", некоторые инструкции для клавиатуры и заголовок партитуры. Tui позволяет мне распечатать этот текст справа от игрового поля, где я хочу:

print tui [
    at 4x21 "TEXTRIS" at 5x21 "-------" 
    at 7x20 "'K' = left" at 8x20 "'L' = right"
    at 9x20 "'O' = spin" at 11x21 "Score:" 
]

Отслеживать счёт очень просто. Когда программа запускается, создаётся переменная "score", которой устанавливается значение 0 ("score: 0"). Каждый раз, когда фигура перестаёт падать, к счету добавляется 10 очков. Это число печатается под заголовком счета (обратите внимание, что номер счета должен быть сначала преобразован в строку, чтобы его можно было распечатать с помощью tui):

score: score + 10
print tui compose [at 13x21 (to-string score)]

Каждый раз, когда строка заполняется, к счету добавляется 1000 баллов. Когда экран перерисовывается, чтобы отразить только что стёртую строку, код tui, который печатает фон, также распечатывает обновлённую оценку:

print tui compose [
    at 4x21 "TEXTRIS" at 5x21 "-------" 
    at 7x20 "'K' = left" at 8x20 "'L' = right"
    at 9x20 "'O' = spin" at 11x21 "Score:" 
    at 13x21 (to-string score)
]

Далее я хочу добавить клавишу паузы. Это впишется в структуру переключателя, отслеживающего нажатия клавиш. Каждый раз, когда нажимается клавиша "P", вывести сообщение о том, что игра была приостановлена. Используйте действие "ask" (спросить), чтобы дождаться ввода, а затем распечатайте две пустые строки, чтобы стереть сообщение о паузе и любые ошибочные символы, которые пользователь может ввести перед нажатием клавиши [Enter]:

"p" [
    print tui [
        at 23x0 "Press [Enter] to continue"
    ]
    ask ""
    print tui [
        at 24x0 "                              "
        at 23x0 "                              "
    ]
]

После того, как часть этого кода была отправлена в список рассылки REBOL, стала очевидной ещё одна ошибка. Если во время игры нажимается клавиша вставки или клавиши со стрелками, игра вылетает. Следующий код выдаёт ошибку "** Math Error: Math or number overflow" (Математическая ошибка: математическое или числовое переполнение) при вычислении этих ключей:

keystroke: to-string to-char to-integer copy keys

Чтобы исправить это, я создаю свою собственную проверку ошибок. Коды клавиш для клавиш со стрелками: # {1B5B41}, # {1B5B42}, # {1B5B43}, # {1B5B44} и # {1B5B327E}. Я сначала проверяю, нажали ли они. Если нет, запустите приведённый выше код:

now-key: copy keys
if not (
    find [
        #{1B5B41} #{1B5B42} #{1B5B43}
        #{1B5B44} #{1B5B327E}
    ] (now-key)
) [keystroke: to-string to-char to-integer now-key]

Это работает, но послание к списку от Габриэль Сантилли предлагает более простое решение. Оказывается, мне следовало более внимательно присмотреться к формату консольного порта. Все, что нужно для нажатия клавиши, это:

keystroke: to-string copy keys

И это не приводит к ошибкам при вводе ключей.

Я добавил в программу весь приведённый выше код, а затем все протестировал. При этом я сделал интересное открытие - оказалось, что код, который вызвал ошибку ввода призрачной клавиши в процедурах печати фигур, находится в разделе диалекта TUI, который позволяет проверить размер экрана. Я думаю, что ошибка как-то связана с тем, что я "compose" (составляю) результаты - не уверен, но это не имеет значения. Поскольку я не использую эту функцию, я просто удаляю её из кода. Пока я занимаюсь этим, я удаляю все другие части диалекта TUI, которые я не использую. Оказывается, все, что мне нужно, это:

tui: func [commands [block!]] [
    string: copy ""
    cmd: func [s][join "^(1B)[" s]
    arg: parse commands [
        any [
            'clear (append string cmd "J") |
            'up    set arg integer! (append string cmd [
                arg "A"]) |
            'down  set arg integer! (append string cmd [
                arg "B"]) |
            'right set arg integer! (append string cmd [
                arg "C"]) |
            'left  set arg integer! (append string cmd [
                arg "D"]) |
            'at   set arg pair! (append string cmd [
                arg/x ";" arg/y "H" ]) |
            set arg string! (append string arg)
        ]
        end
    ]
    string
]

Когда эта ошибка исчезла, я могу удалить все процедуры проверки ошибок в программе (они вызывали дополнительные проблемы). Теперь Textris кажется достаточно законченной программой. Вот последний код:

REBOL [Title: "Textris"]

tui: func [commands [block!]] [
    string: copy ""
    cmd: func [s][join "^(1B)[" s]
    arg: parse commands [
        any [
            'clear (append string cmd "J") |
            'up    set arg integer! (append string cmd [
                arg "A"]) |
            'down  set arg integer! (append string cmd [
                arg "B"]) |
            'right set arg integer! (append string cmd [
                arg "C"]) |
            'left  set arg integer! (append string cmd [
                arg "D"]) |
            'at   set arg pair! (append string cmd [
                arg/x ";" arg/y "H" ]) |
            set arg string! (append string arg)
        ]
        end
    ]
    string
]

shape: [
    ["####"]
    ["#" down 1 left 1 "#" down 1 left 1 "#" down 1 left 1 "#"]
    ["###" down 1 left 2 "#"]
    [right 1 "#" down 1 left 2 "##" down 1 left 1 "#"]
    [right 1 "#" down 1 left 2 "###"]
    ["#" down 1 left 1 "##" down 1 left 2 "#"]
    ["###" down 1 left 3 "#"]
    ["##" down 1 left 1 "#" down 1 left 1 "#"]
    [right 2 "#" down 1 left 3 "###"]
    ["#" down 1 left 1 "#" down 1 left 1 "##"]
    ["###" down 1 left 1 "#"]
    [right 1 "#" down 1 left 1 "#" down 1 left 2 "##"]
    ["#" down 1 left 1 "###"]
    ["##" down 1 left 2 "#" down 1 left 1 "#"]
    ["##" down 1 left 1 "##"]
    [right 1 "#" down 1 left 2 "##" down 1 left 2 "#"]
    [right 1 "##" down 1 left 3 "##"]
    ["#" down 1 left 1 "##" down 1 left 1 "#"]
    ["##" down 1 left 2 "##"]
    ;
    ["    "]
    [" " down 1 left 1 " " down 1 left 1 " " down 1 left 1 " "]
    ["   " down 1 left 2 " "]
    [right 1 " " down 1 left 2 "  " down 1 left 1 " "]
    [right 1 " " down 1 left 2 "   "]
    [" " down 1 left 1 "  " down 1 left 2 " "]
    ["   " down 1 left 3 " "]
    ["  " down 1 left 1 " " down 1 left 1 " "]
    [right 2 " " down 1 left 3 "   "]
    [" " down 1 left 1 " " down 1 left 1 "  "]
    ["   " down 1 left 1 " "]
    [right 1 " " down 1 left 1 " " down 1 left 2 "  "]
    [" " down 1 left 1 "   "]
    ["  " down 1 left 2 " " down 1 left 1 " "]
    ["  " down 1 left 1 "  "]
    [right 1 " " down 1 left 2 "  " down 1 left 2 " "]
    [right 1 "  " down 1 left 3 "  "]
    [" " down 1 left 1 "  " down 1 left 1 " "]
    ["  " down 1 left 2 "  "]
]
floor:  [
    21x5 21x6 21x7 21x8 21x9 21x10 21x11 21x12 21x13 21x14 21x15
]
oc:  [ 
    [0x0 0x1 0x2 0x3] [0x0 1x0 2x0 3x0] [0x0 0x1 0x2 1x1]
    [0x1 1x0 1x1 2x1] [0x1 1x0 1x1 1x2] [0x0 1x0 1x1 2x0]
    [0x0 0x1 0x2 1x0] [0x0 0x1 1x1 2x1] [0x2 1x0 1x1 1x2]
    [0x0 1x0 2x0 2x1] [0x0 0x1 0x2 1x2] [0x1 1x1 2x0 2x1]
    [0x0 1x0 1x1 1x2] [0x0 0x1 1x0 2x0] [0x0 0x1 1x1 1x2]
    [0x1 1x0 1x1 2x0] [0x1 0x2 1x0 1x1] [0x0 1x0 1x1 2x1]
    [0x0 0x1 1x0 1x1] 
]
width: [4 1 3 2 3 2 3 2 3 2 3 2 3 2 3 2 3 2 2]
score: 0

prin tui [clear]
a-line: copy [] loop 11 [append a-line " "] 
a-line: rejoin ["   |"  to-string a-line "|"]
loop 20 [print a-line] prin "   " loop 13 [prin "+"] print ""
print tui compose [
    at 4x21 "TEXTRIS" at 5x21 "-------" 
    at 7x20 "Use arrow keys" at 8x20 "to move/spin."
    at 10x20 "'P' = pause"
    at 13x20 "SCORE:  " (to-string score)
]

keys: open/binary/no-wait [scheme: 'console]
forever [
    random/seed now
    r: random 19
    xpos: 9
    for i 1 20 1 [
        pos: to-pair rejoin [i "x" xpos]
        do compose/deep [prin tui [at (pos)] print tui shape/(r)]
        old-r: r
        old-xpos: xpos 
        if not none? wait/all [keys :00:00.30] [
            switch/default to-string copy keys [
                "p" [
                    print tui [
                        at 23x0 "Press [Enter] to continue"
                    ]
                    ask ""
                    print tui [
                        at 24x0 "                              "
                        at 23x0 "                              "
                    ]
                ]
                "^[[D" [if (xpos > 5) [
                        xpos: xpos - 1
                ]]
                "^[[C" [if (xpos < (16 - compose width/(r))) [
                        xpos: xpos + 1
                ]]
                "^[[A" [if (xpos < (16 - compose width/(r)))  [
                        switch to-string r [
                            "1" [r: 2]
                            "2" [r: 1]
                            "3" [r: 6]
                            "4" [r: 3]
                            "5" [r: 4]
                            "6" [r: 5]
                            "7" [r: 10]
                            "8" [r: 7]
                            "9" [r: 8]
                            "10" [r: 9]
                            "11" [r: 14]
                            "12" [r: 11]
                            "13" [r: 12]
                            "14" [r: 13]
                            "15" [r: 16]
                            "16" [r: 15]
                            "17" [r: 18]
                            "18" [r: 17]
                            "19" [r: 19]
                        ]
                    ]
                ]           
            ] []
        ]
        do compose/deep [
            prin tui [at (pos)] print tui shape/(old-r + 19)
        ]
        stop: false
        foreach po compose oc/(r) [
            foreach coord floor [
                floor-y: to-integer first coord
                floor-x: to-integer second coord
                oc-y:  i + to-integer first po
                oc-x:  xpos + to-integer second po
                if (oc-y = (floor-y - 1)) and (floor-x = oc-x) [
                    stop-shape-num: r
                    stop: true
                    break
                ]
            ]
        ]
        foreach po compose oc/(old-r) [
            foreach coord floor [
                floor-y: to-integer first coord
                floor-x: to-integer second coord
                oc-y:  i + to-integer first po
                oc-x:  old-xpos + to-integer second po
                if (oc-y = (floor-y - 1)) and (floor-x = oc-x) [
                    stop-shape-num: old-r
                    stop: true
                    break
                ]
            ]
        ]
        if stop = true [
            left-col: second pos 
            width-of-shape: length? compose oc/(stop-shape-num)
            right-col: left-col + width-of-shape - 1
            counter: 1
            for current-column left-col right-col 1 [
                add-coord: compose oc/(stop-shape-num)/(counter)
                new-floor-coord: (pos + add-coord)
                append floor new-floor-coord
                counter: counter + 1
            ]
            break
        ]
    ]
    do compose/deep [prin tui [at (pos)] print tui shape/(old-r)]
    if (first pos) < 2 [
        prin tui [at 23x0]
        print "   GAME OVER!!!^/^/"
        halt
    ]
    score: score + 10
    print tui compose [at 13x28 (to-string score)]
    for row 1 20 1 [
        line-is-full: true
        for colmn 5 15 1 [
            each-coord: to-pair rejoin [row "x" colmn]
            if not find floor each-coord [
                line-is-full: false
                break
            ]
        ]
        if line-is-full = true [
            remove-each cor floor [(first cor) = row]
            new-floor: copy [
                21x5 21x6 21x7 21x8 21x9 21x10 21x11 21x12 21x13
                21x14 21x15
            ]
            foreach cords floor [
                either ((first cords) < row) [
                    append new-floor (cords + 1x0)
                ][
                    append new-floor cords
                ]
            ]
            floor: copy unique new-floor
            score: score + 1000
            prin tui [clear]
            loop 20 [print a-line] 
            prin "   " loop 13 [prin "+"] print ""
            print tui compose [
                at 4x21 "TEXTRIS" at 5x21 "-------" 
                at 7x20 "Use arrow keys" at 8x20 "to move/spin."
                at 10x20 "'P' = pause"
                at 13x20 "SCORE:  " (to-string score)
            ]
            foreach was-here floor [
                if not ((first was-here) = 21) [
                    prin tui compose [at (was-here)]
                    prin "#"
                ]
            ]
        ]
    ]
]

Вот краткий обзор программы:

  • Определён диалект TUI.
  • Определён блок "shape", содержащий инструкции TUI для рисования каждой формы.
  • Определены блоки координат "floor", "oc" и "width". Также определяется переменная "score".
  • Распечатываются символы фона (левый, правый и нижний барьеры), инструкции, заголовки и счёт.
  • Цикл навсегда выполняет основные действия программы. Подразделы этого цикла:
    • На экране печатается фигура.
    • Наблюдаются нажатия клавиш пользователем.
    • Структура переключателя определяет, что делать с введёнными нажатиями клавиш (стрелка вправо = перемещение вправо, стрелка влево = перемещение влево, стрелка вверх = поворот фигуры, p = пауза).
    • Другая структура переключателя определяет, какую форму печатать при повороте текущей формы.
    • Текущая напечатанная форма будет удалена.
    • Два цикла foreach проверяют, достигла ли текущая фигура положения, при котором она должна перестать падать.
    • Если фигура достигла точки остановки, координаты, занятые фигурой, добавляются к блоку "floor".
    • Форма печатается на месте окончательного упокоения.
    • Если текущая фигура касается потолка, игра заканчивается.
    • Счёт обновлён.
    • Если какие-либо строки были полностью заполнены, их координаты удаляются из блока этажа, координаты всех других символов перемещаются вниз по строке, и экран печатается с новыми координатами этажа и новым счётом.
    • Цикл повторяется вечно.

Если бы я был настолько продуман и организован, чтобы написать подобный структурированный план в начале тематического исследования, всё пошло бы быстрее. Но любой проект проще в ретроспективе... Я просто стараюсь запомнить это построение как можно более подробным планом, прежде чем писать какой-либо код, всегда избавляет от большого количества работы и путаницы.

Теперь, когда игра соответствует моим первоначальным намерениям, я завершу изучение конкретного случая, но не без предварительного составления списка дел, которые нужно улучшить в программе. Если вы хотите попробовать реализовать некоторые из этих изменений, сначала выясните, где в схеме они должны быть, напишите некоторый псевдокод, чтобы выполнить работу, а затем придумайте код REBOL, чтобы удовлетворить эти выражения псевдокода:

  1. Сохраните рекорды на диск.
  2. Добавьте способ постепенного увеличения скорости падения фигур. Делайте это каждый раз, когда очищается определённое количество строк.
  3. Добавьте предварительный просмотр "следующего фрагмента".
  4. Найдите способ убрать курсор с распечатки, чтобы он не был виден вдоль левой стороны стены при падении фигур.
  5. Добавьте звук. Воспроизводите звуковые сигналы для каждого происходящего события и воспроизводите фоновую мелодию во время игры.
  6. Перепишите всю программу, используя методы графического интерфейса (GUI) вместо текстовых символов консоли и TUI.

Оглядываясь на свой процесс кодирования ретроспективно, я должен отметить некоторые критические замечания. Один элемент, который меня раздражал, - это набор плохо выбранных имён переменных. Я изначально использовал "r", например, для представления текущего номера формы, потому что он сначала использовался для представления случайного числа. "R" не так описательно, и мне было трудно вспомнить, что означает "r", пока я кодировал. То же самое можно сказать и о "i", которое стало более важным по мере того, как петля, отбрасывающая фигуры, усложнялась. Я оставил эти переменные такими, какими они были в этом тематическом исследовании, чтобы строки кода аккуратно вписывались в эту веб-страницу, но в моем собственном коде я предпочитаю использовать более описательные переменные. Это в целом делает код более читаемым и лёгким для понимания.

Мораль истории:

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

В общем, если вы не можете структурированно продумать процесс "чего я пытаюсь достичь", то вы не сможете написать код для этого. Как только вы получите базовое представление о языковых концепциях и синтаксисе, вы увидите, что написание кода требует творческой организации и экспериментов. Держите под рукой справочник по языку, и вы сможете разработать синтаксис практически любого кода, который вам нужно написать. Это просто вопрос того, какие функции и конструкции доступны для решения ваших проблем, и поиска формата для тех, с которыми вы не знакомы. Сложная часть в любой ситуации кодирования - это сопоставление каждого небольшого мыслительного процесса с конструкцией данных, условным выражением, процедурой цикла, определением функции, существующим модулем кода, меткой слова и т.д. для больших проектов вам обычно понадобится схема, потому что это так легко потеряться в мельчайших деталях кодирования по пути. Начните с подхода сверху вниз, придумайте и спроектируйте блок-схему/схему, а затем конкретизируйте детали каждого раздела, пока не получите код, написанный для решения каждой концепции дизайна. Как только вы познакомитесь с этим процессом, опыт покажет, что вы можете кодировать решения практически для любой проблемы, с которой сталкиваетесь.

Вы обнаружите, что во многих случаях REBOL позволяет вам думать непосредственно в коде более легко, чем с псевдокодом. Это потому, что дизайн высокого уровня REBOL предназначен для восприятия человеком и его "мысли". Хотя многие концепции кодирования на всех компьютерных языках, как правило, одинаковы, большинство других языков более открыто спроектированы и ограничены устаревшими концепциями, вытекающими из требований к работе компьютеров. Некоторые языки, как правило, требуют гораздо более низкоуровневого кодирования или приведения разрозненных модулей, чтобы они соответствовали друг другу, чтобы концептуальный дизайн принял форму в окончательной форме кода. Другие языки заставляют вас увязнуть в размышлениях о конструкциях ООП более высокого уровня. Отсутствие универсальных структур данных, таких как блочная/последовательная структура REBOL, отсутствие встроенных собственных типов данных, таких как время, кортежи, пары, деньги и т.д., а также менее естественный способ структурирования функций, переменных и определений модулей (не использование слов и диалектов на естественном языке), требуют создания уникальных и надуманных конструкций для управления данными в каждой отдельной программе. В наиболее популярных языках авторы программ обычно должны больше заботиться об управлении элементарной памятью и действиями процессора для всего, что происходит в программе. Это позволяет в большей степени контролировать то, как компьютер использует свои аппаратные ресурсы, но это очень далеко от того, как люди обычно думают о разрешении реальных жизненных ситуаций. REBOL позволяет делать вещи в образе мышления, близком к стадии набросков. Когда вы привыкнете писать код REBOL, вы обнаружите, что он экономит огромное количество времени по сравнению с другими языками. Попутно помните, что независимо от того, какой компьютерный язык (языки) вы изучаете, понимание того, как обдумывать схему "чего я пытаюсь достичь", необходимо для написания кода, который выполняет вашу задачу.

23.2 Случай: более полные циклы программы: лыжи, змейка и захватчики

Проект Textris был развлекательным и познавательным, поэтому я решил создать ещё одну простую игру. Для этого тематического исследования я хотел создать игру, которая демонстрировала бы больше графических приёмов вместо использования текста. Я нашёл хорошее руководство по игре на http://gm2d.com/2009/02/simple-flash-game-in-haxe. Эта игра была написана на другом языке программирования, но представляет собой хороший пример для подражания в REBOL.

В игре "Лыжный спорт" игрок представлен графическим изображением, которое можно перемещать из стороны в сторону в верхней части экрана. Произвольно расположенные изображения деревьев прокручиваются вверх по экрану на пути лыжника. Цель состоит в том, чтобы как можно дольше избегать ударов по деревьям. Чем дольше лыжник остаётся в живых, тем выше оценка.

Я начал продумывать план нападения со следующей схемы:

  1. Во-первых, мне понадобятся изображения для использования в игре. Ссылка на учебник выше содержит графику с открытым исходным кодом. Я буду использовать двоичный модуль для внедрения ресурсов, предоставленный ранее в этом руководстве, для импорта и использования этой графики в коде этой игры.
  2. Мне нужно построить экран, полный деревьев прокрутки. Для этого вот некоторый псевдокод и описание моего воображаемого контура кода: Я создам графическую игровую область, используя блок "draw" (рисовать) в окне "view layout". Чтобы создать набор деревьев в различных местах, я буду использовать цикл for и функцию random, чтобы создать блок необходимых перечисленных координатных позиций, данных изображения и т.д. чтобы переместить деревья, я используйте "feel engage" на блоке рисования. По прошествии заданного количества времени (несколько миллисекунд, проверенных действием "time" (время) в цикле включения) я увеличиваю координаты каждого дерева и увеличиваю счёт. Большая часть программы будет работать в этом цикле таймера, так что деревья непрерывно перемещаются вверх по экрану. Если какое-либо из деревьев достигает верхней части экрана, я удаляю их из блока рисования и заменяю новыми в случайных горизонтальных положениях внизу экрана. Это создаст вид бесконечного холма из прокручивающихся деревьев и постоянно подсчитываемых очков.
  3. Мне понадобится изображение игрока. Игрок должен контролировать перемещение изображения из стороны в сторону либо с помощью нажатия клавиш, либо с помощью движений мыши. Я могу либо проверять 'key actions' (ключевые действия) в приведённом выше цикле включения, либо постоянно проверять положение мыши, используя опцию просмотра 'all-over' (по всей поверхности). Если движение влево обнаружено, отобразите изображение лыжника влево и обновите его положение на 1 пиксель влево (текущее положение - 1x0). Если обнаружено движение вправо, отобразите график, направленный вправо, и обновите его правое положение на 1 пиксель вправо.
  4. Мне нужно будет проверить наличие столкновений и закончить игру, если лыжник ударится о дерево. Это будет включать сравнение позиций рисунка лыжника с позициями всей древовидной графики на каждой итерации цикла таймера, описанного выше.

На первом этапе я использовал Windows Paint, чтобы изменить изображения лыжников вправо и влево, которые я нашёл на веб-сайте выше. Я создал своё собственное древовидное изображение, отредактировав простой рисунок линии, найденный с помощью Google. Используя двоичный модуль для встраивания ресурсов, описанный ранее в этом руководстве, я преобразовал эти изображения в следующий код REBOL:

tree:  load to-binary decompress 64#{
eJzt18sNwjAQBFDTBSVw5EQBnLjQE1XRngmBQEj8Wa/3M4oYOZKBKHkaWwTO1/sh
jDkNx3N6HI7LcOzCfnz/9v5cMnEai7lj4mokT9C7XczUsrhvGSku6RkgDIbHAEP0
2EiIMBdMDuaOWZCSL91bQvCsSY4MHE9umXz7ydVi3xgltYvEKboexzVSlpTa614d
NonpUauIv176dX0ZTRgJlVgzNl25A3gkGwld1bkrNFqqedQfEI02AU9PjDeMpac/
ShKeTXylROqCImlXRFd9zkQoh4tp+GpqlSTnLnum4HTEzK/gjpmTpDxSASlHFqYU
EE/8nddG9n+9LIm8t9OeIEra2JZWDRSG4VEioa0UFCZFqv/aMQh2Rf790EnGgcJU
SVAer0Bhcp7/epVJvkHzBHjPfz+XSe6BwryC5gmQno3mAY3tpba2KAAA
}
skier-left: load to-binary decompress 64#{
eJyN0U8og2EcB/DvNrz+E5fJZSmRf9Ej76h3Ne1AIspyMQflpJDFU/KO1cQmSnGa
A3PYkvInB3kvuyzlgJolh+fCRUq5iBvP8+5lTvKrX33ep+/zp9/b2Tthhl6zvGt5
W3nX8TYhS1//MOGnSjNEa/AUxd0UVQ3raL9IYbBvA2OBI9Q0DqB6fAujl08Yi97D
Hr3F5EQYSss2OrrWEFo5xB+VO5Vx/skvnxmQbDCFvxcjMJ/b0s6LAZXGA3O0ZtTt
pW3WbJmDeMC8a1gE9o3bTBFI9YvGhrOKSueyEQpu9ri60vQFXFqPMx1K+sNWrdOh
73Y/uMr85fKdcIrJ0z6vxSfsYV5KCU2JEPNIlD9dFZ65AfXwD+HsKdAZiiLdqtvt
Hh65E5ZklTGmDvWLgxxKkjAivwt7XxhJEvIsrCY8ikLs0Tj3yGeCKaQtdsX9fv3G
N1jCJdyv84lHJkNriiM7Li29OIDV0jcU8kuIHaiPLEDEsG9DQYxiQTi0A8sBpEvh
OT65GmBYH9Jx5nf8TFFUFf5ZX2hFdG1uAgAA
}
skier-right: load to-binary decompress 64#{
eJxz8s1jYgCDMiDWAGIJINYCYkYGFrD4D0YGOBBAMBn4++Yz6HjVMSgY1oP5gWdu
M/gHTmCwNutlKJ26l6F03VUGp3XnGGo+/mGILVnMoFkwhaHm7GcGz4m7GbABFwST
eQWSNXMQbM+3DAwlULbmEgaWXih75QUGzvkQJstMBwbPRRA2L1D5yS8QNudioNQF
qNYPDExAZRCtDg78c6Fa7wZK3Ycq940O3L1fAcLWigpctUsZzHTSj5Jd+l7NAKS6
3HnXk6jHSiBF7sUmxi7Gl9VAZrqVOxsZuTirg8TTS0qAQs5FIPF0BhYXFkgog/zg
7gJlq5SXpaWVF4O9lZKuXl6eVl4AZLIfKS82LzYuB2nlOFxWXl5ubA6ytm1KWU65
cXExkMl09lNNR3q5eTFQPYfHE7YT6cXlJgcYGI7cPMAOMtKhgcH9wE8FBuPycgOG
BoYKtl8ODL4gjccY2HSAfr4BVMvgAwyazwwsXSA7ORgY2BQYeH+Cw+sAKPo5wEHj
kQAO/GZwIIHDgc0AaxQSBAAFOXD7bgIAAA==
}

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

; (Включите сюда приведённый выше графический код)

; Определим некоторые переменные для начала (например, начальная 
; оценка = 0 и т.д.). Создадим блок "board" для хранения информации 
; об изображении для всей графики, отображаемой на экране. Сначала 
; должно быть изображение лыжника, затем деревья.
; Используйте цикл "for" для создания данных:

for i 1 20 1 [
   ; Добавим данные изображения дерева в описанный выше блок. 
   ; Используйте функцию "random", чтобы найти 20 случайных 
   ; координатных точек.
]

; Вот базовая структура макета экрана с блоком рисования, таймером и 
; обнаружением ключевых действий, а также изложенные выше идеи, 
; написанные в соответствующих областях кода:

view layout [
    scrn: box effect [draw board] rate 0 feel [
        engage: func [f a e] [
            if a = 'key [
                ; Переместим изображение лыжника влево-вправо
            ]
            if a = 'time [
                ; Прокрутим древовидную графику вверх.
                ; Уберём все деревья выходящие за верхний край экрана.
                ; Заменим удалённые деревья новыми внизу экрана.
                ; Проверим наличие столкновений и завершим игру, 
                ; если лыжник ударится о дерево.
                ; Обновим счёт.
            ]
        ]
    ]
    ; Отображение счёта в графическом интерфейсе с помощью какого-то 
    ; текстового виджета
]

Затем я конкретизировал структуру кода, приведённую выше, с более подробными мыслями о том, как выполнить всё, что указано в первоначальной описательной схеме. Фактического кода ещё нет - только мысли о том, как реализовать каждую из общих идей в соответствующих областях структуры кода. Вот мои мысли:

; (Включите сюда приведённый выше графический код)

; Для начала определите несколько переменных:

; Мне нужно сгенерировать некоторые случайные координаты положения. 
; Подготовьте (запишите) случайную функцию.

; Все элементы на экране будут храниться в блоке (назову его 
; "доской"). Начните с кода, необходимого для отображения 
; изображения лыжника в блоке рисования. Блок должен содержать 
; следующую информацию для каждого изображения:

[
    функция рисования 'image',
    координатное положение изображения,
    фактические данные двоичного изображения,
    цвет прозрачности (черный), поэтому края изображений
    не выглядят квадратными (т. е. черные углы внешней рамки
    файл изображения исчезнет, смешавшись с фоном).
]

; Теперь добавьте двадцать деревьев к вышеуказанному блоку, чтобы 
; они появлялись в случайных местах на экране:

for i 1 20 1 [

    ; Присвойте случайную координату переменной pos в пределах 
    ; игрового экрана.

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

    ; Поместите каждое изображение в блок "board", описанный выше.
]

; Теперь у нас есть блок изображений, который можно отобразить на 
; экране с помощью функции "draw" (рисование) (см. Раздел "2D-рисование, 
; графика и анимация" ранее в этом руководстве).

; Отцентрируйте окно графического интерфейса и избавьтесь от 
; стандартного 20-пиксельного серого отступа по краям 
; ('layout / tight'):
view center-face layout/tight [

    ; Установите цвет экрана белый, как снег, и установите размер 
    ; игровой зоны:

    scrn: box white 600x440 effect [draw board] rate 0 feel [
        engage: func [f a e] [
            if a = 'key [
                if e/key = 'right [

                    ; Второй элемент в созданном выше блоке будет 
                    ; координатой положения изображения лыжника. 
                    ; Если была нажата клавиша со стрелкой вправо, 
                    ; добавьте 5 к горизонтальной части этой 
                    ; координаты положения.

                    ; Третий элемент графического блока - это 
                    ; фактические графические данные, используемые 
                    ; для отображения лыжника. Если была нажата 
                    ; клавиша со стрелкой вправо, эти данные должны 
                    ; быть заменены изображением лыжника, обращённым 
                    ; вправо.

                ]
                if e/key = 'left [

                    ; То же, что и в предыдущем разделе, но для 
                    ; клавиши "влево".

                ]

                ; Теперь, когда в блок данных добавлены изменения  
                ; положения и графики, покажите их на экране:

                show scrn
            ]
            if a = 'time [

                ; Всё в этом блоке происходит каждый раз, когда 
                ; действие таймера обнаруживается в блоке ощущений 
                ; (feel) вышеупомянутой функции рисования (в 
                ; настоящее время скорость установлена на 0 секунд, 
                ; поэтому весь этот код просто продолжает цикл).

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

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

foreach item board [

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

    either all [

        ; Если элемент является координатой, и мы не 
        ; имеем дело с первыми 4 элементами:

    ] [ 

        ; Добавьте новую позицию координат (старая 
        ; позиция + 5) в новый блок.

    ] [

        ; В противном случае добавьте все остальные 
        ; элементы в новый блок как есть (т.е. мы не 
        ; меняем изображение или данные прозрачности).

    ]

    ; Если вновь добавленная координата выше верхней 
    ; части экрана, удалите все 4 её элемента из 
    ; нового блока (т.е. удалите древовидную графику 
    ; из игры).

]

; Теперь скопируйте новый блок обратно в переменную 
; "board".

; Если какие-либо древовидные изображения были 
; удалены из верхней части экрана, замените их 
; новыми графическими данными в блоке. Мы можем 
; проверить удаление, посмотрев на длину блока 
; данных. Он должен быть длиной 84 элемента (1 
; лыжник + 20 деревьев = 21 * 4, или 84 элемента в 
; длину). Координаты новых деревьев должны 
; располагаться в случайных горизонтальных точках 
; внизу экрана (т.е. где-то (случайным образом) x440 
; пикселей). Добавьте новые графические данные в 
; блок "board".

; Обнаружение столкновений:
;
; Проверьте, находится ли положение лыжника в 
; пределах досягаемости ЛЮБОГО положения на дереве. 
; Используйте цикл foreach для сравнения. Чтобы мы 
; не обнаружили, что лыжник сталкивается с самим 
; собой, используйте копию доски без первых 4 
; элементов. Чтобы проверить, соприкасаются ли 
; изображения, нам нужно учитывать размеры и формы 
; как дерева, так и изображения лыжника. Я придумал 
; следующие измерения: если верхний левый угол 
; изображения лыжника находится в пределах от -40 
; до +15 пикселей по горизонтали и от 5 до 30 
; пикселей по вертикали, они будут соприкасаться.
; Это несовершенная оценка, которую я сделал после 
; некоторого пробного и ошибочного просмотра 
; изображений, который учитывает тот факт, что мы 
; начинаем вычисления с верхнего левого угла каждого 
; файла квадратного изображения разного размера.
; Расчёт должен проверять, находится ли 
; горизонтальная skier_position - горизонтальная 
; tree_position в пределах этого диапазона пикселей.

; Я создам новый блок данных для сравнения, из 
; которого удалены элементы лыжников:
collision-board: remove/part (copy board) 4
foreach item collision-board [
    if (type? item) = pair! [
        if all [

          ; "item/1" и "item/2" относятся 
          ; соответственно к горизонтальным и 
          ; вертикальным компонентам проверяемой 
          ; координаты дерева (значения x и y в 
          ; координате). "board/2/1" и "board/2/2" 
          ; относятся к положению изображения 
          ; лыжника (помните, что текущее положение 
          ; лыжника всегда является вторым элементом 
          ; в блоке). Расчёты в этом разделе будут 
          ; проверять диапазоны, описанные выше.

        ] [
          ; Если приведённые выше вычисления верны, 
          ; сообщите об этом пользователю и 
          ; завершите игру.
        ]
    ]
]

; Каждый раз при прохождении цикла увеличивайте и 
; обновляйте отображение счета.

]
]
]

; Поместите текстовую метку "Score:" в верхнем левом углу экрана.

; Счёт игры, обновлённый в приведённом выше коде, может просто 
; содержаться в другом текстовом виджете. Присвойте этому 
; виджету слово "Score".

; Чтобы ключевые действия работали в приведённом выше коде 
; взаимодействия, в макет необходимо добавить следующую строку 
; акций:

do [focus scrn]
]

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

; (Включите сюда приведённый выше графический код)

; Для начала определите несколько переменных:

the-score: 0

; Мне нужно сгенерировать случайные координаты положения. Следующая 
; строка - это стандартный код REBOL, чтобы гарантировать, что числа 
; действительно случайны:

random/seed now

; Все элементы на экране будут сохранены в блоке.
; Начните с кода, необходимого для отображения изображения лыжника в 
; блоке рисования (чёрный - прозрачный цвет).

board: reduce ['image 300x20 skier-right black]

; Теперь добавьте двадцать деревьев к вышеуказанному блоку, чтобы 
; они появлялись в случайных местах на экране:

for i 1 20 1 [

    ; Присвойте случайную координату переменной pos в пределах 
    ; игрового экрана:

    pos: random 600x540

    ; Сдвиньте каждое изображение на 300 пикселей вниз, чтобы у 
    ; пользователя была секунда, чтобы увидеть их приближение и 
    ; занять своё место в начале игры:

    pos: pos + 0x300

    ; Поместите каждое изображение в блок выше:

    append board reduce ['image pos tree black]
]

; Теперь у нас есть блок изображений, который можно отобразить на 
; экране с помощью функции "draw" (рисование) (см. Раздел 
; "2D-рисование, графика и анимация" ранее в этом руководстве).

; Отцентрируйте окно графического интерфейса и избавьтесь от 
; стандартного 20-пиксельного серого отступа по краям 
; ('layout/tight'):

view center-face layout/tight [

    ; Установите цвет экрана белый, как снег, и установите размер 
    ; игровой зоны:

    scrn: box white 600x440 effect [draw board] rate 0 feel [
        engage: func [f a e] [
            if a = 'key [
                if e/key = 'right [

                    ; Второй элемент в созданном выше блоке - 
                    ; координаты положения изображения лыжника.
                    ; Если была нажата клавиша со стрелкой вправо, 
                    ; добавьте 5 к горизонтальной части этой 
                    ; координаты положения:

                    board/2: board/2 + 5x0

                    ; Второй элемент графического блока - это 
                    ; фактические графические данные, используемые 
                    ; для отображения лыжника.
                    ; Если была нажата клавиша со стрелкой вправо, 
                    ; эти данные должны быть заменены изображением 
                    ; лыжника, обращённым вправо:

                    board/3: skier-right
                ]
                if e/key = 'left [

                    ; То же, что и в предыдущем разделе, но для 
                    ; клавиши "влево":

                    board/2: board/2 - 5x0
                    board/3: skier-left
                ]

                ; Теперь, когда в блок данных добавлены изменения 
                ; положения и графики, покажите их на экране:

                show scrn
            ]
            if a = 'time [

                ; Всё в этом блоке происходит каждый раз, когда в 
                ; блоке ощущений функции рисования обнаруживается 
                ; действие таймера (в настоящее время скорость 
                ; установлена на 0 секунд, поэтому весь этот код 
                ; просто продолжает цикл). Сначала переместите 
                ; деревья вверх на 5 пикселей каждое. Мне нужно 
                ; будет работать с каждым элементом графического 
                ; блока, иногда удаляя и добавляя элементы. Чтобы 
                ; упростить весь процесс, я собираюсь создать новую 
                ; копию изменённого блока с нуля:

                new-board: copy []

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

                foreach item board [

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

                    either all [

                        ; Если элемент является координатой:

                        ((type? item) = pair!) 

                        ; и мы не имеем дело с первыми 4 
                        ; элементами: (после того, как мы закончим 
                        ; этот цикл, 4 элемента будут добавлены в 
                        ; новый блок): 

                        ((length? new-board) > 4)
                    ] [ 

                        ; Добавьте перемещённую позицию в новый блок:

                        append new-board (item - 0x5) 
                    ] [

                        ; Добавьте все остальные элементы в новый блок:

                        append new-board item
                    ]

                    ; Если вновь добавленная координата находится 
                    ; выше верхней части экрана, удалите все 4 её 
                    ; элемента из нового блока (т.е. удалите 
                    ; древовидную графику из игры):

                    coord: first back back (tail new-board)
                    if ((type? coord) = pair!) [
                        if ((second coord) < -60) [
                            remove back tail new-board
                            remove back tail new-board
                            remove back tail new-board
                            remove back tail new-board
                        ]
                    ]
                ]

                ; Теперь скопируйте новый блок обратно в переменную 
                ; "board":

                board: copy new-board

                ; Если какие-либо древовидные изображения были 
                ; удалены из верхней части экрана, замените их 
                ; новыми графическими данными в блоке. Мы можем 
                ; проверить удаление, посмотрев на длину блока 
                ; данных. Он должен быть длиной 84 элемента (1 
                ; лыжник + 20 деревьев = 21 * 4, или 84 элемента в 
                ; длину). Координаты новых деревьев должны 
                ; располагаться в случайных горизонтальных местах 
                ; внизу экрана (т.е. где-то (случайным) x440 
                ; пикселей): 
                ; Добавьте новый рисунок на экран:

                if (length? new-board) < 84 [
                    column: random 600
                    pos: to-pair rejoin [column "x" 440]
                    append board reduce ['image pos tree black]
                ]

                ; Обнаружение столкновений:
                ;
                ; Проверьте, находится ли положение лыжника в 
                ; пределах досягаемости ЛЮБОГО положения на дереве. 
                ; Используйте цикл foreach для сравнения. Чтобы 
                ; убедиться, что вы не заметили, как лыжник 
                ; сталкивается с самим собой, используйте копию 
                ; доски без первых 4 элементов. Чтобы проверить, 
                ; соприкасаются ли изображения, нам нужно учитывать 
                ; размеры и формы как дерева, так и изображения 
                ; лыжника. Я придумал следующие измерения: если 
                ; верхний левый угол изображения лыжника находится в 
                ; пределах от -40 до +15 пикселей по горизонтали и 
                ; от 5 до 30 пикселей по вертикали, они будут 
                ; соприкасаться. Это несовершенная оценка, которую я 
                ; придумал путём проб и ошибок, просматривая 
                ; изображения, и которая учитывает тот факт, что мы 
                ; начинаем вычисления с верхнего левого угла каждого 
                ; изображения разного размера. Расчёт должен 
                ; проверять, находится ли горизонтальная 
                ; skier_position - горизонтальная tree_position в 
                ; пределах этого диапазона пикселей.


                collision-board: remove/part (copy board) 4
                foreach item collision-board [
                    if (type? item) = pair! [
                        if all [

                            ; "item/1" и "item/2" относятся 
                            ; соответственно к горизонтальным и 
                            ; вертикальным компонентам проверяемой 
                            ; координаты дерева (значения x и y в 
                            ; координате). "board/2/1" и "board/2/2" 
                            ; относятся к положению изображения 
                            ; лыжника (помните, что текущее 
                            ; положение лыжника всегда является 
                            ; вторым элементом в блоке). Приведённые 
                            ; ниже расчёты проверяют диапазоны, 
                            ; описанные выше:

                            ((item/1 - board/2/1) < 15)
                            ((item/1 - board/2/1) > -40)
                            ((board/2/2 - item/2) < 30)
                            ((board/2/2 - item/2) > 5)
                        ] [

                            ; Оповестить пользователя и завершить игру:

                            alert "Ouch - you hit a tree!"
                            alert rejoin ["Final Score: " the-score]
                            quit
                        ]
                    ]
                ]

                ; Каждый раз в цикле увеличивайте и обновляйте
                ; отображение счета:

                the-score: the-score + 1 
                score/text: to-string the-score
                show scrn
            ]
        ]
    ]

    ; Поместите слово "Score:" в верхнем левом углу экрана:

    origin across h2 "Score:" 

    ; Вот текст счета игры, который обновлён в приведённом выше 
    ; коде. Это просто виджет текстового заголовка, назначьте слово 
    ; "Score":

    score: h2 bold "000000"

    ; Чтобы действия 'key работали в приведённом выше коде 
    ; взаимодействия, в макет необходимо добавить следующую строку:
do [focus scrn]
]

Вот финальная игра с удалёнными комментариями:

REBOL [title: "Ski Game"]

tree:  load to-binary decompress 64#{
eJzt18sNwjAQBFDTBSVw5EQBnLjQE1XRngmBQEj8Wa/3M4oYOZKBKHkaWwTO1/sh
jDkNx3N6HI7LcOzCfnz/9v5cMnEai7lj4mokT9C7XczUsrhvGSku6RkgDIbHAEP0
2EiIMBdMDuaOWZCSL91bQvCsSY4MHE9umXz7ydVi3xgltYvEKboexzVSlpTa614d
NonpUauIv176dX0ZTRgJlVgzNl25A3gkGwld1bkrNFqqedQfEI02AU9PjDeMpac/
ShKeTXylROqCImlXRFd9zkQoh4tp+GpqlSTnLnum4HTEzK/gjpmTpDxSASlHFqYU
EE/8nddG9n+9LIm8t9OeIEra2JZWDRSG4VEioa0UFCZFqv/aMQh2Rf790EnGgcJU
SVAer0Bhcp7/epVJvkHzBHjPfz+XSe6BwryC5gmQno3mAY3tpba2KAAA
}
skier-left: load to-binary decompress 64#{
eJyN0U8og2EcB/DvNrz+E5fJZSmRf9Ej76h3Ne1AIspyMQflpJDFU/KO1cQmSnGa
A3PYkvInB3kvuyzlgJolh+fCRUq5iBvP8+5lTvKrX33ep+/zp9/b2Tthhl6zvGt5
W3nX8TYhS1//MOGnSjNEa/AUxd0UVQ3raL9IYbBvA2OBI9Q0DqB6fAujl08Yi97D
Hr3F5EQYSss2OrrWEFo5xB+VO5Vx/skvnxmQbDCFvxcjMJ/b0s6LAZXGA3O0ZtTt
pW3WbJmDeMC8a1gE9o3bTBFI9YvGhrOKSueyEQpu9ri60vQFXFqPMx1K+sNWrdOh
73Y/uMr85fKdcIrJ0z6vxSfsYV5KCU2JEPNIlD9dFZ65AfXwD+HsKdAZiiLdqtvt
Hh65E5ZklTGmDvWLgxxKkjAivwt7XxhJEvIsrCY8ikLs0Tj3yGeCKaQtdsX9fv3G
N1jCJdyv84lHJkNriiM7Li29OIDV0jcU8kuIHaiPLEDEsG9DQYxiQTi0A8sBpEvh
OT65GmBYH9Jx5nf8TFFUFf5ZX2hFdG1uAgAA
}
skier-right: load to-binary decompress 64#{
eJxz8s1jYgCDMiDWAGIJINYCYkYGFrD4D0YGOBBAMBn4++Yz6HjVMSgY1oP5gWdu
M/gHTmCwNutlKJ26l6F03VUGp3XnGGo+/mGILVnMoFkwhaHm7GcGz4m7GbABFwST
eQWSNXMQbM+3DAwlULbmEgaWXih75QUGzvkQJstMBwbPRRA2L1D5yS8QNudioNQF
qNYPDExAZRCtDg78c6Fa7wZK3Ycq940O3L1fAcLWigpctUsZzHTSj5Jd+l7NAKS6
3HnXk6jHSiBF7sUmxi7Gl9VAZrqVOxsZuTirg8TTS0qAQs5FIPF0BhYXFkgog/zg
7gJlq5SXpaWVF4O9lZKuXl6eVl4AZLIfKS82LzYuB2nlOFxWXl5ubA6ytm1KWU65
cXExkMl09lNNR3q5eTFQPYfHE7YT6cXlJgcYGI7cPMAOMtKhgcH9wE8FBuPycgOG
BoYKtl8ODL4gjccY2HSAfr4BVMvgAwyazwwsXSA7ORgY2BQYeH+Cw+sAKPo5wEHj
kQAO/GZwIIHDgc0AaxQSBAAFOXD7bgIAAA==
}
random/seed now
the-score: 0
board: reduce ['image 300x20 skier-right black]
for i 1 20 1 [
    pos: random 600x540
    pos: pos + 0x300
    append board reduce ['image pos tree black]
]
view center-face layout/tight [
    scrn: box white 600x440 effect [draw board] rate 0 feel [
        engage: func [f a e] [
            if a = 'key [
                if e/key = 'right [
                    board/2: board/2 + 5x0
                    board/3: skier-right
                ]
                if e/key = 'left [
                    board/2: board/2 - 5x0
                    board/3: skier-left
                ]
                show scrn
            ]
            if a = 'time [
                new-board: copy []
                foreach item board [
                    either all [
                        ((type? item) = pair!) 
                        ((length? new-board) > 4)
                    ] [ 
                        append new-board (item - 0x5) 
                    ] [
                        append new-board item
                    ]
                    coord: first back back (tail new-board)
                    if ((type? coord) = pair!) [
                        if ((second coord) < -60) [
                            remove back tail new-board
                            remove back tail new-board
                            remove back tail new-board
                            remove back tail new-board
                        ]
                    ]
                ]
                board: copy new-board
                if (length? new-board) < 84 [
                    column: random 600
                    pos: to-pair rejoin [column "x" 440]
                    append board reduce ['image pos tree black]
                ]
                collision-board: remove/part (copy board) 4
                foreach item collision-board [
                    if (type? item) = pair! [
                        if all [
                          ((item/1 - board/2/1) < 15)
                          ((item/1 - board/2/1) > -40)
                          ((board/2/2 - item/2) < 30)
                          ((board/2/2 - item/2) > 5)
                        ] [
                            alert "Ouch - you hit a tree!"
                            alert rejoin ["Final Score: " the-score]
                            quit
                        ]
                    ]
                ]
                the-score: the-score + 1 
                score/text: to-string the-score
                show scrn
            ]
        ]
    ]
    origin across h2 "Score:" 
    score: h2 bold "000000"
    do [focus scrn]
]

23.2.1 Дополнение

Следует отметить, что при написании и тестировании этой программы я делал много проб и ошибок кодирования. Первоначально я пытался заставить лыжника двигаться влево-вправо, следуя жестам мыши влево-вправо. Я отказался от этой идеи, потому что мой код выполнялся слишком медленно для этого приложения, но полученный код все ещё может быть полезен в других проектах. Он включён сюда для полноты картины.

Я определил эту стартовую переменную в начале программы:

mouse-pos: 0x0

и добавил этот код в блок "feel", непосредственно под функцией "engage" (задействовать):

over: func [f a p] [
    if not mouse-pos = p [  ; i.e., if mouse has moved
        either p/1 > mouse-pos/1 [  ; true = mouse has moved right
            ; обновить данные изображения лыжника в блоке "board": 
            board/3: skier-right
        ] [
            board/3: skier-left
        ]
        ; установить положение лыжника на основе положения столбца 
        ; мыши:
        board/2: to-pair rejoin compose [(p/1 - 35) "x" 20]
        mouse-pos: p
        show scrn
    ]
]

Для того, чтобы REBOL постоянно проверял события мыши, в код 'view layout' должна быть добавлена опция "all-over":

view/options layout [...] [all-over]

Удалите код действия клавиши в функции включения и замените его указанными выше изменениями. Лыжник будет двигаться влево-вправо на основе движений мыши влево-вправо.

Другой способ достичь той же цели, не используя опцию "all-over", - использовать функцию "detect" (обнаружение):

detect: func [f e] [
    if e/type = 'move [
        p: e/offset
        if not mouse-pos = p [  
            either p/1 > mouse-pos/1 [  
                board/3: skier-right
            ] [
                board/3: skier-left
            ]
            board/2: to-pair rejoin compose [(p/1 - 35) "x" 20]
            mouse-pos: p
            show scrn
        ]
    ]
    e
]

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

23.2.2 Змейка

Ниже приведён код классической игры "Змейка". Смысл игры со змеёй состоит в том, чтобы перемещать изображение змеи по экрану, пожирая гранулы еды, которые появляются в случайных местах. Каждый раз, когда вы съедаете гранулу, ваше тело змеи увеличивается на одну единицу. Избегайте ударов по краю игрового поля и как можно дольше избегайте ударов по собственному телу.

Обратите внимание, что схема кода для этой программы почти идентична схеме для лыжной игры:

  1. Вставим изображения, необходимые для отображения фрагментов змей и кормовых гранул (в этом примере я использовал простые изображения зелёной и красной кнопок, созданные с помощью функций "to-image" (в-изображение) и "layout" (макет), но это можно легко изменить).
  2. Установим некоторые начальные переменные (начальный счёт, случайные начальные координаты, начальные значения для флагов, используемых в программе и т.д.).
  3. Создадим блок доски для хранения данных изображения и координат участков змей и изображений еды.
  4. Отобразим игровое поле в виде блока рисования "view layout" и продолжаем игру, постоянно проверяя время "feel engage" (ощущения вовлечённости) и ключевые события.
  5. Изменим направление движения змейки при каждом нажатии клавиши. Отрегулируем координаты змеи (переместите изображения раздела змеи) и отрегулируем счёт каждый раз, когда происходит событие таймера (15 раз в секунду). Как и в игре на лыжах, создадим временный блок для копирования и корректировки всех новых координат змеи (переместите голову змеи в новое соседнее место, затем переместите каждую последующую часть змеи в предыдущее место соседней части).
  6. Проверим наличие столкновений, сравнив координаты секций змейки с другими элементами на доске. Завершим игру, если змея столкнётся со стеной или сама с собой. Каждый раз, когда змея сталкивается с изображением еды, переместим изображение еды в новую случайную координату и добавим новое изображение секции змеи на доску (добавим изображение к блоку доски, чтобы увеличить длину змеи).
REBOL [Title: "Snake Game"]

snake: to-image layout/tight [button red 10x10]
food: to-image layout/tight [button green 10x10]
the-score: 0  direction: 0x10  newsection: false  random/seed now
rand-pair: func [s] [
    to-pair rejoin [(round/to random s 10) "x" (round/to random s 10)]
]
b: reduce [
    'image food ((rand-pair 190) + 50x50) 
    'image snake ((rand-pair 190) + 50x50)
]
view center-face layout/tight gui: [
    scrn: box white 300x300 effect [draw b] rate 15 feel [
        engage: func [f a e] [
            if a = 'key [
                if e/key = 'up [direction: 0x-10] 
                if e/key = 'down [direction: 0x10]
                if e/key = 'left [direction: -10x0]
                if e/key = 'right [direction: 10x0]
            ]
            if a = 'time [
                if any [b/6/1 < 0 b/6/2 < 0 b/6/1 > 290 b/6/2 > 290] [
                    alert "You hit the wall!" quit
                ]
                if find (at b 7) b/6 [alert "You hit yourself!" quit] 
                if within? b/6 b/3 10x10 [
                    append b reduce ['image snake (last b)]
                    newsection: true
                    b/3: (rand-pair 290)
                ]
                newb: copy/part head b 5  append newb (b/6 + direction)
                for item 7 (length? head b) 1 [
                    either (type? (pick b item) = pair!) [
                        append newb pick b (item - 3)
                    ] [
                        append newb pick b item
                    ]
                ]
                if newsection = true [
                    clear (back tail newb)
                    append newb (last b)
                    newsection: false
                ]
                b: copy newb
                show scrn
                the-score: the-score + 1 
                score/text: to-string the-score
            ]
        ]
    ]
    origin across h2 "Score:" 
    score: h2 bold "000000"
    do [focus scrn]
]

23.2.3 Обфускация

Ради интереса я создал обфусцированную (нечитаемую) версию программы-змейки. REBOL - настолько гибкий язык, что можно создавать невероятно компактный код. Я смог втиснуть указанные выше 2030 байт красиво отформатированного кода в следующие 771 байт чистого REBOL:

do[p: :append u: :reduce k: :pick r: :random y: :layout q: 'image z: :if
g: :to-image v: :length? x: does[alert join{SCORE: }[v b]quit]s: g y/tight
[btn red 10x10]o: g y/tight[btn tan 10x10]d: 0x10 w: 0 r/seed now b: u[q
o(((r 19x19)* 10)+ 50x50)q s(((r 19x19)* 10)+ 50x50)]view center-face
y/tight[c: area 305x305 effect[draw b]rate 15 feel[engage: func[f a e][z a
= 'key[d: select u['up 0x-10 'down 0x10 'left -10x0 'right 10x0]e/key]z a
= 'time[z any[b/6/1 < 0 b/6/2 < 0 b/6/1 > 290 b/6/2 > 290][x]z find(at b
7)b/6[x]z within? b/6 b/3 10x10[p b u[q s(last b)]w: 1 b/3:((r 29x29)*
10)]n: copy/part b 5 p n(b/6 + d)for i 7(v b)1 [either(type?(k b i)=
pair!)[p n k b(i - 3)][p n k b i]]z w = 1[clear(back tail n)p n(last b)w:
0]b: copy n show c]]]do[focus c]]]

Приведённый выше код представляет собой полнофункциональную программу-змейку (вставьте её в интерпретатор ...). Я создал его, переименовав функции с помощью однобуквенных меток (r: :random, p: :append и т.д.). Любая функция, которая использовалась в программе несколько раз, была переименована. Я также удалил все пробелы, окружавшие круглые или квадратные скобки. Разрывы строк включены только для того, чтобы код поместился внутри этой веб-страницы - в противном случае они не нужны. У такого способа обфускации кода нет особой практической цели, но с его помощью можно произвести впечатление на всех ваших друзей, которые не знают REBOL :)

23.2.4 Стрельба по космическим захватчикам

Ниже представлен чрезвычайно простой вариант идеи классической игры Space Invaders. Сравните схему кода этой программы с схемами предыдущих игр и снова обратите внимание на аналогичную структуру: определения встроенных изображений, создание блока игрового поля, отображение рисования макета просмотра, циклы событий по нажатию клавиш и времени, вычисления координат для перемещения игры изображения и для обнаружения коллизий и т. д. Обратите внимание также на код "system/view/caret: none" и "system/view/caret: head f/text" до и после каждого "show scrn". Это стирает текстовую вставку (небольшую вертикальную линию), которая появляется на лице всякий раз, когда вызывается функция "focus":

REBOL [title: "Space Invaders Shootup"]

alien1:  load to-binary decompress 64#{
eJx9UzFLQzEQjijUOognHTIVhCd0cXJ1kLe3g7SbFKcsWQoWZ7MFhNKxg0PpH3Cx
WbKUqpPoUNcOPim1Q+kPkCJekvf0NTx7cLl7d8l33+XywvL+FrFyhVpCPUY9QN0g
LnG7ScjjrtM98iedToeM3kbW7/f71k4/p6R+USe9Xo/UqjUbi94jMhgMrL/8XpLm
ZZP4spPyzxVTT35MM2Zir4vFYu4dM7GP2M483Fa8f8w0O/Vy24yzo8RXipfJmdb8
kJxwrdJ7K4gxiSs7/09czYpdW6vcsI+AtrEKQ7ScDPlLHO/aNQ8huzaVeSDaHrNi
3IlBjDI6mqVsWvIA0E5ZJ2OtlUIuAKHmqoS5kHOt9UPMP0sm3TU5PHdHQVIZMs3v
qZTPmrMAQAj6ZXOSUtkwPKRwloKQNlexCDOvR4fpclGq76KNzC2mQPiG681i5gAw
ZusVJEAh5JojBzrEGQYC2dncuh7+y83d7ASVAu8MpAQqkT9+3Gg3Q+wHI2AZSAFm
1+99FzMQkzllVUxeTFUrc4vC4Q4VV4wlLyaerjD1XPe+tLxK8SNbqTrJOIf/Bd4X
V+VU7AfjSm0ZEgQAAA==
}
ship1: load to-binary decompress 64#{
eJx1Us9L3EAU/rTbMdHE9VlYukSQFhUpvXjQXrfizR8XkYAnt0oQVsTLGlgEFdSL
l3jqpXgre6sEJAgDsidPIul/sHjopeIfILj0JdnV3ez0y7zJzLx533vvY2YXhzOI
scs2yfaF7QNbDxLHjzfAu4HEhpKryAgDfccC4rfAws0IjF/96HsWyH7XMNXIon9Z
QJvWsNQw8XZP4NPlKD73Whi/HcZO4z207fv7jyo8/jk4r1TdFQXcSrV+flEtq3x2
5amuB44lyU+BpHRKHq4dKXnZCbLkxl9kOF5BarPVDFWyBAWcEAVFsjrhENGmhyPK
UXe+XNHf9HqZW9GgyzUUoloqXcXE1wv6iSTHohSkQ8yJQ5l2RCiSvPIGbaVkTFuu
Ge5/erfdurb+wM3ETZHPyjaX5NzNHPATOHMsn894sMZJWX4uH78OYSvTrUU+paI2
q8nQl5JHMFaSOZLBbHPnoR2ndHUa5NtPwubfFKziT1YqRDdY2VV3JckT3X2ZIlwW
KQjmUxGhGQ0Ecm5OlhBvUsSi/NpXmjLRoFx4YWuL0789fN24m+jsK2x+wGE+JjLR
DePiqdbKZqZojf1qLZ2ptdO3ZrxXwjCODzuThK3Af4EF8jYSBAAA
}
alien-fire: load to-binary decompress 64#{
eJxz8o1jZACDMiDWAGI+IJYFYkYGFrD4CyAW5oZgAYhSBhZmFoaWphaG48eOMwQF
BDFoaGgwPH36lGHZsmUM4uLiDFk5WQyzZs1iuHHzBsOfv38Ydu7cyWBhZsFQXlrO
EBEVATTBaWlolAoDA/vp3bt37wHyZwPpTUCaedqpUBWGS6HLMj8AedpA0Z1QGqTK
KXrNtCdgF/BLtrCD6GywOAPDabA6BobCTAMwXTfzFMh8uM7ZUBpi/p3QZdMMwLp2
796GbH7omrR2sH6Omc+h5m4C09pQuiKzHWp+O1R+D1QeQjstPQINIwag+wBUhlwj
XgEAAA==
}
ship-fire: load to-binary decompress 64#{
eJxz8t3FAAFlQKwBxOxALAjEjAwsYHEXIBbmhmABqFo2FhYG9l4eBvajbAwKSTIM
/H8FGFjUOBg4tnEyGP1VYWAXZWOwadNg4KhiYdA5JMLAacbJIHNLhUFnkgiDIpMg
2IyDd2UYVMqdGNLLyxoOz7RpCJ5p2pDi4sYAwlFpSz+AcEoJkF8O5KstZWhUkvig
4uLEoAIUO7f7zQcA8m8lvboAAAA=
}
bottom: 270  end: sidewall: false  random/seed now
b: ['image 300x400 ship1 'line -10x270 610x270]
for row 60 220 40 [
    for column 20 380 60 [
        pos: to-pair rejoin [column "x" row]
        append b reduce ['image pos alien1]
    ]
]
view center-face layout/tight [
    scrn: box black 600x440 effect [draw b] rate 1000 feel [
        engage: func [f a e] [
            if a = 'key [
                if e/key = 'right [b/2: b/2 + 5x0]
                if e/key = 'left [b/2: b/2 - 5x0]
                if e/key = 'up [
                    if not find b ship-fire [
                        fire-pos: b/2 + 25x-20
                        append b reduce ['image fire-pos ship-fire]
                    ]
                ]
                system/view/caret: none
                show scrn
                system/view/caret: head f/text
            ]
            if a = 'time [
                if (random 1000) > 900 [ 
                    f-pos: to-pair rejoin [random 600 "x" bottom]
                    append b reduce ['image f-pos alien-fire]
                ]
                for i 1 (length? b) 1 [
                    removed: false
                    if ((pick b i) = ship-fire) [
                        for c 8 (length? head b) 3 [
                            if (within? (pick b c) (
                            (pick b (i - 1)) + -40x0) 50x35)
                            and ((pick b (c + 1)) <> ship-fire) [
                                removed: true 
                                d: c
                                e: i - 1
                            ]
                        ]
                        either ((second (pick b (i - 1))) < -10) [
                            remove/part at b (i - 2) 3
                        ] [
                            do compose [b/(i - 1): b/(i - 1) - 0x9]
                        ]
                    ]
                    if ((pick b i) = alien1) [
                        either ((second (pick b (i - 1))) > 385) [
                            end: true
                        ] [
                            if ((first (pick b (i - 1))) > 550) [
                                sidewall: true
                                for item 4 (length? b) 1 [
                                    if (pick b item) = alien1 [
                                        do compose [
                                          b/(item - 1): b/(item - 1) + 0x2
                                        ]
                                    ]
                                ]
                                bottom: bottom + 2       
                                b/5: to-pair rejoin [-10 "x" bottom]
                                b/6: to-pair rejoin [610 "x" bottom]
                            ]
                            if ((first (pick b (i - 1))) < 0) [
                                sidewall: false
                                for item 4 (length? b) 1 [
                                    if (pick b item) = alien1 [
                                        do compose [
                                          b/(item - 1): b/(item - 1) + 0x2
                                        ]
                                    ]
                                ]
                                bottom: bottom + 2
                                b/5: to-pair rejoin [-10 "x" bottom]
                                b/6: to-pair rejoin [610 "x" bottom]
                            ]
                            if sidewall = true [
                                do compose [b/(i - 1): b/(i - 1) - 2x0]
                            ]
                            if sidewall = false [
                                do compose [b/(i - 1): b/(i - 1) + 2x0]
                            ]
                        ]
                    ]
                    if ((pick b i) = alien-fire) [
                        if within? ((pick b (i - 1)) + 0x14) (
                            (pick b 2) + -10x0) 65x35 [
                            alert "You've been killed by alien fire!" quit
                        ]
                        either ((second (pick b (i - 1))) > 400) [
                            remove/part at b (i - 2) 3
                        ] [
                            do compose [b/(i - 1): b/(i - 1) + 0x3]
                        ]
                    ]
                    if removed = true [
                        remove/part (at b (d - 1)) 3
                        remove/part (at b (e - 1)) 3
                    ]
                ]
                system/view/caret: none
                show scrn
                system/view/caret: head f/text
                if not (find b alien1) [
                    alert "You killed all the aliens. You win!" quit
                ] 
                if end = true [alert "The aliens landed! Game over." quit]
            ]
        ]
    ]
    do [focus scrn]
]

Я создал версию этой игры для своей подруги, используя изображение моего лица для ship1 и изображение её лица как alien1. Его исполняемая версия XpackerX доступна по адресу http://musiclessonz.com/rebol_tutorial/corina_invaders.exe.

А теперь сделайте перерыв в программировании и поиграйте в несколько игр :)

23.3 Случай: фреймворк игральных карт с графическим интерфейсом (создание клона свободной ячейки)

Насколько мне известно, в REBOL нет существующей игры Freecell, и это моя другая любимая компьютерная игра (Textris получает всё большее распространение :). Этот проект даст ещё немного пищи для размышлений о полезных методах графического интерфейса. Вот мой первоначальный план:

  1. Получить изображения карт, сжать и встроить в код REBOL.
  2. Написать код для отображения и перемещения карточек по экрану. Это будет похоже на то, что можно найти в примере Guitar Chord Diagram Maker, представленном ранее. Мне нужно будет щёлкнуть и перетащить изображения по экрану. Я также хочу, чтобы изображения "прикреплялись" к другим картам, а не свободно перемещались.
  3. Создать красивый фон макета графического интерфейса пользователя для игрового поля.
  4. Разложить карты на игровом поле в произвольном порядке по 8 стопок.
  5. Разрешить выбор и перемещение карт в соответствии с правилами Freecell (т.е. карты должны быть размещены в порядке убывания, красный-чёрный-красный-чёрный, стопки целей должны начинаться с тузов и подниматься по одной масти и т.д.). Эти правила могут обрабатываться серией условных оценок, которые запускаются каждый раз, когда карта перемещается. Этот шаг потребует наибольшего количества мыслей о кодировании и, вероятно, потребует подпрограммы.

Чтобы начать с первого шага, я вспомнил, что видел карточную игру REBOL на http://www.rebolfrance.org/articles/bridge/bridge.html. ZIP-пакет в этом месте содержит все изображения карточек в формате .bmp в одном каталоге. Я загрузил пакет и написал небольшую вариацию бинарного средства встраивания ресурсов, представленного ранее в этом руководстве. Он просматривает все карты в каталоге, считывает и сжимает файлы, а затем добавляет каждую единицу данных в один блок с меткой "cards" (карты), который создаётся для хранения всех изображений:

REBOL [Title: "Card Image Embedder"]

system/options/binary-base: 64
cards: copy []
foreach file load %./  [
    uncompressed: read/binary file
    compressed: compress to-string uncompressed
    ; There are some other files in the directory that I don't
    ; want to embed.  Limiting the file size to 10k weeds them out:
    if ((length? uncompressed) < 10000) [
        append cards compressed
    ]
]
editor cards

Поскольку карты читаются в алфавитном порядке из каталога, мне нужно изменить порядок данных карты, чтобы они восходили в следующем порядке: туз, 2, 3, 4, 5, 6, 7, 8, 9, 10, валет, королева, король, в каждой масти. Я также добавил некоторую информацию после графических данных каждой карты, чтобы определить важные характеристики: имя карты, числовое значение (например, туз = 1, валет = 11 и т.д.), цвет и координаты положения, в котором каждая карта должна быть помещена на экран, чтобы начать игру. Это обеспечивает хороший объем данных, который я могу использовать для создания других карточных игр любого типа. Я сохранил приведённый ниже код в текстовый файл с именем cards.r, чтобы потом импортировать его с помощью функции "do". Это сделает мой код игры коротким и читаемым:

REBOL []

cards:  [
    64#{
    eJzt1z0WwiAMAODoc1Wu0OfkOVycvIOncOZoHTwQk2sMBAehL8mzqTqYFlH6Pegj
    2J/j+b6FElcqByonKhcqK9iU9kjHbzsurxHLDjFylTf6Mo4j1bkFyw6IXOUtN9HH
    vu2qi/UwoBZpCKpBcDDBxyTwMZCChyEBquH8iSanK2iGh5NMyp3AfPMccb4x5QIM
    ufAxkECfQwB9Dn0MHQ1q3t3WfB3xb75joGvqTUmjaEiEVrUG8rJqGpufqd4jPmGQ
    iXg+1FHeUDSmOUzt2SxonHI6FX/zW6bP4luGL/iiSf0fajFTb4iymVjlyxnLPGth
    M/VBaLapD2aK6S6AvZm44vSmDCcbVFJqNk5rnh/sPYwSJmN5J7K8Wz0AAI/VC/YN
    AAA=
    } "ace of clubs" 1 "black" 20x20
    64#{
    eJztl7FRxTAMhgVHC14hR8UcNFTswBTUGS0FA6miNbKd49nSn0g5KC1Hzy/yF1vy
    +SLl9f37kap8ir6Ivol+iN7RQ7WvMv711HSUtV60rq0rTf5s2yZ9seR6Uc6tK62Y
    5OdZT2XkflmyJ7wkl8n0d4ZrDOeMuJxcJnOA8f2pw67PkBkt5c4wNdpxIsPUaDuf
    UeyaQbErBu7h6A9kKJWmbXoWojEyw+xHL92e8RkCexhhxB/uI0OM3HNnIyZ4fjpL
    i/J8D48YxbuMjCb/zB+cw8vMvuJkJjOZyUzmCsP0L0x74Z8yDLKsw7Dl7Tww67WE
    eHsG5M89sf6uxGY1xejcfYnhPp8fMJ1EapKj2macG+4hqq4sk9lnks+wKhqRP6D6
    tEzkrIYKZHYZijA2WsOAaIE/joSYyDdR5NvqB5uyj432DQAA
    } "2 of clubs" 2 "black" 100x20
    64#{
    eJztlzF2wyAMhpW+ri1X8OuUc3TJ1Dv0FJ19NA89kKasREDei4V+R4obv3QImNiR
    P7AEQsDn1/GNavqRspdykPItZUevVT7K+9/3VnQa60Xj2G4ly8M0TXIvklwvyrnd
    Si4i+fnomzLpZRiyl3hILpPp74yok+7BcGKXyeTrIw25DNd+N4ySEGRqTaWOZaq1
    NzLI9o6Bfaj1gXZRKrmX9a0QqZYsc3a9dKnjM/VxBSP68NwyxMh/nsmICfrPTNKs
    vN6HS0zHu4y8TQGfD/hzhDl/8ck8hOF+5rixBUq0P0OmnxeI6efXlkwkbkTiT6wP
    jTYbMncaU5SezP9i2Iz0KqYF/KsMg9niMNAP+3agH7YF8VIHxha1ni/EFrVWL8SE
    CMPz9Xzb2AJ2RYYBuyvLZHaZfDOD9WFdE44pqGm/5SBtLLx9dmoHEo+x1q5i3BRi
    ImeiyNnqBBVpT9z2DQAA
    } "3 of clubs" 3 "black" 180x20
    64#{
    eJztlz1WwzAMgAWPFXyFPCbOwcLEHTgFc46WgQNpYhWSVce/WCp0tB3Xrvw1kiyn
    cl7fvx8hlk9uL9zeuH1wu4OHKN95/utJW132eMG+ayeVB8dxcC8SihcQaSdVRPzx
    3N6qK/fbRlbBLZgMwf8ZDA4GPExwMOCwhwGTYYtKphxHY0UVVEzUffJxomMA8hcE
    /UG7Pn9loFnDgT0tA0FqwqV2/qJuqFNtoTkPCvmFhzamYwaqmHoYliNme0LQ6Xpv
    QEDKfpEEk8S9MI+p6pyvYc/0xcOQmG7vQ9dzYTMXjYtZzGIWs5hrGISbMPqHP2Ww
    yLUmM8rv3X36c0u2JybEYa7MfqWcN8i5NTPO3VcxmsjmDBXyZM/wTGKcbfDU8Nsa
    nszorJV1Ad2AweSOFdPGxzamCOTZhzaD812YYmoznmfHYbNZXIznncjzbvUDyCYa
    mfYNAAA=
    } "4 of clubs" 4 "black" 260x20
    64#{
    eJztlzF2wyAMhtW+rg1X8OvUc3Tp1Dv0FJ19NA85kKauVEIGDFKAuM5rhwgTiPwF
    fsBG5O3j+xmCfVF+pfxO+ZPyAzwF/0z3zyfJpc3hgnmWghNVlmWhkj0+XOC9FJzY
    RR8vdVPKHqfJ9wwn12U8/J4hOe4IhhQfwuAePds6grgqPwJsG3AWA5C/IMgPoNK8
    m6k0W3qCLzPgOEWckxovygOVut30nCsb/8rDOk0dJjiuYsiPmPU4J7cLhmro87i8
    Yyk8vM6aSp/tOdSMthHGs/SBZ7XSuZNZe7wzf8OAcmkGofbUTHySW0x6Iy4z+c26
    PVPtGDZT7jw2MzSHWKu5IXPQmlp2Z/4Xo1dxFyMbfpNB/UJdZqz4rtoxzi1JTwiI
    ZqzM44oxz4i5JWPH7qsYCWRtxm/8UY95JmmfbeJoGnOYzljWWSsxfC47gPEr09IT
    meaa6l2pNGGaiGju2CgzpqfLdG2I6Sqm6VR/05Sd4Acse9Xu9g0AAA==
    } "5 of clubs" 5 "black" 340x20
    64#{
    eJztlj1WwzAMgAWPFXyFPCbOwcLEHTgFc46WgQNpYjWS5dS/SHo07WOoHDep/MWS
    bVnO6/v3IyT5pPpC9Y3qB9U7eEj6ldq/nqS2sqYL1lVuXOhh2za6syamC2KUGxdW
    0c9z39Ug98sSLcElmEyE8xnkdpMBhy0CTAZNhmyZDPRM/Ywgqs5WGkPpIMwYgPIH
    QV44jOl8nvnTzTMELjvOZRgvSkCdzFaWy0OlzzzkaTIYaGLDw5AesfgTgjS3MQYB
    YxlXDOwKD89YU7Gpz+HIjOJhIrvuiNXOzz8y2eKNuTIz24NDP5Pc0uln8dwx833R
    MvP9dRnGkzc8+cc3h7N8eCnmoDX9XW7M/2Lq9TuDkYSvMlhvJYtR4rD0o8ShHIhG
    btnPPC235BNYzQkeRg6yq+SWfTTaXt571XJC+i47kFH9yYy6pvU7MxFGRcSWIfan
    SzPPGhM9jMMfmzFHdYo4XX4AibGWIfYNAAA=
    }  "6 of clubs" 6 "black" 420x20
    64#{
    eJztlzF2wyAMQNW+ri1X8OvUc2Tp1Dv0FJ19NA85kKauVAJiQMigpHamCBOI+AaB
    QDyfvn5fIcgP5Q/Kn5S/KT/BS9DP1H5+i7mWOTwwz7HgRJVlWahkjQ8PeB8LTqyi
    n3fZVSPP0+RHgpMbMh7+z5A5bhfG45BBN7bHwngQTFlHUBkEKPjUIBiA/AchvcAG
    HcCo9tQMOE4XnFMzX4wbah22GDlXCn3iIS3TgAmKqxjSI2Z7nIvNIOaFPs/LOzaF
    p+f6Po1j9tewZVqxMJ5NH+9nuQ9vZNKID+bOjHoGZT9abKn12n4WjH4uakY/X8cw
    lrhhiT+2NVTj4UHMFT6NrbpPc3dSI5na4zpTe1xlhKcOZYTHdab2uM7UK7zBoLRm
    X6a2UbenZlSfxoDfETrl7cJuM519mPvpxJZ4IQ5iy+XO68WWdAN3Y4KFiRfZfWIL
    rD1treHKdGPCXgy6sT2J6d4XWLRpEt7t7jAzA6PBmGlPy03MUEyM5ZvI8m31B3Qa
    a6P2DQAA
    } "7 of clubs" 7 "black" 500x20
    64#{
    eJzVlz12wyAMx9W+rC1X8MuUc3Tp1Dv0FJ19NA85kKasRBIkNRhLcmK7r8KECP8e
    SPD318fX5Q3EfqieqH5S/ab6Agfp7+n8+T3V0no5oO9Tw4X+DMNALfdEOSDG1HDh
    Lvo51kNN7LXromXYBZOJ8DyDkoPOUMjBZCLuxtQxIxQnmzHDeMzsVHOh7GflrMY4
    4inzQuDScn6ZpKfQcO4MZtFVjgxmMDLYEoa9MIonOyVDM4dRXsnh9IK+p2lOfQ2n
    zNQ8TOTQHTqMjuvLZvKMi5iW5mumpbGKaWp1I8YRz7aa34SZ0XydV0Pzk/VZaU/n
    7Y8YT8x27p41dOzFztowterS/H+8ln16njUvU0zyOJNu+CqD4y22GGV97oy2PumB
    aGj+9nTVNM+epVUPg7Cb5hFu73DzGsujqlqV97L1GE886p6irp5irl2YIqE244nZ
    NBfj+SbyfFtdAaNeJZ72DQAA
    } "8 of clubs" 8 "black" 580x20
    64#{
    eJzVljtyxCAMhpVM2oQreFLtOdKkyh1yitQ+moscSFVaViDMUyB2vE4msNgr/I0R
    4kf47ePnGXz5onah9k7tk9oDPPn+lZ5/v3Ary+p/sK58c5X+bNtGd9dj/Q+s5Zur
    rosur/WrmvK4LFYruBiVsXCcQT+HMUMuG5Wx+GcMQvYwGLXPkL8zGFDOHb1dGXxR
    GKMzdZwlf2zBILgqGYlhPRnBiAwG0VWGf5nC+JfdwjjLZP4Eo2RoZJPNiw03PWVN
    ecxxDFumLTOMda6rOixieIAJI97ESJqvGVFjJSNq9SRmwp9zNX8KE2WOYTuY1p+k
    eRZptgGaNQWDO9WLIY2EGDdfy6TA1J3nMFgERvaZbFe7PmPKkp25x1TBIRBjuIef
    s7+4FnEZHaOtO3Iba4NXpKMNVatTmv+Pe3kuR3XLLINwF4YT/pBBQeVdZhCfyIzi
    wweieFamee2nq3DmFoyWx2YYhOl8eFTzGD+H+hqLmWKoVX9RGFQZjN+LfX/2WQ7X
    lMfql99m6rwuMDhW/B33sjKOTYoblytIR+ey9g0AAA==
    } "9 of clubs" 9 "black" 20x40
    64#{
    eJzFl01WhDAMgKPPrfYKPFeew40r7+ApXHM0FnOgrNzWtKG/CTSjDFOmdFK+F0IS
    0vL++fMMsX1Tf6P+Qf2L+gM8xfmZrl9euLdtjj+YZx7CQX+WZaExzPj4A+95CEeY
    otNrr0q0x2nyo4aTGzIe/s+w7bsMArgh4/B+DEJjrcpArZOFnsEYz1Y4jOl9qNjT
    xQIhHJpQGM4npwiZwTXpOiEqGzBR2TVMkFyeKULD0J0dRgugCOHxnIzp+jwI+Z7S
    h5AGpzHZNU14NAarqPlguhMMxdD1NnYMVl7aYqCNVNJ9FROdlhgEleFLgJgDJRhO
    T3DomYVNZp3ZZzCdNUbN+Za5bc7fhClpLgSZ81IY1DHBjH240+7EWGweP7vFh4ZY
    nJwbw1w15bzJh5Z30P4u79cES20x1ajimD/Ww0pvX3s1pq/hieGCX2nyvmew1S6s
    aRi5NklGrnGVPXFBVNfK8lxpdVXW3IYZ5aqFWc04IedpP5X2Q5s5TzsumeZdrsZd
    2YHMwB5mdupYZjbb6YxMc8HgoNgftl5Yvoks31a/90iSufYNAAA=
    } "10 of clubs" 10 "black" 100x40
    64#{
    eJzNVzuO2zAQnQRpZoksr7BIlXOkSZU75BSpfQS2hAtvvRVbg0W2yIGmCrAJEObN
    jLQrUVTWQBZIaMuS6afHNx/N0B8+fX9LNr7geI/jI47POF7RG5s/4Pdv136sx8He
    dDj4SV+4uL+/x1lnmr2pNT/pS6fw8a6n2ozXNzftuSE38VlMo7/HiP7+AphGF+j5
    R5idMMQlZmZjzswp5ZTJpy22Txi9lOQYTjojPUYsD5iYKFG2eY6eJY8YW75R9nF1
    i7tyNEf3GJnlAsN5MqjHlFJs/k7hPcb1PGFiC1XXWmAo6sswQkIB16Go5ic9kw7D
    KDpgXnmWmNmxvhY4FFOigHMfQ8Zjc7uYYHpWGOgRtYwUA2MM0/FgUV1YKrntZhe+
    x7iNaSikPPIzFLOrxd6HOAJsN2wxrpUP/QTHUEka/jrxLPLQP08koXImygwtHSaY
    oHwKsAOYnCkl+GGNgTH0cFZMQ4odz8ykS68wFX441lMoAleJ8fS5YZh6/Kpug0Od
    h9qQh5SouR6zvcdk6KkFtk16wkBPzieiyrMey7EhTy1w5PFMzHHEo3oQ0oQHMetD
    NuIxu2plDlwTnNn7uTI8pzxKVDjDO5tY1HBOrqdW3FBTi31MNZI5WboGpBB4YosD
    TM16qw3wiGzXIiVqcg4UWPXEuOWJlCECQUcNquV4HvHECCLkJp4CkOJywAO18C4e
    TJgdwDPyDx4vKC2kmCYmerAWFkuqS6Ok6w7WEnmoUKuaIet4TgO7EG6E0m1HKcT1
    iAc3Tz6kxsdzHvBYilINVga0vm4xluqIrfIg3VKug7UsRZEYxdINdXzkQzgxIMGU
    B+m28SFVTU44CIkKIk/bDoP8xtMC78IDrmeLoRolJUvRB+SYP0ZbHmHWVJeY9njC
    xAMvoSaO9cjEo1dhxy6U3kmPEo15EGrXk5QIelA+pOMpyGTl0WIIcdn6aej93FAG
    1I0almgQ7bnrmEot6LFwM0pTnGvxjLEyDdtLtYZfKnk1X2J+XU88AV0FTY7q4+Zg
    xnjbgDzrfdoIUYi9wzxirP2oK9COZGqWc6daYcgxzZvuvEWJGx7Zx3hbVZfuY6w9
    W2iC9Xhvlt7JVxjlQevSxLemO+0IVj5UnqvbyXZtqJ0Pp9xoP5YY36GsMLqNkdt5
    X7PF6ByjzMkd+/5Ig77GWCtj65XIDWQHcez1IJN9i4eaYNs1fGxjKm6kMPZ8isk8
    pc9Cs8T9PeQC88fxP2KIXgbz3LgIc8l/okv+W/0GSJQzj/YNAAA=
    } "jack of clubs" 11 "black" 180x40
    64#{
    eJzNVzuS5DYMhV1OUCwvrzDlaM/hZCPfwadwPEdgylKgKyBVMZnAB0K0VZ2Yfg9S
    94hU7fYEGyx71B/2mwfgAQTUf/719XeJ9Q+uz7i+4Pob1y/yW+y/4vt/P+3XuF7j
    T15f9xc+8Obt7Q2v3OnxJ73vL3xwC09/zFSX9evLS3+2/CU/xXT5ARgB4AnGI8In
    mNx/Wsw30pDPmBObe865aOO6YI63LkDtGJsxHnVAF6QDYwk0+axhDpfybqvT1pKa
    YfuUixMGz56r1EaMy7sTZww/3ojJnf70qz9eJAswcAiYTqIHht/wAzBd/EaH8uSP
    76L1rtIdjwJjs893YXdMD6fpM9S6YGiKGBgrKcd2njFaSkRPY8lGDPxxXlVLRNer
    aJl48M80rEVjT2FMQp8859SrFIK9ZBjj9n+fZg1JI4bdJDB20XDXWMuyJcC2TWDs
    2D7xUJuatVWzbSmUKE8YR0CqRWRptS4bAyoyYeBKVoVsurS2CbzD88xTa/FiKE8V
    ZWypQcMBA2mYvdSoY2FNm7GCThhphiy5iZn7UiB5alrrxNNYP6YgIo+nZHXG0Eew
    aMEzdETB6wUDTUFhatIaEYCklCZbbLhJqoEPC0k3rInHnQoCY5Rvg5hy5RFZb1oX
    kDVAEslmHiRiLUYMclqpE0S8xFWrNF2QdEmlETrrDJpeWtLg0WQICq8zT72B3hbQ
    2AYFI8oRo1oLDoItiUXRaLnobcwXgsIhZ+CksKZrL+uIYVB+A09DmqCfqGcwjzzI
    jZAHFmmyqftaR58R1Nr55WFLlFtjHeqNTuL7QlQBD/h0rHk0AMQDfQzSUULRy9lR
    CdEaj6AllIUheXnEyCH+QumAQfkg8yMm7Unk+SJPsQhq9EcsigHlk/AW7nYWUJ54
    WEA16iuxwqqKX3iwqpDMWIWJbe/Ck7QiTVHvstF0nv3hQeGJkAgeaJ6kWR+o3BJO
    MM4fwkMlS0YZzpg46nGOkTXj/GljrfLUGRs2+wHQaA+aJ5+RqsbGyr7CIAtbTZ9y
    ofTROSFrzknRr1jNU61SV2Wba3iHRKANTzxZdM2+RLvc2MnzPusGDM6O64aIkraC
    Tt737TvGgxREnviJ/afmfSo+MGz3RK4d/+50jXMlDNwxMTY48GrXGJmk2SfMAxPj
    J0ulMe5wPB2TasQIS5AYTsL6mHgjDxoAjHEAOryaMTFWs7etQCJi6NWEifEcGBrb
    MfdJPmJQiTCGuxoP/Y47gklDVDoOOd3bh65MuSBpokPHrOv3O5R3DIjgTzj9mJ4T
    5vC5waGVvbxfMXE7hWGBzg0M77Su/uy3ZRwSwDAF/d3RAZMODBK32xrrZ+f5xj3k
    GfPd9RNiRJ5i+j3E72Gerg9hPvKb6CO/rf4HC5MFI/YNAAA=
    } "queen of clubs" 12 "black" 260x40
    64#{
    eJytlz2S2zAMhZFMGg4niyvspMo50qTKHXKK1D4CW46LvQJbjZotciBUmUkT5j1Q
    ki1RXjmbpS3/iJ+fQAAE5C/ffn0UHz9wfMbxFcd3HO/kg58/Yf7nQzvW4+RPOZ3a
    Gx/48Pz8jHeeqf6UWtsbHzyFl09bqW68f3ysR8Me9ZCp8v+MiIm+AeMeOGJMjxl3
    7BFj/6RzIwx6bQ9f4ziOpT1mEb1eF18NsxL54DefUNM1U6OEzJHcVGaFXvtQXSin
    yQo3UDeM/8oWSxemTba3QCEw5udeZMzmC0/20IVrBsMNwGLUzcAH0Y7hnOhk2Q2G
    +qqz8QphO2L89VUMFqK0h+bXjuHSjQFSmw7G6xJ3MjrrtA2sLYx67R/8RA98KEF4
    MXWT5+TcxCLkRGvasFvMIHWC7CYzZgmYTm647jEy5IzrhZDDFKyegdA5ea6mKBZL
    3ssfG84DEzpJERmDp/X2WhWnkfVACmR+Y0jHQCgnSMTYZHIW3TJiMGjEPDZYeeLA
    MrdMDTCoRJhzFt9nPZM0wiBs5xKK/HZ7eh2sHj/lXg6SJOzr2DBAhxdjwXhKsXQ6
    MHpM1IH/sA9DGsc1Q+9z8TAnOq6g1zpwF7IHvg1YWchk5p1/YZhusIc6Zeo68PlG
    BzuU9lyK3I6OUifjPGMZku9k6+3BvLZCFXKxttdXOq0DTuM8JBbUjU5mtXCtgrqa
    B+pYr6MhWUAKlYCQJuvtGREcMEI3Qwf24FfreEWc57VCgpuD20Phrc5T8kuWiLiG
    oVQEeRN3zwVJgXXev2ing9PMKhNkItoG9pHu63BrJc/WMMK6XkeZ5cj5GAuS4szF
    bxhtOpxn+nDxtWOM9tgohtTIyOhMV2wYxNJlkMy+a0YIdUxFSkEmTlvrDKGemVqT
    FxeWmVx2GM9YY5GiK85D3mVAVNY69AgLN5naaiareb7B4CuLLxuBhWv/sExfqrbO
    l5Trtf95aDriIOLUmoteMd42Vp3EewvFF2ZqeHM3WnR6ZulqbLSvYrytHjC+ZZdu
    7a1R9pml6/MmoGf2fGiyZub88TJhjZGXmKV0eEJd1u53IWvG76Wqbpmmb7nlqt+T
    1Uu8qiwxrbHdtU33dhfGdMkfm+7+xla/L8xVkduMhTm+HVV31yFzh44eCU3rOmDc
    h2/AHBhzL3PPf6J7/lv9Bas8HtD2DQAA
    } "king of clubs" 13 "black" 340x40
    64#{
    eJztl7EOAiEMhtG4qq9wcfI5XJx8B5/Cmddg4lVu8IGcTDphCy7ShP7x0MR45Xpc
    uC9taQmBw+m+dlkurHvWI+uZdeFWedzz/+um6Kv4/DjvSyeNP8Zx5F5GUn5cSqWT
    JkP82tWmlCyHIVlyG7Ymk1wHJvZhKPRhAsUeDBPBZEIwGUZCtJjsrskQG2Fnk5mn
    9GAIYDgiu6ZmDhOQw24MEg8yLyg/aWb+hYk2k7c3g5EVbDBlW2ozZVuazIir2lnN
    ZKQy9I6dXjFD+UHyDNULqbuWmfk9hgBGLSrNkFqcmtGL/HMMEg8yLyg/lmAMR0zt
    iDBGTontyTOjNy7FYLWQM6DBULRrgTBQvXrV9Gt1R+5EyN3qAeRtVIj2DQAA
    } "ace of diamonds" 1 "red" 420x40
    64#{
    eJztlzFuwzAMRZUia9orGJlyjiyZeoecorOvoUlX8ZADZQrASaHoFpHIH1BAWqCD
    aNMylGfqm1JA+fh52wWxL/YD+4n9zL4JW+mf+ffL++qtzXKGeV6bcvDNsizclp4s
    Z8h5bcpRuviy16GMvU1T9uw6fbhMDq8zFKPLxBiTy2TqYHw9PFKHZsiQCWQZCd8E
    Moxk5PFARu8uGakY8O4cpg4Ec9jDFKRVbTRLmEa1ZWycZBmlB+ZHM3BttEOVR7w8
    Y6adryeMmnd/reJ515Z+5f81mMEMZjCD+edM8hmKPqNqNQGGVK3W1R0wpKs7YOw2
    wejRtfubIV2IAdPk58/2JD+aq84nerqYsi+rB0uASTUD86znHc2X2VlqRBhvAUl+
    bHjLkMvEHsZmxDAgI0CPY11MzzdRz7fVHVe+QnD2DQAA
    } "2 of diamonds" 2 "red" 500x40
    64#{
    eJztlzFuwzAMRdWia9srGJ1yjiyZeoeeorOvoUlX8dADZQrASaHkRSS/QaKJgaII
    bUWJ8kxRFEnbx8/La+ryze3A7cTti9tTeunjM///87Y2KXM/0zyvXTv4y7Is3LeR
    2s9U69q1ow3xx4dWZeR5mqon5+ndZWq6nck5l3swVMhlavbtYUUuQzkjhsRUmOGF
    SHMs07U7TPfIwIC1s5pREfShYuC6GiKstvZ0NcJqYLPRUyyj7OHfPgNjQ07VLvH8
    jBm5XxuM2Hdsj5k6kDvlLvn1YP48QwFGZ6BbNzBjU9nNr98yu9WNHW2O+Cfk58h+
    hfbdyoP5n0zxGTKBZxkVwCjGSCUCilXFwJhXDM4dcyMGOUj6RozydNS9c20h9VCE
    7BEXbjH9YXKYrFimCmartowMri0ktcPaArTbuQIxJs3BTPYUaR/ewLgSYiLvRJF3
    qyuqXC8m9g0AAA==
    } "3 of diamonds" 3 "red" 580x40
    64#{
    eJztlzFOxTAMQPMR64crVEycg+VP3IFTMPcannyVDhyICamTv5OGNnb9ayPEgBS3
    /qnT913XjuT05fXrnIq8sz6zXljfWE/pvsyPfP/jYVEpYznTOC5DPvhimiYe8wyV
    MxEtQz7yFP88aVc7uRsG8uRzeHQZSr9nZgwwEGEwwEAgHnbkMhxRy2B7F+ujQDDl
    HaSxYwAaR9VQ+cn/IW0oJrtFbQD8lCnhgWVsTJn9diQM3w+68ZQ8eQyIetn5ieTZ
    r9eOMequ47Fl9tehys9t6UxnOtOZzvxvBn1G9KQbzNrbhD9smbVHWj1OM1avVIzZ
    c1U8du+W73XINNuqP9yTVGau26GjeCIM+fmpjJfn6mhDjHqt26pNUDLkLKBcL/SZ
    GXyGIoy3oJv8HDOOhJjIN1Hk2+oKvccYTPYNAAA=
    } "4 of diamonds" 4 "red" 20x60
    64#{
    eJztlztSxDAMQA1DC1whQ8U5tqHiDpyCOtdQ5auk2ANtxYwr4Q/xWrJiadhQLLNK
    HK+cN7IseW3n8P716LJ8xvIay1ssH7HcuYfcPsf3x6dSqMz5dvNcqnTFH8uyxDq1
    YL4dYqnSlZri44Wb6uR+mlCT0/SsMuguZwDA78Eghl2Y8Bt/fPu2KJAeDROAdCIy
    ySxXgPoc3TsbWhXGADSGVoX5LDIhmatMsl47I8qZya2rIaLodjxq/uQ4aQwdlxif
    XCtx5oyUL85IeUfLXKXj2hC/y//rxvwLJhiYdlZvMAFAZQA6Q5wJ7WJwAUMWDJkp
    CwYMGYud3Xy2xMcUZ0u+THnv5cZcH+N1JnSTqmfq5CT2fMvUPVLa4zgj7ZWMEfdc
    5o+8d9NxDZkyhL8+k5h9rkevEeNRjc/KjOKMPyfFUb4qY8n7pmTGq4xCmBmbPyqj
    9WRkLN9Elm+rb8X3AoD2DQAA
    } "5 of diamonds" 5 "red" 100x60
    64#{
    eJztl7FSwzAMQF2uK/ALOSYmPoKFiX/gK5jzG578Kxn6QUzceRKyVV8rxZbEteW4
    XpW4jpwXy5YlN3l9/74PVT6xPGN5w/KBZRO2tX3G+7sHKlzmeoZ5pqoceLEsC9al
    BeoZAKgqR2nCnyfZ1Urupgks+ZoeTQbC6UyOycFEhy3syGSyyaAtk4mCybwHYoSt
    Mk0QmmDqNEFogqmmQWicwQePOmoaH3Of4X4ujQdjTGtMZq2kRcEM+0lgjQcvbIav
    Rdc/hbX8LJjuekmmt+7giB8+r5Gks+TXjfm/TDd+BNOLQ8F041kw3bzgzCC/fs1c
    et+4xJg9/nH52bNernUfyo25TibZTI420yJXibHcMkCJ1cZoMd8YNXcO/8DjHMxx
    1brO07Tv/A/2ltzehpTx0BMGU97LLP8Qo+4te0bdWxqj7S18IB0hRkXIliGF0S0d
    +VlnwMM4xmMznm+iF8e31Q9Nm/Jv9g0AAA==
    } "6 of diamonds" 6 "red" 180x60
    64#{
    eJztlzF2wyAMQGlf16ZX8MuUc3TplDvkFJ19DSau4qEHytT3mKiAEBAIpLhOXofK
    JgT5W8hCyM/vx+9XFeQT2gHaB7QTtCf1EvQzXP/axYZlDqea59j5A/4sywK917hw
    Kudi5w+vgp99baqR52lynJynN5Zx6veM1VpvwhjHMkDwPksYmKxkLL5IMtqgSQzB
    wGOWZtJIlwxEojB0HZWMj1Y2lEe3Ml6ZJ0OjxFikjSNdMV07hvfHCnz21pj4VPlD
    xrnOMWq9aoZcd+wPLei5emI22V//zN9lyPypGLJuYKZTN5ygbjhBTbiVuXfduIfP
    kviI4ixZL9G6kyjN4ClJBrtOMzgEJINDuZ7BS0syOCnW29nMZ0l8RHHurBdWkutu
    G+Mtk5wc5Ji9vu3RnChXEzPK+cQM905+A/f3oNWNtt2n5mL8EbVF4o+YYWqLTrdm
    pKktNt4xrC0XZlhbLDLQSogPk2RCRnOThdxot8IahhURI/kmknxb/QDHNOZR9g0A
    AA==
    } "7 of diamonds" 7 "red" 260x60
    64#{
    eJztlz12wyAMgGlf17RX8OvUc2Tp1Dv0FJ19DSZfxUMP1CnvMakCosYSMlJ+nKly
    CAZ/FkII2d5/HHahyBeWNyzvWD6xPISn0j/i9e/nWriM5RfGsVb5wJN5nrHOPVB+
    AaBW+chd+PcqVTXyOAxgyc/wYjIQrmdSjCYTY5xMBtLdGGlz4lerHmEztpajTMpY
    RS2IlmCKWhAtzuCNC0XU4vboDJ9X7jwNxlrElBv/FPGWqWcCyx48sRnuZ9U/mbX8
    LBh1vSSjrTs4YpXPa0Ud88+KWVkahk9PZ7ibVIa7+3KGL7/K8MC5XM/NbPb4x+Xn
    /nolrRKMFoeCUeNZMOq+4MzK/jqbcecN6tXzhgiK83LLFvPy+NC1Fp41dcWGqk5l
    /nPC1czWOYF3JolkJjXKW4aMVJ9NlUk02U4cEtOLZ2K6++L0BF7fXyk2ve0enI7K
    N30nIXuiaU+9w2Dye5nln8p088aR6eYNUttZdy06uSzGugvT7qiG8dhsiovxfBN5
    vq1+ATh8w+n2DQAA
    } "8 of diamonds" 8 "red" 340x60
    64#{
    eJzdlztSxDAMQANDC1whQ8U5aKi4A6egzjVc+SopOBAVM6qEP3ESfWx5w+4ygxLH
    K+dFliXFO3l5+74fknyE9hzaa2jvod0Md2l8Cvc/H3KjMqVzmKbcxSP8mOc59HEE
    0zkg5i4ecShcnrgpIbfjiJZ8jY8mg8PvGXDOZJxz3mQQ/owBcjdr3Oeg7S34hdmv
    PYUCmZYuG5PMItPSZWXCgztDRaNx1hkkTBzcJiNaYdKDqyGqmXa87Q90+eyt+ETW
    ijNj1HxxRss7dtQqXVdN/FneryOMui7GaPFhjBpnxqj5okwl7yczl67nS/i8RgSI
    RpkS2bVIUTLsppovNomed8X14wwNpcrQJBy3Y/kDhGnVfJmqVfO0Yww10K556pbO
    SO1/1LzUTttbevaorr2uKudkvM0Ar1+FKdFqrAuc3AWQxacwrTgXppmv7R+4nndw
    YlTWhl+MX6GeoctnZ/ucY2rUMyyTbQivZ5A7DvJ6dnLXRl7PLs9Vl2szYtOWDA2M
    zpjSxfR8E/V8W/0A+lG2V/YNAAA=
    } "9 of diamonds" 9 "red" 420x60
    64#{
    eJzVlztSxDAMQANDyecKma04Bw0Vd+AU1LmGKl8lBQfaihlVRnbirPWJrR1YdnDW
    m8h+UWRJdpyXt6+HIZcPqs9UX6m+U70Z7nL7RP2fj0vlZcq/YZqWUzroYp5nOqeW
    mH9DjMspHamJ/g5SlSq34xh75Tg+dZk4/JwBiNBhECB0mYDXY5B3mwyEmsmSZGic
    tZosSQagVrRIgqEbK0WrJHxoMiIWqfH0MCYVJt+4KeJSV0+IPXvoos8kbdo/WEuJ
    Nfy8kuVRYMRLoUbc+SOFPZbpYlw7DPPPaXTAE0kxPAg209ODzB40mc2xsEmSwRKg
    whoM76yt0nHnj7QZbjpjLp3Ppj1aOosx57tkUOSW5Wdr/VGMsY4pZrf8c8YzdocP
    PbFwxPS38ucqOd+Yg5657FkTXGuLZ41yrXWGC0yGO+9MPXUTajWJQegzRbf5bloY
    LDda7zjBoIlyBqx3LrennasIqlXnRliVXz6faT+Vt1Ute2jHtW6rmkyE2J7vhUEQ
    sY2aaa0/G9NYxzZmt/w5ozJYMygni2Ka3X7G803U/7a6H78BawWaX/YNAAA=
    } "10 of diamonds" 10 "red" 500x60
    64#{
    eJzNlzGu1EAMhg2iiUYwV3ii4hw0VNyBU1DvEaYdrVCu4Daa5hVc4lVcwRVShFD4
    bU+yySS7byWeBJPNy77Jny8e27GzHz//fEs2vmL/gP0T9i/YX9Ebmz/h/Pd3vm/H
    yT50OvlBN3x5fHzEUWcm+9A0+UE3ncKf9y1qN14/PEzPDXmIz2om+nvNmPsX0Uz5
    Dnv+keZKGOJaU2nSjX3WTx6jT2dsK02vl/Su6dN0pBmzgZSep3GeznmtyerSqcs2
    HCPRHL1ofuf8bWW8rySuD3F6wtU/9Epczq4RvqXhi6a/aEYzYwpEQtDwrNGVzBo3
    tQ8UsZkmmuYJ8JYT7ZxqSDW2ksaelSbgnnbpDY0UxGvLcf+EKDRrqAxbe9zPFOra
    2dzZrMviJSWIc4o9w8PWPxZ3qefAKXHnZ4+PFO5h8hiI9aYybDRd9GzsctK9hCOO
    GRkieWZYmu446tiIaJFLhFI84FAhuNazR6hD5HYcSiVa1E1CCTlELSdjNlAiwSDF
    YLScFBgRSslvppi04wSEkCX5UEyHWzacgmWxDDYcwzI1HKRfIVlhwp6DVOCqqRgk
    fsspHKtGMYFCl3b2cITmYk0JncSWQzPHMMk81vpHrZbZmjPM6XfxQn65RhcUzsZp
    /Zz18SO1ZqBrnDFoiBST4YNwzFEzNODDubOAHXAmzCZ1b+akobrGOeOcYjT66p8j
    zllBwAzqB/j5kANQUEwSJAFf5cAo0+i3odG4PeQY1XCbz5HgHzZMh7AywolEaDgE
    P4cFoxykU8MRYl6sgcYyruXoghZMksBxZ4/IxRq1B0kQdvbIGpNQ6vEwtf4ht8af
    wPpA7p53w5A/yynQPlfJSkBaakKI6q1d/THMXFvgLI4HdUyLxlyjUNHiUR2z/ua1
    DpXoSj0MJXDOqiOf7vYcLbmdwKGpWP/yjtdwULsVj0Cwt/KLpldOPcfoJfIrsLfy
    RWNtg91Ib5JqsbXyRaPtpyvaZsU52sW8Vc0ab2Pw2cyJqvGWN2u8HbKEeF3jHNaF
    XjS1BW/sodqssXZ9/OZW3qzLfAaNWNRbjrVD15CvHUvc2nPx86TV4oYmVk19DWv8
    A48u/dRNzXpq4+dR/6vRn8Yen1Gv2MQLc1Uj9X2tT97KjzTdmOurn996nRvQXH2H
    XGlujv9Ro45+Cc1z4y7NPb+J7vlt9QeRdPNP9g0AAA==
    } "jack of diamonds" 11 "red" 580x60
    64#{
    eJzNlztuXDkQRWuMSQjCri0IE806nDjyHmYVjrUEpoQw4BYqNZgo8CYceQuMBujA
    oO8tvu5pVttuBQ5MqX/vHV3Wn6237/97Lb4+4PE3Hu/w+AePP+RPv/6I+5/erMe+
    Hv1XHh/XC3/w5vn5Ga+8Mv1X5lwv/OElPP0VpW7Wq4eHeW+NB73LTPkFTK3tHnOq
    td5l2vxtmR+kQa+Z/9WGqeqouWPpDdP8XbaBVbP0bjMyp7oYUZlLh8xVDHVFnVvJ
    mLp0FJevcqHza63/ugyuDE3SnXHxC/MZql/IFKqlYtmZdstgqwKDdJS89qInZ4bG
    wbxhqciQ5RgZbnZmHKktSypzcLPD5s8QDzpZqjNKg+C7exLsORhEEQbdxOeKwTUZ
    VQy52HX8E0yuxbM2U+Femz0rzmQyXJ8JoZbo18oXXc8KewqwMkN8Vt7BKLaYgqdR
    9SiZvX4QZjAiyIMwUpHhOzCIcu98VqkamJFNWYR03IfKQGJ3Bltkgkwo9FjLQouu
    GGSadccKG7IAWJZ2xlikk7Eh4CQd2Bg4MmkT7/mCTN4ZFaNrRF2EZWZ+7YrB36OM
    xWz6XgBRtTnoIKg0IftQlYxIWwp7UdzN7Jy2A0iCUNAZbJthBRBWNy1iKegoAy1W
    2HxMxgnmxL1gTRfa6TrWCvaKfg2moyt1aPIJBS3Br7msaOw9aVlbKRwyuw53y4lG
    ZDDUMYlx9qBYlYtO1qAjlLFiT7Us+nTKEnT8Rho59Y/sV+pY1HGZ1nIi0T9CJ8t3
    dHI6Db8h9amYy9zEEO7AVvedA8Floo4V6Kj7zhhTZsa8W7notNLNrdkY/2u3B/2B
    +HBmcMDuNY8YN4YWQWSc0a03MaQRQ3xy4y2qBLUWmCQmTdwKKBS0R/ZS2HQK9ur+
    rQC1Jmwhz8/GQIglRm9gORjcD/FhAcrqcnaW8VwRiX6hm89NzBHEjrToV9ZLF8tg
    Aw3NFnKxOn2NBEUh07GdSS7j1UgSU4ajZs97KgvwK/CPXvToe/WNJh+AOFpp0553
    zD4OShwSDIEdd0LvVPrMHuUIsrlW6EGeW527dRymZNrGNJ+9GKLCcwlHDxk/bS8M
    P+E+hBINRm+Q8aP8wvD4wejFWB+8MiqZdZSfGf+kHG2IEQd15RxcR96Z8eOQI1zK
    GDwnv8MsHexlPEMO5jiCd3smopeYbTBI53GUB7/QMzAIx7+7HnVWfNh7xcus0PXd
    HjxVZzih6LuP98icFgOD1E83Mnt8PKJuc+YJ6hU2Q5wv8emdedVj4m758q9lfgy6
    YwOJ9zvthsmHjuqleCLzo++Q18xP12/IMIZ3mOOL6s+Zu+tFzEv+J3rJ/1bfAGnk
    dy/2DQAA
    } "queen of diamonds" 12 "red" 20x80
    64#{
    eJytl0tuFDEQhgvExjJQV4hYcQ42rLgDp2A9R/DWGqG+greRF2TBJbLKFWqFNFKQ
    +atsd7e7h3QQ8aRnJu4v9a5y59OXX+/I1jdcH3F9xvUV1yt6Y/sn3P/5vl7jOtkP
    nU71Q1/4cnd3h0/dKfZDpdQPfekW3j5sRe3W65ubcrTkhg+ZQv/PxHiJ0wswJeJ1
    xFymYwbajpnLP8n5Sxp4bY++S7aV8MpNOq/9Iv3ijSBPPmFrUuYyLYxjSRCkRMRi
    sxXMOoZgMotXotoBW6drTHGV0ML6HeP3amZZ/GIYobcFG1LKPTQ+jAxPkxjDzIXW
    zLRm4BvbEiFYrAuMmt4ZVscqU8pjMSRODHkPoz2dQVyaHDN98GtmpNuj7MCILfz2
    SJ3ZyEGcK8Pr+KzscUyzKmIpPc5rv5xGrqvSMLZ8reMzM7ahYM37Os4rOaKVoguh
    vM5YIkS1UYvDwJRmsu6K/cWOkbmGLYjK08DoPY20T5XyZLrH2kC2cF8yVXHJ5tmo
    i3AzM4pdEOWIskdB8sYvTjI3lQshUfQUeFurRL71l3MhMaUYA++YXj/Q44KHOkAD
    UyC9tz55SMqZrM9WjGC7NoY2LIwmhTa6og+ezaRExgByI+PCrU9JNIQ+KQM/cxoZ
    iMmw2rsYVEgi5MbnvV8EZWjngG+QANEj43y4hRbumUj4Fp3fMapCICcGGMQQc05b
    Xc5HNUiNNpOjy2MMXXRUGSsMr1k7346+646ZUhlLbQ6bGOKPAmGQWYDQkO7sek0u
    9RMi7PSabtiMWHqXzqMuj4JAXpPJqabvmQQxlYEgxNLKddTlM2n0NZ2kjCQPMZv6
    QbbVZI/CVmWSsjPRQ5wVgg4mK1gog5gNE3ytG251JgliZBNnhRK6oRcs6pVHRgtc
    yxwp4DaBd36xQtYxrhqUM+2YQgHSKYW4nHDyY2QweLSrSJ3RtscAED/Wjw4eqbHV
    OKGzs86RTX/p+NLCUMiGTNnlvfrSa7IdYm7jl41LO7zaoC47pg62NjD7oN4w0uYt
    TF+G+YYpdWiLbV1hJmNs+NvkvSJHb/bD2s6C+WxZmHs7XOthJMT9GMNhNTN2k+uh
    Jm8XVX7FXOoBXJkWB1Xld3Lmw7ozOq5ptKcd1o8LY2N28KvJEZLOcJmmTXzqQSyW
    rVr0A1NWjNVJO4V2tdrjY2NFp/faL3sM6nGuxBmddpWxfOk4BIHntiHO9ljW8i71
    ic26WfO/MKBad/r65GcnQi+ZysQnniF7vqby9FLmAHmunOlIUPPrgLEYvgBzYMxz
    mef8T/Sc/63+AJ3HlmT2DQAA
    } "king of diamonds" 13 "red" 100x80
    64#{
    eJzt1zEOwiAUBmA0ruoVGifP4eLkHTyFM9dg4iodPJCTCRM+3utiX8NPkNaY+FrE
    4BdoQSg9XZ5bw3GjdKR0pnSltDIbLrf0+30n6T0sn8ZaydJBX/q+pzyVRD5NjJKl
    IxXRx2FclYp110UUj24PTTQNjG9jgmtjXPAtDAkHjXPQEHEeGW4uawJVQo19bIZY
    yDgP750uGl5z6uelTCwYr1QR7h+qBpoQFxuLv5nPeGx46QIm/fOAkSUnb2TJyRqe
    CS5vmIwqqjDS1KixLxvVixP3FdC9D8tt1kzF3/yeCQVGTV5tgpq82ujJq0zQk3dG
    46GR5zwwMNqZqZlZY4aNGTBowGTcYR/KhjNvZOPawOinUZ3RT75KA6LIlLwTlbxb
    vQADV0S89g0AAA==
    } "ace of hearts" 1 "red" 180x80
    64#{
    eJztlztSxDAMQA2zLewVMlScg4aKO3AK6lxDla+SYg9ExYwqIdsDG30YaXaXzkoU
    Z5wX2bKVkfLy9vVQunywPrO+sr6z3pVD71/5+elxqJS1n2VdR9MOvtm2jdvWQ/0s
    RKNpR+viy5M2ZeR+WSiSz+UYMlSuZxAgZACghgxhgonnwyMl5uwwyG8CSEOaYUcQ
    +ghnQ4rBwfwaArK+t+cgGOu7YZw1vITp4vgFe78uZVAOBdXbd2GGV51fMMx+BYdV
    Z0/lTvgMAoUM1d19JlYzMe/GjzFEN/kGJzOZyUxmMv/EYIJRqdFjUKVhj9E51lgt
    R1T5HI1Vy+jM7TAIZnqNqaIoMiVAn3PVmRgMI+RG9cZftY0sMK5gWl0WMVUyZhX7
    vku/0PNr36Vr0R/G7rRlokDsDIYMZBgbVYbJxHwoKSbzT5T5t/oGyDsmgPYNAAA=
    } "2 of hearts" 2 "red" 260x80
    64#{
    eJztl0FOxDAMRQNiC3OFihXnYMOKO3AK1r2GV7lKFxyIFZJXxkmESOxfHDEjRkjj
    NpPKfeM6qe2kj88ft6nKq7YHbU/aXrRdpZuqX/X+211ro6z1TOvaunLoxbZt2heN
    1DOJtK4cRaU/99aUk+tlkUjel0PISDqeIaJ8CoYzh4xQ7I8aChkm8gyrj0X/9SjE
    qJLLWDp3LMONoYihgfFjdwyYQ8ugcVlGgD/SkHw0w+OjKKP3PpjRWdc/OKafwWYV
    vNPemz2GSUJGcnc9FasoNpx3eSJW5SQ5eGHOy+SYYYoZm12AYZvJgPFpGuUpZE5U
    E2bqz18zbhaDOg8Zs15ABsmF+X8MTzAueT3DLnk9Y/PJWU0HNjGOaotlUG0xDKwt
    bX3uzKDaUtf53syZawvbzQNgxk3IDlM3kz8zYhhcW76ZvdrC424G1paZ+JEAafGc
    Q4YiQyBWf8uEMsXMfBPNfFt9Aow0CNT2DQAA
    } "3 of hearts" 3 "red" 340x80
    64#{
    eJztlzGSwyAMRUlm22yu4Em150iTKnfYU6T2NVRxFRd7oK0yo0orYACB8aCdOB2y
    CfnKQ8aYjOTr/Xky3h7cvrjduH1zO5gP75/595/P0Eqb/WnmOXTu4C/LsnDvPORP
    QxQ6dzgXf1zqUCs7ThP17Hc6dxkyrzNoFQxoGKtgQDEfDtRleEaJcVd2gQvBlwLB
    sGCPLUXJYHBDJYr18SMkA2oGYB/Gm7wvkPeVxf8YlNGTgPJZyJFR8HrLdSbMYZKo
    noUbm8JEsWIwh4lixZAIE4Wf1g77kBT7mZB2+Q8OZjCDGcxg3sSggpE5aYNBgC6T
    cmQRzkoGY65t5LgV08iVNdPKuZmxIZU3cneesw2pvFUDRCaP3KHe6NU2GKuzFxkC
    RR0Fm7Vfuc7QqiErplmLZoYU+8f2Gc0+JA2DCkYx566pGM07kebd6g9PV+vB9g0A
    AA==
    } "4 of hearts" 4 "red" 420x80
    64#{
    eJztlztuwzAMQN2iYz9XMDr1HF069Q49RWZfg5OukqEH6lSAE0tJsUWKsqm0BoIC
    oS0rpJ8ZWpZM+vX9+2FIcuD2wu2N2we3m+Eu2Sc+//mYm5Yp7cM05S5u/ON4PHIf
    LZT2gSh3cYsmPjzXrozcjiN58jU+uQwNf2cAIOzBEOEuDJ4dD7ICUCvJsDB8RXKs
    FcVgNkOlgIw5mkAy0M/ImFcYjAeHofq+QN5XUc5jUHpfFAjqWcgrZ4XHm9HCYHGz
    KMmffKbCzawYBoubWTEMCTez0jNX1TivSQzLY5B2WYNX5qIMdjByxq4wCOAyAMZR
    zSCAcVQzahG3GVQvjN8zAI0/uywTPAYzQ1tMzBLojGFTrsz/Y+wKt4xd4YZprHDD
    LHNTuQuSWVZ4I8cZppEra6aVcwsTcipv5O4Sc8ipvFUDzEy5cod64/TiWatt8FQM
    bTHJq8fEuqyXQcmgYlLdulJDaqZdi6qYtyQzm0iO2ZFepi8el3Gli+n5JvK/re7H
    H4xdySf2DQAA
    } "5 of hearts" 5 "red" 500x80
    64#{
    eJztlz1OxTAMgAtiBa5QMXEOFibuwCmYew1PuUoHDsSE5Ck4DmntJI2NeOJHem7z
    8px+dZrEdtqHp/frieWFyj2VRyrPVC6mK25f6PrrTS5aFj6nZclVOujPuq5Up5bI
    5xRjrtKRmujnrjbVyOU8R0ve5luTidP3GYTgYMDRFxkyGTQZ6stkQDFpBOkBtQKq
    L7qDDWtFMZiboVIaBiQDfkY+8wGj5vmAYZHjAjmuXfkag9L6pkBQayHvLArNN6E7
    g7uZTWF7ck2FmaI0DO5mitIwUZgpCjj8x+XPjriIGE8Sg2fml5ggL4Qu0/HDhun4
    c8304qJmevFVMf041cyJcoIn//w0g5LBljnI8/Uc9vYLzQzkzPw/Bh2MjOEDBgFM
    ZoskZS5IBot3D3LLxgxyS2FGuSXvz0Zu4X3+D+UW/HwZGjFs1WLSe5mXGeQWZozc
    kplxbgEwnCwzQyT3ZQivqc14/Dl6GMfz2Iznm8jzbfUBqcKs2fYNAAA=
    } "6 of hearts" 6 "red" 580x80
    64#{
    eJztlztSxDAMhgNDC1whQ8U5aKi4A6egzjX+yldJwYGomFFlZHviyLYSmd2dLAVK
    vI7sb+WXLCcvb9/3Q5QPTs+cXjm9c7oZ7mL5xPWfDymVMsV7mKaUhYsf5nnmPJT4
    eA/epyxcoYh/nmpTjdyOo7fka3w0GT+czwBwF2E8mQwTZn96GG5MMMSdAxqlYHgE
    FAZRKZKhVIxaCR2SDCSD4xlfjQtyXKvyO4ak9azAFesu/7koPN+MrgytZrIS7ck1
    FWYWpWFoNbMoDeOFmUXp8VUK3TJ9zNkM+YvswX/mSoyTFU5lFD9sGMWfa0bbFzWj
    7a+K0fdpyVwoJvTEn6MZkgy1zFacr+ZQPS8KZkdahjoY6SEbDAEmAzSGaoakU2ww
    xYLrDBWLcDoDKI0dzVDBOJURRZQYXzPFCvEzKXOYbRdVTjJ5hXZiS2Z2YsvCELZj
    SzqfjdgSz/m/FFtwBYawHVviSyn2Y0tkjNhCedZ1ifPTRIGTGFiNSV89kzGli+n5
    Jur5tvoBfRyUSfYNAAA=
    } "7 of hearts" 7 "red" 20x100
    64#{
    eJzNlztSxDAMhgNDC1whs9Weg4aKO3AK6lxDla+SggNtxYwqI9s4keKHtEMIq8Tx
    SvlW8eOPk7y8fT0O0T6onKm8Unmncjc8xPhE5z+fUpE2xX2YplSFjX7M80x1iPi4
    D96nKmwhRIfTNlVh9+PoNbuMzyrjh98zCKAyAOBUxuNhjGwzUuNCQDqyzeRhCEhH
    XAtTGDZOwQBnwM7w9jQY0a8GE433C3i/Vuc6Bnn2xQEn5p3/Mzs03oSuDK5pFifm
    43PK0mSnYHBNk52C8SxNdixatWjeh2ZpDPpd7sHrmbrmJVPXvGAami8YTYdHMn+s
    eclUNb9laprfMjXNF0xF8wVT03zBtOy/GMdPuCpj6bthDC1zYZhTizZ20uGt3V8h
    jNBfEyxri2mN6tieDBoYrq0GgwAqs8ywSOc4g3nUO5pfmI7mM9PTfHo+K5qPz/kb
    0jz+vAz1mJhVY8J7mZXpaD4yiuYhvw51NG/ST7zWQQwooje2WTUTY/kmsnxbfQMa
    WW519g0AAA==
    } "8 of hearts" 8 "red" 100x100
    64#{
    eJzNlztSwzAQQA1DC1zBQ8U5aKi4A6eg9hFot9JVXHAgKma2EivJknb1W2XIJCiW
    nVVeNvo82/HL28/94ssH1Weqr1Tfqd4sd759o8+/HkKVZfPbsm3h4F70Zt93OroW
    67fF2nBwL9dEu6cyVVVu19Vq5Xt9VBm7/J1BAJUBAKMyFq/EIHXODUIGss8UoWuQ
    AfCxY2iGIvAxZ4AzIfCJFQbL32owdobh/bGh1TSC0xjk2VMARqw7/2YMaL4JzQzm
    NCnw+fiasjQxqBjMaWJQMZalicGMqzPO+0XVGLRnOQdPZ9rOS6btvGA6zleM5uEl
    mawwWt354zQfOY/HDhpzGDVP+2xu6TymbNnc0vnUK3YCFM4jpG7ZFuM0Tww/AQp/
    QI76uowJMxiYdp+PsZvR2I+588J35lBMf2ctRF/bayp06LghBq152GdmnL80g5zB
    mpm5tkxdowblnAxOMIW+LQaZTj0mrbBIZziDcdYb97iKadwrS2bkfLg/K877+/w/
    ch5zdwYMTPhMiVQGjeY8xov3wHmIf4cGzvv/raNyaaa8ZjcY/NSdV8sUM/NMNPNs
    9QvETVBI9g0AAA==
    } "9 of hearts" 9 "red" 180x100
    64#{
    eJzFlztSxDAMQANDC1xhh4pz0FBxB05BnWuo0lW24EBUzKgS/uSj3yYClsVZryPl
    xbEs2UqeXj5vh1beSn0s9bnU11KvhpumH8v197tedRnbbxjH3tSjnByPx9JWDbff
    wNybelRV+XuwXblyfTjwXvk43O8yPPyeAWDYYQgAdxmk/2IIsFqhBcMAQLVCC5qh
    rgYteAYkA2lGzWHMaF+cYFqRdoG0axW+x5DsfRHKkFH4ot/ZjMFJqKcg/UWLsb2F
    uT/p095NY5D7aBxDMA+2ego4YhgFwzgPEm38rMwySLcuLMP11DCknFAVvM+wZ9g8
    KmTId+NjfpodsQB8zNMEkvc7wRzznVsXAAdxSCfjMBOr52L+OOY1w/JOLZiYDwUT
    86FgYj4UTMyHwhlywZ8xKC9gyGRsT8xhxhcJn2Zi40xxeMm1s6q313KfuNN7QmZv
    Se1Rcoa16id7JimDBEMJRm79EDysMCTuO8UsOvS5aWIWa4Mc55ggV1qmOYh0zl0Z
    7KkcfO5ex4w9lYuJUcKFY768T02ht8XMxm4ynGdIMuSZ+B3SMOG7qGE2ysUZt+o8
    Q3bROWbzcp7JfBNlvq2+AMWyKIX2DQAA
    } "10 of hearts" 10 "red" 260x100
    64#{
    eJzNlz2OHDcQhcuCE6Ih8woLRz6HEkW6g0/heI/AlBAWvAJTg8kGvsRGe4WKBBDW
    ovwe2d3TTc5qB7AAiTM9P93fvC4W64fz4dOX99LGXzj+wPERx584fpFf2/l7XP/n
    t36cx317yv19f+MDHx4fH/HOM9aeYtbf+OApvPw+Sk3j3d2dvTX0zr/JmPx/psb0
    XRiLN9jzg5hXlsEfmU3N1VhrSjFF309HPHYmPthLTJo6UwN/rmemxvj8FKM6q2bJ
    IKPZHJh4YfA58ZDYh7elMXT0gYmN2a3tjB1s3hhFvGQM0+JFXmHUr0y2lHx9eLnY
    89SZZwW0YHBOYGKEQRvz0pAHw72sM9m8993GgfE70+w5Mfa0mbwyfV48+7wzlYyp
    wI4DU48+NHo0YeI405nNz+nA4Cdmu804L775+bDu+Ekig+T9F0jJjALPtdsZCb7a
    qtOcWLyNa+FawIhvNYDIMjMaKFCwCCZRcpErOhqEU1mKIDrCsuz2nHUUOiU7IrnV
    oZHhu9cl8/JSSnFVJsbDWnHKyxxSqTTqIMLFRRUSmQjWYNRRcwEQfy+uIRYnHbHa
    II6OjLGqKqlBlNmQPDBeXYPgg9CRpa3H8V6quUHOu44U+ODE0MmlQRKCazf6nGVg
    oEMnmwUHw6CSXRlsbjqAIoSQ8oFuhNCkw0WCRsCTnyg06XCllRFee5xAaNZRvDrM
    PQo/FXHzvDJOI3UQiwrfQFY+X9EpWHuNQRkk+KruBuaKzeO9bGT8Mtg86xjz+zT3
    SYexMfpw1tGwnNdi1rHlb2kL39a0IqxnHaQmgvAQG7MOEnMZYgw6/szkHmHHWIWR
    Z6aIG2I+iNpgz4q01ImE4MfzvdyG9BQkhKwcGHoX+b6lcmx7mIFBtp9LAjLOj4yp
    q0Rwr4IyRnMnHZZUXGaPC6xiqFjBZqaUpbXBUIROlisMixhLHRO9xck1ZmFdRiXX
    nqAox1d0MFiWW/OphrJ+YdJmT+7NhCWVncFf7sW24VhYpTecptM7zM6w/Tj2xt64
    4D3bOtXGtDbm2BvX5iZfZe14O9Pa4ZHhzXqT3pl1e4D82plTB94ZLJ/uzFebGbR5
    n9A2V5tVbdsR7DZzuwBG87rTkNUcyp9jlfZ0xl5h+rz2jc3ExLgxrl8K+47p4MPN
    zwh8hD5ydd15XfycKufEKhXbdq2msO7gzkz74rDnIxNrv3ZYU6POa3vIA/PN8TMy
    LXS/A/PWuIm55T/RLf+t/gNy5vcz9g0AAA==
    } "jack of hearts" 11 "red" 340x100
    64#{
    eJzNlzGO5DYQRcuGE6Js8wqDjTbwKZxstHfwKRzPEZgSwkBXYCowmAl8iY3mChUt
    0DAG9P9FqbtF9Wxv4GDVrelu6emzWFWs4vz5+etv4sffOD/i/ITzL5w/yS9+/RH3
    //m9n/vj0d/y+Ng/+MKXl5cXfPJK87e01j/44iX8+TBKHY6fHx7avcMe4l2myf/A
    5DzfY04557vM3H5Y5p0wxGtmU7NaF4lmtl7eM/mpvcFfWsDgkBsMnPX6Bf5S6jSx
    fj1e+zDi+8zTqGMtalzlL7Egk52pWWCMqaw6rd1gkl8KZbPnyOiSYIxJKPE83Y35
    0plXzfAKXmFy5vT0drH5zZGnZpDh82GSLn/K88jA485oLpsJZ6b5YHMzzvw9ht7K
    lImYuWwMn3y9xGuVkT4ZXWj03s+eui5jpXHyndn5GSd/QcZqbABDZ+joK+ZEBPFU
    WKJSOtPmts8flylSRYo8axz93DyS+BVVpFbRWA6MVQ7BkU6FWfo8MfKDTqBD/K5i
    rBpLpe2DDmyEh010QVJHyBQZmKgF0TaDXVrTqUIG5J7BU7VkSFm3GZNTkT0DC7WG
    5FnKm1Cj0J5hBAQQFAsDq3TSYDNuGEZLDUyEo+SoA1uT5SkkGkTLoVLK0c8yZZjN
    ZSNAjzpYdkh0Mlh+lgt0VEd7MH0LCwbDpTCVW/YgPXEvOyOhwiZObM8EBAfPg0H+
    hEJm1JGc+soLcLdMZcLE6qATcsbig4gnWEYSStCDTuaMcqrQKVNRDJwGPyugKIrC
    onlStzlk3esgnqg7qouCgVmVk0y6H0vce1JTqUVLnjAWTBx0apCoaYERyDaBqJu4
    08HDGCuhitUSUIToiaNOpWsXzj3rhIGxDOKgI+o2MwWzcpIMz0GHcfR7YBakCuO8
    01GuFVtSc+8tmcXebuiYJc8wMBPCZ8MaZKJDJ3qmcrCE1I037ZGe8QgbElFKG/2M
    UgGIDDyzFquDTuVehCuQaYB4mg7+4U1IVfcT8gmjIcWGdUqD6mKIuHkfZH0ZGF8r
    M3ROlXseSOlUxhqF288+WixeOm3JY41CvdDZq9zyL6ti8+I3MA1FrtsMK3zHNfoQ
    f1FJxRPMq8yuJPcyTSZhNWAIlnF2/x3Dck8mu/N1lUFnuGLYNpxBUFnn2Vh6hzkz
    3n6c6cspsj/1TnVmvI3hh6EbGWv7KpPzhfF2uDL0S1tl8OS7DBvL1oEvzKkzmlcm
    HRhv8/yxrjy2/21HMPq5YQH6NgLz23YWV0zXscVJVLF2ZE4r41lq4Raz+plRxbKz
    UJyZr5nNz77VQDwDyuC28zoz82pPM9+shVLPO7gj07wPQsf3UNe50bd37+whr5lv
    Hj8g4xn3bcZjcY+5e3wX8z3/E/1x93+rXx/+A4IDtCr2DQAA
    } "queen of hearts" 12 "red" 420x100
    64#{
    eJytlz9u3DoQxicPaQjihVcwUuUcaV717pBTpPYR2BILQ1dgK7BxkUuk8hWmCqDC
    YL5vKGlF2hs5SKTVaq397cxw/tKf///xr9jxFdcnXP/h+oLrnby35/f4/tuHdvXH
    vb3k/r7deOLD4+Mj7nxS7SW1thtPPsLbx1HUi+Ofu7t6duhdOGWq/DmT0pKmv8DU
    hPOMWaZzBtrOmeW35NwIQzjaw3ctpfh2bsLDcV327iWvZ00P9TlNNSxTx2hQn4sd
    YUnp6TssCUcf4t3n6ktJOCIMTROvgREwLiUzlUwypi1oZaApt5VA6w3Gg6kBR88s
    D89Xm71IESCQhNf3xjz19mhdGeX5bMgD15WOjJZguvDpFsPl7Ewz6Kma0qedSQmy
    dmYhg8e87zZz3YrDVgqSOixeuG1xjzggRdsvAgWZa5E+OwMXp+YerI4FTDEM0XTw
    M8IUmjJbQF32dNgYNznYUw3CS6LUF4wkmipVaTg0r5HrGZHYbCGiSarjNwOTY4Qh
    BcEI6sA7pMLAZIkzZJQc8BFigsPyQscg7M5H3Ev2XuByjaqXPOjyzuWqyFbJVKUR
    67j0jJ8l4hEYqIpUBSelnil4AhNYYzBHNQWs3/X2zFEcHQ1BDj93UVOU3h7PisBy
    NEJZDMBclLm3xzsIIiSFy4c2kUtJgy4rrbQ1Ayy0xDIPNpsgaSVjvoDDysDMaRci
    DoJ9mUfGN0GVeW3KX5FDQTNzei2VV+Q4CmLSILu4JjGbh3hFLBouCRaxahQ+9HIi
    BME1Ck95YdtDF8lu0JX5Q0beeeYR4zbG1BdrPy07sITEB3HwT74YRIMjqxYrH+Ne
    XGktV0JCCoqbEwSNjPjWlh1SjMrgnheMyVnW/i4s7sE/JsfqzqJq4Z19zzizOegW
    1NgE9cylMAhIV0syJQRBQ66iHJiiYslaCbmxdlgxymRPdKKyScioi4WCYsDvWWKo
    L0YwjzFFq0B9JSYAfu9jdKN/2MWi9aHcsmN2zRfHeEW2AchDklKOXqC+Xxf7CFU5
    Fqcwj1we6xTzC+ZUq0INGGXQHIeYss85ilE2u8CnzOyRYU9EE2RrhcloYWwSI4NM
    Z+dYGzDlphfMwnKwCW39l7P3ykyNYZ1rMFswFDgcrvZwbITapkjrpBxSbcpsDBtF
    S66JDJ6oGS7X3mtjLKxTzaytNqTcwR4bh6HaKJ52Zh2eK9MG+Tplrwx8rbcYjm7O
    cCTPkcGY36d+2wMYg7DvNnO7ELbdgw1UswfbmH6m9IzVT65SbzBWXhz2zNTenm1X
    pCQvyEQthznYmG135W2zlYvtwnfGtmVh3aXptm1rqXxllmn9w1puO1FjHZN+sYfc
    YjrVXx9kTpC3ypnOBK3rOmHMh3+BOTHmrcxb/id6y/9WPwHf55TR9g0AAA==
    } "king of hearts" 13 "red" 500x100
    64#{
    eJy9lztSxDAMhgVDC7pChopz0FBxB05BnaOl4ECqaI0eTtavWGYwcWLHE3/zS7Zs
    bfb1/fsRtHxyfeH6xvWD6x086PuVx7+erOZl1RvW1R5ycWfbNn7Km6A3hGAPueQV
    N8+lVFXulyV4hRZ0mQATGJzDEMxhgHAGwwS4jMWvy0i40GPMXI8hEYG/M7vFixga
    YMCPF/nrHAbWWeLlrc8shveYF9MwbX0O7j8YnS04OrbN+wxVMr9mzEpskoG0G0+c
    1hS69WxGIOlk71bMbogg3GyWDJImceLLuhXDA6JvZqTFw1jC6DAeDdAZo6aow+jE
    udj024wqHM0ZY57I45xJ7xN/RIjdwbhQjTU0GyS39lvrLO9pv9tMdEh1bAIVQzEC
    5k/iTsYgJPPC1t5QY+JH9L21x0xoX+5EptjPMkY6K4Lmfo5CiIVMfnaiUCGTM4dH
    mUxxBlWglCmYKJTLlGfZtjsV6jkTI56f+CqPZNFsM+Zz6DKh8qaRW0/yT7lGlTRa
    vk+FQsVQmUirbNxgqjLG5PnxhCFHZui3R8apj1z8+6Wpvf/FNcToV6Jja1osIJAf
    L/TjNcJM+eYYjftle2PkP9HIf6sfYx2h1vYNAAA=
    } "ace of spades" 1 "black" 580x100
    64#{
    eJztlzF2wyAMQNW+rg1X8MuUc3Tp1DvkFJ19NA85kKauqhBNAkggUmcEG5MnPgoS
    epb88fXzDtK+uZ+4f3I/c3+BN5GvPH85pF62VW5Y1zTEi39s28ZjlJDcQJSGeEUR
    P461KtVel4W8hktwGYL9DIoNfYa3HFyGcIDx9yPT7p5NBpUizZQStGwXj9wXkGW7
    eCRjDNsRIFdk+lCiIXQZTCHTs2KI+Qu98BCDA4zhZ55HzBgrxhACUrbnRozlkuTR
    /nk1mHqByxBb4MZzI8YeZ67cZCYzmclMZpx50jta3vd9BnWS9RjUC7QeM+tBkT/L
    XHllsMyfRc69aS7rC7sGyCqnf9UJBrOnbqmLovqvFGOdTGQoP1OtJpV2OWOeV6o3
    m1aQFWNYI8nP/fAZKn5l2o3VyvYGoz2iGMMjxn6cNsSMfBONfFv9Aj95nK32DQAA
    } "2 of spades" 2 "black" 20x120
    64#{
    eJztl01SwzAMhQXTLfgKGVY9BxtW3IFTsM7RsuBAWnVrFBkYWX6OXJqZdlEnrjvK
    F/lPfnZe309PpOlT8lHym+QPyQ90UPssz7+eS67TrDfNcynWS/4syyLlasl6U86l
    WK/VJD8v3lWTHqcpR4mnFDKZLmekOWkPhhOHTKa4PeIoZFjHvWG4qgozlYUay5/3
    gNERMQzoOxNZR3AMNRqSfaWpi0vIbPRijPkJvXQWwwMMtfMlz5kNg/uVOJs2d2LM
    WsqIbs9Xh/EvhEyWHgzEcxyrQ8wvd2euwLBfOaFuIIuPZ8T4ddFhzl+n/2T20hav
    Y5Dh1k2kq5jZZ76y2y86TJvuzG0xO2m06v02gyI4YGCsej8wVqnaP7G2cL1/Ym2x
    W3VPE8xKuAVtYXdwQtriD05IW/RcZh1BbbFMT1uq2IDz5cYeagvw3tYVIGW+BuI5
    cuTH8AImTEPMyDfRyLfVN3+MWqT2DQAA
    } "3 of spades" 3 "black" 100x120
    64#{
    eJztlztyxDAIQEkmbcIVPKn2HGlS5Q45RWofzcUeiGpbBSHZQp9Z2PGWwma9yG8k
    LBiDv35u7yDyx3ph/Wb9ZX2BNxlf+f71I2ktq5ywrukSD/6zbRtf40iQE0JIl3jE
    If75bKfq5HVZgiW0oMkEOM8QOhjwMOhgwOEPAybDHmmmwjEvBRUjz1AbHQN6zmw0
    +0MS88ZomDhtZ0C1h7I0tEbNpFzBgfEQQznpesNmyMHouPMwomKyUcedDaTiczbE
    8/Px6piBeJgARGYeVs9+htm5yUxmMpOZjJ950jta3vf3GVKV02RGvUTLDGpc8QdS
    VRzVyp2hXFxHNXdnRK0egNrhB/sEzTyjb6Hi9LE7evMPZtxrlbWOvkFtszIKczZe
    JTf6XrQw4X76SNzRZshIw5BTx2TIwTh8NsXFeL6JPN9W/6HQFrv2DQAA
    } "4 of spades" 4 "black" 180x120
    64#{
    eJztlzF2wyAMQNW+ri1X8OvUc3Tp1Dv0FJ19NA89kKauqhAQg4QtkjhD34tsTIQ/
    IFsKyO+fv88g8s3ljcsHly8uD/Ak7TPf/3lJpZVZTpjnVMWDfyzLwnVsITmBKFXx
    iE18edVDGXmcJvIEp+AyBNczbE44gmGLD2HwEnsaPCkgl/UGQjNJl4F6zKxAazOK
    z5WiGACwSmszShxoBSWeQtXzNFCjnMVgDjqr+AwOMPF+xYRQMVnRz0UBV5uzon3a
    8ZdM6vjLMB0ZYQhwIJ7rZ7+GKdyd+ccMAriM38J/BiQ9kmI4fBFBNxqmWYduyWCz
    qFzOUGcqw6Adxr5VO8yt/EVplfQYK3fmGOagNVrW+32mF3mbTC+X0EwvJznZA2lX
    7O2VhcG8ufb23MJI8XIA1M1n5gk1c0DeUm3lJkeq3mHpu5F4hbiBl77Va66Uldnz
    F+VMcddfhenlosrmbRHGjVUnUMeZMXtcxptpkBn5Jhr5tvoDMlDOXvYNAAA=
    } "5 of spades" 5 "black" 260x120
    64#{
    eJztl0FywjAMRVWm2+IrZLriHN10xR04BescLYseSKtuXVk2YMtCMiUzDDMocYyc
    R+xE8o/ztf/9ALYjlR2VbyoHKm/wzu0znf/Z5tLazDvMc67SRj+WZaE6tUTeIcZc
    pS010eFTXqqzzTRFz3AKLhPBZjCddxlwGQZcBl2G+nIZGGFkXw0eVAahGYjKQH3N
    4ggGOebCEWMGgN5pnzNyHmjOhcm5EhTnJgZL0vWOz+AAU+dYCnComOK0905OwMuY
    iyPjrsSLO3Xi1TGKjTAR0M/n5t7vYU7ci3lKRtMNyWj60zL6vGiYK/NLMv+e77cy
    a2nUkB7quioYVZ8Fs1K8ovre6Zhr9mLWYVbSaNZ7m8E6LT3GyLEzY+VYTm9bE7C8
    XE1NSMWbyyibH6sJmBcfpiaUf5iawGs3RxMKY2rCiTHjVRhLEwCcRMyMieS+HPOX
    0NVztpk4wgyMx2dGvolGvq3+ALZph/D2DQAA
    }  "6 of spades" 6 "black" 340x120
    64#{
    eJztlztSxDAMhgVDC7pChopz0FBxB05BnaOl2AOpojXyI4llyY/dBGhQ4s1K/mLH
    tvxn8vr+9QjBPrm8cHnj8sHlDh5CfOb6y1Ms0uZwwjzHiz/4z7IsfPURF05wLl78
    4UP881w2pex+mlzPaMIu4+AwQ3EMxxl0XcYTpzCEBSNwNBmCvAEwGcjbXB0QTJyL
    0pGMX2LtCIZCHihHMDFX0HCuYiglnXb6DA0wvj5jEDMmOSTyhx2k/ZmTE568vV6h
    0856KcawEcYBEXb3Vz72I8zK/TO/ypj7vWAs3ZCMnc+CqeyLkrl5n17LnKUtNR0T
    TEUPJWPrqmROWi+XXhGUOkGbGYzwYpMDaDI8HCIog4oRU/OTDImkuJ1xRleKId2M
    nlXdjGZINWNoq71eMkol4uuD3reM19kYSZVp5OrGtHI1zkhbWyi9XJva4ktPE6gM
    /7G27K/7urbAdm9dW/YHzaa50BZY721oS0qNpras6SOyCG2mZqHbdoqNMtDrbMux
    40zXhpiRb6KRb6tvHjBX8/YNAAA=
    } "7 of spades" 7 "black" 420x120
    64#{
    eJzNlz1WwzAMxwWPFXyFPKaeg4WJO3AK5hwtAwfS1FXYslNsWbFUmpeiRHXl/F4s
    W/98vX2cn4HtK/op+nv0z+gP8MT9czz+/ZK9tZl3mOfcpC3+WZYltqmHeAei3KQt
    dcWfV3mqzh6niSzDKZgMwe0M8hzGTEw5mAzhYUyXc4PnQOaM0JxAHQvqc5ZAMMg1
    F4FgAKAP2nyQdSCDdl5ZK0EJrmKwiK4PbAYdTDpeMSFUTAnkvCjgb84lcNSLBzXq
    1TGKeRgCtHXYzP0WZuWuYRTNS0bTvGQ0zQtG1bxgVM23jK75lvkHmv8Do2le5qxp
    XqzhXvXatjsxnpztuTvW0FOLI7Wxl55d147nGnRdyzvVi1z3qE3zMjvdo/l+P2aw
    XnaLGazhhRmtYS7fWPNYHq5DzSe3tIqy+76ax/VtaKB5uLxWbWue38sMzRdmqPmV
    setFQ82jIZ96rEOYZtI648nZNBfj+SbyfFv9AGTB+m/2DQAA
    } "8 of spades" 8 "black" 500x120
    64#{
    eJzNlz9WwzAMxgWPFXyFPCbOwcLEHTgFc46WoQf6Jlbjv7EtK5ZpC8Wp60r5JbWl
    z8rL6/vXI4X26fqL62+uf7h+Rw/Bv7rzp6fY27aGD61rHPzhfmzb5kbvseFD1sbB
    H97lvp75rbp2vyxWa1iMyli6nEFYw5hxUzYqY3E7psGjwecMqpBkEFs71fdMRiCL
    H+EaZoTThSGi3mjjjKCDzmiYqBUjGD9ikETXGzqDCcafrxhjKiYZYOuyBmXOyZjI
    V/hTJV8dI7QZxhJ0HTZrn2UEre7cPsFmtiIjaZUxolYZI2q1ZQ602jD/QKtnMJJW
    +ZwlrbIYFnkWo2NyOTGVwZl4uYElanyccRMFiDs7pknnbzJoAnweQ6kIpFGMTwgt
    cm2S4pxTFKuJmK891YhdynuRjPOoGsNFOrzW3pnapzP7fapuzNSfqTo2Uw8P2zn1
    94AJ9X7MoA6pxgziszOj+OSNMKo/SA/XYR3zXdMhuPu2ekZ5lB/qGTnMQz3vN6rC
    zPWMriRzPedaq+fLDvVMVtXY3zK8rgsMFMlf4x0kMjPvRDPvVt855Lq+9g0AAA==
    } "9 of spades" 9 "black" 580x120
    64#{
    eJzNlztWxDAMRQ2HFrSFHCrWQUPFHlgFdZaWYhb0KlrjXxJLVmzNwJkZJ05G9p34
    o2c5ef/8eXYpfYf8FvJHyF8hP7inVD6H+tNLzjzN6XTznG/xCD+WZQn3WOLT6bzP
    t3jEonB5lY9q0uM0+VHCREPGu78zue9dBs7RkCHckGE4qQxc/QSnMq5+ZjYkg+Rz
    bkgmulgaYg6RdCAN7ousFVKMsxgU0bXGmIHKgDGxvmKI8qqgykApWbvpCbl+N3zq
    eeOvMp7ir9Si9Be2ySON2WeG+bBhwCYm/AWgEcPGXjdVC8nCrNw5TJ4zIDGkMkhz
    T/DZCSpTmihX0e992L54kS+AvuYFo2ueM3eg+QuYInNpKJpvjUGMahglRjXMYboR
    Y+nzeOyGObT44pra+C89m9aOZQ2a1rIlJphiiyVG7YO+MB6yfo+ZJoYXJsX7PgNW
    qDRVM8re1DDKHrf3x+VdUdsrVwZlc9X2XDb2o717ZeDvSfPIo+xqHrTO7LHm01uZ
    19/ZJNOLURvTiVEbo7yLtsxRujojRa4wkCJvmG61nbF8E1m+rX4BEApi6fYNAAA=
    } "10 of spades" 10 "black" 20x140
    64#{
    eJzNlz+OEzEUxh+IxvKyvsKKinPQUHEHTkGdI7i1UuwV3EbTbEG/V3kVEtIK833P
    9mTsTNiVFgmcTDKZ+fnz+2d78unLj/di7RuOjzg+4/iK4428s+sH3P9+W4+xHewt
    h0P94gsnDw8P+OaVYm8ppX7xxUv4+DBLXbS3d3fluaZ34VmmyOsZ5f2/wBR5gT3/
    iLmShrBlupoml5yLMcVYNJRgmd0wyhMXK+NC8fmSsVNN4kSiQObkQ6hVsjJqtaKp
    ttAYBvrMmIkkm7WVKVub7SbOPFqwTmbPwLTelVEqmF/lnNPQxiia0YB0HZUrDC48
    rn6dbe6mdkarjm59nxkOE+rlsMs8Kaw2nZlRXRkpLT4Dg3gqDFTfxmoMPLzIl1+A
    LOZKi3MoU750QQTJlL34tPLJhOS+1PVgzldlFsnZu3T/BNPg+i7joZNdSlJXlx2m
    F0b1WPZ01ELUGYTlkgEiFkJrytjNjDrInHV4HgcGUZYUoZN6PYOKbmSyw1xA32M6
    8R1hCybIyCyEqHP0x6OnZPDzWIKwUCfl03LKJ5qT8zSWXxapOqCOMbME/DiWpxA7
    B7oMt3JAVvLELBQiZS2zk020rV+4RCGpQsitk0lHsq9CraGL00nHQdtqq9cIhJJM
    9phBuUcZfmUKjToZ3vJozVehUQfxpUHwvah68WbRFOcTDeI9xfxE/NDBc6XaMi5m
    PzjGsSdGflL+bDQ98GmKswnl7jwmFw0amYVCMji2yBxnxt7nllLkZGGXKYZMouSe
    UxPKc04zq0FaSknByYmBDjpnqzArfgv1mAsIobOrlWpFS6FRJ3qaeq54TCXMgynO
    nLnSZw50osy14YSzxfUos/ijlHmseSI7bh7jfLfIbRYErnEzw0VCNjpSdhiVDSMV
    mRi1rYGrHFa7pSGzToEJNyUlz1Uz7zK2O+I8co/LFwztC7ou3LaMtx14ZX7d4od3
    2X5wv1iWvgGsixy3jcpwVSQjbYdZmbrQVkZv6h7Xd6qRoT1I0NPK1MsDw03EFuld
    xrZV09GrjG3P1Z7HzvSdfC8+Qa4w9YnBQib9caMONee0x7ky7QllZGqcz4VhMmVj
    c2daPcdSpnzp6hfmQsQEcXEd4Jwv7q60pz6s4anNglkuGPilfOIDk6rQWGMarj9D
    bpg/tv+R6bPytcxz7UXMS/4TveS/1W9aPkPU9g0AAA==
    } "jack of spades" 11 "black" 100x140
    64#{
    eJzNlz1y3DgQhXtdTlAou6+gcrTn2GQj32FP4VhHQNqlgFdAOoVgFeyBELmKibHv
    NTg0CXA1CjYwR9RQmG9e/7JB/fH1+yfx4xvO33H+ifMvnL/JR19/xuf/fO7n+Xj2
    H3l+7m984eL19RXvXGn+I631N764hF9fRqnp+PD01B4d9UkfMk3+B0YAPGCqR/iA
    0fbLMv9RBj0ym1qNJcdQa1XpyyNTqVdyCVWVqcPCxPhyKWCaQKkzhxyqpxQXEbYU
    H246x1qou61kSlBtNfBaodPaT4aqQlvZuB6kM3pktliLgJEqmpzpFjam9oTUGIPB
    mZ2hsYFB8GFRfBlMLfns857YbKltTM7n2HcmWHXGlDq+dsG4E7Iq/RmZWsnY4itX
    DKKtvCuDJXjC4NW/AmyqF9xBivmV5HH9+NzGeoktkpliJkiG/Gz9E0xKjCI5bjrt
    WIt14V8QQ+VBFcQwMmsKC2QYR8k4i+fuxIjFbC2bQSlGFB+choEJ0KlRLQGCRzG7
    TycmyApE8koIOjAVbyOTVWORqKshrx5atCgnJnZfU250KdLtK4YYmhDWtIc2Md7Q
    lrItsIZUi9yWmfGOziiGJYYVb+mCqWYMGdbk9gJ3dGAQF1sDq1UgBKZYHRn2Ukgs
    uYQVDEyNOuJxUUZXUdgslkadiCplyrD3ZIXHppNOKbHsa8EMMrNOYd6OI+xCB8t2
    PGYd7yo9vTyEE8N76fxiYQcGt+DZFjt70tGTzwwiXuvsfyMVZdYJViTULY05IV2z
    jr1gXi4SOJG3nht1xEo09KGw3bLRuzDqrC+ZzJK6DHvlNuoE9lUOtiLqEjjx2uQP
    PJai3qvezzozahm3aMbkaGz9JDIz4SXznoEQrOWt5ybGcysQWmpOjl8wlbNA0WFs
    y4KBeI5LY+AY4DCosFZzlGYtDIxk7UMFhbCFiFg9M4Etk6ETfY6nv9fkO8eRYR19
    XHhbc/eCThv6Odd9WmpzI9ZmBk5gFMCj1frOfWA4pmlrEZ8wGUNRtx14Z3zcUwfL
    yslo+0Z7Z/q2wc+5j7aaVtt3mDvTb0qYCT0dMNXuO9UVQ9ewZbT9fj4y8Kf1zbau
    F0zfVnNutTPononp23O5MxxQ7b6Tn/PD1Cy8z4PFqBPDq41B8BiKcTOmx1pIZ9Q3
    1FTKZuxYr9qZxgcsDZiX+5POyWf1By20Zwuo/H0HbqfYO4MnrLpyF5zr1e4MhGSF
    LTK1TQyv8cRXoYM7SIf+6Y9318eJefP4BRmvzNtMu4f4FvPweBfznv+J3vO/1b/O
    39P79g0AAA==
    } "queen of spades" 12 "black" 180x140
    64#{
    eJytVzuS1DAQbSgSlYrtK2wRcQ4SIu7AKYjnCE5dE3AFpVNKNuBAHREi3mtZXkv2
    4F1AXtuz9pvXrz9qaT59+flefHzD+RHnZ5xfcb6Rd/78gvc/HurZj4v/yeVSbzzw
    4enpCXc+Kf4npdQbDz7C5cNItRtvHx/L2bBHPcUU+XeMiIn+B4xH4Axjeo7xwJ5h
    7FU8d9KgWz28Wl5HI9etX36VxAFI4jf8semAsYkDMP7nL3QbQ/FrmDEA+053ZIfR
    VTlQ9aMu9KtfLsGVeynyKkcYhavFMBZGLZWqw9C6EGM1PACYDLYob8CQbbCl1VYL
    s7q6E4xfX4+BwoYxu4OBPqualUDmSzs9YuAhxGTNV/n1sPXdqKdUW9ryNcTH3JY6
    ka310+XCohDEGMKqHGJUEtWpW+LnA8wEoinBjDJptygHGCFR8oBIihIXzR1mBlGI
    BKSEe4pxXz9CY3iXRMKUJtxm2fHMMMZXOc+44Yg3HXmCC5qzRGBACKK046ExYMAD
    YRhyvfUYjCZaJPoBwh4Tc6qiWfbgy3Tv2mEiZFBQRE1YoRo4GHKPSZYXDAb8I08c
    9CjjQedpK1WeUQ+nMJ8nzkIE+UAPJxK+7GHxjLqe0XefdzGw/WTyMGXzwMMo54WH
    eQdVkJEHmXJb0HWLEV6J9TmFywwcbBFDHhSUDTxBSL/aiqw37XnCVRahVbN3jYEn
    3NzbZ81rj9rkYhZ23bjEkHgGdbTlUZaaCxRimIaGWkImT0o+e5h3DUxzb6vqea4N
    YFBTB3oYuFpjCOCU817PUqGeNmJqG9/qmVjmIYZrzjfOC4Nfw7y4TR6WNF8l3ji/
    FJHvMUrHfH5dc/J61DDyqIXJX80+46vokQfOsKoCW8IiesfDlaDlXVub2WHAhOpB
    c1WfPTS2x4ApedNE3ap599tjADKSsHUqgm1HmLI0XmWHZf5XjMmiWUptz/6lHsN2
    31brdTGA1Q3Glw1tq4hD3FbZYNaFylejZdkpdzD+oerRv8D4snqC8eVZ22rtkkle
    yi4+W4xvRHqeumNo+6Y1hj2m1o+smxF/Xg4wy26GpS5tRmwa6mLdCbBD4kapdjkb
    MfVbrFXfcm3KQeq2zOuw1L2d79yqlGeMtUbDNaGN1GPkD3vIlotGcm8QcwJ5KY+e
    ES1+nWA8hv8BcyLmpZiX/CZ6yW+r3/c0VV/2DQAA
    } "king of spades" 13 "black" 260x140
    64#{
    eJztlzEOAiEQRdHYkKzDFYiV57Cx8g6ewpoj0O5tKCw5BAfYbLayxZmhUpMB+xl4
    y4b9gc3r5nJ7HQ3XAzkjV+SO7MyB9wN+f0LjswJPE0JbaOBLSglX2qk8Ta1toUFb
    +Dh9H/VTe+9rrzbvuplqXO1chQG3WcggYCgzxyVJcAYmK8DnxLXMApEyE9hZgDMZ
    b5WgDGQLAvw/ZUmrQFE/6kf9qB/1o37Uj/pRP+pH/fzlZ6QHGehlRnqikd7qDVAb
    l+X2DQAA
    } "card back side" 0 "none" -200x-200
]

Затем я написал небольшое приложение с графическим интерфейсом, чтобы проверить, правильно ли отображаются все карты. Он создаёт блок графического интерфейса пользователя путём чтения, распаковки и добавления данных в блок карты выше, используя виджеты изображений для отображения каждой распакованной карты. Затем этот блок графического интерфейса просматривается с помощью типичного кода "view layout". Обратите внимание, что указанный выше блок данных карты импортируется в начале скрипта с помощью функции "do":

REBOL []

do %cards.r

; Начните создание блока макета графического интерфейса, задав 
; размер, цвет фона (темно-зеленый) и т.д.:
gui: [size 670x510 backdrop 0.150.0 across ]

; Следующий цикл foreach перебирает импортированный блок карт и 
; добавляет только данные изображения и информацию о координатах в 
; блок графического интерфейса пользователя, который будет 
; отображаться. Обратите внимание, что цикл foreach циклически 
; проходит через блок карты группами по 5 ЭЛЕМЕНТОВ ЗА ВРЕМЯ. Он 
; считывает графические данные каждой карты, текстовую метку, 
; числовое значение, цвет и положение координат каждый раз в цикле. 
; Каждому элементу присваиваются переменные слова card, label, num, ; color и pos:

foreach [card label num color pos] cards [

; Обратите внимание, что в макете графического интерфейса 
; используются только графические данные и информация о положении 
; карт. Переменные label, num и color не используются, но они 
; включены в цикл foreach выше в качестве заполнителей для каждой 
; группы из 5 значений данных в блоке данных карты. Обратите 
; внимание на использование функции "compose" для вставки 
; графических и координатных данных по ссылке непосредственно в 
; блок gui (если вы распечатаете созданный блок gui, вы увидите 
; огромную массу данных):

    append gui compose [
        at (pos) image load to-binary decompress (card)
    ]
]

view layout gui

Приведённый выше код обеспечивает фундаментальный способ повторного использования изображений карточек для создания всех типов игр. Добавление кода "feel movestyle" (чуствительный к движению), представленного ранее в этом руководстве, позволяет нам щёлкать и перетаскивать карточки по графическому интерфейсу. Здесь я внесу некоторые изменения в код, потому что я хочу, чтобы карты перемещались с использованием позиционирования "snap-to" (привязка-к), как если бы они помещались в сетку и могли переходить только с одной позиции сетки на другую (в отличие от плавающей свободно по экрану с каждым щелчком-перетаскиванием):

REBOL []

do %cards.r

; Следующая функция позволяет частям скользить по экрану. Координаты 
; округляются до кратных 80 и 20 пикселей, чтобы обеспечить 
; возможность привязки к позиционированию (расстояние по ширине по 
; горизонтали и высоте по вертикали между началом координат каждой 
; карты). Дополнительные 20 пикселей составляют границу по умолчанию 
; вокруг всего графического интерфейса:

movestyle: [
    engage: func [face action event] [
        if action = 'down [
            face/data: event/offset
            remove find face/parent-face/pane face
            append face/parent-face/pane face
        ]
        if find [over away] action [
            unrounded-pos: (face/offset + event/offset - face/data)
            snap-to-x: (round/to first unrounded-pos 80) + 20
            snap-to-y: (round/to second unrounded-pos 20) + 20
            face/offset: to-pair rejoin [snap-to-x "x" snap-to-y] 
        ]
        show face
    ]
] 

; Вот исправленная версия предыдущего блока графического интерфейса. 
; Единственное отличие состоит в том, что он использует определение 
; привязки к позиционированию, приведённое выше ("movestyle"):

gui: [size 670x510 backdrop 0.150.0 across ]
foreach [card label num color pos] cards [
    append gui compose [
        at (pos) image load to-binary decompress (card) feel movestyle
    ]
]

view layout gui

Этот код действительно начинает выглядеть и действовать как карточная игра. Вы можете взять любую карту, переместить её по экрану, и она автоматически выровняется и наложится на любую другую карту на экране. Чтобы создать другой макет для других карточных игр, всё, что нужно будет изменить, - это начальные позиции карт и округлённое количество пикселей в коде привязки. Итак, шаги 1 и 2 в схеме программы выполнены. Чтобы завершить шаг 3, я просто добавил следующий код, чтобы добавить несколько графических строк в блок графического интерфейса. Он рисует линии на экране, используя виджеты-блоки шириной 2 пикселя, чтобы представить места, куда во время игры должны быть помещены бесплатные карты и восходящие стопки карт:

box-pos: 18x398
loop 4 [
    append gui compose [
        at (box-pos) box green 72x2
        at (box-pos) box green 2x97
        at (box-pos + 320x0) box white 72x2
        at (box-pos + 320x0) box white 2x97
    ]
    box-pos: box-pos + 80x0
]

Далее мне нужно разложить карты в произвольном порядке. Мой мыслительный процесс для достижения этой цели - загрузить блок данных карты, а затем выбрать случайные карты из стопки и поменять местами их координаты. Я использую цикл foreach, чтобы просмотреть колоду несколько раз (52 карты, умноженное на 3 = 156):

random/seed now
loop 156 [
    pos1: pick cards rnd1: (random 52) * 5
    pos2: pick cards rnd2: (random 52) * 5
    poke cards rnd1 pos2
    poke cards rnd2 pos1
]

Это работает, чтобы раскладывать карты случайным образом, но есть ошибка. Цикл в существующем коде графического интерфейса пользователя, который размещает карты на экране, проходит через карты в том порядке, в котором они появляются в нашем исходном блоке данных card.r. Теперь, когда их позиции не в порядке, они больше не размещаются правильно на экране в упорядоченные визуальные столбцы (их шаблон перекрытия "z-порядок" - беспорядок). Чтобы исправить это, я создал несколько функций. Первая функция просто собирает и возвращает список позиций каждой карты на экране:

positions: does [
    temp: copy []
    foreach item cards [if ((type? item) = pair!) [append temp item]]
    return sort temp
]

Следующая функция использует цикл foreach для прохождения блока координат, возвращаемого функцией выше. Затем он использует вложенный цикл foreach для прохождения каждого элемента на экране (для каждой координаты в приведённом выше списке). Удаление и добавление каждого изображения карточки в главном окне (добавление system/view/screen-face/pane/1/pane) изменяет порядок, в котором они отображаются в графическом интерфейсе, и правильно перекрывает их в макете (тот же метод удаления-добавления был использованный ранее в кодексе movestyle). Обратите внимание, что я включил условие if, чтобы убедиться, что эта функция работает только с элементами, находящимися выше точек на 398 пикселей от верхней части экрана. Это потому, что я не хочу, чтобы стопки карточек внизу экрана мешали:

arrange-cards: does [
    foreach position positions [
        foreach card system/view/screen-face/pane/1/pane [
            if (card/offset = position) and (position/2 < 398) [
                remove find system/view/screen-face/pane/1/pane card
                append system/view/screen-face/pane/1/pane card
            ]
        ]
    ]
    show system/view/screen-face/pane/1/pane
]

Поскольку я хочу, чтобы вся эта визуальная перестановка произошла до того, как зритель увидит экран, я буду использовать "view/new" для создания графического интерфейса без немедленного его отображения. Затем я запускаю функцию упорядочивания карточек и, наконец, отображаю макет с помощью функции "do-events":

view/new center-face layout gui
arrange-cards
do-events

Когда я играю с макетом, моё внимание привлекает ещё одна проблема перекрытия z-порядка. Не столько ошибка, сколько вариант отображения, который я хочу усилить для того, как эта игра должна работать. В существующем коде movestyle, когда я нажимаю на карточку, она удаляется и добавляется обратно в верхнюю часть панели экрана, как и в функции упорядочивания карточек. Таким образом, всякий раз, когда я нажимаю на карту, она закрывает любую другую карту(ы), с которой он использует пиксели (другие карты скрываются за ней). Я не хочу, чтобы это произошло - карточки должны оставаться выровненными в красивые аккуратные столбцы. Чтобы исправить эту проблему, я добавил следующий код в цикл событий movestyle, который снова правильно выравнивает (перекрывает) карты при каждом отпускании кнопки мыши:

if action = 'up [
    arrange-cards
]

Теперь я могу щёлкнуть любую карточку, вытащить её из любого столбца, просмотреть, переместить, а затем вставить обратно на место, и другие карточки будут автоматически перекрываться и выравниваться должным образом над/под ней. Очень хорошо!

По мере того, как я больше играю с кодом, я понимаю, что в настоящее время нет возможности отслеживать, где находятся карты, после того, как они были перемещены. Это простое решение. Я просто добавил следующую строку к блоку действия вниз в коде movestyle. Он сохраняет начальное местоположение любой карты при первом щелчке по ней (до её перемещения):

start-coord: face/offset

Затем я добавил одну строчку в блок действий вверх. Это заменяет исходную координату в блоке данных карты на новую координату перемещённой карты после отпускания кнопки мыши (после того, как карта была перемещена):

if action = 'up [
    ; Сохраним новую позицию в карточном блоке
    replace cards start-coord face/offset
    arrange-cards
]

Теперь я могу добавить код, такой как следующая строка, чтобы получить любую желаемую информацию о любой нажатой карточке, просто выполнив поиск координат в блоке данных карточки. В этом примере имя карты печатается каждый раз, когда по ней щёлкают, потому что метка каждой карты находится на 3 элемента до её координатной информации в блоке данных карты:

print card-name: pick cards ((index? find cards start-coord) - 3)

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

btn "card positions" [
    foreach [card label num color pos] cards [
        print [label pos color num]
    ]
]

Когда я тестировал существующий код, я понял ещё одну необходимость, которая, вероятно, возникнет в любой карточной игре. Я хочу, чтобы пользователи не могли перемещать карты куда угодно (за пределами игровой зоны, поверх других указанных карт и т.д.). Чтобы реализовать эту функцию, я просто добавлю несколько условных оценок к приведённому выше блоку действий, чтобы проверять определённые ситуации каждый раз, когда кнопка мыши отпускается. Приведённый ниже код проверяет, находится ли смещение перемещённого лица (face = card) непосредственно поверх (с той же координатой), что и у любой другой карты, или находится ли оно за пределами игровой зоны. Если это так, карта перемещается обратно в исходное положение, устанавливая смещение на переменную начальной координаты, определённую выше. Он использует другую условную операцию, чтобы убедиться, что это выполняется только для карт на игровом поле, а не для стопок в нижней части экрана (только для пикселей 398 и выше):

if action = 'up [
    if any [
        (find cards face/offset)   ; поверх любых других карт
        (face/offset/2 < 20)       ; за пределами игровой зоны 
    ] [
        if (face/offset/2 < 398) [face/offset: start-coord]
    ]
    replace cards start-coord face/offset
    arrange-cards
]

На данный момент у нас есть универсальная платформа для создания практически любой карточной игры. Фактически, сейчас я могу играть в полную игру Freecell, если я не возражаю против того, чтобы компьютер позволял мне выполнять незаконные ходы. Мне все ещё нужно реализовать различные процедуры, чтобы запретить движения, которые считаются мошенничеством в правилах Freecell, но весь код отображения и взаимодействия, необходимый для перемещения карточек по экрану, на месте. Вот полный код - сыграйте в несколько игр:

Rebol [title: "Playing Card Framework"]

do %cards.r

random/seed now
loop 156 [
    pos1: pick cards rnd1: (random 52) * 5
    pos2: pick cards rnd2: (random 52) * 5
    poke cards rnd1 pos2
    poke cards rnd2 pos1
]

movestyle: [
    engage: func [face action event] [
        if action = 'down [
            start-coord: face/offset
            face/data: event/offset
            remove find face/parent-face/pane face
            append face/parent-face/pane face
        ]
        if find [over away] action [
            unrounded-pos: (face/offset + event/offset - face/data)
            snap-to-x: (round/to first unrounded-pos 80) + 20
            snap-to-y: (round/to second unrounded-pos 20) + 20
            face/offset: (as-pair snap-to-x snap-to-y)
        ]
        if action = 'up [
            if any [
                (find cards face/offset)
                (face/offset/2 < 20)
            ] [
                if (face/offset/2 < 398) [face/offset: start-coord]
            ]
            replace cards start-coord face/offset
            arrange-cards
        ]
        show face
    ]
]

positions: does [
    temp: copy []
    foreach item cards [if ((type? item) = pair!) [append temp item]]
    return sort temp
]

arrange-cards: does [
    foreach position positions [
        foreach card system/view/screen-face/pane/1/pane [
            if (card/offset = position) and (position/2 < 398) [
                remove find system/view/screen-face/pane/1/pane card
                append system/view/screen-face/pane/1/pane card
            ]
        ]
    ]
    show system/view/screen-face/pane/1/pane
]

gui: [size 670x510 backdrop 0.150.0 across ]
foreach [card label num color pos] cards [
    append gui compose [
        at (pos) image load to-binary decompress (card) feel movestyle
    ]
]

box-pos: 18x398
loop 4 [
    append gui compose [
        at (box-pos) box green 72x2
        at (box-pos) box green 2x97
        at (box-pos + 320x0) box white 72x2
        at (box-pos + 320x0) box white 2x97
    ]
    box-pos: box-pos + 80x0
]

view/new center-face layout gui
arrange-cards
do-events

Программа в том виде, в котором она существует в настоящее время, представляет собой полезную универсальную основу для любой карточной игры. Теперь нам нужно приступить к работе над игровой логикой Freecell. Вот основные цели, а также некоторые идеи псевдокода, которые помогут организовать мыслительный процесс:

  1. Если чёрная карта помещается под любой красной картой внизу любой из 8 "физических" стопок карт, или наоборот (красно-черная), проверьте, не на 1 карту ли перемещённая карта ниже по значению, чем карта, которую он касается. Если нет, не позволяйте двигаться. Например, красную 8 можно переместить под чёрную 9, но перемещение красной 8 под красной 9 (не чередование красно-чёрных) или чёрного короля под красной 3 (не подряд) запрещено. Вы можете сделать запрещённое перемещение карты программным путём, сбросив лицевую сторону/смещение любой запрещённой карты обратно к значению, которое было перед перемещением.
  2. Можно перемещать только карты, открытые внизу стопки, или одну из карт в нисходящей группе чередующихся красно-чёрных карт внизу стопки. Например, в группе карт r7, b6, r5, b4, r3 в нижней части стопки вы можете переместить красную 7 и все карты под ней в другую стопку с открытой чёрной 8 внизу стопки. . Вы также можете взять чёрную 4 и переместить её вместе с красной 3 под стопку с красной 5 внизу. Однако вы не могли взять красную 7 из этой стопки, не переместив при этом остальные карты (b6, r5, b4, r3) под ней.
  3. Цель игры - переместить все карты из первоначально отображаемых 8 стопок в 4 новые стопки "цели", которые изначально пусты. По завершении каждая стопка должна содержать только карты уникальной масти (трефы, бубны, червы или пики), а их номиналы должны последовательно возрастать от туза к королю. Запретите любые движения карт, которые не позволяют такое расположение.
  4. Есть 4 дополнительных ячейки, или "свободные ячейки" (название игры), которые можно использовать для временного хранения и перемещения карт между стопками. Они полезны при перемещении карт, когда в начальных стопках или в стопках ворот нет позиций, которые позволяют перемещать карту в соответствии с предыдущими правилами. Только отдельные открытые карты (без закрытых карт или стопок) могут быть перемещены в свободную ячейку.

Большинство условных операций в остальной части программы будет происходить в блоке действия "вверх" кода movestyle, потому что они должны выполняться, когда пользователь куда-то роняет карту. Чтобы завершить шаг 1 выше, нам нужно проверить столбцы на наличие чередующихся красно-чёрных узоров. Затем нам нужно проверить порядковый образец, чтобы убедиться, что он идёт от максимума к минимуму. Это не так сложно сделать, потому что у нас есть вся необходимая информация, прикреплённая к каждой карте в нашем исходном блоке данных карты. Я добавил следующий код в блок действия "вверх" кода движения, чтобы определить, какие карты находятся в том же столбце, что и перемещённая карта:

column-coords: copy []

; В приведённом ниже цикле проверяются все координаты в блоке данных 
; карты. Если горизонтальное положение совпадает с положением карты, 
; которую кладут, добавим эту координату в блок "column-coords":
foreach item cards [
    if ((type? item) = pair!) [
        if item/1 = face/offset/1 [
            append column-coords item
        ]
    ]
]

column: copy []

; Просмотрим координаты, собранные выше, найдём имя карты в каждой 
; координате и добавим каждое имя в блок "columns":

foreach item (sort copy column-coords) [
    append column (pick cards ((index? find cards item) - 3))
]

print column

Фантастика! Теперь я могу выполнять любые вычисления, связанные с расположением столбцов карточек :)

Вы можете взять дизайн отсюда и добавить столько функций, сколько захотите!

23.4 Случай: Создание REBOL "Demo"

Следующий короткий пример содержит 10 полных программ и демонстрирует, насколько мощным может быть код REBOL. (Здесь также доступен EXE-файл Windows):

REBOL[title:"Demo"]p: :append kk: :pick r: :random y: :layout q: 'image
z: :if gg: :to-image v: :length? g: :view k: :center-face ts: :to-string
tu: :to-url sh: :show al: :alert rr: :request-date co: :copy g y[style h
btn 150 h"Paint"[g/new k y[s: area black 650x350 feel[engage: func[f a e][
z a = 'over[p pk: s/effect/draw e/offset sh s]z a = 'up[p pk 'line]]]
effect[draw[line]]b: btn"Save"[save/png %a.png gg s al"Saved 'a.png'"]btn
"Clear"[s/effect/draw: co[line]sh s]]]h"Game"[u: :reduce x: does[al join{
SCORE: }[v b]unview]s: gg y/tight[btn red 10x10]o: gg y/tight[btn tan
10x10]d: 0x10 w: 0 r/seed now b: u[q o(((r 19x19)* 10)+ 50x50)q s(((r
19x19)* 10)+ 50x50)]g/new k y/tight[c: area 305x305 effect[draw b]rate 15
feel[engage: func[f a e][z a = 'key[d: select u['up 0x-10 'down 0x10 'left
-10x0 'right 10x0]e/key]z a = 'time[z any[b/6/1 < 0 b/6/2 < 0 b/6/1 > 290
b/6/2 > 290][x]z find(at b 7)b/6[x]z within? b/6 b/3 10x10[p b u[q s(last
b)]w: 1 b/3:((r 29x29)* 10)]n: co/part b 5 p n(b/6 + d)for i 7(v b)1[
either(type?(kk b i)= pair!)[p n kk b(i - 3)][p n kk b i]]z w = 1[clear(
back tail n)p n(last b)w: 0]b: co n sh c]]]do[focus c]]]h"Puzzle"[al{
Arrange tiles alphabetically:}g/new k y[origin 0x0 space 0x0 across style
p button 60x60[z not find[0x60 60x0 0x-60 -60x0]face/offset - x/offset[
exit]tp: face/offset face/offset: x/offset x/offset: tp]p"O"p"N"p"M"p"L"
return p"K"p"J"p"I"p"H"return p"G"p"F"p"E"p"D"return p"C"p"B"p"A"x: p
white edge[size: 0]]]h"Calendar"[do bx:[z not(exists? %s)[write %s ""]rq:
rr g/new k y[h5 ts rq aa: area ts select to-block(find/last(to-block read
%s)rq)rq btn"Save"[write/append %s rejoin[rq" {"aa/text"} "]unview do bx]]
]]h"Video"[wl: tu request-text/title/default"URL:"join"http://tinyurl.com"
"/m54ltm"g/new k y[image load wl 640x480 rate 0 feel[engage: func[f a e][
z a = 'time[f/image: load wl show f]]]]]h"IPs"[parse read tu join"http://"
"guitarz.org/ip.cgi"[thru<title>copy my to</title>]i: last parse my none
al ts rejoin["WAN: "i" -- LAN: "read join dns:// read dns://]]h"Email"[
g/new k y[mp: field"pop://user:pass@site.com"btn"Read"[ma: co[]foreach i
read tu mp/text[p ma join i"^/^/^/^/^/^/"editor ma]]]]h"Days"[g/new k y[
btn"Start"[sd: rr]btn"End"[ed: rr db/text: ts(ed - sd)show db]text{Days
Between:}db: field]]h"Sounds"[ps: func[sl][wait 0 rg: load sl wf: 1 sp:
open sound:// insert sp rg wait sp close sp wf: 0]wf: 0 change-dir
%/c/Windows/media do wl:[wv: co[]foreach i read %.[z %.wav = suffix? i[p
wv i]]]g/new k y[ft: text-list data wv[z wf <> 1[z error? try[ps value][al
"Error"close sp wf: 0]]]btn"Dir"[change-dir request-dir do wl ft/data: wv
sh ft]]]h{FTP}[g/new k y[px: field"ftp://user:pass@site.com/folder/"[
either dir? tu va: value[f/data: sort read tu va sh f][editor tu va]]f:
text-list[editor tu join px/text value]btn"?"[al{Type a URL path to browse
(nonexistent files are created). Click files to edit.}]]]]

Все 10 программ, включённых в эту демонстрацию, представляют собой сокращённые версии других фрагментов кода, которые можно найти в этом руководстве:

Программа "paint" (рисование) была рассмотрена в разделе руководства, посвящённом диалекту рисования draw:

view center-face layout [
    s: area black 650x350 feel [
        engage: func [f a e] [
            if a = 'over [
                append s/effect/draw e/offset
                show s
            ]
            if a = 'up [append s/effect/draw 'line]
        ]
    ] effect [draw [line]]
    b: btn "Save" [
        save/png %a.png to-image s 
        alert "Saved 'a.png'"
    ]
    btn "Clear" [
        s/effect/draw: copy [line]
        show s]
    ]
]

"game" - это запутанная программа-змейка, о которой говорилось ранее:

do[p: :append u: :reduce k: :pick r: :random y: :layout q: 'image z: :if
g: :to-image v: :length? x: does[alert join{SCORE: }[v b]quit]s: g y/tight
[btn red 10x10]o: g y/tight[btn tan 10x10]d: 0x10 w: 0 r/seed now b: u[q
o(((r 19x19)* 10)+ 50x50)q s(((r 19x19)* 10)+ 50x50)]view center-face
y/tight[c: area 305x305 effect[draw b]rate 15 feel[engage: func[f a e][z a
= 'key[d: select u['up 0x-10 'down 0x10 'left -10x0 'right 10x0]e/key]z a
= 'time[z any[b/6/1 < 0 b/6/2 < 0 b/6/1 > 290 b/6/2 > 290][x]z find(at b
7)b/6[x]z within? b/6 b/3 10x10[p b u[q s(last b)]w: 1 b/3:((r 29x29)*
10)]n: copy/part b 5 p n(b/6 + d)for i 7(v b)1 [either(type?(k b i)=
pair!)[p n k b(i - 3)][p n k b i]]z w = 1[clear(back tail n)p n(last b)w:
0]b: copy n show c]]]do[focus c]]]

"Головоломка" - это программа для работы с плитками, описанная в первом разделе учебника по графическому интерфейсу пользователя:

alert {Arrange tiles alphabetically:}
view center-face layout [
    origin 0x0 space 0x0 across
    style p button 60x60 [
        if not find [0x60 60x0 0x-60 -60x0] face/offset - x/offset [exit]
        temp: face/offset face/offset: x/offset x/offset: temp
    ]
    p "O" p "N" p "M" p "L" return
    p "K" p "J" p "I" p "H" return
    p "G" p "F" p "E" p "D" return
    p "C" p "B" p "A" x: p white edge [size: 0]
]

"Календарь" - это простое приложение, в котором пользователь выбирает день с помощью функции запроса даты. События дня вводятся в виджет области, а затем добавляются в текстовый файл. Текстовый файл ищется каждый раз, когда выбирается дата. Если выбранная дата найдена, события этого дня отображаются в виджете области, который можно редактировать и сохранять обратно в текстовый файл:

do the-calendar: [
    if not (exists? %s) [write %s ""]
    the-date: request-date
    view center-face layout [
        h5 to-string the-date 
        aa: area to-string select to-block (
            find/last (to-block read %s) the-date
        ) the-date 
        btn "Save" [
            write/append %s rejoin [the-date " {" aa/text "} " ]
            unview 
            do the-calendar
        ]
    ]
]

Программа "video" была рассмотрена в разделе учебника, посвященном многозадачности. Все, что он делает, - это постоянно загружать и отображать изображения с сервера веб-камеры. Обновление изображения обрабатывается с помощью цикла взаимодействия, который проверяет наличие события таймера:

video-address: to-url request-text/title/default "URL:" trim {
    http://tinyurl.com/m54ltm}
view center-face layout [
    image load video-address 640x480 rate 0 feel [
        engage: func [f a e] [
            if a = 'time [
                f/image: load video-address 
                show f
            ]
        ]
    ]
 ]

О программе "IP" рассказывалось в разделе, посвящённом синтаксическому диалекту REBOL. Эта программа считывает веб-страницу, которая отображает удалённый IP-адрес WAN компьютера пользователя в теге заголовка, затем анализирует весь дополнительный текст и отображает IP-адрес вместе с локальным IP-адресом пользователя (локальный адрес получается с помощью REBOL имеет встроенный протокол dns:// :

parse read to-url "http://guitarz.org/ip.cgi" [
    thru <title> copy my to </title>
]
i: last parse my none
alert to-string rejoin [
    "WAN: " i " -- LAN: " read join dns:// read dns://
]

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

view center-face layout [
    email-login: field "pop://user:pass@site.com"    
    btn "Read" [
        my-mail: copy []
        foreach i (read to-url email-login/text) [
            append my-mail join i "^/^/^/^/^/^/"
            editor my-mail
        ]
    ]
]

Программа "Промежуточные дни" была рассмотрена в более раннем тематическом исследовании. Вот простая версия программы (пример приведён в первой части тематического исследования):

view center-face layout [
    btn "Start" [sd: request-date]
    btn "End" [
        ed: request-date
        db/text: to-string (ed - sd)
        show db
    ]
    text "Days Between:"
    db: field
]

Программа "звуки" также была рассмотрена ранее:

play-sound: func [sound-file] [
    wait 0 ring: load sound-file 
    wait-flag: 1 
    sound-port: open sound:// 
    insert sound-port ring 
    wait sound-port 
    close sound-port 
    wait-flag: 0
]
wait-flag: 0 
change-dir %/c/Windows/media 
do get-waves: [
    waves-list: copy []
    foreach i read %. [
        if %.wav = suffix? i [
            append waves-list i
        ]
    ]
]
view center-face layout [
    waves-gui-list: text-list data waves-list [
        if wait-flag <> 1 [
            if error? try [play-sound value] [ 
                alert "Error"
                close sound-port
                wait-flag: 0
            ]
        ]
    ]
    btn "Dir" [
        change-dir request-dir 
        do get-waves
        waves-gui-list/data: waves-list
        show waves-gui-list
    ]
]

Программа "FTP" - это урезанная версия "FTP Tool", о которой говорилось ранее:

view center-face layout [
    px: field "ftp://user:pass@site.com/folder/" [
        either dir? to-url value [
            f/data: sort read to-url value 
            show f
        ][
            editor to-url value
        ]
    ]
    f: text-list [
        editor to-url join px/text value
    ]
    btn "?" [
        alert {
            Type a URL path to browse (nonexistent files are created).
            Click files to edit.
        }
    ]
]

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

REBOL [title: "Demo"]

view layout [
    style h btn 150 
    h "Paint" [
        ; код для программы рисования находится здесь
    ]
    h "Game" [
        ; код для игровой программы находится здесь
    ]
    h "Puzzle" [
        ; код для программы-головоломки находится здесь
    ]
    h "Calendar" [
        ; код программы календаря находится здесь
    ]
    h "Video" [
        ; код для видеопрограммы находится здесь
    ]
    h "IPs" [
        ; код для программы IP находится здесь
    ]
    h "Email" [
        ; код для программы электронной почты находится здесь
    ]
    h "Days" [
        ; код для промежуточной программы находится здесь
    ]
    h "Sounds" [
        ; код для звуковой программы находится здесь
    ]
    h "FTP" [
        ; код для программы FTP находится здесь
    ]
]

Чтобы сделать демонстрацию как можно более компактной, я использовал те же приёмы, что и в программе обфусцированной змейки (из ранее в руководстве). Вот глобальные функции, которые я переименовал с помощью более коротких словесных меток:

p: :append kk: :pick r: :random y: :layout q: 'image z: :if gg: :to-image
v: :length? g: :view k: :center-face ts: :to-string tu: :to-url sh: :show
al: :alert rr: :request-date co: :copy

Я также переименовал другие функции в их локальном контексте. В следующем коде значение "s/effect/draw" присвоено переменной "pk". Это избавляет от необходимости снова писать "s/effect/draw". Это значение не существует в глобальном контексте, поэтому эта переменная должна быть назначена локально. Этот тип сокращённого присвоения локальной переменной происходит несколько раз в демонстрационном коде:

view layout [
    s: area black 650x350 feel [
        engage: func [f a e] [
            if a = 'over [
                append pk: s/effect/draw e/offset 
                show s
            ]
            if a = 'up [append pk 'line]
        ]
    ] effect [
        draw [line]
    ]
]

Чтобы завершить приложение, я просто удалил все пробелы, окружавшие круглые или квадратные скобки. Окончательный код находится в первом разделе этого руководства. Вот снимок экрана - это меньше 1/2 страницы печатного кода:

24. Прочие скрипты

В этом разделе руководства содержатся различные случайные сценарии, которые могут оказаться полезными и интересными. На http://re-bol.com/examples.txt доступен ряд дополнительных примеров скриптов. Не забудьте просмотреть все скрипты на rebol.org - не только в разделе скриптов, но и в архивах электронной почты и AltME. Это кладезь рабочих примеров кода и ответов практически на любую проблему кодирования!

24.1 Создатель эскизов

Эта программа изменяет размер и упорядочивает список файлов изображений в одно изображение предварительного просмотра. Лист с изображением снимка экрана в начале этого руководства был создан с помощью этого приложения.

REBOL [Title: "Thumbnail Maker"]

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

view center-face layout [

    text "Resize input images to this height:"
    height: field "200"

    text "Create output mosaic of this width:"
    width: field "600"

    text "Space between thumbnails:"
    padding-size: field "30"

    text "Color between thumbnails:"
    btn "Select color" [background-color: request-color/color white]

    text "Thumbnails will be displayed in this order:"
    the-images: area
    across
    btn "Select images" [

        ; Выберем несколько файлов:
some-images: request-file/title trim/lines {Hold
    down the [CTRL] key to select multiple images:} ""

; Проверка ошибок:
if some-images = none [return] 

; Покажем выбранные файлы в виджете области выше, с каждым 
; файлом в новой строке:
foreach single-image some-images [
   append the-images/text single-image
   append the-images/text "^/"
]
show the-images
]

; Эта кнопка создаёт мозаику выходных эскизов:
btn "Create Thumbnail Mosaic" [

    ; Зададим для переменных размера значения, введённые в 
    ; графическом интерфейсе выше:
y-size: to-integer height/text
mosaic-size: to-integer width/text
padding: to-integer padding-size/text

; Установим цвет фона (белый, если не выбрали другой):
if error? try [background-color: to-tuple background-color][
    background-color: white
]

; Список изображений, размер которых будет изменён, хранится 
; в блоке "images"(изображения). Функция синтаксического 
; анализа "parse" рассматривается далее в этом руководстве. 
; Следующий код просто разделяет каждый элемент строки в 
; текстовой области выше и возвращает блок всех элементов:
images: copy parse/all the-images/text "^/"

; Проверка ошибок:
if empty? images [alert "No images selected." break]

; Выходное изображение будет создано из блока GUI "view 
; layout". Этот блок будет помечен как "mosaic"(мозаика) и 
; будет содержать все данные изображения с изменённым 
; размером и форматирование макета, необходимые для создания 
; миниатюрного изображения. Мы начнём строить этот блок с 
; включения цвета фона, интервалов и слова "across" 
; (поперёк), необходимого для компоновки графического 
; интерфейса. Поскольку блок содержит некоторые переменные, 
; мы будем использовать функцию "compose" для их оценки 
; (относиться к ним так, как если бы они были введены явно):

mosaic: compose [
    backcolor (background-color) space (padding) across
]

; Затем мы будем использовать цикл foreach, чтобы 
; просмотреть список изображений, прочитать и изменить 
; размер каждого изображения, а также добавить изменённые 
; данные изображения в блок мозаики. Переменная "picture" 
; (картинка) будет использоваться для ссылки на каждое 
; изображение по мере прохождения цикла по каждому элементу 
; в списке:

foreach picture images [

     ; Дадим пользователю обратную связь небольшим сообщением:

     flash rejoin ["Resizing " picture "..."]

     ; Прочтём данные изображения и присвойте ему метку 
     ; переменной "original" (оригинал):

     original: load to-file picture

     ; После загрузки данных сотрём сообщение выше:
unview

; Мы можем ссылаться на размер исходного изображения, 
; используя формат "orginal/size" (исходный/размер). 
; Это возвращает значения ширины и высоты в виде пары 
; XxY. Чтобы сослаться только на значение высоты (Y), 
; мы можем использовать формат "original/size/2" 
; (второй элемент в паре). Если высота исходного 
; изображения больше, чем значение переменной "y-size", 
; установленное в начале программы, мы изменим размер 
; изображения так, чтобы оно соответствовало этой 
; высоте, и добавим данные изображения с изменённым 
; размером в блок "mosaic". В противном случае мы 
; просто добавим исходное изображение в блок. Мы также 
; собираемся включить слово "image" (изображение), 
; потому что блок "mosaic" должен включать все функции 
; и данные, необходимые для создания окна графического 
; интерфейса макета представления:

; Если исходное изображение выше предписанной высоты:
either original/size/2 > y-size [

    ; Укажем процентное значение ширины, которую 
    ; необходимо изменить:

    new-x-factor: y-size / original/size/2

    ; Вычисляем ширину нового размера изображения и 
    ; присваиваем это значение переменной "new-x-size":

    new-x-size: round original/size/1 * new-x-factor

    ; Создадим изображение с изменённым размером, 
    ; используя функцию "layout" (как в "view layout"). 
    ; Укажите новый размер изображения, повторно 
    ; соединив указанную выше переменную "new-x-size" 
    ; со значением "y-size", указанным ранее, и 
    ; преобразуем это значение в пару. Создадим новое 
    ; изображение из этого макета с помощью функции 
    ; "to-image" и присвойте его переменной "new-image":
new-image: to-image layout/tight [
    image original as-pair new-x-size y-size
]

; Затем добавим данные изображения с изменённым 
; размером в блок "mosaic" (мозаика). Мы составим 
; блок, потому что хотим, чтобы данные нового 
; изображения были включены, как если бы они были 
; введены явно. Слово "image" (изображение) также 
; необходимо включить, потому что оно необходимо 
; для отображения изображения в блоке макета вида:
append mosaic compose [image (new-image)]

][

; Вот вторая часть условия "either" (либо) выше. 
; Если высота оригинала меньше переменной "y-size", 
; просто добавим исходное изображение в блок 
; "mosaic" (мозаика):
append mosaic compose [image (original)]
]

; В текущем цикле foreach каждое изображение с 
; изменённым размером просто добавляется в "mosaic" 
; макет слева направо. Нам нужно проверять размер 
; макета "mosaic" каждый раз, когда мы добавляем 
; изображение. Если макет шире, чем ширина, которую мы 
; установили в начале программы (переменная 
; "mosaic-size"), нам нужно вставить слово "return" в 
; блок макета графического интерфейса "mosaic":

; Создайте временную раскладку блока "mosaic":
current-layout: layout/tight mosaic

; Если ширина текущего макета больше, чем предписанная 
; ширина, вставим слово "return" ПЕРЕД текущим 
; изменённым размером изображения. Одинарная кавычка 
; ставится перед словом 'return, так что фактический 
; неоцененный текст "return" добавляется к блоку 
; мозаики. "back back tail" помещает слово "return" в 
; правильное место в блоке макета:
if current-layout/size/1 > mosaic-size [
    insert back back tail mosaic 'return
]
]

; Предложим пользователю ввести имя файла, чтобы сохранить 
; окончательное изображение макета "mosaic":

filename: to-file request-file/file/save "mosaic.png"

; Создайте изображение из последнего блока макета "mosaic" и 
; сохраните это изображение в файле с именем, указанным выше:
save/png filename (to-image layout mosaic)

; Покажите пользователю сохранённое изображение:

view/new layout [image load filename]
]
]

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

24.2 Циклы и условия - простое приложение для хранения данных

Одно из наиболее важных применений циклических структур - просмотр списков данных. Пошагово проходя по элементам в блоке, можно использовать циклы для обработки и выполнения действий над каждым элементом в заданной серии данных. Этот метод используется во всех типах программирования и является краеугольным камнем того, как программисты думают о работе с таблицами данных (например, с таблицами баз данных). Поскольку многие программы работают со списками данных, вы очень часто будете сталкиваться с ситуациями, требующими использования циклов. Размышление о том, как использовать циклические структуры, является фундаментальной частью обучения написанию кода на любом языке. В приведённом ниже примере показано несколько способов, с помощью которых вы увидите, как обычно используются циклы.

REBOL [title: "Loops and Conditions - a Simple Data Storage App"]

; Сначала определим небольшую базу данных пользователей. Она 
; организована в блочную структуру: блок "users" (пользователи) 
; содержит 5 блоков, каждый из которых содержит 5 элементов 
; информации для каждого пользователя. Пустые элементы представлены 
; пустыми кавычками.
users: [
    ["John" "Smith" "123 Toleen Lane" "Forest Hills, NJ" "555-1234"]
    ["Paul" "Thompson" "234 Georgetown Pl." "Peanut Grove, AL" "555-2345"]
    ["Jim" "Persee" "345 Portman Pike" "Orange Grove, FL" "555-3456"]
    ["George" "Jones" "456 Topforge Court" "Mountain Creek, CO" ""]
    ["Tim" "Paulson" "" "" "555-5678"]
]

; У этой программы нет графического интерфейса. Вместо этого это 
; текстовая "консольная" программа. Поскольку графического 
; интерфейса пользователя нет, нам нужно отформатировать вывод так, 
; чтобы он имел красивый вид на экране. Вот небольшая функция, 
; которая использует цикл для рисования линии. Он печатает 65 тире 
; рядом друг с другом, а затем - возврат каретки. Мы будем 
; использовать эти строки, чтобы напечатать красиво 
; отформатированный вывод:
draw-line: does [loop 65 [prin "-"] print ""]

; Обратите внимание, что это не самый эффективный способ рисования 
; строки символов, потому что программе необходимо выполнять цикл 
; каждый раз, когда рисуется линия. Вы будете видеть некоторое 
; мерцание на экране каждый раз, когда это происходит, потому что 
; компьютеру приходится выполнять функцию "prin" 65 раз для каждой 
; строки. Хотя на современном компьютере это занимает всего долю 
; секунды, это всё же довольно заметно. Вместо этого было бы быстрее 
; создать блок символов один раз, а затем распечатать этот блок 
; следующим образом:
;
;        a-line: copy []
;        loop 65 [append a-line "-"]
;        ; удалим пробелы и превратим в строку: 
;        a-line: trim to-string a-line
;        ; теперь вы можете печатать "a-line" где угодно:
;        print a-line
;
; Приведённый выше неэффективный код оставлен в этом примере для 
; демонстрации того, как мыслительный процесс кодирования может 
; существенно повлиять на производительность создаваемых вами 
; программ. Это особенно верно для программ, которые выполняют 
; сложные циклы с большими списками данных. Более эффективная 
; функция строчной печати реализована в другом примере, следующем за 
; этим, чтобы продемонстрировать разницу в её эффективности.
;
; Далее идёт небольшая функция, которая распечатывает все данные в 
; базе данных. Он использует цикл foreach для циклического перебора 
; каждого блока пользовательских данных, а затем печатает строку, 
; отображающую каждый элемент в блоке (элементы с номерами от 1 до 5 
; в каждом блоке). Это создаёт красиво отформатированный дисплей:
print-all: does [
    foreach user users [
        draw-line
        print rejoin ["User:     " user/1 " " user/2]
        draw-line
        print rejoin ["Address:  " user/3 "  " user/4]
        print rejoin ["Phone:    " user/5]
        print newline
    ]
]

; В следующем коде используется бесконечный цикл, чтобы постоянно 
; запрашивать выбор у пользователя. Он использует несколько циклов 
; foreach для извлечения информации из блока данных и условную 
; структуру "switch", чтобы решить, как отвечать на запрос 
; пользователя. "switch" внутри цикла навсегда - это обычная 
; конструкция в программах командной строки:
forever [

    ; Во-первых, распечатайте красивое форматирование и отобразите 
    ; информацию:

    prin "^(1B)[J"  ; этот код очищает экран

    print "Here are the current users in the database:^/"
    ; Символ "^/" в конце вышеприведённой строки печатает новую 
    ; строку.

    draw-line  ; запустить функцию, определённую выше
; Теперь распечатаем список имён пользователей. Цикл foreach 
; используется для получения имени и фамилии каждого 
; пользователя в базе данных. Имя - это элемент 1 в 
; каждом блоке, а фамилия - это элемент 2 в каждом блоке. Итак, 
; для каждого блока в базе данных печатаются "user/1" и "user/2":
foreach user users [prin rejoin [user/1 " " user/2 "  "]]
print "" 
draw-line

; распечатаем небольшую инструкции:

prin "Type the name of a user below "
print "(part of a name will perform search):^/"
print "Type 'all' for a complete database listing."
print "Press [Enter] to quit.^/"

; Теперь попросим пользователя сделать выбор:

answer: ask {What person would you like info about?  }
print newline

; Решаем, что делать с ответом пользователя:

switch/default answer [

    ; Если он набрал "all", выполним функцию "print-all", 
    ; определённую ранее:
"all" [print-all]

; Если он нажал только клавишу [Enter] (""), распечатаем 
; прощальное сообщение и завершим программу. Обратите 
; внимание, что для отображения сообщения используется 
; "ask", а не "print". Это позволяет программе ждать, пока 
; пользователь нажмёт клавишу, прежде чем завершить программу:
"" [ask "Goodbye!  Press [Enter] to end." quit]

; Если ни один из вышеперечисленных вариантов не был введён, 
; то выполняется блок по умолчанию ниже (это последняя часть 
; структуры переключателя):

][

; Этот раздел начинается с создания переменной "flag", 
; которая используется для отслеживания того, был ли найден 
; ввод пользователя в базе данных - слово "found" (найдено) 
; изначально установлено в false, чтобы указать, что текст 
; введённый пользователем ещё не найден:

found: false

; Затем цикл foreach проходит через каждый пользовательский 
; блок в базе данных:

foreach user users [

    ; Если введённый пользователем текст найден в базе 
    ; данных (имя или фамилия), информация для этого 
    ; пользователя распечатывается в красиво 
    ; отформатированном виде, а флаг "found" (найден) 
    ; устанавливается в значение true. Действие "rejoin" 
    ; используется для соединения имени и фамилии, и 
    ; используется вместе с действием "find", чтобы 
    ; проверить, совпадает ли ответ пользователя с 
    ; какой-либо частью имён в базе данных (при запуске 
    ; этого кода попробуйте ввод отдельных символов или 
    ; части имени, чтобы увидеть, что произойдёт).
if find rejoin [user/1 " " user/2] answer [
    draw-line
    print rejoin ["User:     " user/1 " " user/2]
    draw-line
    print rejoin ["Address:  " user/3 " " user/4]
    print rejoin ["Phone:    " user/5]
    print newline
    found: true
]
]

; Если после перебора всей пользовательской базы данных 
; значение переменной "found" всё ещё равно false, значит, 
; имя пользователя не было найдено в базе данных. Напечатаем 
; сообщение об этом:
if found <> true [   ; "<>" means "not equal to"
    print "That user is not in the database!^/"]
]

; Дождёмся ответа пользователя, а затем продолжим снова выполнять цикл forever "навсегда":

ask "Press [ENTER] to continue"
]

Вот вся программа без комментариев. Попробуйте самостоятельно следить за ходом программы. ПРИМЕЧАНИЕ. В этой версии неэффективная функция рисования линии ("draw-line") заменена предложенной выше процедурой печати строки ("print a-line"). В результате вы увидите резкое уменьшение мерцания экрана:

Rebol []
users: [
    ["John" "Smith" "123 Tomline Lane" "Forest Hills, NJ" "555-1234"]
    ["Paul" "Thompson" "234 Georgetown Pl." "Peanut Grove, AL" "555-2345"]
    ["Jim" "Persee" "345 Pickles Pike" "Orange Grove, FL" "555-3456"]
    ["George" "Jones" "456 Topforge Court" "Mountain Creek, CO" ""]
    ["Tim" "Paulson" "" "" "555-5678"]
]
a-line: copy [] loop 65 [append a-line "-"]
a-line: trim to-string a-line
print-all: does [
    foreach user users [
        print a-line
        print rejoin ["User:     " user/1 " " user/2]
        print a-line
        print rejoin ["Address:  " user/3 "  " user/4]
        print rejoin ["Phone:    " user/5]
        print newline
    ]
]    
forever [
    prin "^(1B)[J"
    print "Here are the current users in the database:^/"
    print a-line
    foreach user users [prin rejoin [user/1 " " user/2 "  "]]
    print "" print a-line
    prin "Type the name of a user below "
    print "(part of a name will perform search):^/"
    print "Type 'all' for a complete database listing."
    print "Press [Enter] to quit.^/"
    answer: ask {What person would you like info about?  }
    print newline
    switch/default answer [
        "all"     [print-all]
        ""         [ask "Goodbye!  Press any key to end." quit]
        ][
        found: false
        foreach user users [
            if find rejoin [user/1 " " user/2] answer [
                print a-line
                print rejoin ["User:     " user/1 " " user/2]
                print a-line
                print rejoin ["Address:  " user/3 " " user/4]
                print rejoin ["Phone:    " user/5]
                print newline
                found: true
            ]
        ]
        if found <> true [
            print "That user is not in the database!^/"
        ]
    ]
    ask "Press [ENTER] to continue"
]

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

REBOL [title: "Loops and Conditions - Data Storage App - GUI Example"]

users: [
    ["John" "Smith" "123 Tomline Lane" "Forest Hills, NJ" "555-1234"]
    ["Paul" "Thompson" "234 Georgetown Pl." "Peanut Grove, AL" "555-2345"]
    ["Jim" "Persee" "345 Pickles Pike" "Orange Grove, FL" "555-3456"]
    ["George" "Jones" "456 Topforge Court" "Mountain Creek, CO" ""]
    ["Tim" "Paulson" "" "" "555-5678"]
]
user-list: copy []
foreach user users [append user-list user/1]
user-list: sort user-list

view display-gui: layout [
    h2 "Click a user name to display their information:"
    across
    list-users: text-list 200x400 data user-list [
        current-info: []
        foreach user users [
            if find user/1 value [
                current-info: rejoin [
                    "FIRST NAME:  " user/1 newline newline
                    "LAST NAME:   " user/2 newline newline
                    "ADDRESS:     " user/3 newline newline
                    "CITY/STATE:  " user/4 newline newline
                    "PHONE:       " user/5
                ]
            ]
        ]
        display/text: current-info
        show display show list-users
    ]
    display: area "" 300x400 wrap
]

24.3 Пример многоколоночной сетки данных Listview

В этом примере используется модуль listview, который находится по адресу http://www.hmkdesign.dk/rebol/list-view/list-view.r. Модуль listview выполняет всю основную работу по отображению, сортировке, фильтрации, изменению и манипулированию данными с помощью знакомого пользовательского интерфейса, который легко программировать. Документация доступна по адресу http://www.hmkdesign.dk/rebol/list-view/list-view.html.

Щелчок по заголовку столбца в приведённом ниже примере сортирует данные по выбранному столбцу, по возрастанию или по убыванию. Если щёлкнуть ромбик в верхнем правом углу, данные вернутся в их несортированный порядок. Выбор строки данных с помощью мыши позволяет редактировать каждую ячейку напрямую. Поскольку возможно встроенное редактирование, для ввода/вывода данных не требуются дополнительные виджеты графического интерфейса. Это делает модуль listview очень мощным инструментом, который может быть полезен в самых разных ситуациях.

REBOL [title: "Listview Data Grid"]

; Функция, представленная ниже, отслеживает нажатие кнопки закрытия 
; окна графического интерфейса пользователя, чтобы предотвратить 
; случайное завершение работы программы. Код был скорректирован из 
; примера по адресу: http://www.rebolforces.com/view-faq.html

evt-close: func [face event] [
    either event/type = 'close [
        inform layout [
            across
            Button "Save Changes" [
                ; при нажатии кнопки сохранения автоматически 
                ; создаётся файл данных резервной копии:
                backup-file: to-file rejoin ["backup_" now/date]
                write backup-file read %database.db
                save %database.db theview/data quit
            ]
            Button "Lose Changes" [quit]
            Button "CANCEL" [hide-popup]
        ] none ] [ 
        event
    ]
]
insert-event-func :evt-close

; Загрузим и импортируем/запустим ("do") модуль list-view.r:

if not exists? %list-view.r [write %list-view.r read
    http://www.hmkdesign.dk/rebol/list-view/list-view.r
]
do %list-view.r

; Следующая условная оценка проверяет, существует ли файл базы 
; данных. Если нет, то создаётся файл с пустыми блоками:

if not exists? %database.db [write %database.db {[][]}]

; Теперь сохранённые данные считываются в переменную database:

database: load %database.db

; Вот внутренняя часть программы. Обязательно прочтите документацию 
; по "list-view", чтобы понимать, как работает виджет.

view center-face gui: layout [
    h3 {To enter data, double-click any row, and type directly 
        into the listview.  Click column headers to sort:}
    theview: list-view 775x200 with [
        data-columns: [Student Teacher Day Time Phone 
            Parent Age Payments Reschedule Notes]
        data: copy database
        tri-state-sort: false
        editable?: true
      ]
    across
    button "add row" [theview/insert-row]
    button "remove row" [
        if (to-string request-list "Are you sure?" 
                [yes no]) = "yes" [
            theview/remove-row
        ]
    ]
    button "filter data" [
        filter-text: request-text/title trim {
            Filter Text (leave blank to refresh all data):}
        if filter-text <> none [
            theview/filter-string: filter-text
            theview/update
        ]
    ]
    button "save db" [
        backup-file: to-file rejoin ["backup_" now/date]
        write backup-file read %database.db
        save %database.db theview/data
    ]
]

В этом примере модуль просмотра списка загружается из Интернета, а затем импортируется с жёсткого диска. Следующие строки требуют либо наличия подключения к Интернету, либо наличия модуля list-view.r на жёстком диске пользователя:

if not exists? %list-view.r [write %list-view.r read
    http://www.hmkdesign.dk/rebol/list-view/list-view.r
]
do %list-view.r

Если вы хотите использовать модуль list-view.r в сценарии без необходимости его загрузки или включения в отдельный файл (чтобы для запуска сценария не требовались внешние зависимости), вы можете заменить приведённые выше строки следующими код. Следующий код представляет собой файл list-view.r, сжатый с использованием "compress read http://www.hmkdesign.dk/rebol/list-view/list-view.r"

do decompress #{
789CCD3D6B73E3C871DFF52BE6369592F67629905A9F73A6B2A7B2EFECD8E557
2AE738A96221551031142181040D8092B8A9FC97FCD474F7BCBA07038ADA3B3B
E6D56909CCABA7BBA75FD333FCB75FFEE28FBF538B33A5FE54F5B59EAB377FFE
CD77EA77BFF9FE4F933FFFE697FFA17E552CF51B28FD558585FF58575D3F79AC
F4D3650B2F7FBEEFD74D3B578B37BFD6DBB67A50BFAF1E0A5DABDFB6504D6F3B
BD7D9343B56F9BDDA1ADEED63DF47E359D7EF55EC1DF9FAA89FAF5EF7FABBED3
5D75B7C521BE6D75D1EB728EA55F4D665793AB9FC1DBEFE01DBDFAE9643A9B5C
CDE0D59F75DB55CD76AEA697D3CBABAFE1CDEFAA250E3757FF0D0F4AFDE2FBEF
D4C5D3D3D365B383D7CDBE5DEACBA6BDCB6A53ADCB6EBB72621F2E77EBDD5B6A
F5EF9D5645AF0E505F354F5B059378B88492FF81FFFF75DFEE1A1AE05FF456B7
45AD76E68D428C2042D453D5AFD5A6D81ED40AE6B16F75A7564DABF650A7DA2A
C0EA2576F487A6F760FE695D411D40AC827F8BC7A2AA8BDB1A619853F1BAEF77
F32CC379AC370F25E1E9B27CC85A7DDBD499A74426690218D39B4615DB52F5BA
EB9705CCF787765E428FAEF36609DDB59A7509B3DB140FBA6C965738E30DE0F0
F3A7D03FF79FD972DD6FEACF6B3A013AF44D7BF8FCC17D0F1608A4F4AFCDAB39
2D2EA5BED740F71FDC717E969F9D75FDA1AE3EE96C53C02A6BA97F6AB0AA740D
CBE7177FFC4FC38C66E00EAAC25279BE9AD2A32EEFE01109465FD5C294CF9E67
4AAF567A098BF4BCBAD58FB08C974D8DABFBEA27B0CACCFF3975317C4FAF57CD
161A6F9BADA6C75DD1167620FCAA164F6DB1BB99AB555177DA74B4AA8BBB0E10
4480ABBEB8BDD5A56A352C9EAD6AB693FD76D52CF71D30D86EDFDB165AD7B6D3
65FF3CE9F5739FE9B2EAD56D050C6F26ACA08BB22D9E60A8FD7609BD830853C5
B257B05E735745A96AA58ABA560B605FAA714ED3EAD46DDD2C1F6EE85D665EE5
2AB452AC60AE76D5F281D7343DFDF337808B65514FF0C9B7CCCFE4BFA5EE09DB
1E614A358FBA152FF4F6AE4072451301EA6C7B3695FA71AEEE748F2B713537F0
00CAA1CE24FEAECED9836FDF01BB2CD7D4359F69895290BF008080AF80E5F45F
F6456D70A4904DBFCCC284D562BFAD51DA1375F2A8036413A069B66D26DDBA79
A22EF2A886E9710980027A9AD5AAD3D05733A117664C4240668AA2C6BE57F69E
0F80388E60024ED836FDC44E8A8DFEF2E083D999BE6C276B40834145D7172D54
4EBE9EF321635CA841577A5B8A16F3410BF56AA4A5D12611C7BF3FE843CC18B0
0889E06C2CCF93C478C05DA0B463C41BA0B0BF8FEA1FDEFCD7E4CD00A382E506
70C3B2D7CFC089B8FC138CBFD5C349E1A78695D5AF6F469AC420E05250AB0C06
A9BAF584A69A980DA068A42148B663AD4E40B9141E3962D5C93F75DEE97A7566
CA480FE0DB97D48011D62448F1AB9FB2D50633608802D6BF91460A358EB6DF0B
6043B0BDCE6BBDEA9D2AB8AD8BE5C35900F015B29F8404BDE95B90726801BA8A
F4B62DB64102EE8AAA9D29FC7BA5B21A458E6AD50EDE5C0539B39BC1C8801F56
D7955C214CCF891298C2128C54B5707846CB0D2ACE32EC1BFECC184F60D901CB
AEB0EC0ACB2A3024617DF7601201404537C1CEA18383A79B6A197A56FBBAB654
62EA72ABD9235374F8D50F6F5404D307CD63EB50B10245404A4EFD78AA019628
8EC019BB7ECC108AC952FB59505F7E56AC2E6958D7A06D9EEC0AC065411A3B51
543FF22500E353E78139A2D579ADBE47D1D5374DADFA6A37B27AAEDD572B4D08
AEF056D124091898172C0754E1386C59F4C5B5AA9BED1DC8DD667FB70E6D72D1
DEB49D23136BFE7E25D06DBB9F87CE79DD6BB009EAE26015231FC9F58EEC11DB
11A336422FF9023919987355FEC8EC615029308116D404451108F5B0922D4780
83B440003FAA733230ECF7A2063F039E39F1A5FDB4906C01E3A04C4517E48678
6C5D959AC46CCC3F25C377C43DB05AE67EBD9EA64B18ED784F82B194F80029B2
6E5DAD62AD4BA800C0417E832538D934A5464C6CF6755FA991F7B852621BC154
45C64A16401FBA9C80A4DE6FA4EA19DA66D0C0CA5B12876047EF61C2D63C8C20
1AEA3A03F86258A01C8A13255285C31868DA7878BBC41CC6E76B3E40D3C1FB7C
685779741E8518A47B18EEE8349C49319842B2769A8992A0C6CF4EC1396265B3
F0F52AAE6B55DE297523E22FE2719D92EC1A5075963586988BA8899527F48E34
E66BAA5F0DAAC7101DD3D2BC0E703294B6465727C8C1D5B7C302D3E202AE6C7E
1892ECB845878A0298B2AB6EABBAEA0FA0AB9461695528E43F80B05096B76F01
BFBAEFABED9D0279026F9BB6ACB6A0F8B0D527DD36D09769F5D4ECC1695E178F
9A6A3E633B57E9BDEAF6E8D07560F0FD945AD8EEA3468774A30FCF5368D46378
CA34D075B5B15040ABAD06371DF1EA56E135859B6E8B4EDB89811EEF804F6A5D
1FC09167B3E8A82621B8734380BAC3316E01E5A0C43072A61E4D84EFF2F2122A
B560FA57184E6BF5535B010CCD8AC020610CCF6D417643DBEC016DB1A9FF1253
0FC58BF5EA8792084898CD86CD9BBA9C5844CC5F12C5B1F21F679A11971C3FC6
2D77961559FC43AB8ABF068B2A9EB6D3B6A11F7C33D2575C34E82F02DD982FE9
6234188CC2DFEFD4A2450F83D011A935F20CFBAC6CF6A0DE27CB1A293270DA53
96C6005B29854CE4307D53D77C76290FCE2162A40914BD20A89C99B42A85D992
1BF50FF81AD82D273A82F41759095C3BE32E2146A7DC1B442E9EAB5FFDFCDB5F
7277705D8238873F93DB7E6BBEAC2A30DFFDD3B269B7DA96D65D4F7F262B0C2F
764B68A94BEEBB18A7F1C314BD4B034659B5FD01FD38E8D41AC5F89A50C71A8E
0423A7CF53E759CE7E32BDB4FFC7F14933F96B0A21BC574D59BEB7D2E73D88A2
E5C31D08846D6986A5E01C389B2C64A9AEAEE0DF0FF0FFD554CDBE867FA730CA
D7533EA20877960D88A08535F3705E18B76C3BEB037760A417E1C1B4EC76C512
24F9C4F600D2AE2D6D0D2A47D181C47322C60AA9C42B60C06AA9DDD3B2814712
B27327D1B02B29AF4C90883FAF7551A2DFE69E3D19B0F15355F66B78E9FEDD3D
FB57A21E460BE09DFD2714451EA12FE80F3BCD2B3A478EFBF8655BDCF1E755D5
33A601866BEA1ABA2778E6C8E293B536FB49369A0153F2D0AE2786E9A69C04C4
533C9EE011ED31081A0698CF2B8181562013C03D300EB66B99C38F9C402B9A55
5C4FCC3C78BD60557828E815DAD748DAA25B824B087FB60DBEB735820D8E753A
985DADADED8D7641B06B0DB083568E1179479E0CBE5F7AB0668A2D78B4BEA7AF
87EB1B57406818E60012C01802AE529899E5830AC39FBD9E60012337185DDB3E
3069C096779E85C6F55F3CA64D1D74970DC5AB12D06B14B7571782DB802ACDD3
0D0301390CB51217713D182E456BF8DF8AACEED0F57A93B9A2CC44417EFC685A
B1240368821EBE67946AB323BC19B7DFB8B6CDED3DE0FC8BDC3976A6A67E3635
8D00B390D1046CFDC00F2BD68C948799C06351EFB5FD3E658BC58DDE7DCA9DE8
EE3EA9FD0EA071513EC26D2C3C0DC2A109AD0858CCADCE1764368096C9D9F085
E5A028463457914982032455B30B8E0F43B7731545652DB1A901ED019168BE71
B063549F74387ED19B1DA837921F14EE777B466635C05B9884D54EDBE2B1BA33
162A228BA4F699957250DBF0AE208D0B789B31409881890B4BA5459F60E1D97D
C6E2210497F351C3CA083ECB2CC4210DF2199D77AD7E044FE74E7350E8253C67
DD43B523CA077E60CD8C394B3CC06AD21F1B7B22D670ABE1D3DC4D2E5426BEC9
171EC2E45CFC4C8FE286194CE8F3CE5C733581A1830565D5368260FC4EE674BA
165B7D87DE4E34E724FEB6A0EFFE8E118181021B9948CCF39DC40C6B6807409F
F8E638AE60C94B039B0CA15091DBB1B2E2098460E025CDE0117AC4FCEC8834CE
CFA4B35F5A8C0CC085503C6C62FEFDA9241A92274D9131B40A1C48CF00DDF63E
E84A3133E79378960739B64293F9668C18E8AE582CF1419CA524442522E5C637
74C2D020011110C942B22C3986AC85967AF7E68D159F5444115CBB3B445B1F45
DF23CA7153268496785CA971E6B9EB5CA24720250CE2E722DA5AE3CDC7C5C40B
D0D467D7185AA76885896C204B6816F2C0221E1B6A566AB9060B887044639890
09E86B1B13DAB8584EB7D3CB6A552DCF184060FE16308752AF0A30032795AAD4
93B23C073A1BDEA9BB8961B8208E1273F17112CBC481A45110DDF536F76C0C64
DA156D87012BC7ED92F7597E83EFDF75A3BE5153B138AE0943B4606DD8E7B6EA
3168B5D9C36AB8B586EFAEAE2815E9F6A0BE668DCD8C9D0B65FE1502C8E3C95A
74A6EF2FD48587E79DFA1AD44778CE32F5F5DBB7AC9327675B9ACF7EDB5771C0
C186172DFE9F4419347F8241645CE909D0E0464C8A3FC4CB1AD882B2AF688582
58DB217291BD040676BAE8D5BDA74E620FC42DAE0D2C422ACFE6F7A2821FCB32
5E0992B332CB371ECF8FB8DD6F025117B80B43CBD070C05D36C772B76D5A61ED
28A4042D2AF5D1E16C9065E11C38CFE7D76A59EBA28546CB624F296FEA767FF7
45DC8E057A39D72B3961117A11159964B6EBC46B22F9C28A3C63E1C29C6F982F
61340FD9E4B45A2B34B0D9AE2D17246CFD71A33320049D7A87F42A22339FEE22
57D508AC8AC61F59F9CCED65A30201B878E5110BDEC2D77F990F39AC088EDF61
B01C99CD2B68EEDD767833B40622D43DF72D98F699A1309F8EF74D69E311FC6B
0E08D62033C180F121AD8C7DAD0CEC608DE22EAEAD3EA82B8FEF0017D79CC0DD
DA7182D0CDE958C3D0B33E3781015BEAF9CAC9F5A6D5C572AD7618D1B3992DB0
AE761939A038771B0004D599B9E01A59330EA1C695F3DD4596965D18C69F264E
76695D47795918440CF56E071577454DFC30693BE5AED4FAF061DAF9822FC7C0
AB2BD1E7C0D4A0CDFE68044669320DA6ECC549B60AB7DB22B2707EADB6A8E131
C6C22C9484C0712D6C7A99CB9E5B10FF2C5B698EF513670C58F73C9A550A7801
35233B6754D7AB60D5CFB0DC1203B0488065A5AECE07A66A6C63FB67261E1306
32DFB5065A9F47B2292E268BE27829C622BC07C2B229B8B91D7AA8A5B01C1AD6
BE2AF11ABA6C5D0D56C74C0DCA2ED0B18542E3BB7917E62DD4F60F9CF1FD4BF5
CF293770C0E7173878D4390BFFA4C761E2D7B063C213B125B19C5BB61925EBDE
B1E132EBBD8761596D13B2B5A222D40061163A584CD9EAC7C964EA8255655378
EB40A7D5C40260D65F212DD56AE061FD888934D269B983395525F33E141E0950
6BD5AACCA833B1EC2CD020F25ACA44B6CF6BDC47F05E86CC51764ACB7A23CE2D
C128412E144988B1D9B622BE27CC1EAB149652693371298D060B028B88A9055A
F630F7A706B733845AE425C6BB37BEC052A62E79E57E6EF7334838F019216ECD
EEA009AA50DA6E46CE13A2296B8B27836DCC1DED5C90A562C94D481943211548
83EDA1692457AA32BDCCC7177F3ED45B0407C70560DFBA15946492D35E3BBC34
FBCBC45A3030BCC817FC4D6A870FE506470771B3C2A473B4C9B7C5C687992AB5
020F9E6121E82E2C988B7C2CA9908D491645336C151C49088B39EDB954E5B303
E0251F0864E0C0CF21B51C99E88620B89EB9A5684CBE2A9B2FCDA2476A8477C3
DDEDDAD98DACC947DE8B410646FC4FDA755DFC75A7E7E7C418213929371356EF
B55389498BCD5934ABB2769E5B81F9629874189628EEF839A6C4EFE873F4FA4E
B75F2894015FE4E935FB43576BE40E792916BDE7ABD487851856088B769E045D
550A0C5A0CE14C6E949BDE88D783C539E1E0F8663D0E47C2D4DA39E0B3B6434B
874D84362CC54C705B1C98ADB8055F6B7F4B4E8E15B62673890285F495B58141
61C8A26D8B0328A7AAAF800005A6440D5ECD22638D32BF5CF6D884F1F8C0EB34
C30B0C3798028E683610D87E600D5E415738BC7F31A317720551CBC0EBAC7168
C63C2E33865732DC9F357FF7DBEA2F6006D02AF26143E7DC1A86054C7B12F91C
3026EBB0EA541A089B625BEDF6F56093E7FA69ADB78ACE7850ECAE51C56EA781
7188B2A815BAF783F01625B35486951FB6CDD3F6CC420EFA85897F162EC06966
A5D63B91836AB7A4C82B256EA56F4E397F2114987094A9DE95BACACD300C7B16
0B0E0AEC273FB222A8DC34C4D08C33508C056769E31345507C7187D26E72922D
C2359F053A48152F70722660F84B436C332D2F78CCAC5E237D52BA32B299B266
5B9BE88430503B3C1185275C089F0BB40878166FBC8FC71640E5E271F0850F51
F4CE584889AB53064D1C476384907E2292C3BE49C84A41282BD88241CC082199
96A68A35CC04F19B353CDDE4F095E5DE882BECAE38AD2561240A52A38F4CE61B
2333C321D9B7AFA5D57124F9436DCC4B37091461DF44B96DEE0446B949309CA8
C0AD014F4C8BCD084B5F07B1834F82E79C7F4CFD10883E7DF57D96A6776ADE33
BF0181F1FD0B7243003E60CA48A362266BA57C01E60847E39106626E73F58A45
3304C546390C3C0696FF5DF00A160EB93D6525E91A8B1202D1F0FCE974E99B5D
208EEA37BBD74BC401950C70B188526C41052B07C70FA20EC6B72A61C41A0B3C
1897041561973F55800E99A1E2332D7E54996769312EF3E88899957C562187DC
1C2FAEB1D28D211A530586299DE3EF2403F504FC69867E993F5FCDA77C4E824F
47E660D394D3E7426C6ADA8277EA4D2A3735675AC5873C38BF28E3418B7E5C7B
1749A0FAA1813D8B229A381B5676CDCE819C50DB198B2F56CD8758E5CE92B7DD
68B7E36FBC729927E5162FB7070377894805338209E6E15249AC6CBEB6DD58AC
7020D85919559FCDFD1439432797A544BAD3DDE9840E11CF922E1057C3C1BDB3
21AA45225665F3F386F915CC8BE3397C30A0B75D9CCD5236A28AD844BC568F55
B707C8305B2D913CE113C8BD7EB65C8A94B2AC22AD2F80CDC714831B50BAD3E1
D9811F2ABF007320034793D21BD155C90E6FD597917AF2F5CD56A2A5CEB8AB18
9DAD10994700076E56A177679289F91BE33E1063924CB299D150946012B266A0
35E74F244A2C6C16836C5FE34F4B2843BFE65E0A02C2C12642FCA307C04238CD
4891A6CEE3B15302C41F6DB44103494D966EECBC94B2A76305FCB02CDBE37D91
3A7E5222E627D57B847D778C34428A3A1727829920F5960C3757CD5405C924B0
3C1CE597875DB9D52772C1CFAC4752E1166AE9F6932830ADB4DFD190F9DE5671
82392C93E473B99C43028CF062995D938A4A0524CACCFEA32EA5B29D9BDAB380
B854969E9066C22774E17E178E4FE47F380F8345FB7DB0AC6F2656F5A8325F9C
FF61BFB9D56D8EAF29C45FE6A22711715C48F8C73D4F8FD95826C4E717068723
5C0F03B2C930B71F405662684874408E45FC7214027FC0A772795282BD727429
C4F18263F2C5C34B67278008E608050D7E82AA3A37572A78E7909F1C89B33B2C
EF8D4AE3646249A8C73D4F3B10CF32E1D62C71E1EA98DC7701EE792296B452C9
A18CC8853629EEB25FAE69DF05A551A7F63B9EB04467A0CC150B8538CB6A8E25
893CB5E8FA0CFC981B1B245AF3D0353B50951AC31E02E015B99149679FAEA697
F67F26C5DB08B2D13354B3A367A8D8112A09F628C429AC9849B83C92085B5702
5D43608E4D6AB53201EB10DCC413294F32B8ACB7E6100521109F9ED6783E74D7
D487BB66AB3E3C7FA5FEE979F613359B3D7F95AB555DA50C49B29EC879227E08
D728C834109F2ECBF2A1C82202E0D5479EB1131FABA75A0EEFCE619269480381
8C394994D9C5D99E58CD62062FEC8A7788E8A8D08BCDA691CC16FB3B1235DCED
C1E38011910677C888C32CEEC80B6344220FB3A246EE38210618BDD522CA2147
9D1BAE8148DE0171C269DAE86590586B963B45F45F0FB3A7D6E9E4293619BBD3
662E0A434C0A3E1AA49033C0792DC76E897A2E5B5F1CAA4A9E759747CC44963F
26C9B3D291E636E530D4CB1751C32102F00384B2E96A6EB91C3922363AF65866
9CFCA421102B3D7D2FD4E088821837003C02DDF124C3735CA06045C1431A4021
298EAEE3CF1A1F45C6622000C63016BF19EE47BB23E47FDDC5F312C5C74D5099
5278ECCE99780A8610A0EFB2E8CC71546F98DA673ED2F71F643A8AA9D9E4C853
3301D31355F20C88ADB248CC43E87D0B86BBFB6DECEE9FA3CA81DB0EFCD4F867
9810A1F9E75A129C978E98177F3BCD732A070EB8E35416F416BA4F0570A6BA5F
7B3380CE7EC7088A586F516F2F2DB6E309C8EE33B62CFEDF79946E327895C53F
9B82FD320D0C66E360AFE0A7976E6C93A41F879DB22729A849BD6058C408D373
7740DE2DC4936BD22D0E27A2C344E258524874619D2352946206D07B3FCC8769
B4BD65E03D2E6573BE89EE13F03EEB2ACA9D32EEB0BF92C0BF0F171760F254A2
D0A4BE7EA344BEEAF0AE04AC928CD971E20CA0727045E35A4F3631CA29033306
1043B9EB0D4400402C9191D06ADF4C4ABDAC36C06E33958DD44A98FCEC0208B2
3CA2595EAB6F8B7A89592FDA5CE3B36B9B9D6EEB43387333081E5852456E9585
CE15632C510A430F0914CD150B4985065FAA0B8A4C3FE359DF656BC2D4CF6F8F
2EAC231435DC98202815847B30CC55BFF0658C6AF69A8C74D4262294B95380DF
33C0AF2BCA8FB0AFA5D00849F9ED1E434AFA38893BE06F73A42FAE00A9249629
513A28798BDBB96D9039C3C4E4725B2AC83B3B82F4CDCC18F13D6AAE2B41BDB8
F400A545C977885C06B5BD5AC35D28E086CD17C2D4C5E1FDFE4614008B2383D3
7CE1A77AC8D5D9708E073F9DCC9D29E4B790940DB27F8D4A0A99071CB34D666E
88C753769EA3D59B45FE4699FFDE29275C07F3F2B340D9CE261140C0847D363F
D9A078AE00A4F367F9D64CC46FC8FABE9E5F85D735CBE12FAB478CDE8BA5C8DA
BD65180AB75BF20E180CD978378E73176BC72C74BD0B5394160FF3D4E4049A24
C2317ACB639F830D82F9405E244F1B470B3B0A05B3B80D5E0410401B179C815F
0601D3D4360648A58AFAEE3E01F6B8F81C9182A94E16AC5D1215DDA744A4173A
0B44C6432701F10B77330F7B3761B5638327488D49DDD85D9CBA41A3059EC174
EA76E2EC4DD8A484118A43B3EFB31E2F09F2E72EECA52E74EB187D93A7EB1B7F
B9D2867ED801CCF5B6E93ABA4148AB29D83BA5BA70FC3475170B6587B75CC6BC
669F132F27F27288A784C76D4018B9D19EA5552C2E2E6C02E8297B9CEE8AF613
E04C812B6CC7AE557A1B351733879A5359211D3B245630141F1CEAF68047F09F
0740DD70B68700DCC8607593E0EC24E5DDD551C35DED3CA1B96D7A496C100CE8
CC7B4BDC21BE6284FE26DC3E4E3684B938CC98110BFA472C549A5DD43BBA71FC
22226134A4AFDECB532BC5FD8E81B8A82C360C643023664EB33C5DEFC6297039
391866AB31AEB7A42D6912F56C5FC6A43A27F66ED59038CF0941C96E28129917
D7E682CB0298A279C0AD6AB44F2F2F2F55853BFF7A0336F11A89D753B3ED3970
5B4919327D43BC62B1115C9F607132859B2538289260FCDE24016130D68F5E12
42671812DBBDB2B615D78B5C1C334EB88F99BBE8C8E510ECC51E073FABCE12C5
EC3484E1ED4EE281898867F5A6FC24DEC849445256F66A6D33F4E0F021BD662F
A354D944C8E9874FEA07D969C61EB6C684E5D0F064EF1032470F112D9E7F317D
66ED7028BCE77006915BE6369BC25FD5FD6AF56674A6DBAC948A8D25D9BD4279
71C96D35725AFB845FA69189444E8C599E993ECFD2DE5A226BDB8C274639418B
B2081D3F283E32197F4011955F42FBE6128A1CAF47F7911017FFD62C91468F0B
EC452E486DE4452C5CBC0B228ED80E73F030C9F0661E3CC79642328B9C4B562B
F5C55D96B1D2C45B356FF8A1DA4EE3B593FEB64B71F7A5B703F91D98D28CBB56
DD7EB7031292D4A72546DCCFC0323CCE226546B2D958D9D25DD5A11E14B8E094
CE4363A121B8B037ED90187FB257148338DF14DBE24E2BF30B01F67783FCE599
78B930ABBBA4DF1FB387663BB56A9B0D9DD1F189BAAD06389650B969F15663BA
D5A7061FC2A30E80999FE4118CA47C053E3672F646258E69870D032C7A27CE88
F35F352247801F8B26D78588CD6FB35E3A7D11AD5B42F05C3DC8BB0F5E6FF98C
5AB9C376E3762E7422564454780F168C1328713BE62D5F18A4E0C1FA2FA1C0A5
324E822D1F3576D9825E8724F6706C9D207EEF990A20240EB7B42C6E0D0FBF53
F7CE1B7A0790A406F04702FD28C221B3275B1D77A1F598DC3FBDF7A6D4C0FA1F
D47E69F7EF5E0A9141FBA389E1AC9A4D114F148D5DDD6C1B0E5978ACA64D74FD
F88A3628553EAA9897CCE7B4DD539BB23E3EAF0430C7414D6C8D8674F4D12EED
4C928D6D8EFCAB710F7DFE60AAA4CE469F363A8DF31934B5908FD2153FE96D79
FCF09BF1E4EF0354AF8021DDFFA91CC54E2AFCBDD2ECF55CFD778C7190A2FCCA
6E7B4D8379F0E64896A92B3CF91F374F8A6D160F39A60A53E2DB0B5CFB0554D8
F4F2672F4869BC77AD2EEE68F01BB09C283534D13968F987C1F505665493E87C
0AC2EF333A6D3156683610EF33F6E357DE547A318DD97CA2E3E02966F309E90F
6A48CFD13EC7BA1B76319ADC556DC0D0BCB1F31C51BF6E83DB7A4A36AF31C9A1
836447FEE9613D74B4F3767171EF3C2833B2D9AD51C0947114CF7D08507561AA
A72B9D2A901C51937958496CDF67C90C8DF039F2334A7CCC6491BB5A3E59C842
5847BAB8E8FD5D4DF16F28DE7BF9E3EDB5D9576FD1C41E41B45BDDE11ECCD181
D3F61A4D96FD289E885F0C71028BABC1DB9B306376873F37190FACF026E40FEA
CDE5E5E59B14100B391CFBB13E51EDA8CC89B236AC91CD2FED272BD7DADBEA5D
D499F77F702D8B3B9D7C9B68574FB91F8CE5E1597E5AC216B3E89559E3E9081E
17E52C5469F60747829532403A7AC82058FDEC22753301EB52F1CB9AA36E5E19
92F1B93FE4F2BA5CBB74842699DB4BC4A02C908BE46F05BF9C5A2E458BDB90F3
FB08C6F989B6120C1B5F444C11F8D89D3B21A12D31342697AD128ECE77708259
95915E83E18053BE8882F212744A328B21E730F0C4F1D9689C2A0505CCBAAEEC
A55A3CCF7CC10B6437831D0409ACA1E345FAE7CC1C835AE039ACA3BC1D4F182D
45BA50BAEAFD36722EB614019F21203A9029B266483B381C9954AB6BF3FBF12E
B7342D1106EE7733769BDDF806D6F1A5E6CE738CAC37897C374498E2EB174E1E
4FC9F49998D25D7DD8213D8A5ADBCBFEE4DE3A0703E4E387A86151DEEFBB7E98
BF11FF480BDA1FE2AA2096397218141EC766480A8DED26F6D3994E86452AF214
E41DC926FD31456044F590E6324279D795E49DA459586CC12D04AD547438E1A8
707848067FE8C8FEEF8ECA446D2EA6CF138C91316E01A6E22C10DB8D17B3E7E9
EB1AE0B190573598BC6E88B1132DFEABE334BF5DE95E881A2E62166568BC2EC3
5D1E3C58477B73E62F9A16738CDBDA63ABE6ADB928D59FEB8661CDB924CBFA89
7B1DF88657E2AE3FEA810F3308FCBB94499F3EC92BF39B56E2B38FB875257E31
96D241F19761DCC656986F7E969FFD1F598CCF767B840000
}

24.4 Эффектор изображения

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

; Заголовок по-прежнему требуется, даже если он пуст:

REBOL [] 

; Следующая строка создаёт короткий список эффектов для изображения, 
; встроенных в REBOL, и присваивает блоку слово "effect-types":

effect-types: [
    "Invert" "Grayscale" "Emboss" "Blur" "Sharpen" "Flip 1x1" "Rotate 90"
    "Tint 83" "Contrast 66" "Luma 150" "None"
]

; Приведённый ниже код импортирует простую функцию воспроизведения 
; звука, созданную ранее в этом руководстве. Чтобы это работало 
; правильно, файл play_sound.r должен быть сохранен в C:\. Любое 
; условие проверяет, существует ли файл. Если это так, он запускает 
; код и устанавливает переменную, которую мы будем использовать 
; позже, чтобы решить, воспроизводить звук или нет. Если файл не 
; существует, для переменной просто устанавливается значение false:

either exists? %/c/play_sound.r [
    do %/c/play_sound.r
    sound-available: true
][
    sound-available: false
]

; Строка ниже запрашивает у пользователя URL-адрес нового 
; изображения (с расположением по умолчанию) и назначает этот адрес 
; слову "image-url":

image-url: to-url request-text/title/default {
    Enter the URL of an image to use:} trim {
    http://rebol.com/view/demos/palms.jpg}

; Теперь будет создан блок графического интерфейса, который будет 
; отображён позже с помощью "view layout":

gui: [

    ; Эта первая строка выравнивает по горизонтали все следующие 
    ; виджеты GUI, поэтому они появляются рядом друг с другом в 
    ; макете (поведение по умолчанию в REBOL - выравнивание 
    ; элементов по вертикали):

    across 

    ; Эта строка изменяет интервал между последовательными 
    ; виджетами, чтобы они располагались друг над другом:

    space -1 

    ; Следующий код отображает меню программы, используя виджет 
    ; кнопки "choice" (выбор) (кнопка menu-select, встроенная в REBOL).
    ; Кнопка имеет ширину 160 пикселей и размещается в крайнем 
    ; верхнем левом пикселе графического интерфейса пользователя 
    ; (0x0) с использованием встроенного слова "at".
at 20x2 choice 160 tan trim {
    Save Image} "View Saved Image" "Download New Image" trim {
        -------------} "Exit" [

    ; Это блок действий для селектора выбора. Он содержит 
    ; различные функции, которые должны выполняться в 
    ; зависимости от выбора, выбранного пользователем. Условные 
    ; оценки "if" (если) используются для определения действий, 
    ; которые необходимо выполнить. Это можно было сделать с 
    ; меньшим количеством кода, используя структуру 
    ; "switch" (переключателя). Однако "if" использовалось, 
    ; чтобы продемонстрировать, что всегда есть альтернативные 
    ; способы выразить себя в коде - точно так же, как в 
    ; разговорной речи:

    if value = "Save Image" [ 

        ; Запросим имя файла для сохранения изображения (по 
        ; умолчанию "c:\effectedimage.png"):

        filename: to-file request-file/title/file/save trim {
            Save file as:} "Save" %/c/effectedimage.png

        ; Сохраним изображение на жёсткий диск:

        save/png filename to-image picture 

    ]

    if value = "View Saved Image" [

        ; Запросим имя файла у пользователя (по умолчанию
        ; "c:\effectedimage.png"):

        view-filename: to-file request-file/title/file {
            View file:} "" %/c/effectedimage.png

        ; Считаем выбранное изображение с диска и отобразим его 
        ; в новом окне графического интерфейса:

        view/new center-face layout [image load view-filename]

    ]

    if value = "Download New Image" [

        ; Спросим, где находится новое изображение, и присвоим 
        ; введённому URL слово "new-image":

        new-image: load to-url request-text/title/default trim {
            Enter a new image URL} trim {
            http://www.rebol.com/view/bay.jpg}

        ; Заменим старое изображение новым:

        picture/image: new-image

        ; Обновим изменённый GUI элемент на экране:

        show picture

    ]

    if value = "-------------" []  ; ничего не делаем

    if value = "Exit" [

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

        if sound-available = true [
            play-sound %/c/windows/media/tada.wav
        ]

        ; Завершим программу:

        quit

    ]         
]

; Вот ещё одна кнопка выбора, которая просто отображает
; небольшое сообщение "about" (о программе):

choice tan "Info" "About" [
    alert "Image Effector - Copyright 2005, Nick Antonaccio"
] 

; Следующая строка выравнивает все следующие друг за другом 
; виджеты графического интерфейса по вертикали - в 
; противоположность "across" (поперек):

below 

; Раздвинем следующие виджеты на 5 пикселей:

space 5 

; Поместим 2 пикселя пустого пространства перед следующим 
; виджетом:

pad 2 

; Этот прямоугольный виджет рисует линию шириной 550 пикселей и
; высотой 1 пиксель (просто косметический разделитель):

box 550x1 white

; Поставим ещё немного места перед следующим виджетом:
pad 10

; Вот большой текстовый заголовок для графического интерфейса:

vh1 "Double click each effect in the list on the right:"

; Перейдём к следующей строке в графическом интерфейсе, а затем 
; снова начнём размещать последовательные виджеты на экране:

return across

; Загрузим изображение, введённое в начале программы, и дадим 
; ему метку:

picture: image load image-url

; Приведённый ниже код создаёт виджет текстового списка 
; (text-list) и назначает ему блок действий, который запускается 
; всякий раз, когда пользователь щёлкает элемент в списке. 
; Первая строка присваивает слово "current-effect" 
; (текущий-эффект) значению, которое пользователь выбрал из 
; списка. Вторая строка применяет этот эффект к изображению 
; (слова "to-block" и "form" необходимы для синтаксического 
; применения эффектов. В третьей строке отображается только что 
; обработанное изображение. Слово "show" _очень_важно. Оно 
; необходимо использовать всякий раз, когда обновляется элемент 
; графического интерфейса:
text-list data effect-types [
    current-effect: value 
    picture/effect: to-block form current-effect 
    show picture
]

]

; Следующая строка отображает блок графического интерфейса выше. 
; "/options [no title]" отображает окно без строки заголовка 
; (поэтому его нельзя перемещать), а "center-face" центрирует окно 
; на экране:
view/options center-face layout gui [no-title]

24.5 Пример маленького меню

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

REBOL [Title: "Simple Menu Example"]

; "center-face" центруем окно программы:

view center-face gui: layout [

    size 400x300
    at 100x100 H3 "You selected:"
    display: field

    ; Вот меню. Убедитесь, что оно идёт ПОСЛЕ другого кода 
    ; в графическом интерфейсе. Если вы поместите его перед другим 
    ; кодом, меню появится позади других виджетов в графическом 
    ; интерфейсе. Меню в основном представляет собой виджет 
    ; текстового списка ("text-list"), который изначально скрыт за 
    ; экраном в позиции -200x-200. При щелчке по элементу в списке 
    ; блок действий для текстового списка проходит через структуру 
    ; условного переключения, чтобы решить, что делать с выбранным 
    ; элементом. Код для каждой опции сначала скрывает меню, 
    ; перемещая его за пределы экрана (снова на -200x-200). Для 
    ; использования в ваших собственных программах вы можете 
    ; поместить в список столько элементов, сколько хотите, и блок 
    ; действий для каждого элемента может выполнять любые действия, 
    ; которые вы хотите. Здесь каждая опция просто обновляет текст в 
    ; поле ввода текста "display", созданном выше. Измените, 
    ; добавьте или удалите элементы "item1", "item2" и "quit" в 
    ; соответствии с вашими потребностями:
origin 2x2 space 5x5 across
at -200x-200 file-menu: text-list "item1" "item2" "quit" [
    switch value [
        "item1" [
            face/offset: -200x-200
            show file-menu
            ; РАЗМЕСТИТЕ СВОЙ КОД ЗДЕСЬ:
            set-face display "File / item1"
        ]
        "item2" [
            face/offset: -200x-200
            show file-menu
            ; РАЗМЕСТИТЕ СВОЙ КОД ЗДЕСЬ:
            set-face display "File / item2"
        ]
        "quit" [quit]
    ]
]

; Первоначально меню отображается в виде некоторых вариантов 
; текста в верхней части графического интерфейса. При щелчке по 
; меню "File" (Файл) блок действий этого текстового виджета 
; перемещает текстовый список выше, так что он появляется 
; непосредственно под меню "File" ("face/offset" - это 
; местоположение текущего выбранного текстового виджета). Он 
; исчезает при повторном нажатии - код проверяет, находится ли 
; текстовый список под меню. Если это так, он перемещает его из 
; поля зрения.
at 2x2
text bold "File" [
    either (face/offset + 0x22) = file-menu/offset [
        file-menu/offset: -200x-200
        show file-menu
    ][
        file-menu/offset: (face/offset + 0x22)
        show file-menu
    ]
]

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

text bold "Help" [
    file-menu/offset: -200x-200
    show file-menu
    ; РАЗМЕСТИТЕ СВОЙ КОД ЗДЕСЬ:
    set-face display "Help"
]
]

24.6 Shoot-Em-Up Компьютерная Игра

Это очень простая графическая стрелялка, которая не имеет ничего общего с бизнесом, но демонстрирует важные концепции, необходимые для перемещения графики на экране:

REBOL [title: "VID Shooter"]

; Сначала мы установим некоторые начальные значения для переменных,
; которые будут использоваться на протяжении всей игры. Функция 
; "random/seed now/time" гарантирует, что сгенерированные случайным 
; образом числа будут действительно случайными:

score: 0   speed: 10   lives: 5   fire: false   random/seed now/time

; Эта строка покажет пользователю маленькую инструкцию:

alert "[SPACE BAR: fire] | [K: move left] | [L: move right]"

; Когда происходят определённые события, мы захотим перезагрузить 
; полностью свежий игровой экран. Простой способ сделать это - 
; пометить весь раздел кода графического интерфейса пользователя, 
; отменить просмотр существующего графического интерфейса и снова 
; запустить весь этот фрагмент кода. Здесь мы обозначим раздел 
; "game" (игра) и сначала запустим его с помощью функции "do". Вы 
; также можете использовать эту технику, например, для реализации 
; функции "play again" (воспроизвести снова). Для этого вам нужно 
; просто обернуть всю программу в блок, пометить его и "сделать" 
; ("do") в самом начале кода. Чтобы сыграть снова, просто сделайте 
; ярлык ещё раз:
do game: [

    ; Вот окно игры:

    view center-face layout [

        ; Зададим некоторые свойства макета:

        size 600x440
        backdrop black

        ; Отобразите простое текстовое табло:

        at 246x0 info: text tan rejoin ["Score: " score " Lives: " lives]

        ; В этой игре мы будем использовать некоторые общие кнопки и 
        ; поля для графики, но мы могли бы так же легко использовать 
        ; изображения любого типа (просто вставьте изображения в код 
        ; с помощью программы двоичного встраивания, пометьте каждое 
        ; изображение, а затем используйте слово "image" 
        ; (изображение), чтобы отобразить их). В приведённом ниже 
        ; коде жёлтое поле с меткой "x" - это ракета, оранжевая 
        ; кнопка с меткой "y" - это движущаяся цель, по которой 
        ; стреляют, а синяя кнопка с меткой "z" - это изображение 
        ; игрока. Обратите внимание, что целевая графика размещается 
        ; с начальной координатой в 50 пикселей слева от 
        ; графического интерфейса пользователя и со случайной 
        ; высотой от 30 до 330 пикселей. Функция "as-pair" 
        ; (как-пара) объединяет два числа в координату:
at 280x440 x: box 2x20 yellow
at (as-pair -50 (random 300) + 30) y: btn 50x20 orange
at 280x420 z: btn 50x20 blue

; Следующие поля невидимы из-за их размера "0x0". Они 
; существуют здесь исключительно для выполнения действий, 
; когда определённые клавиши нажимаются пользователем 
; (каждому полю назначена отдельная клавиша - их блоки 
; действий будут выполняться всякий раз, когда эти клавиши 
; нажимаются пользователем). Обратите внимание, что когда 
; поле, назначенное клавише "l", активировано, оно 
; перемещает изображение игрока на 10 пикселей вправо. 
; Клавиша "k" перемещает игрока влево, а пробел используется 
; для установки некоторых переменных, которые будут 
; использоваться ниже для запуска ракеты: 

box 0x0 #"l" [z/offset: z/offset + 10x0 show z]
box 0x0 #"k" [z/offset: z/offset + -10x0 show z]
box 0x0 #" " [

    ; Переменная "fire" используется для отслеживания того, 
    ; находится ли в данный момент ракета в воздухе. Когда 
    ; пользователь нажимает клавишу пробела, следующий код 
    ; запускается только тогда, когда ракета НЕ движется в 
    ; данный момент:
if fire = false [

    ; Установим флаг запуска ракеты в значение true::

    fire: true

    ; Установим положение ракеты по центру изображения 
    ; игрока в нижней части экрана:

    x/offset: as-pair (z/offset/1 + 25) 440
]
]

; Этот "box" (коробка) также невидим. Его единственная 
; цель - дать возможность расслабиться и развлечься. Коробка 
; имеет скорость, установленную на нашу переменную 
; "speed" (скорость), и процедура взаимодействия проверяет 
; прохождение заданного количества времени. Каждый раз, 
; когда это происходит (количество раз в секунду, указанное 
; переменной "speed"), выполняется вложенный блок кода. По 
; сути, это работает точно так же, как цикл forever, но БЕЗ 
; остановки каких-либо других операций в игре:

box 0x0 rate speed feel [
engage: func [f a e] [
    if a = 'time [

        ; Если для переменной "fire" в настоящее время 
        ; установлено значение true, переместите ракету 
        ; на 30 пикселей вверх:

        if fire = true [x/offset: x/offset + 0x-30]

        ; Если ракета достигает верхней части экрана, 
        ; переместите её обратно в нижнюю часть (вне 
        ; поля зрения ниже нижнего края графического 
        ; интерфейса пользователя) и установите для 
        ; переменной flag значение false, чтобы она 
        ; перестала двигаться:

        if x/offset/2 < 0 [x/offset/2: 440  fire: false]

        ; Обновим дисплей:

        show x

        ; Приведённый ниже код перемещает целевую часть. 
        ; Части случайных X и Y координат перемещают 
        ; графику, как правило, слева направо, но по 
        ; несколько непредсказуемой траектории, по
        ; которой немного сложнее стрелять:

        y/offset: y/offset + as-pair 
            ((random 20) - 5) ((random 10) - 5)

        ; Проверим, дошла ли цель до правой части экрана:

        if y/offset/1 > 600 [

            ; Если да, уменьшим количество жизней на одну:

            lives: lives - 1

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

            if lives = 0 [
                alert join "GAME OVER!!! Final Score: " score
                quit
            ]

            ; В противном случае уведомим пользователя 
            ; и обновим экран (это автоматически 
            ; обновит доску результатов и сбрасывает 
            ; графику на новые начальные позиции):

            alert "-1 Life!"   unview   do game
        ]

        ; Обновим дисплей:
show y

; Теперь, когда графика перемещена, проверьте, 
; не произошло ли столкновение между ракетой и 
; целью (если цель и стартовые точки ракеты 
; находятся в пределах указанного расстояния):
if within? x/offset (y/offset - 5x5) 60x30 [

    ; Если произошло столкновение, уведомим 
    ; пользователя, изменим счёт и скорость, 
    ; установим для флага огня значение false и 
    ; обновим игровой экран:

    alert "Ka-blammmm!!!"
    score: score + 1   speed: speed + 5  fire: false
    unview   do game
]
]
]
]
]
]

24.7 Бинго Доска

Вот программа доски для бинго, используемая для ведения реального бизнеса по бинго:

REBOL [title: "Bingo"]
write/append %bingo_history.txt rejoin [
    newline newline "NEW GAME:  " now newline newline
]
write %bingo_designer.r {rebol []
    view center-face board-gui: layout/tight [
        size 200x240 across space 0x0
        style b button red 40x40 font-size 28 [
            alert {
                Click the squares, then press the 'S' 
                key to save the image.
            }
        ]
        style n button blue 40x40 effect [] [
            either face/color = blue [
                face/color: white show face
            ] [
                face/color: blue show face
            ]
        ]
        b "B" b "I" b "N" b "G" b "O" return
        n n n n n return
        n n n n n return
        n n n n n return
        n n n n n return
        n n n n n return
        key keycode #"s" [
            save/png file-name: to-file request-file/only/save/file
                %bingo-board_1.png to-image board-gui
            view/new layout [image load file-name]
        ]
    ]
}
insert-event-func [
    either event/type = 'close [
        really: request "Really close the program?"
        if really = true [quit]
    ] [event]
]
cur-let: copy ""
view center-face layout/tight [
    size 1024x768 across space 0x0
    style bb button 64x72 red bold font [size: 48] [
        if ((request/confirm "End game?") = true) [quit]
    ]
    style nn button 64x72 black bold font [size: 14 color: 23.23.23] [
        either face/font/size = 14 [
            set-font face size 46 
            set-font face color white
            show face
            cur-num: to-integer face/text
            case [
                (cur-num <= 15) [cur-let: "B"]
                ((cur-num > 15) and (cur-num <= 30))  [cur-let: "I"]
                ((cur-num > 30) and (cur-num <= 45))  [cur-let: "N"]
                ((cur-num > 45) and (cur-num <= 60))  [cur-let: "G"]
                ((cur-num > 60) and (cur-num <= 75))  [cur-let: "O"]
            ]
            write/append %bingo_history.txt rejoin [
                now "   " cur-let " " face/text newline
            ]
            box1/text: cur-let show box1
            loop 3 [
                box2/text: "" show box2 wait .4
                box2/text: face/text show box2 wait .85
            ]
        ] [
            set-font face size 14 
            set-font face color white
            show face
        ]
    ]
    bb "B" nn "1" nn "2" nn "3" nn "4" nn "5" 
        nn "6" nn "7" nn "8" nn "9" nn "10" 
        nn "11" nn "12" nn "13" nn "14" nn "15" return
    bb "I" nn "16" nn "17" nn "18" nn "19" nn "20" 
        nn "21" nn "22" nn "23" nn "24" nn "25" 
        nn "26" nn "27" nn "28" nn "29" nn "30" return
    bb "N" nn "31" nn "32" nn "33" nn "34" nn "35" 
        nn "36" nn "37" nn "38" nn "39" nn "40" 
        nn "41" nn "42" nn "43" nn "44" nn "45" return
    bb "G" nn "46" nn "47" nn "48" nn "49" nn "50" 
        nn "51" nn "52" nn "53" nn "54" nn "55" 
        nn "56" nn "57" nn "58" nn "59" nn "60" return
    bb "O" nn "61" nn "62" nn "63" nn "64" nn "65" 
        nn "66" nn "67" nn "68" nn "69" nn "70" 
        nn "71" nn "72" nn "73" nn "74" nn "75" return
    box white 512x60 
    box 200.200.255 512x60 font-color blue font-size 52 "Prize: $" [
        face/text: request-text/title/default 
            "Enter Prize Text:" face/text
    ]
    return
    box1: box white 512x80 "" font [size: 50 color: (blue / 2)] 
    box 200.200.255 512x80 font-size 38 font-color black "Current Game:"
    return
    box2: box white 512x240 "" font [size: 200 color: blue] 
    box 200.200.255 136x240
    image1: image 235.235.255 240x240   [
        either true = request/confirm "Create new image?" [
            launch %bingo_designer.r
        ] [
            if error? try [
                image1/image: load request-file/only show image1
            ] [alert "Error loading image."]
        ]
    ] 
    box 200.200.255 136x240
    return
    box white 512x30 
    box 200.200.255 512x30
]

24.8 Голосовые оповещения

Вот программа, которая позволяет вам записывать свой голос или другие звуки, которые будут воспроизводиться в качестве сигналов тревоги для любого количества событий. Сохранение и загрузка списков событий. Все звуковые сигналы будильника повторяются до тех пор, пока не будут остановлены. Запишите, как вы говорите: "Напомнить боссу о его встрече с Джимом" или "Позвонить домой и убедитесь, что дети выгуливают собаку", а затем установите будильник, чтобы воспроизвести эти голосовые сообщения в любой день/время. Если вы установите будильник как дату/время, будильник сработает только один раз в этот день. Если вы установите будильник как время, будильник будет срабатывать каждый день в это время. Код записи .wav предназначен только для MS Windows, но программа может воспроизводить любой волновой файл, который можно использовать в REBOL:

REBOL [title: "Voice Alarms"]

lib: load/library %winmm.dll
mci: make routine! [c [string!] return: [logic!]] lib "mciExecute"

write %play-alarm.r {
    REBOL []
    wait 0
    the-sound: load %tmp.wav
    evnt: load %event.tmp
    if (evnt = []) [evnt: "Test"]
    forever [
        if error? try [
             insert s: open sound:// the-sound wait s close s
        ] [
             alert "Error playing sound!"
        ]
        delay: :00:07
        s: request/timeout [
            join uppercase evnt " alarm - repeats until you click 'stop':"
            "Continue"
            "STOP"
        ] delay
        if s = false [break]
    ]
}

current: rejoin [form now/date newline form now/time]

view center-face layout [
    c: box black 400x200 font-size 50 current rate :00:01 feel [
        engage: func [f a e] [
            if a = 'time [
                c/text: rejoin [form now/date newline form now/time]
                show c
                if error? try [
                    foreach evnt (to-block events/text) [
                        if any [
                             evnt/1 = form rejoin [
                                 now/date {/} now/time
                             ]
                             evnt/1 = form now/time  
                        ] [
                            if error? try [
                                 save %event.tmp form evnt/3
                                 write/binary %tmp.wav 
                                 read/binary to-file evnt/2
                                 launch %play-alarm.r
                            ] [
                                 alert "Error playing sound!"
                            ]
                            ; request/timeout [(form evnt/3) "Ok"] :00:05
                        ]
                    ]
                ] []  ; do nothing if user is manually editing events
            ] 
        ] 
    ]
    h3 "Alarm Events (these CAN be edited manually):"
    events: area  ; {[8:00:00am %alarm1.wav "Test Alarm - DELETE ME"]}
    across
    btn "Record Alarm Sound" [
        mci "open new type waveaudio alias wav"
        mci "record wav"
        request ["*** NOW RECORDING *** Click 'stop' to end:" "STOP"]
        mci "stop wav"
        if error? try [x: first request-file/file/save %alarm1.wav] [
            mci "close wav"
            return
        ]
        mci rejoin ["save wav " to-local-file x]
        mci "close wav"
        request [rejoin ["Here's how " form x " sounds..."] "Listen"]
        if error? try [
            save %event.tmp "test"
            write/binary %tmp.wav 
            read/binary to-file x
            launch %play-alarm.r
        ] [
            alert "Error playing sound!"
        ]
    ]
    btn "Add Event" [
        event-name: request-text/title/default "Event Title:" "Event 1"
        the-time: request-text/title/default "Enter a date/time:" rejoin [
            now/date {/} now/time
        ]
        if error? try [set-time: to-date the-time] [
            if error? try [set-time: to-time the-time] [
                alert "Not a valid time!"
                break
            ]
        ]
        my-sound: request-file/title/file ".WAV file:""" %alarm1.wav
        if my-sound = none [break]
        event-block: copy []
        append event-block form the-time
        append event-block my-sound
        append event-block event-name
        either events/text = "" [spacer: ""][spacer: newline]
        events/text: rejoin [events/text spacer (mold event-block)]
        show events
    ]
    btn "Save Events" [
        write to-file request-file/file/save %alarm_events.txt events/text
    ]
    btn "Load Events" [
        if error? try [
            events/text: read to-file request-file/file %alarm_events.txt
        ] [return]
        show events
    ]
]

24.9 Мелочи в конце

Этот скрипт предоставляет быстрый визуальный справочник по всем встроенным цветам REBOL:

REBOL [Title: "Quick Color Guide"]

echo %colors.txt ? tuple! echo off
lines: read/lines %colors.txt
colors: copy []
gui: copy [across space 1x1]
count: 0
foreach line at lines 2 [
    if error? try [append colors to-word first parse line none][] 
]
foreach color colors [
    append gui [style box box [alert to-string face/color]]
    append gui reduce ['box 110x25 color to-string color]
    count: count + 1
    if count = 5 [append gui 'return count: 0]
]
view center-face layout gui

Следующий быстрый сценарий демонстрирует, как преобразовать кортежи цветов REBOL в цвета HTML и наоборот:

to-binary request-color
to-tuple #{00CD00}
view layout [box to-tuple #{5C743D}] ; просмотрем цвет HTML на экране!

Это быстрый способ просмотреть историю консоли текущего сеанса REBOL:

foreach line reverse copy system/console/history [print line]

Вот как удалить последние 2 элемента из блока:

x: ["asdf" "qwer" "zxcv" "uiop" "hjkl" "vbnm"]
y: head clear skip tail x -2
probe y

Приведённый ниже сценарий демонстрирует, как использовать порты электронной почты для чтения одного сообщения с поп-сервера за раз. Обязательно установите настройки своей учётной записи электронной почты перед запуском этого (это объяснялось ранее в этом руководстве):

for i 1 length? pp: open pop://user@site.com 1 compose [
    ask find pp/(i) "Subject:"
]

Вот несколько примеров того, как можно использовать функцию "request" (запрос):

; Два разных формата включают передачу строки или блока. Если вы 
; передадите строку, кнопки по умолчанию будут "yes" (да), 
; "no" (нет) и "cancel" (отменить). Если передать блок, можно 
; указать текст на кнопках:
request "Could this be useful?"
request ["Just some information."]
request ["Here are 2 buttons with altered text:" "Probably" "Not Really"]
request ["3 buttons with altered text:" "Probably" "Not Really" "Dunno"]

; "Request" возвращает только "true" (истина), "false" (ложь) или 
; "none" (нет). В примере, подобном приведённому ниже, ответ 
; пользователя можно преобразовать в строки с помощью структуры 
; переключения "switch":

answer: form request ["Complex example:" "choice 1" "choice 2" "choice 3"]
switch/default answer [
    "true"  [the-answer: "choice 1"]  
    "false" [the-answer: "choice 2"]
    "none"  [the-answer: "choice 3"]
] []
print the-answer

; Модификатор "/type" изменяет отображаемый значок:

request/type ["Here's a better icon for information display."] 'info
request/type ["Altered title and button text go in a block:" "Good"] 'info
request/ok/type "This example is the EXACT same thing as 'alert'." 'alert
request/ok/type "Here's alert with a different icon." 'info
request/ok/type "Here's another icon!" 'stop
halt

Вот самодельный запросчик с изменяемым размером, который я использую, когда не хочу, чтобы отображалась строка заголовка "REBOL -". Он имеет тайм-аут по умолчанию (установлен на 6 секунд в приведённом ниже примере), а также может быть закрыт нажатием кнопки. Это особенно подходит для полноэкранных приложений коммерческих киосков):

sz: 5
view layout [
    btn "Click here to see a requestor with a 6 second timeout" [
        view/new/options center-face information: layout [
            text font-size (8 * sz) "Here's a message!" rate :00:06 feel [
                engage: func [f a e] [
                    if a = 'time [unview/only information]
                ]
            ]
            box 1x1  ; spacer
            btn as-pair (12 * sz) (8 * sz) font-size (5 * sz) "Ok" [
                unview/only information
            ]
        ][no-title]
    ]
]

Чтобы отредактировать источник любой функции мезонина, используйте следующий формат:

editor mold :request
editor mold :inform

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

REBOL [title: "Find Long Lines"]

doc: read/lines to-file request-file
the-text: {}
foreach line doc [
    if ((find/part line "   " 4)) [
        if ((length? line) > 78) [
            print line
            the-text: rejoin [the-text newline line]
        ]
    ]
]
editor the-text

Вот пара скриптов, которые я использую для синхронизации часов моего компьютера с временем и датой на моем веб-сервере. Функция установки времени Windows API основана на скрипте синхронизации Nist Clock Ладислава Мечира:

REBOL []

dif: 7:00  ; difference between web server and your local time zone
date: (to-date trim read http://site.com/time.cgi) + dif

lib: load/library %kernel32.dll

set-clock: make routine! [
    systemtime [struct! []]
    return:    [integer!]
] lib "SetSystemTime"

current: make struct! [
    wYear         [short]
    wMonth        [short]
    wDayOfWeek    [short]
    wDay          [short]
    wHour         [short]
    wMinute       [short]
    wSecond       [short]
    wMilliseconds [short]
] reduce [
    date/year
    date/month
    date/weekday
    date/day
    date/time/hour
    date/time/minute
    to-integer date/time/second
    0
]

either ((set-clock current) = 1) [
    ask rejoin ["Time has been set to:  " now "^/^/[Enter]... "]
] [
    ask "Error setting time.  Please check your Internet connection."
]

free lib

Вот сценарий CGI, который необходим приведённому выше коду (для получения даты и времени с веб-сервера). Поместите его по URL-адресу, который читается, когда установлено указанное выше слово даты:

#! /home/path/public_html/rebol/rebol -cs
REBOL [title: "time"]
print "content-type: text/html^/"
print now

Вот версия Ладислава (лучшая) вышеупомянутой функции Windows. Сценарий на http://www.fm.tul.cz/~ladislav/rebol/nistclock.r может устанавливать системные часы как Linux, так и Windows ("set-system-time-lin" делает то же самое в Linux):

the-date: to-date trim read http://site.com/time.cgi

set-system-time-win: func [
    {set system time in Windows; return True in case of success}
    [catch]
    date
    /local set-system-time
] [
    unless value? 'kernel32 [kernel32: load/library %kernel32.dll]
    set-system-time: make routine! [
        systemtime [struct! []]
        return: [int]
    ] kernel32 "SetSystemTime"
    date: date - date/zone
    date/zone: 0:0
    0 <> set-system-time make struct! [
        wYear [short]
        wMonth [short]
        wDayOfWeek [short]
        wDay [short]
        wHour [short]
        wMinute [short]
        wSecond [short]
        wMilliseconds [short]
    ] reduce [
        date/year
        date/month
        date/weekday
        date/day
        date/time/hour
        date/time/minute
        to integer! date/time/second
        0
    ]
]

set-system-time-win the-date

Я использую следующий скрипт для загрузки снимков экрана своего рабочего стола прямо на свой веб-сайт (в той версии, которую я использую, я помещаю текст включённого скрипта прямо в свой код):

REBOL []

do http://www.rebol.org/download-a-script.r?script-name=capture-screen.r

the-image: ftp://user:pass@site.com/path/current.png

; Вы также можете сохранить на локальный жёсткий диск, если хотите:
; the-image: %current.png

view center-face gui: layout [
    button 150 "Upload Screen Shot" [
        unview gui
        wait .2
        save/png the-image capture-screen
        view center-face gui
    ]
]

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

view gui: layout  [
    button1: button
    button2: button "remove" [
        remove find gui/pane button1
        show gui
    ]
    button3: button "add" [
        append gui/pane button1
        show gui
    ]
]

Вот способ получить уникальный строковый идентификатор из текущего времени (полезно для имён буферов MCI и других ситуаций, когда вам нужно сгенерировать абсолютно уникальные строки идентификаторов без каких-либо нечётных символов):

replace/all replace/all replace/all form now "/" "" ":" "x" "-" "q" "." ""

; precise version (w/ milliseconds):

replace/all replace/all replace/all replace/all form now/precise trim {
    /} "" ":" "x" "-" "q" "." ""

Этот сценарий создаёт образ, сохраняет его на жёсткий диск, а затем открывает его в mspaint.exe:

save/bmp %test.bmp to-image layout [box]
call/show join "mspaint.exe " to-local-file join what-dir %test.bmp

Этот сценарий демонстрирует, как использовать AutoIT DLL для управления mp3-плеером madplay.exe:

REBOL []

if not exists? %AutoItDLL.dll [
    write/binary %AutoItDLL.dll
    read/binary http://musiclessonz.com/rebol_tutorial/AutoItDLL.dll
    write/binary %madplay.exe
    read/binary http://musiclessonz.com/rebol_tutorial/madplay.exe
]

lib: load/library %AutoItDLL.dll

move-mouse: make routine! [
    return: [integer!] x [integer!] y [integer!] z [integer!]
] lib "AUTOIT_MouseMove"

send-keys: make routine! [
    return: [integer!] keys [string!]
] lib "AUTOIT_Send"

winactivate: make routine! [
    return: [integer!] wintitle [string!] wintext [string!]
] lib "AUTOIT_WinActivate"

set-option: make routine! [
    return: [integer!] option [string!] param [integer!]
] lib "AUTOIT_SetTitleMatchMode" 

set-option "WinTitleMatchMode" 2
call/show {madplay.exe -v *.mp3}

view layout [
    across
    btn "forward" [
        winactivate "\reb" ""
        send-keys "f"
    ]
    btn "back"[
        winactivate "\reb" ""
        send-keys "b"
    ]
    btn "volume up" [
        winactivate "\reb" ""
        send-keys "+"
    ]
    btn "volume-down"[
        winactivate "\reb" ""
        send-keys "-"
    ]
    btn "pause" [
        winactivate "\reb" ""
        send-keys "p"
    ]
    btn "quit" [
        winactivate "\reb" ""
        send-keys "q"
        quit
    ]
]

Вот быстрый и грязный способ распечатать справку по всем встроенным функциям. Также включает полный список стилей VID (виджеты графического интерфейса "view layout"), слов макета VID и фасетов VID (стандартные свойства, доступные для всех стилей VID). Дайте ему минутку на пробежку ...

REBOL [title: "Quick Manual"]

print "This will take a minute..."  wait 2
echo %words.txt what echo off   ; "echo" saves console activity to a file
echo %help.txt
foreach line read/lines %words.txt [
    word: first to-block line
    print "___________________________________________________________^/"
    print rejoin ["word:  " uppercase to-string word]  print "" 
    do compose [help (to-word word)]
]
echo off
x: read %help.txt
write %help.txt "VID STYLES (GUI WIDGETS):^/^/"
foreach i extract svv/vid-styles 2 [write/append %help.txt join i newline]
write/append %help.txt "^/^/LAYOUT WORDS:^/^/" 
foreach i svv/vid-words [write/append %help.txt join i newline]
b: copy [] 
foreach i svv/facet-words [
    if (not function? :i) [append b join to-string i "^/"]
]
write/append %help.txt rejoin [
    "^/^/STYLE FACETS (ATTRIBUTES):^/^/" b "^/^/SPECIAL STYLE FACETS:^/^/"
]
y: copy ""
foreach i (extract svv/vid-styles 2) [
    z: select svv/vid-styles i
    ; additional facets are held in a "words" block:
    if z/words [
        append y join i ": "
        foreach q z/words [if not (function? :q) [append y join q " "]]
        append y newline
    ]
]
write/append %help.txt rejoin [
    y "^/^/CORE FUNCTIONS:^/^/" at x 4
]
editor %help.txt

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

m: system/schemes/default q: system/schemes/pop
view layout [ style f field
    u: f "username" p: f "password" s: f "smtp.address" o: f "pop.address"
    btn bold "Save Server Settings" [
        m/user: u/text m/pass: p/text m/host: s/text q/host: o/text
    ] tab
    e: f "user@website.com" j: f "Subject" t: area 
    btn bold "SEND" [
        send/subject to-email e/text t/text j/text alert "Sent"
    ] tab
    y: f "your.email@somesite.com"               
    btn bold "READ" [foreach i read to-url join "pop://" y/text [ask i]]
]

В этом примере в виджете области ведётся подсчёт слов текста в реальном времени. Изменение скорости сократит использование системных ресурсов, но также замедлит время отклика (для большинства случаев подходит скорость 3-4 обновления в секунду):

view layout [
    i: info rate 0 feel [
        engage: func [f a e] [
            if a = 'time [
                l: length? parse m/text none
                i/text: join "Wordcount: " l
                show i
            ]
        ]
    ]
    m: area 
]

Вот две версии программы VOIP, приведённые ранее в разделе о портах. Это, вероятно, самые компактные программы VOIP, которые вы где-либо найдёте. Первый включает обработку ошибок порта, автоматическое тестирование локального хоста (просто нажмите [ENTER], чтобы использовать локальный хост в качестве IP-адреса), работу в режиме громкой связи и автоматическое тестирование минимального объёма (шумоподавление - данные не отправляются, если данный том не обнаружен, для сохранения пропускная способность). Его можно вставить прямо в консоль REBOL или сохранить в файл и запустить. Второй - barebones (пользователь видит ошибки при разрыве соединения, его необходимо сохранить в файл и запустить и т. Д.), Но он работает. Размеры файлов этих скриптов составляют 693 байта и 554 байта !!:

REBOL[]do[write %w{REBOL[]if error? try[p: first wait open/binary/no-wait
tcp://:8][quit]wait 0 s: open sound:// forever[if error? try[m: find v:
copy wait p #""][quit]i: to-integer to-string copy/part v m while[i >
length? remove/part v next m][append v p]insert s load to-binary
decompress v]}launch %w lib: load/library %winmm.dll x: make routine![c[
string!]return:[logic!]]lib"mciExecute"if(i: ask"Connect to IP: ")=""[i:
"localhost"]if error? try[p: open/binary/no-wait rejoin[tcp:// i":8"]][
quit]x"open new type waveaudio alias b"forever[x"record b"wait 2 x
"save b r"x"delete b from 0"insert v: compress to-string read/binary
%r join l: length? v #""if l > 4000[insert p v]]]

REBOL[]write %w{REBOL[]if error? try[p: first wait open/binary/no-wait
tcp://:8][quit]wait 0 s: open sound:// forever[m: find v: copy wait p #""
i: to-integer to-string copy/part v m while[i > length? remove/part v next
m][append v p]insert s load v]}launch %w lib: load/library %winmm.dll x:
make routine![c[string!]return:[logic!]]lib"mciExecute"i: ask"IP: "p:
open/binary/no-wait rejoin[tcp:// i":8"]x"open new type waveaudio alias b"
forever[x"record b"wait 2 x"save b r"x"delete b from 0"insert v:
read/binary %r join length? v #""insert p v]

В следующем коротком сценарии показано, как использовать dll-кодировщик Intelligent Mail от почтовой службы США.

REBOL [title: "USPS Intelligent Mail Encoder"]
unless exists? %usps4cb.dll [
    write/binary %usps4cb.dll read/binary http://re-bol.com/usps4cb.dll
]
GEN-CODESTRING: make routine! [
    t [string!]  r[string!]  c [string!]  return: [integer!]
]  load/library request-file/only/file %usps4cb.dll "USPS4CB"

t: request-text/title/default "Tracking #:" "00700901032403000000"
r: request-text/title/default "Routing #:" "55431308099"
GEN-CODESTRING t r (make string! 65)
alert first second first :GEN-CODESTRING

Вот пример мгновенного сообщения, которое позволяет пользователям загружать информацию о своём подключении (имя пользователя, WAN IP, LAN IP и сетевой порт) в текстовый файл на FTP-сервере. Затем другие могут просто щёлкнуть своё имя пользователя в раскрывающемся списке (кнопка выбора), чтобы подключиться:

REBOL [title: "Instant Messenger"]

servers: ftp://username:password@yoursite.com/public_html/im.txt  ; EDIT
flash "Retrieving server list..."
if error? try [server-info: reverse read/lines servers] [
    alert "Internet connection not available."
    server-info: copy []
]
unview
name-list: copy []
foreach server server-info [append name-list first to-block server]
insert head name-list "Connect to a Server:"

view center-face layout [
    across
    choice 280 data name-list [
        mode: request [ {
            SELECT MODE: By default, this program is able to connect to
            a server running on any other computer in your Local Area
            Network.  Choosing "LAN" mode connects you to a server's local
            IP address.  If you select "Internet" Mode, you will connect
            to the server's WAN IP address (typically the address of
            the user's _router_).  In order for Internet mode to work
            correctly, the selected port number chosen by the server user
            must be exposed on the Internet, or be forwarded from their
            router to the IP address of the computer running the server
            program.
        } "LAN" "Internet"]
        foreach server server-info [
            server-block: parse server " "
            if ((form first server-block) = form value) [
                b/text: server-block/1  show b
                either mode = false [
                    remote-ip: server-block/2
                ] [
                    remote-ip: server-block/3
                ]
                j/text: server-block/4
                show j
                p: open/lines rejoin [tcp:// remote-ip j/text]
                z: 1
                focus g
                break
            ]
        ]
    ]
    text "OR run as server:"
    b: field 106 "Username"
    text "Port: "
    j: field 55 ":8080"
    q: button 84 "Start Server" [
        parse read http://guitarz.org/ip.cgi [
            thru <title> copy p to </title>
        ]
        parse p [thru "Your IP Address is: " copy wan-ip to end]
        write/append servers rejoin [
            b/text " "                           ; username
            wan-ip " "                           ; wan ip
            read join dns:// read dns:// "  "    ; local ip
            j/text "^/"                          ; port
        ]
        alert {
            Server is running.  Connections from clients running on
            your Local Area Network should work without any problems.
            If you want to accept connections from the Internet, and
            you are connected by a router, then the port number you've
            selected must be forwarded from your router to the IP
            address of this computer (see portforward.com for more
            information about forwarding ports).
        }
        focus g 
        p: first wait open/lines (join tcp:// j/text)
        z: 1
    ]
    return
    r: area 700x400 rate 4 feel [
        engage: func [f a e][
            if a = 'time and value? 'z [
                if error? try [x: first wait p] [quit]
                r/text: rejoin ["-->  " x "^/" r/text]
                show r
            ]
        ]
    ]
    return
    g: field 400 "Type message here, then press [ENTER]" [
        r/text: rejoin ["<--  " value "^/" r/text]
        show r
        insert p value
        focus face
    ]
    tabs 618
    tab
    button "Save Chat Text" [editor r/text]
    return
]

Вот хороший общий пример CGI, который демонстрирует, как вводить и декодировать данные Get и Post, немного изменённый по сравнению с предыдущими примерами в этом тексте:

#!./rebol276 -cs
REBOL[]
print "content-type: text/html^/"
either system/options/cgi/request-method = "POST" [
    data: copy "" buffer: copy ""
    while [positive? read-io system/ports/input buffer 16380][
        append data buffer
        clear buffer
    ]
] [
    data: system/options/cgi/query-string
]
cgi: construct decode-cgi data
if (length? first cgi) < 2 [
    print {
        <FORM METHOD="post" ACTION="./test.cgi">
        <CENTER>
        <INPUT TYPE=hidden NAME=hiddenvalue VALUE="foo">
        <INPUT TYPE=text size=50 name=text><BR><BR>
        <TEXTAREA cols=75 name=message rows=5></textarea><br><br>
        <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Submit">
        </CENTER>
        <FORM>
    }
    quit
]
print rejoin [
    cgi/hiddenvalue "<br><br>"
    cgi/text "<br><br>"
    "<pre>" cgi/message "</pre>"
]

Вот ещё одна версия функции decode-multipart-form-data Андреаса Болка (описанная в разделе этого руководства о программировании CGI):

decode-multipart-form-data: func [
    p-content-type
    p-post-data
    /local list ct pd bd delim-beg delim-end mime-part ] [
    list: copy []
    if not found? find p-content-type "multipart/form-data" [return list]

    ct: copy p-content-type
    pd: copy p-post-data
    bd: join "--" copy find/tail ct "boundary="

    delim-beg: join crlf crlf
    delim-end: rejoin [ crlf bd ]

    mime-part: [
        ( ct-dispo: content: none ct-type: "text/plain" )
        thru bd
        thru "content-disposition: " copy ct-dispo to crlf
        opt [ thru "content-type: " copy ct-type to crlf ]
        thru delim-beg copy content to delim-end
        ( handle-mime-part ct-dispo ct-type content )
    ]

    handle-mime-part: func [
        p-ct-dispo p-ct-type p-content /local fieldname 
    ] [
    p-ct-dispo: parse p-ct-dispo [describe ;=" here]
    fieldname: select p-ct-dispo "name"

    append list to-set-word fieldname
    either found? find p-ct-type "text/plain" [append list content][
        append list make object! [
            filename: select p-ct-dispo "filename"
            type: copy p-ct-type
            content: either none? p-content [ none ] [ copy p-content ]
        ]
    ]
    ]
    use [ ct-dispo ct-type content ] [
    parse/all pd [ some mime-part ]
    ]
    return list
]
]

Это набор сценариев Эндрю Гроссмана и Луиса Родригеса Хурадо, которые также работают с данными, состоящими из нескольких частей (просто пример - НЕ требуется для производственного использования, если у вас есть функция Андреаса):

#!c:/rebol.exe -cs

REBOL [
TITLE: "form-upload"
FILE: %form-upload.r
DATE: 29-April-2000
]
print "Content-Type: text/html^/^/"
print {
<form METHOD=POST ACTION="post.r"
 enctype="multipart/form-data">
 Enter a description of the file:: <input TYPE=text
 SIZE=50 NAME=text><br>
 Select the file: <input TYPE=file SIZE=15
 NAME=myUpload><br><br><br>
 <input TYPE=submit VALUE="post.r">
</FORM></INPUT>
}

#!c:/rebol.exe -cs
REBOL [
    Title:      "multipart POST"
    Date:       15-Sep-1999
    Version:    1.2
    File:       %POST.r
    Author:     {Andrew Grossman (modified by: Luis Rodriguez J -
.29-April-2000)}
    Email:      [grossdog--dartmouth--edu]
    Owner:      "REBOL Technologies"

    Purpose:    {
        To decode multipart encoded POST requests, including file uploads.
    }
    Usage:      {
        Call this file in your CGI scriptand do decode-multi with a
        file argument of the directory where uploaded files will go and
        a logic argument to set whether files will be given the field they
        were uploaded as a name.  Files are saved and variables are
        decoded and set.
    }
    Notes:      {
        Fixed problem recognizing EOF.
        Functionality is now rock solid.  Function
        calls won't change, so this is certainly useable.
        See the comment in the decode-multi function if you need mime
        types.  Tested with MSIE and Netscape for Mac.
    }
    category: ['web 'cgi 'utility]
]
decode-multi: func [
    file-dir      [file!]  {Directory in which to put uploaded files}
    save-as-field [logic!] {save files as field name or uploaded filename}
    /local str boundary done name file done2 content
][

if equal? system/options/cgi/request-method "POST" [
    either not parse system/options/cgi/content-type
        ["multipart/form-data" skip thru {boundary=} skip some {-}
            copy boundary to end]

        str:   make string! input do decode-cgi str][
        str:    make string! input
        done:   false

        while [not done] [
            name:   make string! ""
            str:    make string! input
            either equal? "" str [done: true] [
                either parse/all str [skip thru {name="} copy name to {"}
                    skip thru {filename="} copy file to {"} skip to end] [
                    str: make string! input
                    if not equal? str "" [str: make string! input]
                    comment {if you need mime put "parse/all str [
                      "Content-Type:" skip copy mime to end
                    ]" into the preceding if block.}
                    done2:      false
                    content:    make string! ""
                    while [not done2] [
   content-length: to-integer system/options/cgi/content-length
   str: make string! content-length
   read-io system/ports/input str content-length
                        either d: find/reverse tail str boundary [
                           e: find/reverse tail copy/part str (index? d)
                               {^/}
                               content: copy/part str (index? e) - 2
                         done2: true]
                             [
                             append content str
                         ]
                    ]
                    if not none? file

                       either save-as-field [name: dehex name write/binary
                            file-dir/:name content] [
                            file: dehex file write/binary file-dir/:file
                            content
                        ]
                    ]
                ][
                    parse str [skip thru {name="} copy name to {"}]
                    str: make string! input str: dehex make string! input
                    set to-word name str str: make string! input
                ]

            ]

        ]
    ]
]
]

decode-multi %. true

Это слегка отредактированная версия программы 3D Maze (движок raycasting) Оливье Оверло:

REBOL [title: "3D Maze - Ray Casting Example"] 

px: 9 * 1024  py: 11 * 1024 stride: 2 heading: 0 turn: 5
laby: [ 
    [ 8 7 8 7 8 7 8 7 8 7 8 7 ] 
    [ 7 0 0 0 0 0 0 0 13 0 0 8 ] 
    [ 8 0 0 0 12 0 0 0 14 0 9 7 ] 
    [ 7 0 0 0 12 0 4 0 13 0 0 8 ] 
    [ 8 0 4 11 11 0 3 0 0 0 0 7 ] 
    [ 7 0 3 0 12 3 4 3 4 3 0 8 ] 
    [ 8 0 4 0 0 0 3 0 3 0 0 7 ] 
    [ 7 0 3 0 0 0 4 0 4 0 9 8 ] 
    [ 8 0 4 0 0 0 0 0 0 0 0 7 ] 
    [ 7 0 5 6 5 6 0 0 0 0 0 8 ] 
    [ 8 0 0 0 0 0 0 0 0 0 0 7 ] 
    [ 8 7 8 7 8 7 8 7 8 7 8 7 ] 
] 
ctable: [] 
for a 0 (718 + 180) 1 [ 
    append ctable to-integer (((cosine a ) * 1024) / 20) 
] 
palette: [ 
    0.0.128 0.128.0 0.128.128 
    0.0.128 128.0.128 128.128.0 192.192.192 
    128.128.128 0.0.255 0.255.0 255.255.0 
    0.0.255 255.0.255 0.255.255 255.255.255 
] 
get-angle: func [ v ] [ pick ctable (v + 1) ]
retrace: does [ 
    clear display/effect/draw 
    xy1: xy2: 0x0 
    angle: remainder (heading - 66) 720 
    if angle < 0 [ angle: angle + 720 ] 
    for a angle (angle + 89) 1 [ 
        xx: px 
        yy: py 
        stepx: get-angle a + 90
        stepy: get-angle a 
        l: 0 
        until [ 
            xx: xx - stepx 
            yy: yy - stepy 
            l: l + 1 
            column: make integer! (xx / 1024) 
            line: make integer! (yy / 1024) 
            laby/:line/:column <> 0
        ] 
        h: make integer! (1800 / l) 
        xy1/y: 200 - h 
        xy2/y: 200 + h 
        xy2/x: xy1/x + 6 
        color: pick palette laby/:line/:column 
        append display/effect/draw reduce [ 
            'pen color 
            'fill-pen color 
            'box xy1 xy2 
        ] 
        xy1/x: xy2/x + 2  ; set to 1 for smooth walls 
    ] 
] 
player-move: function [ /backwards ] [ mul ] [ 
    either backwards [ mul: -1 ] [ mul: 1 ] 
    newpx: px - ((get-angle (heading + 90)) * stride * mul) 
    newpy: py - ((get-angle heading) * stride * mul) 
    c: make integer! (newpx / 1024) 
    l: make integer! (newpy / 1024) 
    if laby/:l/:c = 0 [ 
        px: newpx 
        py: newpy 
        refresh-display 
    ] 
] 
evt-key: function [ f event ] [] [ 
    if (event/type = 'key) [ 
        switch event/key [ 
            up [ player-move ] 
            down [ player-move/backwards ] 
            left [ 
                heading: remainder (heading + (720 - turn)) 720 
                refresh-display 
            ] 
            right [ 
                heading: remainder (heading + turn) 720
                refresh-display 
            ] 
        ] 
    ] 
    event 
] 
insert-event-func :evt-key 
refresh-display: does [ 
    retrace 
    show display 
] 
screen: layout [ 
    display: box 720x400 effect [ 
        gradient 0x1 0.0.0 128.128.128 
        draw [] 
    ] 
    edge [ 
        size: 1x1 
        color: 255.255.255 
    ] 
] 
refresh-display 
view screen

Вот пара крошечных служебных скриптов, которые мне показались полезными:

; для замены определенной строки внутри специальных символов:

code: "text1 <% replace this %> text3"
replace code "<% replace this %>" "<% text2 %>"
print code

; заменить все между специальными символами: 

code: "text1 <% replace this %> <% replace this %> text3"
parse/all code [
    any [thru "<%" copy new to "%>" (replace code new " text2 ")] to end
]
print code

Этот сценарий демонстрирует, как вставить строку в файл в найденной позиции:

REBOL []

file: %mp3.html
a-string: {<BODY bgcolor="#C8C8C8">}

; first way:

write file read http://re-bol.com/examples/mp3.html
content: read file
insert (skip find content a-string length? a-string) trim {
    <center><h1>MP3 Example!</h1></center>}
write file content
editor file

; second way:

write file read http://re-bol.com/examples/mp3.html
content: read file
begin: (index? find content a-string) + (length? a-string)
altered: rejoin [
    (copy/part content begin)
    {<center><h1>MP3 Example!</h1></center>}
    (at content begin)
]
write file altered
editor file

Этот код определяет используемую вами операционную систему:

switch system/version/4 [
    2 [print "OSX"]
    3 [print "Windows"]
    4 [print "Linux"]
    7 [print "FreeBSD"]
    8 [print "NetBSD"]
    9 [print "OpenBSD"]
    10 [print "Solaris"]
] [alert "Can't be dertermined"]

Вот CGI-программа, которую я храню на своём веб-сервере для удаления большого количества писем, содержащих любой заданный текст "спама":

#! /home/path/public_html/rebol/rebol -cs
REBOL []
print "content-type: text/html^/"
print [<HTML><HEAD><TITLE>"Remove Emails"</TITLE></HEAD><BODY>]

spam: [
    {Failure} {Undeliverable} {failed} {Returned Mail} {not be delivered}
    {mail status notification} {Mail Delivery Subsystem} {(Delay)}
]

print "logging in..."
mail: open pop://user:pass@site.com
print "logged in"

while [not tail? mail] [
    either any [
        (find first mail spam/1) (find first mail spam/2)
        (find first mail spam/3) (find first mail spam/4)
        (find first mail spam/5) (find first mail spam/6)
        (find first mail spam/7) (find first mail spam/8)
    ][
        remove mail
        print "removed"
    ][
        mail: next mail        
    ] 
    print length? mail 
]
close mail
print [</BODY></HTML>]
halt

Следующий служебный скрипт принимает входную строку и возвращает строку HTML со всеми веб-URL, надлежащим образом обернутыми в виде ссылок:

bb:  "some text http://guitarz.org http://yahoo.com"
bb_temp: copy bb
append bb_temp " " ; in case the URL doesn't have a trailing space
append bb " "
parse bb [any [thru "http://" copy link to " " (
    replace bb_temp (rejoin [{http://} link]) (rejoin [
    {<a href="} {http://} link {" target=_blank>http://} 
    link {</a>}]))] to end
]
bb: copy bb_temp
print bb

Я использую следующий служебный сценарий CGI для копирования целых каталогов файлов с одного веб-сервера на другой:

#!/home/path/public_html/rebol/rebol -cs
REBOL []
print "content-type: text/html^/"
print [<HTML><HEAD><TITLE>"wgetter"</TITLE></HEAD><BODY>]
foreach file (read ftp://user:pass@site.com/public_html/path/) [
    print file
    print <BR>
    write/binary (to-file file) 
        (read/binary (to-url (rejoin [http://site.com/path/ file])))
]
print [</BODY></HTML>]

Я использую этот следующий сценарий, чтобы убедиться, что на моих веб-серверах нет файлов chmod 777. Встроенная процедура, которая записывает имя каждой папки и каждого файла на моем сервере в текстовый файл. Я запускаю это в консоли CGI с командой "do chmod777to555.r":

REBOL [title: "chmod777to555"]

start-dir: what-dir
all-files: to-file join start-dir %find777all.txt

write all-files ""

recurse: func [current-folder] [
    out-data: copy ""
    write/append all-files rejoin["CURRENT_DIRECTORY:  " what-dir newline]
    call/output {ls -al} out-data
    write/append all-files join out-data newline
    foreach item (read current-folder) [ 
        if dir? item [
            change-dir item 
            recurse %.\
            change-dir %..\
        ] 
    ]
]
recurse %.\

file-list: to-file join start-dir %found777.txt
write file-list ""
current-directory: ""
foreach line (read/lines all-files) [
    if find line "CURRENT_DIRECTORY:  " [
        current-directory: line
    ]
    if find line "rwxrwxrwx" [
        write/append file-list rejoin [
            (find/match current-directory "CURRENT_DIRECTORY:  ")
            (last parse/all line " ")
        ]
        write/append file-list newline
    ]

]

foreach file (read/lines file-list) [
    call rejoin [{chmod 755 } (to-local-file file)]
]

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

foreach file read %. [
    if (suffix? file) = %.src [
        write (to-file first parse file ".")(read to-file file)
    ]
]

Вот как можно переместить один элемент в серии в другое место:

x: ["one" "two" "three" "four" "five" "six"]
move/to (find x "five") 2
print x

Я использую следующий скрипт для обновления моей коллекции языковых библиотек Haxe:

REBOL []

write %haxelibs.txt read http://lib.haxe.org/files/

the-list: read/lines %haxelibs.txt
clean: copy []

foreach line the-list [
    x: (parse/all form (find line ".zip") ">")
    if (length? x) > 2 [
        y: parse form second x "<"
        append clean first y
    ]
]

errors: copy []
make-dir %haxelibs
change-dir %haxelibs
save %list.txt clean  ; comment this if you need to edit list.txt

downloaded: read %.
if exists? %previously_downloaded.txt [
    append downloaded load %previously_downloaded.txt
]
save %previously_downloaded.txt unique downloaded
; editor downloaded

foreach file clean [
    if not (find downloaded (to-file file)) [
        either error? try [size? (join http://lib.haxe.org/files/ file)] [
            print join "ERROR:  " file
            append errors file
        ] [
            print rejoin [
                {Downloading:  } file {  (} 
                size? (join http://lib.haxe.org/files/ file) { kb)}
            ]
            if error? try [
                write/binary 
                    (to-file file)
                    (read/binary (join http://lib.haxe.org/files/ file))
            ] [
                print join "ERROR:  " file
                append errors file
            ]
        ]
    ]
]

save %haxe_lib_download_errors.txt errors
halt
foreach file clean [if find downloaded (to-file file) [print file]]

Я использую следующую строку для просмотра/редактирования кода сценария, который был запущен непосредственно из zip-файла (сжатой папки) в Windows:

editor to-file request-file system/script/path

Вот как обращаться к виджетам на панели главного окна графического интерфейса пользователя:

foreach item system/view/screen-face/pane/1/pane [
    remove find system/view/screen-face/pane/1/pane item
    ; это удаляет все виджеты
]

Вот функция Windows API для использования функций MCI:

lib: load/library %winmm.dll
mciSendString: make routine! [
    command [string!]
    rStr [string!]
    cchReturn [integer!] 
    hwndCallBack [integer!] 
    return: [integer!]
] lib "mciSendStringA"

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

REBOL []

svv/vid-face/color: white
view/new/options gui: layout [
    across
    t1: text "50x50"
    t2: text "- 50x25"
    t3: text "- 25x50"
] [resize]

insert-event-func [
    either event/type = 'resize [
        fs: t1/parent-face/size
        t1/offset: fs / 2x2
        t2/offset: t1/offset - 50x25
        t3/offset: t1/offset - 25x50
        show gui  none
    ] [event]
]

ss: system/view/screen-face/size
gui/size: (ss - 200x200)
gui/offset: (ss / 2x2) - (gui/size / 2x2)
show gui  do-events

Следующие скрипты демонстрируют несколько различных способов запуска кода из блока действий другого виджета (т.е. Для имитации щелчков мышью или других действий на любом заданном лице). Чтобы понять больше, запустите "source do-face" и "source do-face-alt" в консоли REBOL, чтобы увидеть, как работают функции "do-face" и "do-alt-face":

view layout [
    button1: btn "Button 1" [alert "Button 1 action block has been run."]
    btn "Button 2" [do-face button1 1]
]

view layout [
    b1: btn "B1" [alert "B1 left click"] [alert "B1 right click"]
    btn "B2" [do-face b1 1] [do-face-alt b1 1]
]

view layout [
    button1: btn "Button 1" [alert "Button 1 action block has been run."]
    btn "Button 2" [button1/action button1 none]
    ; "button1 none" в строке выше освобождает выключенное состояние btn
]

Следующий сценарий из http://www.pat665.free.fr/gtk/rebol-view.html демонстрирует другой способ сделать то же самое:

view layout [
    b: button "Test" [print "Test pressed"]
    button "In" [b/state: true show b]
    button "Out" [b/state: false show b]
    a: button "Action" [
        b/feel/engage :b 'down none
        b/feel/engage :b 'up none
        a/state: false show a ; Not sure why this line is needed...
    ]
]

Вот 92-символьная версия классической программы "FizzBuzz":

repeat i 100[j:""if i // 3 = 0[j:"fizz"]if i // 5 = 0
    [j: join j"buzz"]if j =""[j: i]print j]

В следующем примере показано, как использовать библиотеку Captcha от Softinov:

REBOL []

write/binary %Caliban.caf read/binary http://re-bol.com/Caliban.caf
do http://re-bol.com/captcha.r

captcha/set-fonts-path %./
captcha/level: 4
write/binary %captcha.png captcha/generate
write %captcha.txt captcha/text

view center-face layout [
    image (load %captcha.png)
    text "Enter the captcha text:"
    f1: field [
        either f1/text = (read %captcha.txt) [
            alert "Correct"
        ] [
            alert "Incorrect"
        ]
    ] 
]

Вот способ получить отдельные символы из случайных слов (изначально это предназначалось для использования как часть процедуры ввода капчи):

x: copy []
wrds: (first system/words)
foreach ch mold pick wrds (random length? wrds) [append x ch]
print x

Вот версия приложения "База данных деталей", которое добавляет изображения для каждой записи, средства поиска и другие полезные функции:

REBOL [title: "Student Photo Database (variation on Data Card File)"]
write %StudentList.csv {STUDENTID,LASTNAME,FIRSTNAME,DOB,GRADE
111111,Doe,Steven D,6/16/1992,12
111112,Doe,Jonathan Daniel,12/16/1991,12
111113,Smith,Karen J,12/3/1991,12
111114,Jones,Michael J,6/4/1992,12
111115,Taylor,Ryan C,1/10/1992,12
111116,Adam,Kaitlan C,4/30/1992,12
111117,Washington,Gabryela,3/31/1992,12
111118,Travolta,Juan D,1/24/1992,12
111119,Cruise,Amber E,5/8/1992,12}
either exists? %data.txt [
    database: load %data.txt
][
    filename: %StudentList.csv
    database: copy []
    lines: read/lines filename
    foreach line lines [
        append database parse/all line ","
    ]
    remove/part database 5                    ; get rid of headers
    for counter 6 ((length? database) + 12) 6 [
        insert (at database counter) to-file rejoin [
            "/C/Photos/image_" (pick database (counter - 5)) ".jpg"
        ]
    ]
    save %data.txt database
]
update: func [marker] [
    n/text: pick database marker
    a/text: pick database (marker + 1)
    p/text: pick database (marker + 2)
    o/text: pick database (marker + 3)
    g/text: pick database (marker + 4)
    i/text: pick database (marker + 5)
    if error? try [photo/image: load to-file i/text] [
        ; alert "No image selected"
        photo/image: none
    ]
    photo/text: ""
    show gui
]
view center-face gui: layout [
    text "Load an existing record:"
    name-list: text-list blue 300x80 data sort (extract database 6) [
        if value = none [return]
        marker: index? find database value
        update marker
    ]
    text "ID:"       n: field 300
    text "Last:"     a: field 300
    text "First:"    p: field 300
    text "BD:"       o: field 300
    text "Grade:"    g: field 300
    text "Image:"    i: btn 300 [
        i/text: to-file request-file 
        photo/image: load to-file i/text
        show gui
    ]
    at 340x20 photo: image white 300x300
    across
    btn "Save" [
        if n/text = "" [alert "You must enter a name." return]
        if find (extract database 6) n/text [
            either true = request "Overwrite existing record?" [
               remove/part (find database n/text) 6
            ] [
               return
            ]
        ]
        save %data.txt repend database [
            n/text a/text p/text o/text g/text i/text
        ]
        name-list/data: sort (extract copy database 6)
        show name-list
    ]
    btn "Delete" [
        if true = request rejoin ["Delete " n/text "?"] [
            remove/part (find database n/text) 6
            save %data.txt database
            do-face clear-button 1
            name-list/data: sort (extract copy database 6)
            show name-list
        ]
    ]
    clear-button: btn "New" [
        n/text: copy  ""
        a/text: copy  ""
        p/text: copy  ""
        o/text: copy  ""
        g/text: copy  ""
        i/text: copy  ""
        photo/image: none
        show gui
    ]
    next-btn: btn "Next"  [
        if error? try [
            old-num: copy n/text
            n/text: form ((to-integer n/text) + 1)
            show n
            marker: index? find database n/text
            update marker
        ] [n/text: copy old-num show n alert "No more records"]
    ]
    prev-btn: btn "Previous"  [
        if error? try [
            old-num: copy n/text
            n/text: form ((to-integer n/text) - 1)
            show n
            marker: index? find database n/text
            update marker
        ] [n/text: copy old-num show n alert "No more records"]
    ]
    key keycode [down] [do-face next-btn 1]
    key keycode [up] [do-face prev-btn 1]
    at 340x340 d1: drop-down 300 data [
        "Last Name" "First Name" "Birthday" "Grade"
    ]
    at 340x380 f2: field 300 "Select field above, type search text here" [
        if d1/data = none [alert "Select a search field above" return]
        search-field: to-integer select [
            "Last Name" 2 "First Name" 3 "Birthday" 4 "Grade" 5
        ] d1/data
        results: copy []
        for counter search-field (length? database) 6 [
            if find (pick database counter) copy f2/text [
                append results pick database (counter - search-field + 1)
            ]
        ]
        t/data: copy results  show t
        if [] = results [alert "None found"]
    ]
    at 340x420 t: text-list 300x60 [
        name-list/picked: copy value
        show name-list
        if value = none [return]
        marker: index? find database value
        update marker
    ]
]

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

REBOL [title: "Feed The Gator"]
bird: load to-binary decompress 64#{
    eJzF2LuL4kAcwHG3shBURHDRRW3E+wuuPOEaC9lGC60EXyCClQjiM4qdzVWWVjYW
    iiA2Cj7wPOzEThALG7HYRixF737EU/bMTTLGzOQLgxCTyaeYxMTv77/0CravML7A
    +AbjDcaL4pXd/gO+/6m+jGu/n6vRaDAMY7fbTSaTzWa7zOn3+xm2zWbz5Pw8fSzF
    HTcajYLBoAIjn8/XbDalVV963J5KpXDIdwUCgV6vJ6tdhPour9crh/15+K1QKDSZ
    TER4t9ttsVhUsJcpnr3b7UoI/1wsFiuVSsw12HI+nz+fulAoVCoV7oF49sFgQAjO
    TaVSwRmPxyOQjUYjz54Oh0PIPh6PqcGhVqsluI/Vak2n08PhkNc+nU4pePGDlb5e
    r288tL3f78tt/ZtGo8lkMlwh2q5Wq4WnJZZSqYRreLnkWxYIuyyrJZFIZLPZxWLB
    48WwwzQ01ZFIBNMrZJ/NZtTUuVxOhBpt12q1FNSwLEWrEXZYbqTVtVrtSTXCbjab
    yanhuV4S9b/21Wp1OBzIwaPRaKfTkRDO2vP5PCHvrWQyKa36aj+dToTIHo+n3W6T
    UF/tl09425TEa7FYnE4nPCuRI3PskF6vF4F1u93lchmwu92Oghdhh3Q63UNwae8b
    j8a5RxoMBkx4vV6XhXzrf7+rOPBqtUofexfiWSwcDvPAKRtR8b43xePxO7XL5aJG
    Ewzvf4L9fg8v8fP5nDTnoT6WfwB19HCVxhQAAA==
}
alligator: load to-binary decompress 64#{
    eJxd0s1KW0EUB3C1K+lDuHVjXWSnkAvuXfgCBRG7bBeFlormPkKXQhuaN9BNxYUm
    UxF00UUWhUoIcRQXoYTkUm7T8Tof/845Z0o/5nL43XPmn+HmJmvrzx7N8KrHWoy1
    HGs71my8aC2mfVpzf25nAMQKaAe69zqZLbEu/8hWaf+31u+JlnM3NXtK6kt7xBrL
    OYVyTzSpNx0yQ3FA5iiekA3oV9Hg8had4x6yjHLefUKD9Ajk/RR4SvkN4Dnl5xEc
    9Y9lH1fgPAxCEF0yXCcn/+77/3VJL1Zt0Y7/nv9AxZa5rchuZi3ZUiXPc2VO2IEp
    k4aeF4PhS3Y0pPxP3JU0vwFYYwHyHGIrQp4Npa8ugSJa7ou3x0A3erEKT71/I3O/
    K3nnVxYMv78z7uHjL0YGmPei/sxq3EmPMply4O+D13Biwe8JL87RYJvicsewh40R
    +yHf7LDYYpuoXZMDv8p9L6g62beK8/1Ri/1itPTQnOsXb8XDrqgL+bwapr7kc3vw
    E+7Vw1eZhzXym/I75Ah2l1VY4bkWxxnekVOEC/J7Bj7nPkf6/4txRX8BGfoeSmYD
    AAA=
}
gulp-sound: load to-binary decompress 64#{
    eJxtlHtMU1ccx6vGR3kWLMWpm5pskc2pi3MkqFNYJsh0oigV5KEULK2lpdDb9vbe
    9r56W1pogSKltBQqSGGAlYeimAlDkU1H1KgssrhBmFNhjAECHcZl2bVo1LjfyTm/
    nO/vdf45n9joyMjphTTaoQj2Tq4QWB1Io9HmUSt8Fc2z59GWUEp6KpB6k8pC3zJs
    7nzTvXbB/sdhmMdjcx5744q9SkHf1l4f/LLhq+K33jZnOI6/3gZBVAjq8Qj6Mkzl
    ek5KwBEpL11saGyrANhx4kKHzUBgmFKhUKKIAgBAlNRqNaSayiO1pEqeLeTzxZjJ
    nA+KAK2ByBaidle1XoGVnrLjx3kSlULEOSaBIBmkMZkL9Lo8owYUZYqyRLwMMVJo
    NqgkoL7MQoqOiXUVzpPl1a7OiyXS6K2RBxJiE1KgitrTteYyq0ktStgbE3MwTUha
    7BXFmDxHjpGITE5Unu++dLaupc2UFTLfd5PEZi8xObp7W/Ts8N3Jx3npXMjqarQA
    8XFHpJBUeAwscNRZ8wwF1a7auvIzVWRCwCL/XY6B4c5CWGYol+8LWfk52X63t1Ka
    mnw0Izk+XkCcbDpVKueKUdvpRofVfulms3TlIh8mODQ7fjpx2359ERTFZB3u+Gt2
    MH+zr8+mgwJMZ2/vajYeDt2aYr72U7eNUDf+es+yYQFzQcQvs+MTmQH0zaTTsued
    j/HHT2b6vloc6L0+71Z/hw2RykVHvt6bVtTZd+ecraCq3sT28fNnFD4bdQ/sWhwQ
    xHG14HH7iHtPpi6ELgn0CnFNTQybdwSviYLs39TYyqwN565cq+evoQctDvv+6cjT
    ns/ogQGH6r4tVwh0PWMjzq1egb5rqqfHJhq3+Pt9kOW61l2jh3TOG0Pt/BV01sL9
    A+7H/5xi+TGWpl/pq0cScxruP2qI8g70W2Zxj7ovb/cJYCS0D49dMXC5up4H13OC
    vFkLEodnhv8t8WUE+AsHn9w27totPj94XbXCZ6l31uj45MOkJUz65tbJiZGahJ1p
    rv6fcZYXa/7eodlHtLplXqyFkXenJ7qSwz4VNlxuTlrK8NvYNDPqrl9LDcq4Pzk5
    aIyNFpdftITSmfQP256NuH9jL2L6MhVDk2Mdiojt8WBZniB8bWha7f2JOyf2BLO2
    4b1jk301pAxAeFtWegfvbJgan36ofc/Hj7G/cuCPvlaYx04SZPCT4lKExvqLbRXg
    wS+jBZbv+vuuNhWDieuCAoN3EP0zU5PnOcu9vEO41Tdu9zp1AOc4N4PDPpSYlV/T
    dtaBcWJi+YbWH+/e7HAoD6zyZyyP0t9z//1nq2Cjj/8nKaXtP1ytOyHn8TkZ3DRu
    ugjWWU9WFqMivhCznunq6W42Z4Yt8wrYwGt++NR9x5K0/t33I0TFrZ1tTj3ATU7l
    iUFQli2SwFrTiaJ8jdZgrmpou+CqkOz+yD94XZK568Hvt5rwo3u+iEmR6IttZUYS
    yBRkyhFCDcnEmdkArMkvsTlqapx1VeWGnCNRYWHhidKi+pbW+tJ8pVwuQ3G1BoVz
    eOkcbg6caywoyMWVIKTEqf9ozNPrcrUYLMuRABBO6nUkppACAIzpqLQ8LYmpIBAE
    FRAEwbASwQmCwFSwgpIgJarW6inTUfUakiTVOAJTMkJoNCSuAmVSQCqHUdwTec4X
    tRrHVEpYqaKMIg9OqNVqAvcgh9RoczUkgSKeGMUlRKWkBqpQah6Gzkko/oJjOE7g
    L2FFAc2DM8zTjHoZDKsw4vlEDwyRF5xUKZVUj7lagsCxOfYR/wHwfuhW/AUAAA==
}
alert "Arrow keys move left/right, up goes faster, down goes slower"
random/seed now/time   
speed: 9   score: 0
view center-face layout [
    size 600x440   backdrop white   across
    at 270x0 text "Score:"  t: text bold 100 (form score)
    at 280x20  y: image bird
    at 280x340 z: image alligator
    key keycode [left]   [z/offset: z/offset - 10x0  show z]
    key keycode [right]  [z/offset: z/offset + 10x0  show z]
    key keycode [up]     [speed: speed + 1]
    key keycode [down]   [if speed > 1 [speed: speed - 1]]
    box 0x0 rate 0 feel [engage: func [f a e] [if a = 'time [
        y/offset: y/offset + (as-pair 0 speed)  show y
        if y/offset/2 > 440 [
            y/offset: as-pair (random 550) 20   show y
            score: score - 1
        ]
        if within? z/offset (y/offset - 50x0) 100x20 [
            y/offset: as-pair (random 550) 20   show y
            score: score + 1
            insert port: open sound:// gulp-sound wait port close port
        ]
        t/text: (form score)  show t
    ]]]
]

Вот пара коротких версий игры в ловушку:

REBOL [title: "Catch Game"]
s: 1  p: 3  d: 10  n: now/time  random/seed now
r: func [x] [y/offset: random 550x-20  p: p + .1  s: s + x]
view/new center-face g: layout [
    size 600x440  backdrop white  t: text 200
    y: btn red #" " [d: negate d]  at 350x415 z: btn blue 
]
forever [
    y/offset/2: y/offset/2 + round p   z/offset/1: z/offset/1 - d
    z/offset/1: switch/default z/offset/1 [0 [0 + d] 550 [550 + d]][
        z/offset/1
    ]
    if overlap? z y [r +1]  if y/offset/2 > 420 [r -1]
    t/text: rejoin ["Pieces: " s  "  |  Time: " now/time - n]
    wait .01  if s = 0 [alert t/text quit] show g  
]


REBOL[title: "Catch"]
s: 1   p: 3   d: 10   n: now/time   random/seed now 
r: func [x] [y/offset: random 550x-20   p: p + .1   s: s + x]
view/new center-face g: layout[
   size 600x440 t: text 200 y: btn red #" "[d: negate d] at 350x415 z: btn
]
forever [
   y/offset/2: y/offset/2 + round p   z/offset/1: z/offset/1 - d 
   z/offset/1: switch/default z/offset/1[0[0 + d]550[550 + d]][z/offset/1]
   if overlap? z y [r +1]   if y/offset/2 > 420 [r -1]
   t/text: form now/time - n  wait .01  if s = 0[alert t/text quit] show g
]

Вот простая программа тестирования по математике, которую могут использовать и изменять математическое действие:

REBOL [title: "Math Test - Simple"]
random/seed now
ceil: counter: total: score: 0
calculate: does [
    if error? try [
        either (to-integer f3/text) = do rejoin [
            (to-integer f1/text) " " oprtr/text " " (to-integer f2/text)
        ] [
            alert "Correct!"
            score: score + 1
        ] [
            alert "Wrong!!!!"
        ]
        total: total + 1
        counter: counter + 1
        if (counter > 10) [
            ceil: ceil + 10
            counter: 0
        ]
        f3/text: copy ""
        f1/text: copy form (random 9 + ceil)
        f2/text: copy form (random 9 + ceil)
        show gui
        focus f3
    ] [alert "** ERROR: Please type a number" focus f3]
]
show-score: does [
    alert rejoin [
        {You answered } score { CORRECT and } (total - score) { WRONG.}
    ]
]
view center-face gui: layout [
   f1: field copy form (random 9 + ceil)
   f2: field copy form (random 9 + ceil)
   f3: field 
   key #"^M" [calculate]
   across
   oprtr: rotary 40 "+" "-" "*" "/"
   btn "Score" [show-score]
   do [focus f3]
]

Вот версия программы математического тестирования, в которой используется таблица спрайтов:

REBOL [title: "Math Test"] code: [
random/seed now
ceil: counter: total: score: 0
calculate: does [
    if error? try [
        either (to-integer f3/text) = do rejoin [
            (to-integer f1/text) " " oprtr/text " " (to-integer f2/text)
        ] [
            alert "Correct!"
            score: score + 1
            update-pic true
        ] [
            alert "Wrong!!!!"
            update-pic false
        ]
        total: total + 1
        counter: counter + 1
        if (counter > 10) [ceil: ceil + 10  counter: 0]
        f3/text: copy ""
        f1/text: copy form (random 9 + ceil)
        f2/text: copy form (random 9 + ceil)
        show gui
        focus f3
    ] [alert "** ERROR: Please type a number" focus f3]
]
show-score: does [
    alert rejoin [
        {You answered } score { CORRECT and } (total - score) { WRONG.}
    ]
]
update-pic: func [correct] [
    z: either correct [
        first random [0x0 64x0]
    ] [
        first random [0x64 64x64]
    ]
    img/image: to-image layout/tight [
        image 64x64 pic effect [crop z]
    ]
    show img
]
view center-face gui: layout [
   backdrop white
   f1: field copy form (random 9 + ceil)
   f2: field copy form (random 9 + ceil)
   f3: field 
   key #"^M" [calculate]
   across
   oprtr: rotary 40 "+" "-" "*" "/"
   btn "Score" [show-score]
   text ""
   img: image 64x64
   do [focus f3  update-pic true]
]]
pic: load to-binary decompress 64#{
eJyVlnc02w/09z9GhdRetUcTIzGqKRWxaYMgiMSmatQqNUrNIqjWFqvUFsQetVWV
1mxLEZtWja/aOpSq1q+/55znnOff5/3v69z3eZ977rn3ns+drwDsGLQBGqD5J1ka
WQA4XwR0AQb6CwwX6BkYLjCAQAyMYHYwmIkJzM3Kxsx+iZuP7xI3L6+AsISogCBE
iJdXTFYMIikFg8P4ReUU5KQVJKRh0v9rQgMCgcCMYC4wmEtagFdA+v9b568ADkYg
Boiho7kM0HLQ0HHQnL8BhAGA5sL/pgX+r0DAv7SMdDS0TP+wOjtAQ0dHR0tPy0h/
gZ7uH6ahpfvHOQBOcdBVLm2z25f9uHkQOrjo9BJGiFMT76Vr/kSoIulQt+/rv3IB
GoCW7v/xBgC6CzS09ADDP4bgAGj/edPSMTDQ0v0fRkML0NFzcIpfvaDNZXab+7Jf
dEkTAw8EkX5YOtH3+ev5AsBM98+Qg44D0ASmUb9s2lIjjUOxqszbnHsdWzMDDASH
llOrOGEVy4/yXK8werlSUZqNVGwG2FRtM3qzwEymx7rZvdnowU6Sfk4+w20JHKHI
ZINI8xPq7hVgYW7p+uSPZa26j21zym3b9IS/6RKiCQb68L61zyusGiP0gq7RD38M
tNtWLEwWTy0FMOrega/lsN3OqoL4v3QpmtUO7UMvjlxUbajL/q4X8ONZbnb4sn92
S9lmAXbR3uy5vGXae/luvhHrhWyoaxzlFGhUFr1dcOPoCmWn1ZP7VE4k88hplQKt
VsNzZkJdkkc1IlLpPqwns5VObfdo1/G1110eCpO6G6ub21Ymys+/FHdFNWzPbIgP
ORYr37NsPDRoazTh7gDuUAuoj9q0Pt65WFTmz48gj3TK3qxV7onkfiyWT1NkIsbK
qU+3IkW/caNGt84cQvVILTWCycXbBAo/1ljuOwK1KYYSsqu3ddPh2kIjwsFlSX6f
9IUyBvOmJdGV/aEum8+cZBjlUTvxTDogaNFdFt4b++UuP4d6/36tjhunhunMRxhZ
yPg9y4yJ4a99sW1SokMDO3v1AXy242UpbHDijak2r8semxSLbZd/0JxvRpM93ggf
og+34hmNtMxt5+Rv9gYrCTOtDEfAs6Cp9uvkzc6+lfBWnP6qfvzRUf2nwZ9bl2Rq
ncV/mPM0ptptIs9a/sbPCAo0frPy7KBag1JZ/XlEQVP+r7CR6vSoOOpCvo2zZCLV
zXOqECZ/lfMpNWheG4u1jOOszUy64Dfmi1YUq7cLzNvX6KgazdMeRgjJwH65NVEh
vEtPkEUllEANPht9HCO9jJpXKTY8RSkO5T0N/pMo0XIzIgDG/zpN1HpOr/D1OTDY
4DdEQQrMW6jPG3bv/fSkWoajhZF+9Lfv1jr786r0P4o04kjTvr6AlkgVsDmU/stV
XpX15kQqKCadNjSROh+wTtx77jV8qoZ+PAodE6rrVxXOd/4v4z/0k0s9ZnhVjXaC
tkkimGu8WGQUZ1qafpCRzIpSRjzTa/E+ZQzxIWzcPB2hSpTLRm195F2NEofQQ12S
VmgK5LeVGeJkH+PtciEh09SNWb0yW+lKJbuanLwgynem4AHX6E5HthcNjz9DVzj9
VZAcNoQ8nnHrIly1+VI1ugLW/tG/hBUlZO7pK4biHmXXpifaWD4mpdVKp10Le1nX
RarndSC+Wy/bumVUid/qg/vnn05mdsULmOnbXoRdRG76WfizMr+QcfAUMc8R1kl5
bJiWe+W5YdRTp7Aqi/VOcdcbtZsFZUDhLyGeEWod6dOne8G/Av6c9Ad7F5DbzfIr
B+8UwR/ECMu/9WROIEZh1oZ8z5zf7MdSoojafJ/StPk/lN4V+IP60CURyCqHyH/M
wna4k7bommObibDlSESa6s2P+QZ9P2m83bl9X1J6x3M1g6b6fmwmDLGEMV3RsNL0
+jYWz7vXOxW/cdAiaR5HeH4OYNtv4eMUAn6mTSX2QaTAYlG+IYNKKa9rgu9b4XUi
HgyN2VNIQYgNT3yFHfNs1bEgi8Zb18c73u6CD+QhhiEZO3HSZ+3Ob/FjOe/CKtVx
OU5vQ4akW3VccUv3N50eD/MfL5BFCVAhV70b4AgVRRt4CS6T93iwl9dmlPq9Glrl
kz76BJqRmjYkMN5j1lmN+O+5pLlvnP9s9fXZyeIqB64SDxn9/hQdUoWvXtVPVINe
54Mup97G73/PgVxP6ivrJu2FwiHRP16qkZzK3Ns/trIjOPr5K3QI2FLOEN9Kz5jI
r5gYcnjq/R3tNyGcJc8lYrKXtQJBVI60yAjglOD9pss+J8IgVJegQvDAtrRIc3dy
n4nQPREjJl1gu6z2eLaiq8vQM/TTBuLyNjI3DKTmIhKJDTgHDh6GUOM60bm+s7dx
MHeZHNirBa5hJhFPyAfAQO2QLdy9F/5uawN7DnRspeIJPkNRO+7eMk8n2b+NHXA5
2Zn0c2rxjewjgaiLPRptu4vGcefAo08JZ/HmrSNepJx8q4OklNdIpD6wEsri2L3D
dBPBvQ3x7v2PxyMDclo2xeE/9yOPwD+zWHDjrjJzl28OJzsnrlSVkzMjX0QLxD/8
drobvVIybK6EUTxZl3HH5UrAf8mGw38RA9uh1FIFYuWGwmLEMewVg4bbOCFhHFeH
wuCrsk4wlnnOtS8bJvj/nEzKaLT6v9yekmN2T/Lxu9ZuWAt+r9Mq0IDIgXF3hb58
2sDRk5Bme0KaclwhlQLDK8zf5wTrKt6blLbH0WyTGzhTb3nIUbUhzleCxsLCBaPH
88KNQjxS8aFN8q03jULVRe6iwvaJAxhtWy/bx2IbpTF9QgEdh75a08+qwvZ9Ym5Q
xk/+axdR3LgCcDYDuKZUxbobfpsVDxcfYOtQWbP6AUnhbYiXnI3iJlx6fl8UW023
XAnbx9i90xgy8ns6e7xF2Wi+3qt/g03CZvcwVGXSbKpv/zaMLJ/FCK4nBIwe95sa
QL4JjDfffOtt/uVST2Fq+c0MdKJAgAb/fchrb9iIiW9RfMeuTjM6ZFqJMYkqZhAP
U117uZDrp3ETzgf5MRZVwEdRxaQSnsM9F3Elozbs3H495VfEdTWpBmsj7keT9Y0H
mNSEmjTWtQ+vbSl3phh4xjqWj5Af6QN1KjbFvR5NJoyn9+GyUuaH+44/zNyfJzBJ
3vhlMhqhlyPiW4cZ71TZ/IT2qahIvXdkPD8rzEdwP7pjazPJhFnEcWfWljiS47yx
fh1sj91mHayPbxlU1eOWcrxe5jK66thOCiI4Vr/GicMDdflBb1MUH+ZnCiTMlEbR
QvHMSdrETt7lwEQfOY/tu+SJQlzaO2LRxjGd5iRbOhLkMehHsR8Zkkr5pZamHhGp
7pGr+UZOoMiiRsWkwphyM21RFO2xXCsBx+CzYn4+NhC8GlukscC2tGxQM1Oh0THh
8eONigSC/TJaK7C5oBzG5hwsmN5NZMW8aEx7rR+z9FXQZuKeYIlh+aQb1eK1uwjC
UsXDlSxCo2KhEB4F0oi3+Eu2mZCtz31dKUHt1rD7oS6xcpWUPduv55u5CYhG2l0U
1YKZaJXg4d/6MJtyMA/DfVstTdFGOC5+BJLkbHaMMzFFdeu0QS6osMrkZrU3mxeM
xWwdVt77naL038xq+ZDpoQfzZR+/jwjuOJlIjl/XsItGconYLJ4PuFWxloXOdfY5
kJbIs49J839Y79x/PGbgLzTrLEcWLkZWgpvLHfew3Cp9B0kBdZJhrJHGdwxa+HYd
jnNMquvhagJTq3VGgTB6CVcH8QU20G6V+u/rJYMBFcIWJGqp0iGj1OePt0u6Hf1d
6Lz8m6zvnMYblrCvioTeUGEKpOqfgJlseA73+aDOQL7QmsDzZQXpH6EZd+3U1VvL
T2ojJGTKDe10zLpBTDCA4uurf/AJVYdH+QXZ/mDyNpfoL2JqbFeTpH6zD3ZLY0fC
IW+D/PIDYmqEObN0t10NEgamI159DYciqtUUKwQWmCQcRL9zW6A3ZKCs4VlJTj8J
HgR7RT4e0eFsSlJvh5ORFbd8okO1adkERqLlU9Kuh7y+zjOT3kFc5OVAz68ogmdv
m6yAjmtxWmpZH8J+eRcXJ/MD1WUu1LpXbevncTBZ1cjPz2Q47A2hZhQafJd6WZ1z
ydI9PeHzQskDOcfDR2LCuzHba0slsOORvY26gTQfP1pEMPHtx+qhfztBu7wHUzVA
Nl/rWntn4m22ZKO/3O0uboBJsNFXARVYn774rRDqauFNLmmLgbm2QOnLxShQAOTI
gBRuaqhBxSOEFpKDPkdw+ghUE9fr6Bg96GJbUrf4TL6AkCOlR22UI4ub8OlyqcFn
cusUssj6mTT/D7jK5uFm24s4z82qQKiNNbxoqCp5a+JQbr4og0UYuG18/9GyYZxW
JqoR3OhbmV0yNJwsZ++9YI6HX7ieIexhruDczg833dBmYhr5tYay+amNvmyvFUR6
xhr2d+FiYP9olbJZcviC9CdrIjHXj8jE+ZROKHopSh/SyTcE41favCWD3g0gqgrr
VnfAl/hdUdgb79HePdOqrnKYpcU9lhMwzUn1md2xweC1uVQdZPH0qf7SpclL982t
dRPERQ1NiKXBK/7oEOGUlIKbX1rAyzcJ3Q5OX5RWS5vXVFhQDQSaUXw4tfCvYwHz
dubfCtNcPGaverqPOVxi+D3ozGQEyI/sBLbYGu1trsvdWvp2Dsy9l5fbYZvh0/Cv
SMkzc75uad5T9RgdYniTPmPXMPfP46nqm/DRVJwut35Gfe7woOHQ66TZI9nM6HYq
e3EEa2fVL/D9cmHZyC3+db5E0/Hh1t+eQVmegaj/vK8Wm+W2VZH/lFd+KVH7K2In
/HIbXbd7nBXk/+AIiEw5B6y9g0Y6IBv5f5z3iz+9x1krLqBnq2Vee7cML/EPAy6s
yg+5H+wV+Wse/plyq8PYTuf9t+aLR9uD1xvnVQ5Hzm6cYY5f/SbnRMh+eS8yGsLD
5BPhYUd32ZxyIiDV6S2C0Xz8fWQv3702c+/7WPvcnPlzIympjKvL9s9i1ul/zUXc
V9xxjf0zr5eIeNwL3qiQGYr9Ii4RLJb9LXgTIAbTO6Zx38fOKqP1D355BSkx8+dA
jb47fdqbIVF4GN7Ad8PbzvQYXndK0ZNFgsU2pDJ/QsZNfOauVmYa+EQb1F4Rfedp
xfKiZKO1n1FzDwmatRSp+FGdGI6UKh+Y8qIsmKlHjz7+LP6GqEwzJBbHDPBEoWO/
7/2tXtD27jTxXI9KijoLDE6ELDHL1iyV31IPCUuJpW6ftFXek16NO5IS8GLLd+qo
a3uhvYXXFJw+tjqcjAggKm5UG0/jmTzLTZ/cw8UHK0ZxrsY/HQ0WCllP9dbrDFn6
cit2I9HScHM/U/03G5tmdtdVrmpL1Au4a8eGeNahQyZx7EP4yAOw5lcDjYVq61sa
C1pHYw/X+/PaDDr9I4A3+d1XwIFlaB9ZF9pVAcZUQT84TusVXK3gZSXpYfi2yUnY
1tkWQfneYh2Yy4VIYZF1JEs7dqeKt3/WP4MWKQTKTBXGTm1xTVl0b4A3HKVlr9db
65wDHMtvE45N6U79aSaaGjRCphJZ87LhJrJhA8c5j1ZWmGkWQjmio8rKG3rkrj2t
fKWK1HRrRJ1KCRiuBA2j3DTuhobrQd8FMr/xkPAKdGPC6EFJwUOii7P2KJVlEkJT
zfN6gwPZzBazw6OvwF6S5qqv65jqPVTAotHdhXpcSbg34j/fgtab8SnjkmRDTGLV
FB/0dye5hJAJq3rNTu9DeJOLUGdV4ra3qjFWIcSST/fj/ZM/hmD8DgoYp+k8kArV
hZVz277iygSIeu8+DrRTIwyvM64Z+rp1bXXc7f1wgwT1b3F6sefOMMcyQZececkJ
QVwrFTpBWCwA0JrXTCODbPeuGCtWmp4+eDBu0FpImW+3MkmY8oAnHUDEYnm8cX7j
r4r9xvxKnqX1dj9BXkMr/6d1sNXQQ+cRoduxC36r6FNZjtgar3md6JCSBF3b7wIO
UkyAJK44tkT03IGyeCaqf3INC5W7qNMuYaBGZHs6mNRKl2zYkYZWKVB6P+2LUDv6
sJOxTr9h/+IoxEqowsGHiirA5qK8km2nhHONKhFx3C2Vz3CcOUIanr4VWvpqjlJ0
UJIlBe84uVZtjHEJTnK7MM9a3uL2Z4jO+voiZaLSunlv9M3wpx84e63AK1IgeZhB
YOuffYO9/FPHrY0aZ2rNA8MAr+jA2Kum7sqoYt6ttA8nhTqkz2IlMHoXZ1ZuMheR
K1fGhzDEFeil0zJTnR9znMcUYdKUtj+wrEisXWPKzJI6Agnkly3hUXNmpdNb08zM
RWUGuh0iv02Op/2CpttieafuTbyOaPvDVOt8DogelHzCdYXdU6F5omjjzD1a+h/a
lXOV9oOj3Dq0wUnfQExUe4wtm2wHQxKv7yX1b5wdprpaQi8VJjn3h6x3TVV7e2e2
4RlhkPy1cyCYKoqpN8jCz0gfPTA8gusOGwRHXXzXkbxXZbUWfEKq9cvcHNf+7RT4
7x+ULwi04IGnLWW0tqO3sMGGEK8U0e4OWu3RJ3xSPjifHrZg6r3ESfL4AIfcmwQh
4cyWeO6S79gVxmOqnDdBsYG9wd3jw1fRuwUlNVJThnb4L9rits93r5XXaRmpQFq+
PJ9z1uhti5nl3ScHsizer8K0yh2VjKaBnpDGYW7qoxxLa+/Y9ebFivynuW62ewYS
osh9CbskdR1OQuqi7sNE80+VP7qESOT6GnNWvIPfR9zaWMEIrmnkJMhN5PQWVT6u
R7b/8vrI28KPJe9eHyth9KYZAUmu73TsjVejyFI+X9OPR3uNCZ7HBPjLNbyq5CfP
dY4oL8+Mz7hV5yYH9ADHOiAFrC33jEi+aauWq9632b+YFSxxpcW1J2OY0WsXoc5J
G3yml7FhUqWEbFFJtf0p4Z19ubVHpoBv0IPpaA6B0VNR8yvUQ1qkWlAucBcVxyLT
/bqs7Y3BajCFWmuJWPpd//wvNql5iSMwh7XClZCf36jJsDxj+xbr9iSVzO23Kg/z
b2WtVTGNg59Ak0cX0L6leFXhPiiTypUxPX0ytWz62enEyS0reMBTHXCCzs+oh6NB
nxvV4h9iA3lYN5+f1qCdinCXlcwk5C+jlg7JiH0G7v7xhW+DbJA9NrkrPxdaeZ4s
a0PEfDS32SWDc9ykGWrc6+OCXizb+l80TBM8zG5U0fma0eoMrPsVagm3Yr4kh3pX
BVJR93xfVsYAPZi0klSOFz5zGh4nUd/6B4MFGj+Iz1D8n8XBKzbukF6UVBZpcTqz
I2izkX4geh+khf4+q9Y50FBD4GzwdODgpH/K+dpL7cwD9Abz0Mow5q/eRtWW8ruc
h0zemE8hni3YpFUP+/lvPz/k1BBClE53VRlSTtci5MokmyRaw410hwVBR7JnyB8Q
sTghnlycvau8orZOFM6EAoH8/sI2peLUZN4tX14TloUiLd7G/G5aELQVLRl6qbTj
hT+claSN0+T4evne8KGcZxniCzyGZSGAEXODchc5ibvaKmGrZle4YXvsahcwWK+u
vk0vM/jUIvbpNAOJ5zIMKcau0PJzKSY9ZjG9CKdo2ygV6Mpi13exUsnGJCUHbWq2
sqpiETxHaBf7OjJe/6x9CluMRz1BldlS5DMhOXZKpV/M3bXoyeLiukyQ/RV6cuHc
YPMU2VPcyfli3pObE9xBH0UIUurNIYOgiGt2ZI0x9Kygjf/VYVO1h46b2in5A9VL
07z1qD93bsyIda0pPGN59KEp+aXkB4CucG5kTSrJUyTeEw+fFuyKvmdRMbWujXGo
nKQncsgJHQAkYJBT5UxXqnOGUFbXwrBkzPpfwVV8/SD26HqnD766qrLlyGwh2sz1
tTHpkY1FyOES7hspWA3Aoq/lJ6tsEfw+FtzicG8Ox9sq8846l2zQUhr+va2lSClV
U/19MHcYR8VXd39v+cS3pfZGb+KtivA+eOR6W6VpLKy5nZecEd0qw69xgzm57zJ4
23W0V3+O0mW5OkBqn7P+fbscRYa6Df87EKnQSptHcktXCj8wV2zH4NVSNGg9f2Vt
28EupsKrWOLTk993bONKBkPoN9tRNvSRWc1dBSLCn/9eNrKEp/PZ1j6NDrMzNPAO
3+c/SauTu3qxBaeTnKgzbk17DozW1PQPW07vF6rThvx5TnnLoKWu2c49xsg9t8KG
Y09PnbVM2FDpGuCMuK4tntu0lNKszkvK+POD40ug2fjhcyo4hQNfou8PeV+44mql
eaUt/xzQPprNPdNPmtkxQ4JCSudo5xDXyAqFzaevJYsCXiwllAcBIblvIiqfLO24
m4l4gn+Mvfk6PK1MuW6/Ok7Yzqq8bRAEe+qipCQzelHCSlc75Let79y9TiTgmMzZ
EOqS65865T5mCfs8ISmTDg2+5qgyGCSUPAKlVagl+hFp/WaIR9gI3LWMqqOBa6K7
I/Vvtc3DnX/dbe/z/fmxlh38EjKsrlcn3Ur1o7C+iXj+VNdzYd0S/1m/4r6NnFBP
FuJpU1HAyG2om6YS1G/xLR0+UfhXewMu3eMjfkbylccVvcMpYWn/wmxSXaHgYxoD
fXHpPjNQpBhlE/02QSi0h9OFP4mf+eXEW31VvP46d8bcOi3TKxD9Gt7Q6ahqorgT
zOIzHTJDyg63Gb0k4K8TzBjSRCxNTx8Pf7Cp/UX5iweqV2X8+5VA7RhmPUVM5jPD
apO5VOr6tMdA1K5yuUzYxDmQbfm5ysHZHTmeDbxCUh98FTk1TmVp5egMSNMbd/7X
+wiDObOtqTtjdRfUCVVdv+QtXJQ+P7kI8YyB/a4jA/lr/FDmNfw+BVkzeg60yyEx
uMl287XM7y2ySUV5tmDL4R24nGphrUDCCmvky+foDOxczjWjhb6JO5N05PgbDMSe
B8HOqLE/nNXX32dCXmwLmssEDsBuAAz+pQFzNCmBlaSoTDKzBIwPCBpsqnsI3wv7
Vuel722QsufaLgC4wK8lXhzqfd0k9RZ66kX9aK7ERUtXgZLAiXbkdH+U6Fj7Ws2j
8O3OO72JPO9Ynm4Z9fTPjlnOkLcQ5LTuQDFvBm2rTcNgZflNWLx6FaMLtwwfo5m0
oP4g40rK1aov6V45IHP4vQnbGIlT3TT+O2rlmesw6stKzmJGk1MoV8whJwOWplcZ
3+lsy9qEmPM2m72lasjF+cCHEnd3gVwpRiLHCdEymgEu0aZto7RWB4qW23LYEq0y
4Jg/a85SuIm2cNFMhrtirURNAvFs7/SYyhJSfWH0t89AvDnFEKhjNXdU21L1I3Fn
/nLx5+mDoPkPuSu3Hl3IfKOXRL0kvNQPxmVWhWeKvCGzpU91Hiwldn3maxm14X2K
uA6br4epdYxDJYoRYD3+dZE1oW8/jiymKcMPG3cVuMP/C00EL9n1sTYu37n2ndCk
aaW4JmoAs5gsYifhA9KJDWafkpEv6pEI2dFW1LULObmiN/zipJi8wABNioKWoN/G
1qJ9aJNJh/nUqbDOCmXIKICeF9LEHBdPXcOYmifgep8WW5Sat/9BpCnoK7IWo07r
Q67a3K03GiyTmJkaMS9mBfYKkVT1RlaFgE97LIaUHPJ7U5ro5EcqghF3KH9FJnu/
hX6EOyyVfwO1p/ynedfDc95l4kKvu1mVlFMOq8zdjpJ5QybB1P9qC+SuDtVqR+9I
ZWbs/HYHzWC4f8vv3qPw/r45dy9q2gmOHF4BRbok/ZKG5hNrRY8GdVmGXb01Yjsm
J3Etsq5wEJHxu4CwjQEBg5zbk00y3XZwDM0NMpf6YEyocleg8/TDBhzGJlGkWUeF
1h/s7VXcWv3SuBdU09oqSW/5ypYlDd+TsMEkrj5TzPSKTygmjVSneyRYklEaoeGG
3Qi+a2xglKdbw6QzGHa93O8bpinFt1ihwBR+CVyy2LRXWG5LuM7FD/OunCTOW/S1
wrG7zNGD2mLfB9k0MGuH3yaXtWXS8/1BZN+cvkGGIVHr+8kzgk62H8FLBTxSzxsU
wJHq5XqiOVvrG53vnuUpSa448RggFs1/LBBzteVjv5Fl6ASj+kZ9SVdfodikf2y0
HsA7hYXfYg5fQg5OWQOKIgyHq2cPMBsSFsa01qmGOrmJjxL6laHQLJldKSaAuQgy
slfzEBvXpG1Fy2qwt5Fy9h1p72HqmYYN04n7GnyjeIo7MztHyV2DC4h+EZ3iN8eK
iJRT+yt0F1arKvmzE9dbJxXzw2JZlFPQUqJfLeERbQTX390LU9hHVV0kmT86pLwC
S1+/q8r3vOmD5GUO0hfdTyKXy/r94wKGj6bGnk1ZDITPfbuj2t3rjhSkUZ8L3kzf
TKY/B57IF2reqtDroNw9ycpz0riell/1U3vp6jaZEw96ChfKZxFsXZ3oiNna2Km4
4eZ8lQGuzcU2dcVqH2uM77Yst4jx8B5pmY4DZQzSfvDnRgo5Nwp/LbD5vNTKb/xo
qVP+wYKBzELxZYjKmD87a8CY9sFWqR4eJbmMK90LfZeZQPDot/gu9fkOJlXCjhZX
Gnaorf5q/YuElf96a3ZF/eJDO/sXERItkXJ687cc/2KGUAMOBTdFy0Wt7Kynw5dX
ROOP9yTdpj1Za2MHzD86yOa1ELf2Nkl69+yDl/tw0fyIdH+7+4HMPIqu1V4G50DC
8yGBWsHBgpa0XLpxQb3jMc2IEM7npnWaP8I8k+UDNhu9m3vKqOX8a59Qo4m3kIOQ
0HMgC57tpH79HNAyEOjxKYAbKtkS15/5Gye/C7I7Mc3I/5PahQAVD/OPC3ScA2wz
RjUVO0FH9YoPia8HzDib/na/OizWBKtHsovEemxiT0Ea5epK2GJZPRf23po5048r
B62eJkyikySTLmxLvvTd1wlU97rr74LS2BeNyeLS+isACKBZVxzNulxpLpS7se9D
OAewpM9uLmRpl6QtD17TsixakyaIIXpMUih0LN7Q0NAeFY7yzUgVw3bKtX38KhDI
aOumgiExqrGeA/XZ2V0mHtzL7brYIXN0BQj88kPCnOYizhugKwZoeWKjVJlZ5OT8
D95tFCxAj3/Ozm++zM4uxqRcxmZnZ3cYYn38G9XaMk6dHSTEaskp17c8PPvvJxHa
dUN18oOSjW1zxzZuaqv8WjN5Ibl2rVnV3YNJFGyaBqwHCdjYNIQQVoO89joUFEjA
2pzXAcrGJkNV9WaBnJzc/Rfn8/8D1e5Xw6EgAAA=
}
do code

Вот ещё один простой игровой пример, который иллюстрирует несколько полезных техник графического интерфейса:

REBOL [Title: "Collect the Boxes"]
random/seed now
start-time: now/time
level: to-integer request-text/title/default "Number of Boxes?" "10"
gui: [
    size 600x440  backdrop white
    at -99x0 key keycode [up]    [p/offset: p/offset + 0x-10 show p]
    at -99x0 key keycode [down]  [p/offset: p/offset + 0x10  show p]
    at -99x0 key keycode [left]  [p/offset: p/offset + -10x0 show p]
    at -99x0 key keycode [right] [p/offset: p/offset + 10x0  show p]
    at -99x0 box rate 0 feel [engage: func [f a e][if a = 'time [
        foreach f system/view/screen-face/pane/1/pane [
            if  f <> p [
                if within? (f/offset + 10x10) p/offset 30x30 [
                    remove find system/view/screen-face/pane/1/pane f
                    show system/view/screen-face/pane/1/pane
                ]
            ]
            if (length? system/view/screen-face/pane/1/pane) < 8 [
                alert rejoin ["Your time: " now/time - start-time]
                quit
            ]
        ]
    ]]]
]
for counter 1 level 1 [
    append gui [at random 590x420 box 10x10 random 255.255.255]
]
append gui [p: btn red 20x20]
view center-face layout gui

Вот версия программы для веб-камеры Windows, использованная ранее в этом руководстве. Эта версия была написана для REBOL/face и включает все стандартные константы avicap32.dll. Он также содержит файлы "do", необходимые в REBOL/face (их следует удалить при использовании REBOL/view). Он также содержит функцию, которая может использоваться для скрытия и отображения окон:

REBOL []

do %gfx-colors.r
do %gfx-funcs.r
do %view-funcs.r
do %view-vid.r
do %view-edit.r
do %view-feel.r
do %view-images.r
do %view-styles.r
do %view-request.r
do %view.r

;WM_CAP_START: 0x400
;WM_START: to-integer #{00000400}
WM_CAP_START: 1024
WM_CAP_UNICODE_START: WM_CAP_START + 100 
WM_CAP_PAL_SAVEA: WM_CAP_START + 81 
WM_CAP_PAL_SAVEW: WM_CAP_UNICODE_START + 81 
WM_CAP_UNICODE_END: WM_CAP_PAL_SAVEW 
WM_CAP_ABORT: WM_CAP_START + 69 
WM_CAP_DLG_VIDEOCOMPRESSION: WM_CAP_START + 46 
WM_CAP_DLG_VIDEODISPLAY: WM_CAP_START + 43 
WM_CAP_DLG_VIDEOFORMAT: WM_CAP_START + 41 
WM_CAP_DLG_VIDEOSOURCE: WM_CAP_START + 42 
WM_CAP_DRIVER_CONNECT: WM_CAP_START + 10 
WM_CAP_DRIVER_DISCONNECT: WM_CAP_START + 11 
WM_CAP_DRIVER_GET_CAPS: WM_CAP_START + 14 
WM_CAP_DRIVER_GET_NAMEA: WM_CAP_START + 12 
WM_CAP_DRIVER_GET_NAMEW: WM_CAP_UNICODE_START + 12 
WM_CAP_DRIVER_GET_VERSIONA: WM_CAP_START + 13 
WM_CAP_DRIVER_GET_VERSIONW: WM_CAP_UNICODE_START + 13 
WM_CAP_EDIT_COPY: WM_CAP_START + 30 
WM_CAP_END: WM_CAP_UNICODE_END 
WM_CAP_FILE_ALLOCATE: WM_CAP_START + 22 
WM_CAP_FILE_GET_CAPTURE_FILEA: WM_CAP_START + 21 
WM_CAP_FILE_GET_CAPTURE_FILEW: WM_CAP_UNICODE_START + 21 
WM_CAP_FILE_SAVEASA: WM_CAP_START + 23 
WM_CAP_FILE_SAVEASW: WM_CAP_UNICODE_START + 23 
WM_CAP_FILE_SAVEDIBA: WM_CAP_START + 25 
WM_CAP_FILE_SAVEDIBW: WM_CAP_UNICODE_START + 25 
WM_CAP_FILE_SET_CAPTURE_FILEA: WM_CAP_START + 20 
WM_CAP_FILE_SET_CAPTURE_FILEW: WM_CAP_UNICODE_START + 20 
WM_CAP_FILE_SET_INFOCHUNK: WM_CAP_START + 24 
WM_CAP_GET_AUDIOFORMAT: WM_CAP_START + 36 
WM_CAP_GET_CAPSTREAMPTR: WM_CAP_START + 1 
WM_CAP_GET_MCI_DEVICEA: WM_CAP_START + 67 
WM_CAP_GET_MCI_DEVICEW: WM_CAP_UNICODE_START + 67 
WM_CAP_GET_SEQUENCE_SETUP: WM_CAP_START + 65 
WM_CAP_GET_STATUS: WM_CAP_START + 54 
WM_CAP_GET_USER_DATA: WM_CAP_START + 8 
WM_CAP_GET_VIDEOFORMAT: WM_CAP_START + 44 
WM_CAP_GRAB_FRAME: WM_CAP_START + 60 
WM_CAP_GRAB_FRAME_NOSTOP: WM_CAP_START + 61 
WM_CAP_PAL_AUTOCREATE: WM_CAP_START + 83 
WM_CAP_PAL_MANUALCREATE: WM_CAP_START + 84 
WM_CAP_PAL_OPENA: WM_CAP_START + 80 
WM_CAP_PAL_OPENW: WM_CAP_UNICODE_START + 80 
WM_CAP_PAL_PASTE: WM_CAP_START + 82 
WM_CAP_SEQUENCE: WM_CAP_START + 62 
WM_CAP_SEQUENCE_NOFILE: WM_CAP_START + 63 
WM_CAP_SET_AUDIOFORMAT: WM_CAP_START + 35 
WM_CAP_SET_CALLBACK_CAPCONTROL: WM_CAP_START + 85 
WM_CAP_SET_CALLBACK_ERRORA: WM_CAP_START + 2 
WM_CAP_SET_CALLBACK_ERRORW: WM_CAP_UNICODE_START + 2 
WM_CAP_SET_CALLBACK_FRAME: WM_CAP_START + 5 
WM_CAP_SET_CALLBACK_STATUSA: WM_CAP_START + 3 
WM_CAP_SET_CALLBACK_STATUSW: WM_CAP_UNICODE_START + 3 
WM_CAP_SET_CALLBACK_VIDEOSTREAM: WM_CAP_START + 6 
WM_CAP_SET_CALLBACK_WAVESTREAM: WM_CAP_START + 7 
WM_CAP_SET_CALLBACK_YIELD: WM_CAP_START + 4 
WM_CAP_SET_MCI_DEVICEA: WM_CAP_START + 66 
WM_CAP_SET_MCI_DEVICEW: WM_CAP_UNICODE_START + 66 
WM_CAP_SET_OVERLAY: WM_CAP_START + 51 
WM_CAP_SET_PREVIEW: WM_CAP_START + 50 
WM_CAP_SET_PREVIEWRATE: WM_CAP_START + 52 
WM_CAP_SET_SCALE: WM_CAP_START + 53 
WM_CAP_SET_SCROLL: WM_CAP_START + 55 
WM_CAP_SET_SEQUENCE_SETUP: WM_CAP_START + 64 
WM_CAP_SET_USER_DATA: WM_CAP_START + 9 
WM_CAP_SET_VIDEOFORMAT: WM_CAP_START + 45 
WM_CAP_SINGLE_FRAME: WM_CAP_START + 72 
WM_CAP_SINGLE_FRAME_CLOSE: WM_CAP_START + 71 
WM_CAP_SINGLE_FRAME_OPEN: WM_CAP_START + 70 
WM_CAP_STOP: WM_CAP_START + 68

avicap32.dll: load/library %avicap32.dll
user32.dll: load/library %user32.dll

; Hide rebface console:

get-focus: make routine! [return: [int]] user32.dll "GetFocus"
hwnd-hide-console: get-focus
hide-window: make routine! [
    hwnd [int] 
    a [int]
    return: [int]
] user32.dll "ShowWindow"
hide-window hwnd-hide-console 0

view/new center-face layout/tight [
    image 320x240
    across
    btn "Take Snapshot" [
        sendmessage cap-result WM_CAP_GRAB_FRAME_NOSTOP 0 0
        sendmessage-file cap-result WM_CAP_FILE_SAVEDIBA 0 "scrshot.bmp"
    ]
    btn "Exit" [
        sendmessage cap-result WM_CAP_END 0 0
        sendmessage cap-result WM_CAP_DRIVER_DISCONNECT 0 0
        free user32.dll
        quit
    ]
]

; Set window title:

set-caption: make routine! [
    hwnd [int] 
    a [string!]
    return: [int]
] user32.dll "SetWindowTextA"
hwnd-set-title: get-focus
set-caption hwnd-set-title "Web Camera"

find-window-by-class: make routine! [
    ClassName   [string!]
    WindowName  [integer!]
    return:     [integer!]
] user32.dll "FindWindowA"
hwnd: find-window-by-class "REBOLWind" 0

cap: make routine! [
    cap [string!]
    child-val1 [integer!]
    val2 [integer!]
    val3 [integer!]
    width [integer!]
    height [integer!]
    handle [integer!]
    val4 [integer!]
    return: [integer!]
] avicap32.dll "capCreateCaptureWindowA"

sendmessage: make routine! [
    hWnd [integer!] 
    val1 [integer!]
    val2 [integer!]
    val3 [integer!]
    return: [integer!]
] user32.dll "SendMessageA"

sendmessage-file: make routine! [
    hWnd [integer!] 
    val1 [integer!] 
    val2 [integer!]
    val3 [string!]
    return: [integer!]
] user32.dll  "SendMessageA"

cap-result: cap "cap" 1342177280 0 0 320 240 hwnd 0
; 1342177280 в строке выше указано значение, полученное мной от 
; BitOR (WS_CHILD, WS_VISIBLE) в двух отдельных средах разработки, 
; но я не уверен, что оно  будет верным. 
sendmessage cap-result WM_CAP_DRIVER_CONNECT 0 0
sendmessage cap-result WM_CAP_SET_SCALE 1 0
sendmessage cap-result WM_CAP_SET_OVERLAY 1 0
sendmessage cap-result WM_CAP_SET_PREVIEW 1 0
sendmessage cap-result WM_CAP_SET_PREVIEWRATE 1 0

do-events

Вот последняя версия программы веб-камеры с более приятной функцией сохранения. Чтобы процедура сохранения работала правильно, этот код следует сохранить в .r-скрипт и запустить оттуда:

REBOL []

avicap32.dll: load/library %avicap32.dll
user32.dll: load/library %user32.dll
get-focus: make routine! [return: [int]] user32.dll "GetFocus"
set-caption: make routine! [
    hwnd [int] a [string!]  return: [int]
] user32.dll "SetWindowTextA"
find-window-by-class: make routine! [
    ClassName [string!] WindowName [integer!] return: [integer!]
] user32.dll "FindWindowA"
sendmessage: make routine! [
    hWnd [integer!] val1 [integer!] val2 [integer!] val3 [integer!]
    return: [integer!]
] user32.dll "SendMessageA"
sendmessage-file: make routine! [
    hWnd [integer!] val1 [integer!] val2 [integer!] val3 [string!]
    return: [integer!]
] user32.dll  "SendMessageA"
cap: make routine! [
    cap [string!] child-val1 [integer!] val2 [integer!] val3 [integer!]
    width [integer!] height [integer!] handle [integer!] 
    val4 [integer!] return: [integer!]
] avicap32.dll "capCreateCaptureWindowA"

view/new center-face layout/tight [
    image 320x240
    across
    btn "Take Snapshot" [
        sendmessage cap-result 1085 0 0
        sendmessage-file cap-result 1049 0 "scrshot.bmp"
        save-path: first split-path system/options/script
        view/new center-face layout [
            image load join save-path %scrshot.bmp
            btn "save" [
                (write/binary 
                    to-file pp: request-file/save/file %photo1.bmp
                    read/binary join save-path %scrshot.bmp
                )
                alert join "Saved " pp
                unview
            ]
        ]
    ]
    btn "Exit" [
        sendmessage cap-result 1205 0 0
        sendmessage cap-result 1035 0 0
        free user32.dll
        quit
    ]
]
hwnd-set-title: get-focus
set-caption hwnd-set-title "Web Camera"  ; title bar
hwnd: find-window-by-class "REBOLWind" 0
cap-result: cap "cap" 1342177280 0 0 320 240 hwnd 0
sendmessage cap-result 1034 0 0
sendmessage cap-result 1077 1 0
sendmessage cap-result 1075 1 0
sendmessage cap-result 1074 1 0
sendmessage cap-result 1076 1 0
do-events

Это пример синтеза звука, взятый из демонстрации Cyphre:

REBOL []

wait 0 
octave: ["c" "cs" "d" "ds" "e" "f" "fs" "g" "gs" "a" "as" "b" "c"]
notes: copy [] 
oct: -1 
repeat n 12 * 6 [
    if (n - 1 // 12 + 1) = 1 [oct: oct + 1] 
    insert tail notes reduce [
        to-word join pick octave n - 1 // 12 + 1 oct 440 / (
            2 ** ((46 - n) / 12)
        )
    ]
] 
make-sound: func [type freq ln /local tone freq2 result] [
    switch type [
        square [
            freq: to-integer 22050 / freq 
            tone: head insert/dup copy #{} to-char 0 freq 
            result: copy #{} 
            freq2: to-integer freq / 2 
            repeat n freq2 [
                poke tone n to-char 0 
                poke tone n + freq2 to-char 255
            ] 
            insert/dup result tone ln / freq 
            return result
        ] 
    ]
] 
make-pattern: func [
    tracks 
    /local out snd-tracks t tempo mix
] [
    out: make sound [
        rate: 22050 
        channels: 1 
        bits: 8 
        volume: 0.5 
        data: #{}
    ] 
    snd-tracks: copy [] 
    loop (length? tracks) / 2 [
        insert tail snd-tracks copy #{}
    ] 
    t: 0 
    tempo: (60 / 120)         ; УСТАНОВИТЕ ТЕМП ЗДЕСЬ
    foreach [inst track] tracks [
        t: t + 1 
        repeat n length? track [
            either track/:n = 'xx [
                insert/dup tail 
                    snd-tracks/:t to-char 128 to-integer 22050 * tempo / 4
            ] [
                insert tail snd-tracks/:t 
                make-sound inst select notes 
                track/:n to-integer 22050 * tempo / 4
            ]
        ]
    ] 
    out/data: head insert/dup copy #{} to-char 0 length? snd-tracks/1 
    mix: array/initial length? snd-tracks/1 0 
    foreach track snd-tracks [
        repeat n length? snd-tracks/1 [
            poke mix n mix/:n + track/:n
        ]
    ] 
    repeat n length? snd-tracks/1 [
        poke out/data n to-char to-integer mix/:n / ((length? tracks) / 2)
    ] 
    return out
] 
soundtrack: make sound [
    rate: 22050 
    channels: 1 
    bits: 8 
    volume: 0.5            ; УСТАНОВИТЕ ГРОМКОСТЬ ЗДЕСЬ
    data: #{}
]

; Вот ноты, которые нужно сыграть. На всех треках должно быть 
; одинаковое количество нот. Названия нот для музыкального алфавита:
; c2 cs2 d2 ds2 e2 f2 fs2 g2 gs2 a2 as2 b2. Используйте "xx" для 
; пропуска ноты.

; -----------------------------------------------------------

tracks-1: [
    square [
        c1 cs1 d1 ds1  e1 f1 fs1 g1  gs1 a1 as1 b1
        c1 xx cs1 xx   d1 xx ds1 xx  e1 xx f1 xx fs1 xx
        g1 xx gs1 xx   a1 xx as1 xx  b1
    ] 
    square [
        e2 f2 fs2 g2  gs2 a2 as2 b2  c3 cs3 d3 ds3
        e2 xx f2 xx   fs2 xx g2 xx   gs2 xx a2 xx as2 xx
        b2 xx c3 xx   cs3 xx d3 xx   ds3
    ]
] 

; -----------------------------------------------------------

; Это инициирует игру

p1: make-pattern tracks-1
insert/dup tail soundtrack/data p1/data 2
; p2: make-pattern tracks-2
; insert/dup tail soundtrack/data p2/data 1
; последняя цифра - это количество раз, для повтора soundtrack
sp: open sound:// 
insert sp soundtrack 

; Вот несколько элементов управления старт-стопом:

ask "press enter to quit"
; wait sp
close sp

Этот сценарий Фолькера Нитша демонстрирует, как использовать функцию "set-it" стиля списка GUI:

stuff: copy []
view layout [
    lst: list [across info info] 400x400 supply [
        either count > length? stuff [face/text: "" face/image: none] [
            lst/set-it face stuff index count
        ]
    ]
    with [probe words source set-it] ; get some hints
    button "add now" [
        append/only stuff reduce [mold 1 + length? stuff mold now/time]
        show lst
    ]
]

Следующий код демонстрирует, как проверить наличие асинхронных нажатий клавиш (включая клавиши со стрелками) в оболочке REBOL:

print ""
p: open/binary/no-wait console://
q: open/binary/no-wait [scheme: 'console]

forever [
    if not none? wait/all [q :00:00.30] [
        wait q
        qq: to string! copy q
        probe qq
    ]
]

Обязательно посетите http://re-bol.com/examples.txt и http://rebol.org, чтобы узнать больше!

25. Дополнительные сведения о REBOL - важные ссылки на документацию

Очень старая редакция этого текста с несколькими сотнями снимков экрана доступна по адресу http://musiclessonz.com/rebol_tutorial-images.html). Если вы новичок в программировании, этот текст может предложить некоторую полезную простую точку зрения.

См. Http://re-bol.com/examples.txt для полной справки по коду REBOL.

Учебник по адресу http://www.rebol.com/docs/rebol-tutorial-3109.pdf предоставляет хорошее резюме основных концепций. Это отличный документ для следующего чтения. Чтобы серьёзно изучить REBOL, прочтите основное руководство пользователя REBOL: http://rebol.com/docs/core23/rebolcore.html. Он охватывает все типы данных, встроенные функции слов и способы работы с данными, которые составляют язык REBOL / Core (но не графические расширения в View). Он также включает множество основных примеров кода, которые вы можете использовать в своих программах для выполнения общих программных задач. Кроме того, обязательно держите словарь функций REBOL под рукой всякий раз, когда вы пишете код REBOL: http://rebol.com/docs/dictionary.html. Он определяет все слова на языке REBOL и их конкретное использование синтаксиса. Словарь также полезен для перекрёстных ссылок на функциональные слова, которые выполняют связанные действия на языке (отлично, когда вы не можете вспомнить имя функции, которую ищете). Попутно прочтите документы REBOL View и VID по адресу: http://rebol.com/docs/easy-vid.html, http://rebol.com/docs/view-guide.html, http://rebol.com/docs/view-system.html, http://www.rebol.com/how-to/feel.html, http://www.pat665.free.fr/gtk/rebol-view.html и запустите сценарий по адресу http://www.rebol.org/download-a-script.r?script-name=vid-usage.r. Эти документы объясняют, как писать графические пользовательские интерфейсы на REBOL. Как только вы поймёте грамматику и словарный запас языка, погрузитесь в кулинарную книгу REBOL: http://www.rebol.net/cookbook/. Он содержит множество простых и полезных примеров кода, необходимого для создания реальных приложений. Когда вы все это прочитаете, завершите остальные документы на http://rebol.com/docs.html.

Помимо базовой документации, на http://rebol.org есть библиотека из сотен закомментированных сценариев REBOL. Также есть доступный для поиска архив списка рассылки и AltME (форум сообщества), содержащий несколько сотен тысяч сообщений на rebol.org. Этот архив содержит ответы на многие тысячи вопросов, с которыми сталкиваются программисты REBOL. Rebol.org - важный ресурс! Существует множество других веб-сайтов, таких как http://www.codeconscious.com/rebol, http://www.rebolforces.com (дублируется на http://www.rebolplanet.com), http://www.reboltech.com/library/library.html, http://www.fm.vslib.cz/~ladislav/rebol, http://www.compkarori.com/vanilla/display/index, http://www.rebol.net, http://reboltutorial.com, http://blog.revolucent.net/search/label/REBOL, http://www.reboltalk.com/forum, http://anton.wildit.net.au/ rebol, http://rebolweek.blogspot.com, http://groups-beta.google.com/group/Rebol и rebolfrance (переведено Google), которые помогают лучше понять и использовать язык. Не пропустите личный блог Карла Сассенрата, обсуждения REBOL3, альфа-загрузку REBOL3 и документацию REBOL3. Полный список всех веб-страниц и статей, связанных с REBOL, см. Http://dmoz.org/Computers/Programming/Languages/REBOL/.

Не забудьте щёлкнуть значки сайтов в папках "REBOL" и "Public" прямо на рабочем столе интерпретатора REBOL. Щёлкните правой кнопкой мыши любой из сотен отдельных значков программы и выберите "edit" (изменить), чтобы просмотреть код любого примера. Это отличный способ узнать, как что-то делать в REBOL.

26. Помимо REBOL

Современные компьютеры - это сложные системы, построенные на нескольких уровнях технологий. Физическое оборудование (ЦП, оперативная память, жёсткий диск, клавиатура, мышь, монитор и т.д.) составляет основу. Операционная система (Windows, Mac, Linux и т.п.) управляет этим оборудованием, включает программные драйверы, предоставляет общий пользовательский интерфейс и предоставляет множество базовых возможностей, делающих всю систему полезной (управление файлами, подключение к сетевым протоколам и т.д.). Программное обеспечение, построенное на основных компонентах операционной системы, делает возможными более конкретные приложения (текстовые процессоры, игры и т.д.). В нашем современном мире многие из используемых нами приложений построены на нескольких уровнях программного обеспечения поверх и без того сложной основы. Интернет состоит из множества типов аппаратных систем, работающих под управлением множества различных операционных систем, соединённых совместимыми сетевыми протоколами, выполняющих множество различных программ веб-серверов и почтовых серверов, хранящих информацию через программы баз данных всех типов и т.д. все эти уровни работают вместе, чтобы обслуживать данные через обычно совместимые форматы (файлы HTML, содержащие макеты страниц, стандартные типы изображений, такие как .jpg и .gif, стандартные звуковые форматы, такие как .mp3 и .wav, и стандартные видеоформаты, такие как Flash). Все это доступно множеству различных программ веб-браузера, почтовых клиентов, приложений для мобильных телефонов и т.д., которые подключаются к этим стандартным протоколам через ОС и читают/сохраняют информацию в этих форматах. Помимо этой сложной структуры, в программном обеспечении веб-браузера работают такие языки, как Javascript, для управления данными, которые появляются на веб-страницах. Такие языки, как PHP и другие, работают на программном обеспечении веб-сервера, чтобы контролировать способ вывода данных.

REBOL - это язык, который работает на многих из этих уровней. Он может работать как надстройка браузера для управления отображением данных на веб-страницах. Он может работать на веб-сервере для создания и обслуживания веб-сайтов. Он аккуратно "обертывает" большинство общих функций, которые поддерживают различные операционные системы, обеспечивая обработку файлов, управление сетью и другие возможности системного уровня. Он предоставляет единый простой формат, который позволяет вам общаться со всеми разными компьютерами одинаково на всех уровнях. У него своя манера речи, отличная от многих других языков. Эта грамматика и словарь называется "API". Если вы продолжите заниматься программированием в различных средах, вы столкнётесь с множеством разных языковых API, которые, в конце концов, делают почти то же самое, что и REBOL, но используют совершенно разные подходы к грамматике и синтаксису. В конце концов, вы научитесь работать с необработанным API операционной системы (используя компиляторы собственного языка, библиотеки DLL и другие собственные интерфейсы). API операционной системы - это базовый язык, на который фактически переводится большинство других языков. Поскольку операционной системе требуется быстрый доступ к аппаратному обеспечению компьютера, она написана на языке "нижнего уровня" - языке, который отформатирован так, чтобы думать больше как необработанные вычисления компьютера, а не как человеческая речь.

С REBOL вы можете делать самые типичные вещи, которые хотят делать программисты, но есть много функций в различных API-интерфейсах операционной системы, которые не включены (например, доступ к веб-камере, ввод звука, низкоуровневое управление оборудованием и т.д.). Для этого будьте готовы изучить необработанный API операционной системы и языки, на которых он был написан. В Windows, Unix, Macintosh и других платформах это обычно означает изучение синтаксиса и структуры языков "C" и "C++". Кроме того, очень важно изучить общие методы доступа к файлам с общим кодом, таким как .dll. После того, как вы изучите полный API REBOL, это хорошее направление для ваших исследований.

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

  1. Java - самый популярный язык программирования. Если вы хотите работать программистом, JAVA должна быть в вашем кратком списке языков, которые нужно выучить. Программы, написанные на JAVA, могут работать в Windows, Mac, Linux, сотовых телефонах, веб-браузерах и большинстве других современных операционных платформ с использованием того же кода. JAVA насчитывает десятки миллионов пользователей, поэтому поддержка для неё огромна, а интеграция с другими инструментами является повсеместной. На подавляющем большинстве настольных компьютеров уже установлена ​​виртуальная машина JAVA, и вы можете достичь практически любой цели программирования с помощью инструментов JAVA. Обратной стороной JAVA является то, что он намного больше и сложнее (как по языковой структуре, так и по размеру загрузки), чем REBOL и другие инструменты. Вам нужно будет больше узнать о традиционных шаблонах объектно-ориентированного проектирования для работы с JAVA.
  2. Python - ещё один очень популярный бесплатный инструмент программирования с открытым исходным кодом, который работает в большинстве операционных систем. Он меньше и легче в освоении, чем JAVA, но по-прежнему эффективен и пользуется сильной поддержкой во всем мире. Он отлично подходит для создания скриптов веб-сайтов, а также любых настольных приложений. Python охватывает большую часть той же проблемной области и имеет некоторые функции размера/простоты, аналогичные REBOL (хотя REBOL намного меньше и проще в использовании :).
  3. LiveCode - коммерческий продукт разработки на основе визуальной IDE, который кросс-компилирует приложения для всех настольных ОС, iOS (iPhone, iPad и т.д.) и Android. Вы можете создавать скомпилированные приложения для Mac, Linux, Android или iOS на компьютере с Windows, и наоборот, между любой операционной системой разработки. LiveCode стоит дорого, но инструменты высокого качества, а результаты выглядят профессионально. LiveCode изначально был построен на старой системе HyperCard для Mac, но превратился в мощную современную платформу разработки. Язык должен быть легко читаемым, с использованием конструкций естественного языка, которые текут, как разговорный английский. Доступные дополнительные компоненты предоставляют такие мощные функции, как анимация, трёхмерная графика, интеграция с базами данных, отчётность и т.д. вся экосистема LiveCode ориентирована на предоставление "непрограммистам" мощных возможностей с использованием эффективных шаблонов рабочего процесса и простого синтаксиса. Доступна коммерческая поддержка, а выпуск с открытым исходным кодом запланирован на 2013 год.
  4. Purebasic - хороший компилятор, который создаёт очень маленькие и быстрые нативные приложения для Windows, Mac и Linux. Это не бесплатно, но стоит недорого, а обновления бесплатны на всю жизнь. Purebasic предлагает множество преимуществ программирования на языках более низкого уровня, таких как ассемблер и C/C++ (скорость выполнения и доступ к оптимизации на низком уровне). Он поставляется с очень хорошей интегрированной средой разработки и использует дружественную и очень продуктивную кроссплатформенную языковую реализацию.
  5. Haxe/Neko - компилирует ваш код на несколько разных языков/платформ. Он работает в Windows, Mac и Linux, используя тот же самый код. Он содержит весь API-интерфейс Flash Actionscript3 и может компилироваться непосредственно в стандартные файлы .swf, что делает его чрезвычайно эффективным для создания мультимедийных приложений для использования как в онлайн-приложениях, так и в настольных. Haxe может компилироваться в виртуальную машину Neko для использования в серверных и настольных приложениях. Приложения Neko можно напрямую преобразовать в исполняемые файлы Windows, Mac и Linux. Haxe также может компилироваться напрямую в код Javascript, PHP и C ++, используя один и тот же базовый язык. Он использует традиционный синтаксис, знакомый тем, кто знает Java и C ++. Это очень небольшая загрузка, работает очень быстро, бесплатна/с открытым исходным кодом и очень стабильна. Haxe - отличный инструмент для дополнения REBOL, потому что его сильные стороны перекрывают некоторые из слабых сторон REBOL (разработка мультимедиа Flash и интеграция с другими популярными инструментами разработки высокого и низкого уровня). Мобильные приложения для всех популярных платформ телефонов/планшетов и HTML5 можно создавать с помощью набора инструментов Haxe/NME. Учебное пособие по Haxe от этого автора доступно на http://haxe.us. Mtasc - ещё один бесплатный компилятор Flash, созданный тем же человеком, что и Haxe. Он старше, но может быть полезен, если вы хотите скомпилировать код, написанный в API Actionscript2. Openlaszlo - ещё один бесплатный кроссплатформенный инструмент для тех, кто заинтересован в разработке многофункциональных мультимедийных веб-приложений. Он имеет собственную языковую реализацию (отличную от API Flash Actionscript), но может компилировать тот же самый код либо для Flash, либо для DHTML, поэтому приложения, написанные на Openlaszlo, могут работать практически в любой веб-среде.
  6. AutoIt - Уникальной характеристикой Autoit является то, что он включает множество встроенных функций для управления другими программами Windows. Вы можете программно нажимать кнопки, набирать текст, выбирать пункты меню, выбирать пункты из списков и т.д. в любом окне программы, как если бы эти действия были выполнены пользователем, нажимая и печатая на экране. Это позволяет автоматизировать и ускорить повторяющиеся процедуры, а также настроить использование существующих приложений. AutoIt чрезвычайно прост в освоении, он бесплатен и быстро загружается/устанавливается, имеет большую базу пользователей, легко компилирует сценарии в стандартные программы .exe и является мощным языком сценариев общего назначения, который можно использовать для создания всех типов приложений для Windows.
  7. RFO Basic! это простой и практичный инструмент для разработки приложений для телефонов и планшетов Android. RFO Basic! полностью работает на вашем Android-устройстве. Это крошечное, автономное решение для программирования на устройстве, которое не требует установки какого-либо программного обеспечения на настольный компьютер. Вы можете создавать приложения RFO Basic на своём настольном ПК, и если вы установите Eclipse IDE и Android API, вы сможете создавать полноценные файлы APK Android ("приложения", "программы"), которые можно распространять в Магазин приложений для Android. Для тех, кто хочет максимально простое решение для развёртывания, есть небольшая бесплатная программа для Windows, которая автоматически создаёт для вас APK. Никаких дополнительных знаний о среде Android не требуется. RFO Basic - мощный инструмент. Он обеспечивает доступ к оборудованию, датчикам, звуку, графике, мультитач, файловой системе, SQLite, сетевым сокетам, FTP, HTTP, Bluetooth, графическому интерфейсу HTML, шифрованию, SMS, телефону, электронной почте, преобразованию текста в речь, распознаванию голоса, GPS, математические, строковые функции, функции списков и т. д. В отличие от других разновидностей BASIC, которые ограничиваются введением фундаментальных концепций программирования, RFO Basic - это богатый, современный язык с множеством функций. Учебник этого автора по RFO Basic! доступно на http://rfobasic.com. NS Basic - ещё один инструмент, предназначенный для мобильной разработки. Он создаёт мобильные и веб-интерфейсы пользователя, которые работают в Интернете и на всех популярных мобильных платформах через веб-интерфейс. Для доступа к аппаратным функциям в NS Basic можно интегрировать библиотеку PhoneGap. Учебное пособие по NS Basic от этого автора доступно на http://ns-basic.com. Такие инструменты, как GL Basic и http://Basic4Android, следует изучить, если вам нравится писать код BASIC и вы хотите создавать приложения для всех мобильных и веб-платформ. GL Basic особенно прост в использовании и имеет мощный 3D API, который кросс-компилирует приложения для всех самых популярных настольных и мобильных платформ, а также HTML5.
  8. Если ваша цель - работать коммерческим программистом, вы должны свободно владеть наиболее популярными инструментами. Чтобы работать с командами разработчиков, вам необходимо знать язык(и), который они используют. Список на http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html укажет вам правильное направление (C/C++, C#, Visual Basic, PHP, PERL и т.д.). Вам также необходимо знать HTML и Javascript, если вы хотите работать в качестве веб-разработчика. Погружение в каждую из этих языковых экосистем требует изучения популярных вспомогательных инструментов, а также изучения языков. Знакомство с такими IDE, как Eclipse и Netbeans для Java, Dreamweaver для HTML, MySQL для хранения данных в PHP и т.д., Потребует не меньше времени и опыта, чем изучение синтаксиса языков программирования и API.

Изучение этих инструментов должно сосредоточить ваше внимание на дальнейших исследованиях. Удачи и приятного времяпровождения!

27. Об авторе

Привет, меня зовут Ник Антоначчо. Я опытный разработчик с обширным опытом создания аппаратных и программных систем, удовлетворяющих практическим потребностям управления данными. Выполните поиск в Google по запросам "учебник по программированию", "выучить базовый уровень", "научиться haxe", "научиться реболу", "изучить базовый уровень rfo", и вы увидите мои учебные тексты вверху каждой первой страницы в результаты поиска. Я программировал более 30 лет и был предпринимателем/владельцем бизнеса более четверти века. Я написал программное обеспечение и разработал вычислительные системы, которые помогают управлять сотнями предприятий и организаций, от маленьких семейных и популярных стартапов до крупных корпораций. В отличие от многих программистов, у меня есть большой реальный опыт запуска и ведения ряда собственных предприятий, и я помог множеству других организаций начать и успешно работать, используя специально разработанные аппаратные и программные системы.

27.1 Мой бизнес

Ниже приведены 2 предприятия, которыми я владею и управляю:

  1. Торговая деревня: этот крытый рынок площадью 120 000 кв. Футов управляет продажами более чем 170 продавцам. Он работает с 2010 года и обработал миллионы транзакций по покупке товаров. Я разработал эту бизнес-идею и написал программное обеспечение, которое запускает все операции в этом месте. Программы включают в себя систему точек продаж промышленного качества, систему отчётности, программное обеспечение для записи чеков, программное обеспечение для видеонаблюдения, систему планирования, систему резервного копирования данных, часы, программное обеспечение для обслуживания веб-сайтов, программное обеспечение для печати и считывания штрих-кодов и многое другое, с мобильным и веб-компоненты, используемые менеджерами, сотрудниками и поставщиками для работы с 1 000 – 10 000 клиентов в неделю.
  2. Rockfactory и Musiclessonz.com: Эта студия музыкальных уроков и связанный с ней бизнес уроков видеоконференций является одним из самых известных в своей нише. Этот бизнес, представленный на первой странице New York Times в январе 2012 года, полностью основан на программном обеспечении, которое я написал. Программы для управления расписанием ежедневных встреч, бухгалтерским учетом, печатью нот, организацией плана уроков, аудиозаписью, выступлением в караоке, составлением музыки, видеоконференцсвязью, планированием мероприятий, уведомлением о входе учащихся и т. Д. Позволяют проводить сотни уроков в неделю. С 2004 года на этом предприятии работают более 25 инструкторов (поискайте в Google "уроки музыки по видеоконференции" или "уроки живой музыки онлайн" в Google, чтобы узнать, как это работает).

Это всего лишь два из сотен успешных бизнес-вычислительных проектов и программных приложений, которые я задумал, создал и запустил в коммерческое производство. Другие аппаратные системы и программные приложения, которые я написал, надёжно работают в критических средах более двух десятилетий.

27.2 Список клиентов и предыдущий опыт

Некоторые из моих клиентов включали:

P & R Grocery
Trinity Lending
Peddlers Mall
Tennessee Branch Water
Indianapolis State Museum
Farm Bureau Insurance
Allied / Spectrum Janitorial
Palm Harbor Homes
Diana Michaels Jewelers
Hays and Sons Fire Restoration
KWK Property Management
Bowen Productions Recording Studios
Advantage Graphics
Garden Homes Real Estate
Oscar Renda Construction
Blue Star Battery Company
Brehob Electrical Engineering
Flanner and Buchanan Funeral Homes
Orchard Park Retirement Centers
The Law Offices of Scott Montgomery
American Cabaret Theatre
Indiana Mulch and Stone
Carriage Cleaners
Kirk Automotive
EBC Business Centers
Eagle Point Apartments
Pennsylvania Powered Paragliding
The Indiana state school system
The Princeton, NJ school system
Milestone Veneer
Deaton's Mechanical
Earl's Auction Company & Liquidators, Inc.
A number of state government offices 
Hundreds of additional businesses and home users

Я знаю, каково работать с самыми разными организациями и разными видами бизнеса. В течение 8 лет я владел и управлял розничным компьютерным магазином с 4 точками, где операции включали сборку оборудования, установку и обслуживание сетевых бизнес-систем, обучение и предоставление услуг ИТ/технической поддержки, а также разработку программного обеспечения для настольных компьютеров/сетей/веб-приложений для широкого спектра предприятия.

Я создал и внедрил множество критически важных систем инвентаризации, планирования, бухгалтерского учёта, точек продаж, отчётности, автоматизации, управления оборудованием и настраиваемых систем управления данными для десятков занятых предприятий. Я написал программное обеспечение, которое поможет автоматизировать местные кабельные отсеки для FOX TV. Я написал программное обеспечение, используемое корпорацией BJs для обучения сотрудников. Я написал программу, которая обрабатывает все операции с запасами и ценообразованием для P&R Grocery. Я написал систему штрих-кодов, которая обрабатывает покупки одежды в Zeus's Closet. Я написал приложение для Palm Harbour Homes, чтобы автоматизировать отслеживание продаж и последующие процедуры. Я создал веб-приложение для школьной системы Принстона, штат Нью-Джерси, чтобы подбирать учеников с репетиторами в зависимости от расписания, предмета и других потребностей. Я написал программу доски для бинго для отображения досок для местного заведения бинго. Я создал систему регистрации участников и доску объявлений для парамоторного клуба PAPPG. Я написал программу, чтобы помочь продавцам Etsy быстро добавлять, изменять и искать/заменять данные о листингах в своих интернет-магазинах. Я написал много сотен других приложений для управления данными, которые помогают предприятиям и частным лицам отслеживать важную информацию. Я хорошо знаю, что значит иметь бизнес-идею, которая требует безупречно работающей вычислительной системы и/или программного обеспечения. Мне действительно нравится создавать полезное программное обеспечение, которое позволяет реализовать успешные бизнес-концепции, повысить чистую прибыль и продуктивность и/или создать возможности для удовлетворения личных интересов.

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

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

В 2010 году меня назвали Реболером года за большой объем учебных материалов и программ, которые я предоставил публично. Вы можете найти более 100 моих приложений и примеров кода на http://rebol.org (я пожертвовал этому сайту больше кода, чем любой другой разработчик в сообществе). Мой 450-страничный учебник на http://howto-program.com и более 80 связанных с ним обучающих видео популярны в Интернете (найдите в Google "учебник по компьютерному программированию", чтобы увидеть результаты). Мои учебные тексты для NS Basic, Etsy Developer API, Haxe и RFO Basic распространяются создателями этих инструментов в качестве основных учебных ресурсов для разработчиков. Я работаю с rebolforum.com (и написал программу форума, на которой работает этот сайт), чтобы регулярно помогать другим разработчикам учиться повышать производительность.

Ниже приведены несколько снимков экрана программы, которую я создал для Merchants 'Village. Они дают представление о типах простых интерфейсов, которые я создаю для бизнес-приложений.

Полный набор снимков экрана: http://re-bol.com/merchants_village_screen_shots.zip

27.3 Свяжитесь со мной

Если у вас есть проект программирования или вам нужно найти разработчика для завершения части программного обеспечения, отправьте электронное письмо по адресу com1@com-pute.com. Рад обсудить варианты!

Ключевые слова:

разработка программного обеспечения, программирование компьютера, как писать программное обеспечение, компьютерное программирование, бизнес-вычисления, как создавать программы, писать код, узнавать о программировании, кодировании, приступить к программированию

Авторские права © Ник Антоначчо, 2013, Все права защищены.

MakeDoc2 by REBOL - 12-Sep-2021