The OpenNET Project / Index page

[ новости /+++ | форум | wiki | теги | ]

Каталог документации / Раздел "Программирование, языки" / Оглавление документа

Программирование графического интерфейса с помощью Qt 4, Часть 5

Механизм сценариев Qt 4

Довольно картинок! Сегодня речь пойдет о том, что может порадовать взгляд разве что прожженных программистов и заядлых линуксоидов о системе скриптов Qt 4.3 (и выше). На протяжении многих лет библиотека Qt пополнялась различными классами, не имеющими прямого отношения к GUI. Сейчас есть даже специальная версия Qt Console для создания неграфических программ. Пользователям открытой редакции Qt доступны все модули Qt Console, в том числе, QtScript, пришедший на замену Qt Script for Applications (QSA) для Qt3.

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

Для чего в приложении могут понадобиться скрипты? Они широко используются в офисных пакетах (для автоматизации рутинных задач и передачи вирусов), почтовиках (для создания оригинальных заголовков к письмам и передачи вирусов), web-браузерах (для поддержки динамических web-страниц и передачи вирусов). Теперь все эти возможности стали доступны и разработчикам открытых Qt-программ.

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

Часики 

В качестве примера напишем программу-часы (рис. 1), без кукушки, но зато с мощной системой настройки. Исходные тексты вы найдете по ссылке в конце страницы.

 

Рисунок 1. Часы-будильник ничего необычного

Модуль QtScript использует язык ECMAScript, на котором основаны JScript и JavaScript. Вдаваться в детали синтаксиса ECMAScript мы не станем: если вы имели дело с JavaScript, то уже все знаете, остальных же отсылаем к doc.trolltech.com. С точки зрения Qt-приложения, центральным элементом системы скриптов является класс QScriptEngine, который инкапсулирует скриптовый движок. Чтобы программа поддерживала сценарии, ее необходимо собрать с подключением модуля QtScript и создать хотя бы один объект класса QScriptEngine. Рабочая лошадка QScriptEngine метод evaluate(): именно он выполняет скрипты.

Желая убедиться, что система работает, можно просто дописать в программу две строки:

engine = new QScriptEngine();
    
qint32 result = engine->evaluate("2 + 2").toInt32();

В pro-файл следует добавить директиву

QT += script

В результате выполнения программы в переменную result будет записано число 4. Главным аргументом метода evaluate() (есть и другие, со значениями по умолчанию) является строка, содержащая тело скрипта.

 Для того чтобы оптимально использовать сценарии в вашей программе, необходимо понимать некоторые ключевые особенности встроенного скриптового языка. Какими бы широкими возможностями он ни обладал, для выполнения полезной работы ему потребуется доступ к объектам вашей программы. Каким же образом выполняется передача данных между приложением и скриптом? Вспомним, что переменные в интерпретируемых языках (ECMAScript не исключение) обычно полиморфны, то есть, например, могут рассматриваться и как число, и как строка в зависимости от контекста. Это обстоятельство играет роль не только при написании сценариев: значение любой переменной скрипта QtScript может быть представлено в виде объекта класса QScriptValue. В зависимости от контекста, вы можете привести его к тому или иному простому (составному) типу. Объекты QScriptValue своего рода посредники между скриптом и приложением. Например, метод evaluate() возвращает результат выполнения сценария как раз в виде объекта QScriptValue. Он может содержать величину простого типа (как в примере выше), сведения об ошибке (если таковая произошла) или пустое значение (аналог типа void). Вот так, например, можно вывести на консоль ругательное сообщение:


QScriptValue result = engine->evaluate(script);
if (result.isError())
    qDebug() << "Script error:" << result.toString();

Создаем среду окружения

Таким образом, чтобы передать объект приложения в среду выполнения скрипта, надо предварительно упаковать его в QScriptValue (а также задать имя, под которым объект будет известен сценарию). Для потомков QObject это делается легко. Например, передадим скрипту объект, реализующий главное окно нашей программы (это фрагмент конструктора):

QScriptValue objectWnd = engine->newQObject(this);
engine->globalObject().setProperty("MainWindow", objectWnd);

Методу newQObject() передается указатель на объект, который нужно упаковать в QScriptValue (и другие аргументы со значениями по умолчанию). Во второй строке мы задаем имя объекта в среде скрипта и назначаем ему глобальную область видимости. В результате наше главное окно будет доступно сценариям как MainWindow.

Глобальный объект

 Для тех, кто хочет добраться до сути вещей, приведем более строгое объяснение. Язык ECMAScript является чистым объектно-ориентированным. Иначе говоря, весь код скрипта выполняется в контексте некоего глобального объекта. Последний создается автоматически движком скрипта и доступен в Qt-приложении посредством свойства globalObject() объекта QScriptEngine (как и все, что связано со скриптами, глобальный объект среды представлен в Qt-приложении объектом класса QScriptValue). Метод setProperty() просто создает новое свойство глобального объекта.

Передача объекта в среду окружения скрипта предполагает, что последний сможет получить доступ к его элементам. Возможно, это покажется вам странным, но не все методы переданного объекта доступны в контексте скрипта. Причина в том, что сценарий может видеть только те сущности, описания которых доступны во время выполнения программы, а в языке C++ информация о полях и методах объекта доступна лишь на этапе компиляции. У этой проблемы существует несколько решений. Самое простое задействовать сведения о типах времени выполнения (RTTI), предоставляемые Qt: они реализованы для классов, производных от QObject, благодаря чему вы можете получить информацию о сигналах, слотах и свойствах объектов Qt во время работы программы. Обычные поля и методы объектов будут, увы, недоступны. Создавая новый класс, необходимо позаботиться о том, чтобы нужные элементы были видны скрипту. Рассмотрим объявление класса Clock, который реализует главное окно программы-часов.

class Clock : public QDialog, public Ui::MainForm
{
    Q_OBJECT
public:
	Clock(QWidget *parent = 0);
	virtual ~Clock();
	Q_PROPERTY(QString timeFormat READ getTimeFormat WRITE setTimeFormat)
	Q_PROPERTY(QString dateFormat READ getDateFormat WRITE setDateFormat)
	Q_PROPERTY(QString currentTime READ getCurrentTime)
protected slots:
	void onTimeout();
	void execute(const QString & command);
private:
	...
};

Здесь перечислены три свойства: timeFormat, dateFormat и currentTime. Первые два устанавливают формат отображения даты и времени на наших часах. Свойство currentTime, доступное только для чтения, содержит текущее значение времени. Обратите внимание на слот execute() он выполняет команду Linux, переданную в качестве параметра. Второй способ сделать метод объекта видимым в скрипте использовать в его объявлении макрос Q_INVOKABLE:

Q_INVOKABLE void visibleMethod ();

Теперь, когда мы знаем, как передать объект, рассмотрим простейший сценарий (программа Clock читает его из файла clock.config):

MainWindow.timeFormat = "hh:mm:ss";
    
MainWindow.dateFormat = "MM/dd/yy (dddd)";
MainWindow.windowTitle = "Configurable Clock";

Первые две строки не должны вызывать вопросов. Мы используем определенные нами свойства timeFormat и dateFormat для задания форматов даты и времени с помощью принятых в Qt символов-спецификаторов. Но взгляните на последнюю строку мы не определяли свойство windowTitle! Оно является унаследованным, а мы получили дополнительную возможность настройки, о которой даже не думали. Как уже отмечалось выше, свобода, которую предоставляют скрипты, несет в себе потенциальный риск. Если добавить в сценарий строку

MainWindow.close();

конфигурационный файл завершит работу всей программы. Метод close() класса Clock является слотом, а значит, будет доступен из скрипта как метод объекта MainWindow. Но и это еще не все. Посмотрите на следующую конструкцию:

MainWindow.dateText.text = "Date:";

Что есть свойство dateText объекта MainWindow? Имя dateText в нашей программе присвоено объекту QLabel, который выводит пояснение к строке с текущей датой (под именем здесь понимается не идентификатор переменной-указателя, а значение свойства objectName()).name()). В приведенной выше строке мы присваиваем новое значение свойству text объекта dateText. В результате внешний вид окна программы изменится (рис. 2).

 

Рисунок 2. Cценарий может изменить внешний вид программы до неузнаваемости. Этого ли вы хотели?

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

MainWindow.dateLabel.hide();
MainWindow.dateText.hide();
MainWindow.setStyleSheet("QDialog { background: yellow }");
MainWindow.timeLabel.setStyleSheet("QLabel {border: 2px solid green;}");
MainWindow.timeText.setStyleSheet("QLabel {border: 2px solid red;}");

С помощью слота hide() мы убираем строки, связанные с отображением даты. Далее, при помощи слота setStyleSheet() мы изменяем цвета и внешний вид элементов окна (при этом используются описания стилей style sheets).

Когда речь идет о свойствах в контексте Qt, мы ставим скобки после имени, (например, objectName() ), что соответствует синтаксису Qt. Когда же мы говорим о свойствах в контексте скрипта, их имена пишутся без конечных скобок.

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

Например, вызов:

QScriptValue objectWnd = engine->newQObject(this,
    QScriptEngine::QtOwnership, QScriptEngine::ExcludeSuperClassMethods | QScriptEngine::ExcludeSuperClassProperties);

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

Тогда

MainWindow.close();

вызовет ошибку

ReferenceError: close is not defined

Добавление в третий аргумент константы QScriptEngine::ExcludeChildObjects запрещает доступ к дочерним объектам.

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

Делегация полномочий

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

function onTimer() 
{
	if (MainWindow.currentTime == "00:00:00")
		MainWindow.execute("konsole");
}  

Функция onTimer() должна стать обработчиком сигнала timeout(), который периодически генерируется объектом-таймером в нашей программе. Она проверяет текущее значение времени (свойство currentTime) и ровно в полночь запускает программу konsole с помощью слота execute(). После того, как функция-обработчик будет объявлена, ее следует связать с сигналом таймера:

MainWindow.timer.timeout.connect(onTimer);

timer это имя объекта-таймера в Qt-приложении. Сигнал timeout() объекта timer представляется в скрипте как свойство-объект, у которого есть методы connect() и disconnect(). Первый выполняет связывание обработчика и сигнала, второй разрывает эту связь. Необходимо понимать разницу между обращениями

MainWindow.timer.timeout() 

и

MainWindow.timer.timeout 

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

В обратную сторону

Qt-приложение может передавать в контекст скрипта ссылки не только на объекты, но и на функции, не являющиеся методами. Для этого необходимо выполнить те же самые действия: упаковать указатель на функцию в объект QScriptValue и назначить ей имя и область видимости в контексте скрипта. Но с точки зрения сценария и приложения, одна и та же функция выглядит по-разному. В скрипте у нее нет прототипа, так что она может принять любое число аргументов; в программе же нужно обработать два параметра: указатели на объекты QScriptContext и QscriptEngine.

В качестве примера напишем функцию toUnicode(), преобразующую текст из кодировки скрипта в UTF-16, которой пользуется Qt. То же самое можно сделать, используя язык ECMAScript, но для нас важен процесс, а не результат:

QScriptValue toUnicode(QScriptContext *context, QScriptEngine *engine) 
{
	QString s = context->argument(0).toString();
	return QScriptValue(engine, QString::fromUtf8(s.toAscii().data()));
}  

Кода скрипт вызовет нашу функцию, все переданные ей аргументы будут сохранены в специальной коллекции объекта context (их общее число есть context.argumentCount()). Все аргументы, равно как и возвращаемое значение, являются, естественно, объектами QScriptValue. Для присвоения функции имени в контексте скрипта нужно проделать следующее:

QScriptValue func = engine->newFunction(toUnicode);
engine->globalObject().setProperty("toUnicode", func);

Метод newFunction() класса QScriptEngine создает объект QScriptValue, содержащий указатель на функцию. Вторая строка нам уже знакома это задние свойства глобального объекта скрипта. Теперь мы можем использовать в скрипте функцию toUnicode(), например:

MainWindow.windowTitle = toUnicode("Часы с настройкой");

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

Я тебя породил...

Скрипты Qt могут не только пользоваться объектами, предоставленными им приложением, но и создавать свои собственные. Это можно делать на основании хранимых в сценариях описаний типов, но мы пойдем более простым путем переложим ношу на плечи Qt. В нашем демонстрационном приложении Clock определен класс TextInputDialog, который создает диалоговое окно со строкой для ввода текста, пояснением к ней и кнопками OK и Отмена. Его можно использовать в настроечных скриптах программы clock для запроса данных у пользователя (рис.3). Чтобы сценарий мог создать объект класса TextInputDialog тогда, когда ему это нужно, в приложении clock следует объявить функцию

QScriptValue newTextInputDialog(QScriptContext *context, QScriptEngine *engine) 
{
	return engine->newQObject(new TextInputDialog(), QScriptEngine::ScriptOwnership);
}    

Список параметров стандартный. В теле функции мы создаем QScriptValue, инкапсулирующий объект класса TextInputDialog. Он и возвращается в качестве результата функции. Обратите внимание на второй аргумент метода newQObject(). Константа QScriptEngine::ScriptOwnership указывает, что создаваемый объект должен принадлежать скрипту (еще один важный момент: объект, создаваемый специально для сценария, не должен иметь родительского объекта в приложении, то есть parent() должно равняться NULL). Мы делам функцию newTextInputDialog() доступной скрипту так же, как и в случае ToUnicode(). Теперь в сценарии можно писать следующее:

var Dlg = newTextInputDialog();
var Text;
function onAccept()
{
	Text = Dlg.lineEdit.text;
	print(Text);
}
Dlg.accepted.connect(onAccept);
Dlg.modal = true;
Dlg.show();    

Здесь создается объект TextInputDialog, ссылка на который сохраняется в переменной Dlg (поскольку адресное пространство приложения скрипту недоступно, уместно говорить именно о ссылках, а не об указателях). Сигнал accepted() диалогового окна (генерируемый при щелчке на OK) связывается с обработчиком onAccept. В нем мы считываем строку, введенную пользователем (свойство text объекта lineEdit), и распечатываем ее в окне консоли с помощью встроенной функции print().

 

Рисунок 3. Наши часы окончательно обнаглели может быть, во всем виноват вирус?

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

 

JavaScript основан на ECMAScript, так что конфигурационные файлы Clock можно править в KWrite со всеми удобствами.

Исходные тексты примеров


Статья впервые опубликована в журнале Linux Format

© 2008  Андрей Боровский <anb @ symmetrica.net>



Спонсоры:
Inferno Solutions
Hosting by Hoster.ru
Хостинг:

Закладки на сайте
Проследить за страницей
Created 1996-2020 by Maxim Chirkov
Добавить, Поддержать, Вебмастеру