Диалог по сценарию
Автор: Леонид Новиков
[Обсудить в форуме]
Начал писать очередную прикладную программку под Автокад. Когда проделал половину работы, возникло знакомое ощущение – сделаешь еще один шаг и запутаешься в собственной программе окончательно и бесповоротно. Тоска и беспомощность... Спасла природная лень. Остановился. Задумался.
Итак, пускай разрабатываемая нами программа будет предназначена для редактирования группы текстов в Автокаде. В графической системе AutoCAD используется несколько разновидностей текста, мы охватим только три: текст, отдельный атрибут и атрибут в составе блока.
На первый взгляд задача выглядит так – Пользователь подробно описывает, что ему нужно, а программа выполняет это. Но ведь Пользователь существо не менее ленивое чем автор, тем более что в данном случае обе эти ипостаси совмещаются в одном лице. Поэтому программа должна помогать Пользователю в определении того, что ему нужно. Пользователь не должен созидать, конструировать, сооружать нечто... Его задача всего лишь выбрать один из предложенных вариантов. Есть такая концепция, называется дружественный интерфейс, кажется. Такой интерфейс "дружески" предлагает вполне ограниченный (или вполне конкретный) ассортимент услуг. В итоге, смотрите что получается - программа должна не только выполнить команду Пользователя, но и предугадать его дальнейшие шаги. Предугадать и подготовить. Вспоминается почему-то глас вопиющего в пустыне – "Приготовьте пути Господу, сделайте прямыми его стези!" А куда он собирается идти?
В нашем случае Пользователь задумал, положим, заменить значения OldValue во всех атрибутах по имени MyAttrib расположенных в блоках по имени MyBlock на значение NewValue. Откуда программа знает, что у него на уме? И как она может ему помочь? Однако помогает. Бедняга-программист держит в уме все варианты и конструирует поведение программы таким образом, что если нажать кнопку А, то Пользователь увидит список всех имеющихся в наличии блоков, выбирай – не хочу. А когда Пользователь выберет из списка имен MyBlock (не напишет, заметьте, а выберет), то появится список атрибутов этого блока, выбирай – не хочу. А нажмет кнопку В, программа подсунет список имен отдельных атрибутов Ну и так далее. Пользователь в свободном полете выбирает чего хочет, программа предугадывает его желания, услужливо предоставляя возможность выбора, совместными усилиями они заменяют OldValue на NewValue. А расплачивается за все это благолепие программист, его распухшая от вариантов головушка. Поневоле задумаешься о чем-то таком, типа, сценария.
Ну что ж, мы ведь пишем не Программу вообще, а программу определенного назначения, для редактирования .. и.т.д. Работающую в режиме диалога с Пользователем. Поэтому имеется возможность описать этот диалог как набор целенаправленных действий приводящих к определенному результату. А если предполагается получать различные результаты, то сценарий описывает набор вариантов действия. Таким образом у Пользователя возникает иллюзия свободного выбора, но ведет его к цели железной рукой наш сценарий. А у программиста появляется возможность сесть и расписать все варианты поведения системы еще до того как он примется за код. Основная идея такого подхода – разделить программу на логический каркас в виде сценария и исполнительный код (в идеале, просто набор библиотечных функций ), который "нанизывается" на этот каркас.
Подумаем над сценарием. Если компьютер это устройство по обработке данных (в просторечии - информации), то программа управляющая действиями компьютера это набор команд по обработке данных. Процесс обработки делится на этапы или стадии. Каждая стадия представляет собой определенное состояние обрабатываемых данных. Если же говорить о процессе управлении этой обработкой, то для него характерно такое же деление на стадии и этапы, отражающее специфику данных, вид обработки и характер работы с этими данными. К чему все это? А к тому что структура сценария должна отражать некий упорядоченный набор состояний системы (нашей программы), где переход от одного состояния к другому это шаг в работе программы. Каждое такое состояние зависит от предыдущего и имеет перспективу в виде последующего. Таким образом, взаиморасположение узлов-состояний в сценарии определяет логическую связь между ними. Для структуры диалога характерно наличие альтернативы при переходе к следующему состоянию, иными словами у Пользователя, как правило, должен быть выбор из нескольких возможных продолжений действий. И вся работа диалога это движение по веткам древа выбора вниз (или вверх при реализации отката).
Итак, в сухом остатке мы имеем - древовидную структуру с узлами-состояниями системы. В какой форме реализовать запись и чтение такого сценария? Я выбрал XML-файл. Во-первых, XML-технологии это модное направление сегодняшнего дня, а всем нам хочется,и все мы подвержены ... ну а рациональное объяснение всегда найдется, например, что имеется масса готовых инструментов для работы с такими файлами, причем, для любых языков программирования. Во-вторых, очень уж удобно и приятно писать такой файл-сценарий диалога, используя иерарахическую XML-структуру (помните, про природную лень и т.п.?). Я писал свой файл используя один из простейших специализированных редакторов – встроенный XML-редактор MS Visual Studio.NET. Практически, он дает возможность проверить структуру файла, для этого нужно перейти на вкладку: Data. Если появится таблица, значит с тэгами все в порядке. Для просмотра файла в удобном виде – древовидной структуры с раскрывающимися узлами - использовал просмотровщик EasyXML4.
В силу того, что автор человек осторожный (или трусоватый?), было принято решение прежде чем писать что-то полезное и стоящее с использованием сценария, написать некий прототип диалога и отработать на нем реализацию вышеизложенных идей. Так появилась программка ScriptDialog, о которой и пойдет речь ниже. ScriptDialog решает условную задачу по выбору символа - цифры или буквы, из предложенного набора. См. рис. 1
Рис. 1
Выбор осуществляется по-этапно, так работает программа. Например, мы решили выбрать букву "C". Тогда нужно проделать следующие шаги: - нажать Select1? Получаем рис.2
Рис. 2
- Select 2
Рис. 3
- Select 1
Рис. 4
- Select 1
Если выбрать другую ветку, выбор пойдет по-другому, но прицип, вроде бы, ясен. А теперь посмотрите, как это записано в сценарии:
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<choice>
<ABCDE>
<AB>
<A>EndSelect</A>
<B>EndSelect</B>
</AB>
<CDE>
<CD>
<C>EndSelect</C>
<D>EndSelect</D>
</CD>
<E>EndSelect</E>
</CDE>
</ABCDE>
<T12345>
<T123>
<T1>EndSelect</T1>
<T23>
<T2>EndSelect</T2>
<T3>EndSelect</T3>
</T23>
</T123>
<T45>
<T4>EndSelect</T4>
<T5>EndSelect</T5>
</T45>
</T12345>
</choice>
или в другом варианте:
По моим подсчетам эти пол-странички содержат 18 вариантов выбора. Попробуйте удержать их в голове. И обратите внимание как все это просто, наглядно и , не побоюсь этого слова, элегантно!
Теперь дело осталось за малым, научить программу читать и понимать этот сценарий и строить свою работу на его основе. Язык программы – Visual Basic. Этот выбор чистой воды вкусовщина, отражает привычки и предпочтения автора. Наша программка должна читать и интерпретировать сценарий и выстраивать интерфейс в соответствии с прочитанным. Поэтому разбиваем код на следующие блоки-модули:
M_Main – запуск программы и диспетчерские функции
M_Interpret – чтение и интерпретация сценария
M_Interface – управление формами (пользовательским интерфейсом)
M_Constant – служебные функции
Начнем с главного, с интерпретатора. Его задача двоякая:
1. Прочесть и истолковать пункт сценария как вызов соответствующей процедуры программы
2. Принять и истолковать команду Пользователя (событие Интерфейса) как выбор одного из предлагаемых сценарием продолжений.
Работаем с файлом XML стандартными средствами, подключаем к проекту references:MicrosoftXML v. 4.0 (или v.3.0) и читаем файл в объектную модель MSXML2.DOMDocument30. Здесь будет использован код, который приводит Leon Platt в своей статье "XML in 20 minutes."
Вот так примерно будет выглядеть объектная модель документа:
Отметим только что эта модель прекрасно вписывается как техническию так и идеологически в VB-код.
Итак, программа и Пользователь движутся шаг за шагом к конечной цели, и мы попробуем рассмотреть такой шаг. Пользователь нажал кнопку и выбрал тем самым продолжение. Действия программы? Для определенности пусть это будет переход от положения зафиксированного на рис.2 к положения на рис.3. Мы намерены получить в конце концов букву "С", поэтому выбираем группу букв содержащую выбираемую, то-есть группу "CDE", и с этой целью жмем на Select2. Кнопка обращается к диспетчеру, через которого проходят все команды:
Private Sub Cmd2_Click()
Call Dispatch("Cmd2") ' программное имя контрола (кнопки)
End Sub
Public Sub Dispatch(ControlName As String
'Диспетчер передает интерпретатору программное имя активизированного контрола
' для определения выбранного продолжения
CurrentStatus = M_Interpret.GetSelectedVar(ControlName)
Теперь о самом, наверное, главном, о том как связан код со сценарием. Для этой цели контролам помимо программного имени присвоены еще и логические имена. Это имена тех продолжений (или вариантов), которые запускаются в действие именно этим контролом, когда Пользователь производит свой выбор. Эти имена мы загоняем в соответсвующие массивы в процессе стартовой инициализации системы. Так для нашей кнопки: Select2 набор логических имен выглядит следующми образом (см. рис. 1-4):
Public Cmd2_Array(9) As String
Public Sub FullControlArray()
Cmd2_Array(1) = "T12345"
Cmd2_Array(2) = "T45"
Cmd2_Array(3) = "T23"
Cmd2_Array(4) = "T3"
Cmd2_Array(5) = "T5"
Cmd2_Array(6) = "CDE"
Cmd2_Array(7) = "D"
Cmd2_Array(8) = "E"
Cmd2_Array(9) = "B"
Вот как работает интерппретатор:
For Each ContinueName In SetToContinue
Select Case (ControlName)
Case "Cmd1"
For i = 1 To UBound(Cmd1_Array())
If (Cmd1_Array(i) = ContinueName) Then
GetSelectedVar = Cmd1_Array(i)
Exit Function
End If
Next i
Case "Cmd2"
For i = 1 To UBound(Cmd2_Array())
If (Cmd2_Array(i) = ContinueName) Then
GetSelectedVar = Cmd2_Array(i)
Exit Function
End If
Next i
End Select
Next ContinueName
Имеется набор имен актуальных продолжений SetToContinue (о том как он создается чуть позже). На нашем шаге SetToContinue содержит два имени узла-продолжения - "AB" и "CDE", (рис.2). Сопоставляя логические имена задействованного контрола из массива Cmd2_Array() с набором актуальных продолжений находим, что совпадают логическое имя "CDE" и актуальное продолжение "CDE". Следовательно, выбрано продолжение "CDE". Интерпретировали!
Такой подход (связь через логические имена) позволяет отделить сценарий от кода и дает возможность реализовать этот сценарий на любом языке программирования.
Дальнейшие действия программы. Диспетчер зафиксировал переход системы в новое состояние, сделав его текущим - CurrentStatus , и определяет с помощью интерпретатора новый набор актуальных продолжений. Давайте обратимся к нашему сценарию. Мы только что совершили переход из узла ABCDE в узел CDE:
<ABCDE>
<AB>
<A>EndSelect</A>
<B>EndSelect</B>
</AB>
<CDE>
<CD>
<C>EndSelect</C>
<D>EndSelect</D>
</CD>
<E>EndSelect</E>
</CDE>
</ABCDE>
и теперь у нас дочерними узлами являются CD, E - это и есть новый набор. актуальных продолжений. А как поймет ситуацию программа? Для этого есть интерпретатор, вызываем процедуру заполнения набора актуальных продолжений:
Call M_Interpret.FullSetToContinue
Работа процедуры понятна из кода:
Dim objNode As IXMLDOMNode
Dim objListOfNodes As IXMLDOMNodeList
Set xmlDoc = Get_xmlDoc(XML_SCRIPT)
xmlDoc.setProperty "SelectionLanguage", "XPath"
Dim QueryString As String
QueryString = "//" & CurrentStatus
Set objNode = xmlDoc.selectSingleNode(QueryString)
'получаем список дочерних узлов
Set objListOfNodes = objNode.childNodes
'получаем родительский узел (на случай, если будет выбран Previous)
Set objParentNode = objNode.parentNode
Вот так выглядит эта ситуация в объектной модели:
Далее идет заполнение набора
Call FullSet(objListOfNodes)
End Sub
Private Sub FullSet(objListOfNodes As IXMLDOMNodeList)
Dim CollToContinue As New Collection
Dim objNode As IXMLDOMNode
For Each objNode In objListOfNodes
CollToContinue.Add objNode.nodeName
Next objNode
Set SetToContinue = CollToContinue
И наконец, заключительная фаза цикла – подготовка интерфейса Пользователя. Собственно говоря, подготовка к чему? Да к следующему шагу, естественно. Программа должна дать Пользователю возможность принять решение по выбору варианта и осуществить, реализовать этот выбор. Итак, должна быть предоставлена информация к размышлению и рычаги управления (контролы).
Разбиваем задачу на две части:
1. Выяснить, какие контролы должны быть задействованы
2. Подготовить их к использованию
По первому пункту обращаемся к интерапретатору –
Dim ControlsName As Collection
Set ControlsName = M_Interpret.GetControlsToUse
Public Function GetControlsToUse() As Collection
Dim VarToContinue As Variant
Dim ControlsToUse As New Collection
Dim i As Integer
Для каждого варианта продолжения из набора SetToContinue сравнить имя продолжения (из сценария) с логическим именем контрола:
For Each VarToContinue In SetToContinue
For i = 1 To UBound(Cmd1_Array())
If (Cmd1_Array(i) = VarToContinue) Then
ControlsToUse.Add "Cmd1"
ControlsToUse.Add VarToContinue, "Cmd1" 'присваиваем элементу
'коллекции "Cmd1" key "Cmd1".
'Эта пара значений нам вскоре пригодится
End If
Next i
For i = 1 To UBound(Cmd2_Array())
If (Cmd2_Array(i) = VarToContinue) Then
ControlsToUse.Add "Cmd2"
ControlsToUse.Add VarToContinue, "Cmd2"
End If
Next i
Next VarToContinue
Set GetControlsToUse = ControlsToUse
Запускаем процедуры подготовки контролов к использованию –
Dim ControlName As Variant
Dim NameToContinue As String
For Each ControlName In ControlsName
Select Case ControlName
Case "Cmd1"
NameToContinue = ControlsName("Cmd1")
Call Cmd1_ToUse(NameToContinue)
Case "Cmd2"
NameToContinue = ControlsName("Cmd2")
Call Cmd2_ToUse(NameToContinue)
End Select
Next ControlName
В чем смысл такой подготовки? Во-первых, контрол должен быть доступен. Для этого выводим на экран содержащий его контейнер, т.е. форму и устанавливаем
Form1.Cmd2.Enabled = True. Во-вторых, выполняем сопутствующие действия, например, - заполнить список, поменять Caption, заблокировать соседний контрол, ну и т.д. Эти действия зависят от программы, и процедура подготовки к использованию
Private Sub Cmd2_ToUse(NameToContinue As String) выписывается вручную. Вот как она выглядит в нашей программе –
Form1.Lbl2.Caption = NameToContinue
Form1.Lbl2.Enabled = True
Form1.Cmd2.Enabled = True
If (Form1.Visible = False) Then
Form1.Show
End If
Form1.Cmd3.Enabled = True
Form1.Cmd4.Enabled = True
Ну вот и все. Дело сделано. Мы создали программу которая работает в режиме диалога с Пользователем и ведет эту работу основываясь на предварительно составленном сценарии. Такая себе получилась программка для души! Теперь самое время написать что-нибудь полезное, для денег, например. .