Qt-Forth - учебный forth с графикой Qt для Windows и Linux |
Учиться никогда не поздно,
тем более классическим алгоритмам. |
Учебник
|
Для начального ознакомления с фортом будем использовать консольный вариант Qt-Forth, коротко - консоль. Консоль, это обычная, программа на Qt-Forth, использующая визуальные компоненты графической библиотеки Qt. Итак, перейдите в каталог (сделать текущем), содержащим qtforth.exe и файлы *.zim. Обычно в Linux выполняемые файлы не имеют расширения EXE. Исходный текст консоли находится в файле console.zim и запуск консоли осуществляется следующей командой:
./qtforth -s console.zim
Ключ -s означает запуск Qt-Forth с исходным файлом console.zim, содержащим исходный (forth) текст программы нашей консоли. Команды языка forth в консоле вводятся в строке "Команда". Ввод команды заканчиваем нажатием Enter. Так как наша консоль в общем то тоже учебная программа (около 300 строк), то она не предполагает какой либо защиты от ошибок. По этому вводя команды будьте внимательны и не удивляйтесь, если консоль просто "развалиться" после очередной вашей команды. Можно использовать буфер обмена для ввода и коррекции информации в строке ввода команд консоли.
Поддерживаются обычные фортовские операции на стеке. Например введем три цифры через пробел:
1 2 3
Внизу в строке состояния печатается текущее содержимое стека. Там мы и увидим наши значения. Они будут в том же порядке, в котором мы их ввели (1 2 3). Крайнее с права число лежит на вершине стека, а крайнее слева в глубине стека. Полностью сбросить (очистить) стек можно введя непонятное для Qt-forth команду (слово), например любую случайную последовательность букв.
Рассмотрим несколько классических команд (слов) форта. В каждом примере предпологаем, что на стеке лежит три числа (1 2 3).
dup - дублировать вершину стека. После выполнения данного слова на стеке будет ( 1 2 3 3 )
drop - сбросить число с вершины стека. После выполнения данного слова на стеке будет ( 1 2 )
swap - обменять местами два верхних значения на стеке. Результат ( 1 3 2 )
over - обменять местами два верхних значения на стеке. Результат ( 1 2 3 2 )
rot - "прокрутить" значения на стеке. ( 2 3 1 )
Это классика форта.
Форт есть форт. Поддерживаются обычные фортовские операции на стеке. Например введем следующий текст :
2 3 + .
Обратите внимание, что все слова и числа разделены хотя бы одним пробелом. Если все введено правильно увидим ответ:
==> 2 3 + . это повторена строка ввода, для удобства
5 - это вычисленное выражение
Таким образом, введенное выражение было вычислено и результат отображен. На стеке ни чего не осталось. Допустимы операции + сложение, - вычитание, * умножение, / деление. Адреса (указатели) на стеке воспринимаются как числа. По этому легко сместить указатель просто прибавив или отняв от него число. Смещение всегда на количество байт. Единица = один байт. Три = три байта.
Таким же образом обрабатываются отрицательные числа.
Строка при вводе записывается во временную область памяти, а её начало выкладывается на стек. Команда w вывести строку на экран консоли. Обратите внимание, после первой кавычки нет пробела (как в обычном форте):
==> "Привет мир" w
Будет напечатано "Привет мир". Строка храниться с первым длины строки и последним байтом равным нулю. Таким образом прощен передавать строки в функции API. Достаточно обойти первый байт. Пример строки в памяти:
==> "ABC" dump
000000019203 | 003 065 066 067 000 004 068 085 077 080 000 032 109 083 116 114 075 032 032 032 |
Вот так выглядит строка в памяти. Команда dump печатает 20 байт в десятичном формате начиная с адреса лежащего на стеке. Для совместимости с обычным фортом существует и слово кавычка. Его реализацию можно посмотреть в файле stdlib.zim
В памяти строка состоит из первого байта длины строки, самой строки и завершающего байта равного нулю (нечто среднее между строками PASCAL и C++). Длина строки 256 байтов. Завершающий 0 нужен для работы внешних функций API, которые как правило все на С.
==> "ABC" 1+ strlen
На стеке 3. Строка на стеке представлена указателем, который увеличивается на 1 при помощи слова 1+ тем самым обходя байт длины строки. Далее вызов strlen стандартной С функции, которая вернет на стек длину строки.
Новые слова определяются при помощи стандартных слов форта. Пример:
==> : hello "Привет мир" w ;
==> hello
Будет напечатано "Привет мир". Вот мы и сделали новое слово (функцию). Слово hello не изменяет значение стека, ни чего не забирает и ни чего не возвращает.
Имя (через пробел после начального двоеточия) может быть задано латинскими буквами (регистр не учитывается) или русскими (регистрозависимое). Внутри определения нового слова можно применять структурные операторы (определены в stdlib.zim). Кстати чем интересен Qt-Forth, что его интерпретатор написан на форте и может быть изменен в любое время. Структурные операторы:
if ... else ... then - Ветвление по условию
do .. loop - Цикл со счетчиком
begin ... while ... repeat - Цикл по условию.
case( ... of( ... of) ...case) - оператор case
и т.д. и т.п. Если чего то не хватает, всегда можно это дописать используя слова организации переходов в шитом коде, такие как <mark, >resolve и т.д. Пример в stdlib.zim.
Создадим простую программу. Описание Qt начинается с этого примера. Привожу текст программы, как он должен быть набран в консоле без комментариев (можно перенести копированием, правда построчно). Комментарии, естественно, можно не набирать. Обращаю внимание, что перед комментарием необходимы пробелы и за комментарием // необходим хотя бы один пробел, т.к. коментарий, это слово форта.
==> def qs1:CQStr // определяем объект qs1 класса CQStr (QString)
==> 0 0 def label1:CQLabel // определяем объект label1 класса CQLabel (QLabel)
==> "Привет мир!" 1+ qs1.set // присваиваем строку qs1 обходя начальный байт длины строки
==> qs1.% label1.setText // устанавливаем текст в label1.
==> : привет label1.show ; // определяем новое слово, которое выполнит метод show класса CQLabel
Вот программа и готова. Набираем привет и видим форму с приветствием.
Всё же удобнее писать программы в редакторе. Сохраним в файле hello.zim следующий текст:
// Наша программа def qs1:CQStr // опред объект qs1 класса CQStr (QString) 0 0 def label1:CQLabel // опред объект label1 класса CQLabel (QLabel) "Привет из нашей программы!" 1+ qs1.set // присв строку qs1 обходя начальный байт длины строки qs1.% label1.setText // устанавл текст в label1. : привет label1.show ; // опред новое слово
Теперь в консоли выполним команду
==> load hello.zim
Произойдет загрузка файла и слово привет станет доступно для выполнения. Набираем в консоли привет и видим форму с приветствием.
При написании программы необходимо использовать следующий шаблон.
// Описание программы
Редактор kwrite читает первую строку и включает подсветку синтаксиса. Это значительно упрощает редактирование исходного текста.
load stdlib.zim
Грузим стандартную библиотеку. В ней собраны все определения. Изначально в ядре определены только самые необходимые слова, а всё остальное, циклы, условия, переменные и т.д. определены в stdlib.zim
ncodec "UTF-8" sz_:= // Имя кодека кодовой страницы
Скопируем строку "UTF-8" в переменную ncodec, тем самым сказав QtForth, что исходный текст в кодировке Linux. Для Windows "Windows-1251"
def myApp:CQApplication // Инициализация класса Приложение
Библиотека Qt имеет класс QApplicatin. Вот тут мы его как раз и создадим. Это позволяет по разному организовать программу, например как MDI.
........... сама программа .............
Здесь определяем новые слова, которые будут составлять нашу программу. Тело так сказать.
: main // Основная функция
.... инициализация, подготовительные действия ...
myApp.exec // главный цикл, ожидание графических событий
;
Таким образом определили слово (функцию) которая является основной функцией запуска.
main // Запускаем на выполнение основную программу
Здесь запускаем на выполнение основную программу.
mstop // Стоп виртуальная машина, ожидаем событий графического интерфейса
Останавливаем работу виртуальной машины, физически заканчивая цикл выборки и выполнения шитых слов.
Оформим предыдущий пример в стиле самостоятельной программы, без участия консоли. Сохраните текст в файле hello.zim
// Наша программа. Имя hello.zim // Запуск ./qtforth -s hello.zim load stdlib.zim ncodec "UTF-8" sz_:= // Имя кодека кодовой страницы def myApp:CQApplication // Инициализация класса Приложение def qs1:CQStr // опред объект qs1 класса QString : str-->qstr // Переобразовать строку в QString 1+ qs1.set qs1.% ; 0 0 def label1:CQLabel // опред объект label1 класса QLabel "Привет из нашей программы!" str-->qstr label1.setText // устанавл текст в label1. : привет label1.show ; // опреденовое слово : main // основная программа "Внимание!" str-->qstr label1.setWindowTitle // Поставим заголовок на форму привет myApp.exec // главный цикл, ожидание графических событий ; main mstop // Остановить виртуальную машину и ждать графических событий
Запуск командой:
./qtforth -s hello.zim
Работа с API основана на динамической загрузке библиотек. Для Windows - *.DLL, а для Linux - *.so. Рассмотрим в качестве примера небольшую прогу, которая выводит в консоль список всех зарегистрированных в Windows экземпляров окон верхнего уровня.
// Пример работы с API Windows // GetWindow() Constants из winuser.h 0 const GW_HWNDFIRST 1 const GW_HWNDLAST 2 const GW_HWNDNEXT 3 const GW_HWNDPREV 4 const GW_OWNER 5 const GW_CHILD // Предпологаеться, что USER32.DLL уже загружена ( смотри stdlib.zim ) и h_user32 не пустой. h_user32 @ "GetDesktopWindow" 1+ gpaddres : GetDesktopWindow 0 1 lit, acall ; // -- hwnd h_user32 @ "GetWindow" 1+ gpaddres : GetWindow 2 1 lit, acall ; // -- hwnd h_user32 @ "GetWindowTextA" 1+ gpaddres : GetWindowText 3 1 lit, acall ; // hwnd str Ndlstr -- hwnd h_user32 @ "GetClassNameA" 1+ gpaddres : GetClassName 3 1 lit, acall ; // hwnd str Ndlstr -- hwnd 256 const$ СтрокаЗаголовка // Строка char[256] для Заголовка 256 const$ СтрокаКласса // ---- " ------ " для Имени Класса 256 const$ СтрокаВывода // ---- " ------ " для результата : g-> // hwnd -- hwnd Дай описатель соседа GW_HWNDNEXT GetWindow ; : g-V // hwnd -- hwnd Дай описатель ребенка GW_CHILD GetWindow ; : g-title // ( hwnd -- ) заголовок окна по HWND dup СтрокаЗаголовка 255 GetWindowText drop ; : g-class // ( hwnd -- ) класс окна по HWND dup СтрокаКласса 255 GetClassName drop ; : СписокОкон // ( -- ) Печатает в консоли список окон первого уровня GetDesktopWindow g-V begin dup >r g-title g-class // по HWND найдем имя класса и загол. окна и сохраним в СтрокаКласса и СтрокаЗаголовка СтрокаВывода "%d-->[%s]-->%s" 1+ r> СтрокаКласса СтрокаЗаголовка 5 6 sprintf drop // sprintf(СтрокаВывода, "%d-->[%s]-->%s", hwnd, СтрокаКласса СтрокаЗаголовка); СтрокаВывода 1- w // Напечатаем СтрокаВывода в консоли. g-> dup not // Пока указатель след окна не нулевой, крутим цикл until drop ;
В начале формируем константы для функций GetWindow. Эта функция перебирает список окон, доступных в данный момент в системе Windows. Т.к. каждая форма, зарегистрированная в системе, может в свою очередь состоять из различных областей (окон) вложенных друг в друга, то обходим в цикле только окна уровня следующие за уровнем Desktop. GetDesktopWindow g-V получает HWND экрана и по нему вычисляет первый и единственный указатель на список окон первого уровня. Ну а дальше цикл перебора по списку, пока не найдем HWND==0. Для Каждого найденного HWND формируем строку при помощи API функции sprintf, которой в качестве параметров передаем аргументы строки, для формирования результирующей строки. Обратите внимание, что перед вызовом sprintf мы указываем ей, что будет 6 аргументов на стеке, причем 5 аргументов для переменного количества аргументов самой sprintf. Это пример вызова функции API с переменным числом аргументов. Выводимая информация показывает HWND, ИмяКлассаОкна и ЗаголовокОкна. То же самое можно увидеть в утилите SPY++ из состава компилятора C++.
Выделите и сохраните текст программы в файле a.zim в каталоге с файлами *.zim. Теперь в консоли выполним команду
==> load a.zim
Произойдет загрузка файла и слово СписокОкон станет доступно для выполнения.
Правильнее назвать эту конструкцию - управляемый комментарий. Фактически это комментарий (определен в stdlib.zim) и сделан он на базе hard слова OsName, которое выкладывает на стек код символа. W - для windows, L - для linux.
Для определения объектов используется слово def. Синтаксис следующий:
[параметры если есть] def ИмяОбъекта:ИмяКласса
Для создания окон используется класс CQWidget. Слово def - это макрос (stdlib.zim), который разварачиваеться в большое определение. В связи с этим используеться только в режиме интерпретации. Желательно использовать один def на одной строке.
def Окно1:CQWidget // Определим объект Окно1 класса CQWidget Окно1.show // Вызовем метод show : Окно Окно1.show ; // Методы можно компилировать
И в Linux и в Windows возможно перенаправление входных и выходных потоков.
программа.exe > файл.txt // Пример запуска программы с перенаправлением выходного потока в файл
Такое же действие можно выполнить и в Qt-Forth, даже когда он работает в графическом режиме.
qtforth.exe -s console.zim > log.txt // Пример запуска Qt-Forth с перенаправлением выходного потока в файл
Однако, если выполнить такую команду, то вывода в файл не произойдет, поскольку изначально все сделано в графическом режиме. Для включения такой возможности используются следующие слова:
putchsd // ( N -- ) Вывести байт в поток. По умолчанию в stdout
getchsd // ( -- N ) Читать байт из потока. По умолчанию из stdin
print // ( Aстроки -- ) Вывести строку в поток, причем без CR и LF
cr // ( -- ) Вывести в поток CR и LF
В качестве примера напишем небольшую прогу, которая побайтно читает файл из входного потока и пишет в выходной задваивая все байты:
-1 const EOF // getchsd выдаёт -1, если достигнут конец файла : ЗадвоениеПотока // Читать входной поток и выводить в выходной задвоенную информацию begin getchsd // Читаем из входного потока байт dup EOF = not // Пока не дойдем до конца файла while dup putchsd putchsd // Выводим задвоенный байт repeat drop cr "Вот и задвоили всё ..." print cr ;
Запустим на выполнение консоль, перенаправив входной поток с файла console.zim а выходной на файл con22.zim:
qtforth -s console.zim < console.zim > con22.txt
Так как закрытие входных потоков у нас в программе отсутствует, то для правильного формирования выходного файла нужно выйти из Qt-Forth. Произойдет закрытие всех файлов, в том числе и стандартных потоков. Результат работы программы можно посмотреть в файле con22.txt.
Getchsd и putchsd - работают изначально с s stdin и stdout. Однако их можно переключить на работу с файлами, открытыми стандартными файловами функциями open. В Qt-Forth определены две переменных, dev_i и dev_o, которые содержат указатель на структуру FILE (C++). Переприсваивая им новый файловый дискриптор, можно перенаправить ввод или вывод в файл, непосредственно из программы.
"w" "f.txt" openf dev_o ! // Переключим вывод на f.txt, записав новый дискриптор в dev_o "Строка для записи в файл" print cr // Запишем в файл строку dev_o @ closef // Закроем файл
dev_o // ( -- Afile ) Адрес *FILE с которой работает stdout
dev_i // ( -- Afile ) Адрес *FILE с которой работает stdin
openf // (Aстр_шаблона Aстр_имяфайла -- Nфайловыйдескриптор) Открыть файл на запись или чтение (см описание формата fopen C++)
closef // ( Nфайловыйдескриптор - 1/0) Закрыть файл (см описание fclose C++)
Более подробно о файловых функциях можно почитать в документации на C++, особенно об режимах открытия. Можно подключить другие функции ввода вывода, используя загрузку динамических библиотек.