4. СТРУКТУРА ДАННЫХ ПРОГРАММНОГО
КОМПЛЕКСА
Язык Lua, который используется для написания данной программы, имеет относительно долгую историю. Lua (от португальского “луна”) - облегченный скриптовый язык c расширяемой семантикой.был создан и поддерживается представителями Pontifical Catholic University Рио-де-Жанейро. У него нет официального стандарта, и стандартом считается описание в руководстве пользователя.
В настоящее время Lua является самым популярным скриптовым языком в индустрии игр и используется в ряде приложений в других предметных областях.является сравнительно новым языком и позаимствовал черты и идеи из ряда более старых языков:
синтаксис структур управления логикой программы - из Modul;
семантику более поздних версий - из Scheme;
концепцию локальных переменных - из C++;
концепцию наличия единственной встроенной структуры данных, используемой несколькими способами - из Lisp;
использование ассоциативных массивов - из SNOBOL;
множественные присвоения и возвраты из функций - из CLU etc.
Основополагающим принципом Lua является расширяемость семантики, т.е. предоставление мета-механизмов для реализации переменного набора инструментов вместо предоставления фиксированного набора инструментов. Это позволяет языку быть небольшим и простым, в то же время сохраняя мощность. Таким образом, Lua можно считать мультипарадигменным языком, поскольку он позволяет вести разработку в различных стилях. Lua поддерживает логические, числовые (по умолчанию - числа с плавающей точкой двойной точности) и строковые атомарные типы данных. Единственным “родным” сложным типом данных является таблица - гетерогенный ассоциативный массив, позволяющий использовать разные типы данных для разных пар ключей и значений. Функции являются объектами первого класса, то есть ими можно манипулировать точно так же, как переменными, передавать и получать как аргументы и так далее [8].
Вся структура данных моей программы строится на использовании таблиц, которые имеют наборы данных, необходимые для вычисления в разных местах кода. В данном типе данных можно хранить вообще все типы переменных, даже пользовательские функции и иные таблицы. Все строится на парах ключей и значений, то есть в любой таблице всегда есть такие пары.
Например, пустая таблица a={}. До того, как в нее что-либо не будет добавлено, это будет просто пустая таблица, то есть будет зарезервирована переменная с именем a. Если в нее добавить какие-нибудь значения, командой table.insert(a, [pos], data), то, соответственно, в таблице будут храниться уже эти значения на своих местах. В этой функции (table.insert()) a имя таблицы, в которую будет добавлено значение. Необязательный параметр [pos] - позиция в таблице, на которую будет добавлено значение. По умолчанию это значение начинается с первого и каждая последующая позиция увеличивается на единицу.
Допустим, в этой таблице хранятся числа: 1, 3,
5, 7, тогда она будет выглядеть следующим образом - a={1,
3, 5, 7}. Уже сейчас сформировались пары индекс-значение, как в таблице 4.1.
Таблица 4.1 - Графическое представление таблицы a={}
|
Индекс |
Значение |
|
1 |
1 |
|
2 |
3 |
|
3 |
5 |
|
4 |
7 |
Только в данном случае индекс не отображается в самой таблице.
Можно внести сразу переменные со значениями,
даже если значение будет nil,
то есть пустое. Тогда, a1={x=1,
y=2, z=3},
пары индекс-значение теперь будут такими, как в таблице 4.2.
Таблица 4.2 - Графическое представление таблицы a1={}
|
Индекс (поле) |
Значение |
|
x |
1 |
|
y |
2 |
|
z |
3 |
Можно хранить таблицы в таблице вместе с другими типами данных: a2={{2, 4, 6, 8}, {1, 3, 5, 7}, number=12, name=”name”, 5}.
В данном случае пары индекс-значение будут, как
в таблице 4.3.
Таблица 4.3 - Графическое представление таблицы a2={}
|
Индекс (поле)Значение |
|
|
1 |
{2, 4, 6, 8} |
|
2 |
{1, 3, 5, 7} |
|
number |
12 |
|
name |
“name” |
|
3 |
5 |
При этом таблицы по первому и второму индексам
также имеют свои пары индекс-значение, как в таблицах 4.4 и 4.5. Для данного
случая есть свое обозначение в коде: первая таблица, находящаяся в таблице a2,
называется по первому индексу и носит название a2[1]={},
вторая, соответственно - a2[2]={}.
Таблица 4.4 - Графическое представление таблицы a2[1]={}
|
Индекс |
Значение |
|
1 |
2 |
|
2 |
4 |
|
3 |
6 |
|
4 |
8 |
Таблица 4.5 - Графическое представление таблицы a2[2]={}
|
Индекс |
Значение |
|
1 |
|
|
2 |
3 |
|
3 |
5 |
|
4 |
7 |
Все данные в таких таблицах доступны в любом месте кода, где происходит обращение к ним. Можно получить длину таблицы командой #a (a - имя таблицы) и использовать в цикле for i=1, #a do … end, только нужно учитывать, что длинной в данном случае будет являться номер индекса, поэтому длину таблицы, например b={val=1, val1=3, val2=”string”, val3=function()} получить не удастся, так как при таких обозначениях индексом будет являться имя переменной. То есть длину таблицы можно получить лишь в том случае, если есть значения без указанного индекса, например a={1, 3, 5, 7} - длина таблицы будет равна четырем.
Cуществует возможность получения значения прямо по индексу таблицы. То есть, если использовать такой способ, то не нужно использовать циклы или подобные конструкции, а можно просто использовать команду вывода (print()) по нужному индексу. Например, в таблице a2 по индексу number хранится число 12, чтобы вывести это число на экран, можно использовать функцию print() следующим образом: print(a2.number) - на экран выведется число 12. Чтобы вывести значение по неименованному индексу, нужно использовать конструкцию: print(a2[3]) - выведется 5. Могут быть последовательности индексов, например, чтобы получить значение из вложенной таблицы: print(a2[1][4]) - на экран выведется 8.
Все эти способы удобны для манипулирования данными в программе еще и потому, что можно изменять значения при вычислениях, при этом нужно просто переназначить нужное значение, обращаясь к нему одним из перечисленных выше способов. a2[1][4]=88 - тогда таблица a2 теперь будет выглядеть так: a2={{2, 4, 6, 88}, {1, 3, 5, 7}, number=12, name=”name”, 5}.
Данные способы удобны до того момента, когда нужно работать с многомерными таблицами, то есть когда в одну таблицу вложено еще несколько таблиц с именованными индексами (или полями). В моей программе как раз используются именно такие таблицы. Например, таблица contacts={} - изначально она пустая, но когда происходит чтение из файла, то в нее заносится еще несколько таблиц. В итоге получается примерно так: contacts={{x=20, y=20, h=15, state=”open”, id=1, name=”РТО”, active=false, line=1}, {x=30, y=30, h=15, h1=17, state=”close”, id=2, name=”ПМВ1”, active=true, line=2}, {x=40, y=40, h=15, state=”open”, id=2, name=”ПМВ2”, active=false, line=2}}. x, y - координаты контакта, h - значение, необходимое для отрисовки контакта, h1 - дополнительное значение для отрисовки нормально закрытого контакта, id - значение, необходимое для ассоциации контакта с реле, на котором он находится, name - название контакта, state - состояние контакта (нормально открытый или закрытый), active - имитация проводимости контакта, line - параметр, необходимый для просчета срабатывания выбранной цепи.
Перечисленными способами работать с такой многомерной таблицей очень неудобно. Поэтому существует еще один способ - работа с парами значений. Описывается следующим образом:
for k, v in pairs(contacts) do … end. Здесь k, v - переменные, в которые циклически записываются пары индекс-значение.
Следующий код выведет значения первой таблицы в таблице contacts:
for k, v in pairs(contacts[1]) do print(k, v) end
На экране отобразится следующее:
x 20
y 2015“open”1“РТО”false1.
То есть все, что есть в таблице, только без знаков «равно». Можно вывести отдельно значения k или v, то есть просто в теле цикла написать print(k) или print(v).
При таком способе также можно назначать и переназначать значения полей (индексов) таблицы и сравнивать поля одной таблицы с полями другой. В моей программе есть еще несколько подобных таблиц, например relays={} - таблица со всеми реле схемы. После чтения из файла она становится примерно такой: relays={{x=110, y=110, idrelay=2, active=false, name=”ПМВ”, line=1}, {x=120, y=120, idrelay=3, active=false, name=”ПМО”, line=2}, {x=130, y=130, idrelay=4, active=false, name=”РПЗ”, line=3}}. x, y - координаты реле, idrelay - значение, необходимое для ассоциации реле с контактами, active - подано ли на реле напряжение или нет, name - название реле, line - значение, для выбора цепи, в которой находится реле.
Для того, чтобы сравнить, например, id
и idrelay в этих двух
таблицах (contacts
и relays), необходимо
использовать такой код:
for k,v in pairs(contacts) dor,f in pairs(relays) dov.id == f.idrelay then … end
end
Переменные k, v и r, f могут иметь любые имена, важно лишь, чтобы в этих циклах они были разными, иначе они переназначатся и возникнет путаница, в итоге схема не будет работать.
Таблица в языке Lua
самый удобный способ для манипуляции большими количествами переменных, которые
должны иметь одинаковые поля (индексы), поэтому в своей программе именно их я и
использую, язык выбран по причине наличия такого мощного инструмента для работы
с данными.
5. РАЗРАБОТКА АЛГОРИТМОВ И ПРОГРАММНОГО
КОМПЛЕКСА
Так как программа имеет графический интерфейс,
то в этой главе будут продемонстрированы код программы и связанные с ним
функции программы.
Код интерфейса строится на использовании
библиотеки Wx.lua,
которая является портом wxWidgets.
Все классы и методы даны на сайте, посвященном этим виджетам [10].
.1 Программа-имитатор
В первую очередь объявляем некоторые переменные
и создаем фрейм.
Рисунок 5.1 - Создание фрейма
Фрейм, в данном случае, как на рисунке 5.1, пустое окно без кнопок и остальных элементов. Так как сейчас демонстрируется программа-имитатор работы схем, то лишних кнопок здесь не установлено.
Необходима для отображения схемы и изменений в ней при работе функция painter(event). Рисуются главные питающие шины и отображается строка срабатывания защиты. Код показан на рисунке 5.2, окно программы - рисунок 5.3.
Рисунок 5.2 - Код изначального окна программы
Рисунок 5.3 - Изначальное состояние окна
программы
Так как таблицы контактов, реле и проводов не
заполнены, то отображается только такое окно. Для этого в коде осуществляются
проверки, как на рисунке 5.4.
Рисунок 5.4 - Проверки таблиц контактов и реле
Данная функция, onDown(event),
необходима для обработки события нажатия правой кнопки мыши на одном из
элементов. Код показан на рисунке 5.5, действие - рисунок 5.7.
Рисунок 5.5 - Код функции onDown(event)
В программе-имитаторе это выглядит так, как на
рисунке 5.6.
Рисунок 5.6 - Изначальное состояние схемы
При нажатии на контакт «РТО» подаем на реле
«РПЗ» напряжение. Контакт «РПЗ2» этого реле замыкается. Выводится сообщение о
сработавшей защите и такое состояние поддерживается до тех пор, пока первый
контакт не вернется в изначальное положение, то есть до тех пор, пока не будет
отпущена правая кнопка мыши. За перерисовку схемы отвечает функция repaint().
Рисунок 5.7 - Сработавшая схема
Она необходима для перерисовки схемы. Вызывается из функции onDown (event). В ней просчитываются все изменения в таблицах контактов и реле, как показано на рисунке 5.8.
Функция onUp(event)
возвращает схему в исходное состояние. Аналогична функциям onDown(event)
и repaint() вместе, то есть
перерисовка схемы происходит внутри функции onUp(event)
без вызова дополнительных функций, как на рисунке 5.9. Также, нужно пояснить,
что слово event в функции
обозначает событие, например нажатие кнопки мыши, нажатие клавиши, потеря
фокуса указателя, фокус, при наведении указателя мыши и так далее. В
программном коде всегда ставится ссылка на функцию, соответствующую
необходимому событию. Это делает отдельно, что позволяет удобно и без путаницы
в коде подключать события к функциям, а затем вызывать эти функции по
какому-либо из указанных в программе событий. Выше говорилось, что для
сохранения таблиц используется их сериализация. Для десериализации необходима
функция, как показано на рисунке 5.10. Для того, чтобы занести данные в таблицы
контактов, реле, проводов и линий используется функция setValues(newTable).
Она необходима потому, что в файле-сохранении в одной таблице хранится четыре.
Поэтому нужно из одной сохраненной сделать четыре рабочие и код функции показан
на рисунке 5.11.
Рисунок 5.8 - Функция repaint()
Рисунок 5.9 - Функция onUp(event)
На фрейм устанавливается панель меню и в нее
вносятся файл-меню и меню помощи, как показано на рисунке 5.12.
Рисунок 5.10 - Функция десериализации таблиц
Рисунок 5.11 - Функция setValues(newTable)
5.2 Редактор схем
Программа для редактирования схем строится таким
же образом. То есть изначально точно таким же способом, что и в
программе-имитаторе работы схем создается фрейм, определяются некоторые
необходимые переменные, а также функция painter(event)
содержит точно такой же код, как и в первом случае, так как принципы рисования
схемы одинаковы в обоих случаях. Таблицы те же самые, чтобы потом можно было
легко их ассоциировать в программе-имитаторе. Редактор схем может и сохранять
схемы, для последующего их открытия в программе-имитаторе, и открывать
сохраненные схемы для их редактирования.
Рисунок 5.12 - Установка элементов интерфейса на
фрейм
Так как на панели инструментов находятся незарезервированные инструменты, то для них нужны собственные id. Для этих целей используется соответствующая функция, соответствующие переменные для этих id и конструктор панели инструментов. Хотя в конструкторе нестандартны только названия id. Эта функция позволяет назначать новые инструменты не сбивая порядок между уже существующими и установленными на панель. Код функции и инструментов, а так же интерфейс показаны на рисунке 5.13 и рисунке 5.14.