[Предыдущая: Модуль QtOpenGL] [Модули Qt] [Следующая: Модуль QtScriptTools] Модуль QtScript
|
QScriptClass | Интерфейс для описания пользовательского поведения (класса) объектов Qt Script |
---|---|
QScriptClassPropertyIterator | Интерфейс итератора для пользовательских объектов Qt Script |
QScriptContext | Представляет вызов функции Qt Script |
QScriptContextInfo | Дополнительная информация о QScriptContext |
QScriptEngine | Среда для исполнения кода Qt Script |
QScriptEngineAgent | Интерфейс к событиям отчета, относящихся к выполнению QScriptEngine |
QScriptExtensionPlugin | Абстрактный базовый класс для пользовательских подключаемых модулей расширения QScript |
QScriptString | Действует как дескриптор к "interned" строкам в QScriptEngine |
QScriptSyntaxCheckResult | Результат проверки синтаксиса сценария |
QScriptValue | Действует как контейнер для типов данных Qt Script |
QScriptValueIterator | Итератор в стиле Java для QScriptValue |
QScriptable | Доступ к окружению Qt Script из функций-членов Qt C++ |
Приложения, использующие классы Qt Script, нужно сконфигурировать для сборки вместе с модулем QtScript. Для включения определений классов этого модуля используйте следующую директиву:
#include <QtScript>
Для линковки приложения с этим модулем, добавьте в ваш qmake файл проекта .pro:
QT += script
Модуль QtScript является частью Выпуска Qt Full Framework и Версий Open Source Qt.
Модуль QtScript предоставляет только основные возможности работы со сценариями; модуль QtScriptTools предоставляет дополнительные компоненты Qt, связанные со сценариями, которые разработчики приложений могут найти полезным.
Qt Script основан на языке сценариев ECMAScript, как описано в стандарте ECMA-262. JScript от Microsoft и JavaScript от Netscape также основаны на стандарте ECMAScript. Обзор ECMAScript смотрите в Справочнике по ECMAScript. Если вы не знакомы с языком ECMAScript, существует несколько учебных пособий и книг, которые освещают эту тему, например, JavaScript: Полное руководство.
Существующие пользователи Qt Script for Applications (QSA) при портировании сценариев QSA в Qt Script могут найти полезным документ Миграция с QSA на Qt Script.
Для выполнения кода сценария создайте QScriptEngine и вызовите его функцию evaluate(), передавая в качестве аргумента код сценария (текст) для вычисления.
QScriptEngine engine; qDebug() << "the magic number is:" << engine.evaluate("1 + 2").toNumber();
Возвращаемое значение будет результатом вычисления (представленный объектом QScriptValue); его можно конвертировать в стандартные типы C++ и Qt.
Пользовательские свойства можно сделать доступными для сценариев, зарегистрировав их с помощью механизма сценариев. Сделать это легче всего установив свойства глобального объекта механизма сценариев:
engine.globalObject().setProperty("foo", 123); qDebug() << "foo times two is:" << engine.evaluate("foo * 2").toNumber();
Это поместит свойства в окружение сценария, делая их таким образом доступными из кода сценария.
Любой экземпляр, основанный на QObject, можно сделать доступных для использования в сценариях.
Когда QObject передаётся в функцию QScriptEngine::newQObject(), создаётся объект-обёртка Qt Script, который может быть использован для того, чтобы сделать доступными скрипту сигналы, слоты, свойства и дочерние объекты QObject'а.
Вот пример создания экземпляра подкласса QObject, доступного из кода сценария под именем "myObject":
QScriptEngine engine; QObject *someObject = new MyObject; QScriptValue objectValue = engine.newQObject(someObject); engine.globalObject().setProperty("myObject", objectValue);
В окружении сценария создаётся глобальная переменная с именем myObject. Переменная служит в качестве посредника (proxy) для базового объекта C++. Обратите внимание на то, что имя переменной сценария может быть любым; т.е., оно не зависит от QObject::objectName().
Функция newQObject() принимает два дополнительных необязательных аргумента: один - режим владения, а другой - набор опций, которые позволяют вам управлять некоторыми аспектами того, как будет себя вести QScriptValue, который служит обёрткой QObject. Мы вернёмся к использованию этих аргументов позднее.
Qt Script адаптирует основную функциональную возможность Qt - Сигналы и слоты - для работы со сценариями. Имеется три основных способа использования сигналов и слотов с Qt Script:
Используйте функцию qScriptConnect() для соединения сигнала C++ с функцией сценария. В следующем примере определяется обработчик сигнала сценария, который будет обрабатывать сигнал QLineEdit::textChanged():
QScriptEngine eng; QLineEdit *edit = new QLineEdit(...); QScriptValue handler = eng.evaluate("function(text) { print('text was changed to', text); }"); qScriptConnect(edit, SIGNAL(textChanged(const QString &)), QScriptValue(), handler);
Первые два аргумента в qScriptConnect() те же самые, что вы передаёте в QObject::connect() чтобы установить обычное соединение C++. Третий аргумент - объект сценария, который при вызове обработчика сигнала будет работать в качестве объекта this; в примере выше мы передаем неверное значение сценария, поэтому объектом this будет глобальный объект (Global Object). Четвертый аргумент - это сама функция сценария ("слот"). Следующий пример показывает, как можно использовать аргумент this:
QLineEdit *edit1 = new QLineEdit(...); QLineEdit *edit2 = new QLineEdit(...); QScriptValue handler = eng.evaluate("function() { print('I am', this.name); }"); QScriptValue obj1 = eng.newObject(); obj1.setProperty("name", "the walrus"); QScriptValue obj2 = eng.newObject(); obj2.setProperty("name", "Sam"); qScriptConnect(edit1, SIGNAL(returnPressed()), obj1, handler); qScriptConnect(edit2, SIGNAL(returnPressed()), obj2, handler);
Мы создали два объекта QLineEdit и определили одну функцию-обработчик сигнала. Соединения используют одну и ту же функцию-обработчик, но функция будет вызываться с разными объектами this в зависимости от того, какой сигнал объекта был инициирован, поэтому вывод оператора print() будет разным.
В коде сценария для соединения с сигналами и отсоединения от сигналов Qt Script использует синтаксис, отличный от привычного синтаксиса C++; т.е., QObject::connect(). Для соединения с сигналом вы ссылаетесь на соответствующий сигнал как на свойство объекта отправителя и вызываете его функцию connect(). Имеется три перегрузки для connect(), каждой из них соответствует перегруженная disconnect(). В следующих подразделах описываются эти три формы.
connect(function)
В этом виде соединения аргумент для connect() - функция для соединения с сигналом.
function myInterestingScriptFunction() { ... } ... myQObject.somethingChanged.connect(myInterestingScriptFunction);
Аргумент может быть функцией Qt Script, как в примере выше, или он может быть слотом QObject, как в следующем примере:
myQObject.somethingChanged.connect(myOtherQObject.doSomething);
Когда аргументом является слот QObject, типы аргументов сигнала и слота необязательно должны быть совместимы; QtScript будет, если необходимо, выполнять преобразование аргументов сигнала к типам, соответствующих слоту.
Для отсоединения от сигнала вы вызываете функцию сигнала disconnect(), передавая в качестве аргумента функцию для отсоединения:
myQObject.somethingChanged.disconnect(myInterestingFunction); myQObject.somethingChanged.disconnect(myOtherQObject.doSomething);
Когда функция скрипта вызывается в ответ на сигнал, объект this будет глобальным объектом.
connect(thisObject, function)
В этом виде функции connect() первый аргумент является объектом this, который будет связан с переменной когда вызывается функция, заданная вторым аргументом.
Если у вас в форме имеется кнопка, обычно вам нужно что-нибудь сделать на форме в ответ на сигнал кнопки clicked; в таком случае имеет смысл передать форму в качестве объекта this.
var obj = { x: 123 }; var fun = function() { print(this.x); }; myQObject.somethingChanged.connect(obj, fun);
Для отсоединения от сигнала передайте те же аргументы в disconnect():
myQObject.somethingChanged.disconnect(obj, fun);
connect(thisObject, functionName)
В этой форме функции connect() первый аргумент - объект this, который будет связан с переменной, когда в ответ на сигнал вызывается функция. Второй аргумент задает имя функции, которая соединяется с сигналом, и это относится к функции-члену объекта, переданного в качестве первого аргумента (thisObject в вышеприведённой схеме).
Обратите внимание на то, что функция разрешается когда производится соединение, а не при испускании сигнала.
var obj = { x: 123, fun: function() { print(this.x); } }; myQObject.somethingChanged.connect(obj, "fun");
Для отсоединения от сигнала передайте те же аргументы в disconnect():
myQObject.somethingChanged.disconnect(obj, "fun");
Когда connect() или disconnect() выполнены успешно, функция будет возвращать undefined; в противном случае возбуждено исключение сценария. Вы можете извлечь сообщение об ошибке из полученного объекта Error. Пример:
try { myQObject.somethingChanged.connect(myQObject, "slotThatDoesntExist"); } catch (e) { print(e); }
Для испускания сигнала из кода сценария просто вызовите функцию сигнала, передав соответствующие аргументы:
myQObject.somethingChanged("hello");
В настоящее время определить новый сигнал в сценарии невозможно; т.е., все сигналы должны быть определены классами C++.
Когда перегружается сигнал или слот, QtScript будет пытаться выбрать правильную перегрузку основываясь на фактических типах аргументов QScriptValue, содержащихся в вызове функции. Например, если ваш класс имеет слоты myOverloadedSlot(int) и myOverloadedSlot(QString), следующий код сценария будет работать корректно:
myQObject.myOverloadedSlot(10); // будет вызвана перегрузка int myQObject.myOverloadedSlot("10"); // будет вызвана перегрузка QString
Вы можете указать определённую перегрузку, используя доступ к свойству как к массиву, с нормализованной сигнатурой функции C++ в качестве имени свойства:
myQObject['myOverloadedSlot(int)']("10"); // вызывает перегрузку int; аргумент преобразуется к типу int myQObject['myOverloadedSlot(QString)'](10); // вызывает перегрузку QString; аргумент преобразуется к типу string
Если перегрузки имеют разное количество аргументов, QtScript выберет перегрузку с тем количеством аргументов, которое лучше всего соответствует фактическому количеству аргументов переданных в слот.
Для перегруженных сигналов Qt Script возбудит ошибку, если вы попытаетесь соединиться с сигналом по имени; вы должны ссылаться на сигнал с полной нормализованной сигнатурой конкретной перегрузки, к которой вы хотите соединиться.
Свойства QObject доступны как свойства соответствующего объекта QtScript. Когда вы работаете со свойством в коде сценария, методы получения/установки значения C++ для этого свойства будут вызываться автоматически. Например, если ваш класс C++ имеет свойство, объявленное как изложено ниже:
Q_PROPERTY(bool enabled READ enabled WRITE setEnabled)
тогда в коде сценария можно делать примерно такие вещи:
myQObject.enabled = true; ... myQObject.enabled = !myQObject.enabled;
Каждый именованный дочерний объект QObject (то есть, у которого QObject::objectName() не равно пустой строке) по умолчанию доступен как свойство объект-обёртки QtScript. Например, если у вас имеется QDialog с дочерним виджетом, чьё свойство objectName равно "okButton", вы можете получить доступ к этому объекту в коде сценария посредством выражения
myDialog.okButton
Так как само свойство objectName является Q_PROPERTY, вы можете воспользоваться именем в коде сценария, например, чтобы переименовать объект:
myDialog.okButton.objectName = "cancelButton"; // в дальнейшем myDialog.cancelButton ссылается на кнопку
Для поиска дочерних объектов вы можете также использовать функции findChild() и findChildren(). Эти две функции ведут себя точно также как QObject::findChild() и QObject::findChildren(), соответственно.
Например, мы можем использовать эти функции для поиска объектов, используя строки и регулярные выражения:
var okButton = myDialog.findChild("okButton"); if (okButton != null) { // делаем что-нибудь с кнопкой OK } var buttons = myDialog.findChildren(RegExp("button[0-9]+")); for (var i = 0; i < buttons.length; ++i) { // делаем что-нибудь с buttons[i] }
Обычно вы хотите использовать findChild() когда обрабатываете форму, которая использует вложенные компоновки; при этом способе сценарий изолирован от деталей о том, в какой именно компоновке располагается виджет.
Qt Script использует сборку мусора для восстановления памяти, используемой объектами сценария, когда они больше не нужны; занимаемая объектами память может быть автоматически восстановлена, когда в окружении сценария на них больше нигде нет ссылок. Qt Script позволяет вам управлять тем, что случится с базовым QObject C++ когда объект-обёртка восстанавливается (т.е., удалён ли QObject или нет); вы делаете это когда создаёте объект, передавая в качестве второго аргумента в QScriptEngine::newQObject() режим владения (ownership mode).
Знание того, как Qt Script обрабатывает владение важно, так как оно может помочь вам избежать ситуаций, где объект C++ не удаляется, когда должен (вызывая утечки памяти), или где объект C++ удаляется когда не должен был (обычно приводит к аварийному завершению, если позднее в коде C++ пытаются получить доступ к этому объекту).
По умолчанию, механизм сценариев не получает владения над объектом QObject, который передаётся в QScriptEngine::newQObject(); объект управляется в зависимости от принадлежности объекта Qt (смотрите Деревья объектов и владение объектами). Этот режим подходит тогда, когда, например, вы обёртываете объекты C++, которые являются частью ядра вашего приложения; то есть, они будут продолжать не обращать внимание на то, что происходит в окружении сценария. Сформулируем другим способом - объекты C++ будут жить дольше механизма сценария.
Указание QScriptEngine::ScriptOwnership как режим владения будет приводить механизм сценария к получению полного владения над объектом QObject и удалению его, когда это определено что делать это безопасно (т.е., когда на него больше нет ссылок в коде сценария). Этот режим владения подходит, если QObject не имеет родительского объекта и/или QObject создаётся в контексте механизма сценария и не предназначен для существования после механизма сценария.
Например, функция конструктора, которая создаёт объекты QObject только для использования в окружении сценария, является лучшим кандидатом:
QScriptValue myQObjectConstructor(QScriptContext *context, QScriptEngine *engine) { // позволим механизму управлять временем жизни нового объекта. return engine->newQObject(new MyQObject(), QScriptEngine::ScriptOwnership); }
С QScriptEngine::AutoOwnership владение основано на том имеет ли QObject родителя или нет. Если сборщик мусора QtScript обнаружит, что на QObject больше никто не ссылается в окружении сценария, то QObject будет удалён только если у него нет родителя.
Может так произойти, что обёрнутый QObject удалён за пределами элемента управления Qt Script; т.е., безотносительно заданного режима владения. В этом случае, объект-обёртка остаётся объектом (в отличие от указателя C++, который он обёртывает, объект сценария не хочет становиться нулевым). Любая попытка получения доступа к свойствам объекта сценария будет, не смотря на это, вызывать в сценарии возбуждение исключения.
Обратите внимание на то, что QScriptValue::isQObject() по-прежнему будет возвращать true для удалённого QObject, поскольку она проверяет тип объекта сценария, а не то, является ли внутренний указатель нулевым. Другими словами, если QScriptValue::isQObject() возвращает true, но QScriptValue::toQObject() возвращает нулевой указатель, то это указывает на то, что QObject был удалён за пределами Qt Script (возможно случайно).
QScriptEngine::newQObject() может получить третий аргумент, который позволяет вам контролировать различные аспекты доступа к QObject посредством возвращаемого им объекта-обёртки
QScriptEngine::ExcludeChildObjects указывает, что дочерние объекты QObject не будут появляться в качестве свойств объекта-обёртки.
QScriptEngine::ExcludeSuperClassProperties и QScriptEngine::ExcludeSuperClassMethods могут использоваться чтобы не показывать члены, которые унаследованы от суперкласса QObject'а. Это полезно для определения "чистого" интерфейса, где унаследованные члены не имеют смысл с точки зрения перспективы работы со сценарием; например, вы не хотите, чтобы авторы сценария могли изменить свойство objectName объекта или вызывать слот deleteLater().
QScriptEngine::AutoCreateDynamicProperties указывает, что свойства, которые ещё не существуют в QObject, будут созданы как динамические свойства QObject, а не свойств объект-обёртки QtScript. Если вы хотите чтобы новые свойства действительно стали постоянными свойствами QObject, а не свойствами, которые уничтожаются вместе с объектом-обёрткой (и которые не используются совместно, если QObject обёрнут несколько раз с помощью QScriptEngine::newQObject()), вы должны использовать эту опцию.
QScriptEngine::SkipMethodsInEnumeration указывает, что сигналы и слоты будут пропущены при подсчёте свойств обёртки QObject в операторе for-in сценария. Это полезно при определении объектов-прототипов, поскольку по соглашению свойства функции прототипов не будут подсчитываться.
Функция QScriptEngine::newQObject() используется для обёртывания существующего экземпляра класса QObject, так что он может быть сделан доступным для сценариев. Другой сценарий - вы хотите, чтобы сценарии были способны создавать новые объекты, а не только получать доступ к существующим.
Система мета-типов Qt в настоящее время не предоставляет динамическое связывание (dynamic binding) конструкторов классов, основанных на QObject. Если вы хотите создать такой класс, способный использовать оператор new из сценариев, Qt Script может сгенерировать для вас подходящий конструктор сценария; смотрите QScriptEngine::scriptValueFromQMetaObject().
Вы также можете использовать QScriptEngine::newFunction(), чтобы обернуть вашу функцию-фабрику, и добавить ее к окружению сценария; пример смотрите в QScriptEngine::newQMetaObject().
Значения для перечислений объявленных с помощью Q_ENUMS не доступны в качестве свойств отдельных объектов-обёрток; вернее, они являются свойствами объекта-обёртки QMetaObject, который можно создать с помощью QScriptEngine::newQMetaObject().
QtScript выполняет преобразование типов когда значение нужно преобразовать со стороны сценария на сторону C++ или наоборот; например, когда сигнал C++ запускает функцию сценария, когда вы получаете доступ к свойству QObject в коде сценария, или когда вы вызываете в C++ QScriptEngine::toScriptValue() или QScriptEngine::fromScriptValue(). QtScript предоставляет операции преобразования по умолчанию для многих встроенных типов Qt. Вы можете изменить операцию преобразования типа (включая ваши пользовательские типы C++), зарегистрировав свои собственные функции преобразования с помощью qScriptRegisterMetaType().
В следующей таблице описывается преобразование по умолчанию из QScriptValue в тип C++.
Тип C++ | Преобразование по умолчанию |
---|---|
bool | QScriptValue::toBool() |
int | QScriptValue::toInt32() |
uint | QScriptValue::toUInt32() |
float | float(QScriptValue::toNumber()) |
double | QScriptValue::toNumber() |
short | short(QScriptValue::toInt32()) |
ushort | QScriptValue::toUInt16() |
char | char(QScriptValue::toInt32()) |
uchar | unsigned char(QScriptValue::toInt32()) |
qlonglong | qlonglong(QScriptValue::toInteger()) |
qulonglong | qulonglong(QScriptValue::toInteger()) |
QString | Пустая строка, если QScriptValue равен null или не определена; в противном случае - QScriptValue::toString(). |
QDateTime | QScriptValue::toDateTime() |
QDate | QScriptValue::toDateTime().date() |
QRegExp | QScriptValue::toRegExp() |
QObject* | QScriptValue::toQObject() |
QWidget* | QScriptValue::toQObject() |
QVariant | QScriptValue::toVariant() |
QChar | Если QScriptValue - строка, результатом будет первый символ строки или пустой QChar, если строка пустая; в противном случае результатом будет QChar, созданный из unicode, полученного преобразованием QScriptValue в ushort. |
QStringList | Если QScriptValue - массив, результатом будет QStringList, созданный из результата применения QScriptValue::toString() ко всем элементам массива; в противном случае, результат - пустой QStringList. |
QVariantList | Если QScriptValue - массив, результатом будет QVariantList созданный из результата применения QScriptValue::toVariant() ко всем элементам массива; в противном случае, результат - пустой QVariantList. |
QVariantMap | Если QScriptValue - объект, результатом будет QVariantMap с парами (ключ, значение) в форме (propertyName, propertyValue.toVariant()) для каждого свойства, используя QScriptValueIterator для перебора свойств объекта. |
QObjectList | Если QScriptValue - массив, результатом будет QObjectList, созданный из результата применения QScriptValue::toQObject() ко всем элементам массива; в противном случае, результат - пустой QObjectList. |
QList<int> | Если QScriptValue - массив, результатом будет QList<int> созданный из результата применения QScriptValue::toInt32() ко всем элементам массива; в противном случае, результат - пустой QList<int>. |
Кроме того, QtScript будет обрабатывать следующие случаи:
В следующей таблице описывается поведение по умолчанию при создании QScriptValue из типа C++:
Тип C++ | Структура по умолчанию |
---|---|
void | QScriptEngine::undefinedValue() |
bool | QScriptValue(engine, value) |
int | QScriptValue(engine, value) |
uint | QScriptValue(engine, value) |
float | QScriptValue(engine, value) |
double | QScriptValue(engine, value) |
short | QScriptValue(engine, value) |
ushort | QScriptValue(engine, value) |
char | QScriptValue(engine, value) |
uchar | QScriptValue(engine, value) |
QString | QScriptValue(engine, value) |
qlonglong | QScriptValue(engine, qsreal(value)). Обратите внимание на то, что при преобразовании может привести к потере точности, поскольку не все 64-разрядные целые числа могут быть представлены с использованием типа qsreal. |
qulonglong | QScriptValue(engine, qsreal(value)). Обратите внимание на то, что преобразование может привести к потере точности, поскольку не все 64-разрядные целые числа без знака могут быть представлены с использованием типа qsreal. |
QChar | QScriptValue(this, value.unicode()) |
QDateTime | QScriptEngine::newDate(value) |
QDate | QScriptEngine::newDate(value) |
QRegExp | QScriptEngine::newRegExp(value) |
QObject* | QScriptEngine::newQObject(value) |
QWidget* | QScriptEngine::newQObject(value) |
QVariant | QScriptEngine::newVariant(value) |
QStringList | Новый массив сценария (созданный с помощью QScriptEngine::newArray()), чьи элементы созданы применением конструктора QScriptValue(QScriptEngine *, QString) к каждому элементу списка. |
QVariantList | Новый массив сценария (созданный с помощью QScriptEngine::newArray()), чьи элементы созданы применением QScriptEngine::newVariant() к каждому элементу списка. |
QVariantMap | Новый объект сценария (созданный с помощью QScriptEngine::newObject()), чьи свойства инициализируются в соответствии с парами (ключ, значение) отображения. |
QObjectList | Новый массив сценария (созданный с помощью QScriptEngine::newArray()), чьи элементы созданы применением QScriptEngine::newQObject() к каждому элементу списка. |
QList<int> | Новый массив сценария (созданный с помощью QScriptEngine::newArray()), чьи элементы созданы применением конструктора QScriptValue(QScriptEngine *, int) к каждому элементу списка. |
Другие типы (включая пользовательские типы) будут обёрнуты используя QScriptEngine::newVariant(). Для нулевых указателей любого типа, результатом будет QScriptEngine::nullValue().
Этот раздел объясняет, как реализовать объекты приложения и предоставляет необходимые технические первоисточники.
Сделать классы и объекты C++ доступными для языка сценариев - это не является тривиальным, потому что языки сценариев имеют тенденцию быть более динамичными, чем C++, и они должны иметь возможность анализировать свои объекты (запрашивать во время выполнения такую информацию, как имена функций, сигнатуры функций, свойства и так далее). Стандартный C++ не предоставляет средств для этого.
Мы можем добиться желаемой функциональности расширив C++, используя собственные возможности C++ так что наш код останется стандартным C++. Мета-объектная система Qt предоставляет необходимую дополнительную функциональность. Это позволяет нам писать используя расширенный синтаксис C++, но преобразовать это в стандартный C++ используя небольшую утилиту с именем moc (мета-объектный компилятор). Классы, которые желают получить преимущества от мета-объектных возможностей, либо являются подклассами QObject, либо используют макрос Q_OBJECT. Qt использует этот подход многие годы и он доказал свою компактность и надёжность. QtScript использует эту мета-объектную технологию чтобы представить создателям сценариев динамический доступ к классам и объектам C++.
Чтобы полностью понять как сделать объекты C++ доступными для Qt Script, очень полезны некоторые базовые знания мета-объектной системы Qt. Мы рекомендуем прочитать Объектную модель Qt. Информация в этом документе и в документах, на которые в нём ссылаются, очень поможет в понимании того, как реализовывать объекты приложения.
Однако, это знание не важно в простейших случаях. Чтобы сделать объект доступным в QtScript, он должен быть унаследован от QObject. Все классы, которые унаследованы от QObject, могут быть подвергнуты интроспекции и могут предоставлять информацию, необходимую механизму сценариев во время выполнения; например, имя класса, функции, сигнатуры. Поскольку мы получили нужную нам информацию о классах динамически во время выполнения, нет необходимости писать обёртки для классов унаследованных от QObject.
Мета-объектная система также делает динамически доступной во время выполнения информацию о сигналах и слотах. По умолчанию, для подклассов QObject только сигналы и слоты автоматически становятся доступными для сценариев. Это очень удобно, поскольку на практике мы обычно хотим сделать доступными для создателей сценариев только специально отобранные функции. Когда вы создаете подкласс QObject, убедитесь что функции, которые вы хотите сделать видимыми для QtScript, являются открытыми слотами.
Например, в следующем определении класса работа со сценариями разрешается только для некоторых функций:
class MyObject : public QObject { Q_OBJECT public: MyObject( ... ); void aNonScriptableFunction(); public slots: // эти функции (слоты) будут доступны в QtScript void calculate( ... ); void setEnabled( bool enabled ); bool isEnabled() const; private: .... };
В примере выше, aNonScriptableFunction() не объявлена как слот, поэтому она не будет доступна в QtScript. Другие три функции автоматически станут доступны в QtScript, поскольку они объявлены в разделе public slots определения класса.
Можно сделать любую функцию вызываемой из сценария указав модификатор Q_INVOKABLE при объявлении функции:
class MyObject : public QObject { Q_OBJECT public: Q_INVOKABLE void thisMethodIsInvokableInQtScript(); void thisMethodIsNotInvokableInQtScript(); ... };
Однажды объявленный с Q_INVOKABLE, метод может быть вызван из кода QtScript как если бы он был слотом. Несмотря на то, что такой метод не является слотом, вы можете по-прежнему указывать его как целевую функцию в вызове connect() в коде сценария; connect() принимает и "родные" и "не родные" функции в качестве целевых.
В предыдущем примере, если вы хотите получить или установить свойство используя QtScript нам нужно написать код аналогичный следующему:
var obj = new MyObject; obj.setEnabled( true ); print( "obj is enabled: " + obj.isEnabled() );
Языки сценариев часто предоставляют синтаксис свойств для модификации и получения значения свойств (в нашем случае состояние задействованности) объекта. Многие создатели сценариев захотят переписать вышеприведённый код примерно так:
var obj = new MyObject; obj.enabled = true; print( "obj is enabled: " + obj.enabled );
Чтобы сделать это возможным, вы должны определить свойства в подклассе C++ QObject. Например, в следующем объявлении класса MyObject объявлено булево свойство с именем enabled, которое использует функцию setEnabled(bool) как свою функцию установки значения и isEnabled() - как свою функцию получения значения:
class MyObject : public QObject { Q_OBJECT // определяем задействованное свойство Q_PROPERTY( bool enabled WRITE setEnabled READ isEnabled ) public: MyObject( ... ); void aNonScriptableFunction(); public slots: // эти функции (слоты) будут доступны в QtScript void calculate( ... ); void setEnabled( bool enabled ); bool isEnabled() const; private: .... };
Единственным отличием от первоначального кода заключается в использовании макроса Q_PROPERTY, который получает в качестве аргументов тип и имя свойства, а также имена функций установки и получения значения.
Если вам не нужно, чтобы свойство вашего класса было доступно в QtScript, установите атрибут SCRIPTABLE в значение false когда объявляете свойство; по умолчанию, атрибут SCRIPTABLE равен true. Например:
Q_PROPERTY(int nonScriptableProperty READ foo WRITE bar SCRIPTABLE false)
В объектной модели Qt сигналы используются как механизм уведомления между объектами QObject. Это означает, что один объект может соединить сигнал со слотом другого объекта и каждый раз, когда испускается сигнал, будет вызываться слот. Это соединение устанавливается используя функцию QObject::connect().
Механизм сигналов и слотов также доступен программистам QtScript. Код для объявления сигнала в C++ такой же, независимо от того соединен сигнал со слотом в C++ или в QtScript.
class MyObject : public QObject { Q_OBJECT // определяем задействованное свойство Q_PROPERTY( bool enabled WRITE setEnabled READ isEnabled ) public: MyObject( ... ); void aNonScriptableFunction(); public slots: // эти функции (слоты) будут доступны в QtScript void calculate( ... ); void setEnabled( bool enabled ); bool isEnabled() const; signals: // сигналы void enabledChanged( bool newState ); private: .... };
Мы сделали единственное изменение в коде в предыдущем разделе - объявили раздел сигналов с соответствующим сигналом. Теперь, создатель сценария может определить функцию и соединить с объектом примерно так:
function enabledChangedHandler( b ) { print( "state changed to: " + b ); } function init() { var obj = new MyObject(); // соединяем функцию скрипта с сигналом obj["enabledChanged(bool)"].connect(enabledChangedHandler); obj.enabled = true; print( "obj is enabled: " + obj.enabled ); }
В предыдущем разделе описывается, как реализовать объекты C++, которые могут быть использованы в QtScript. Объекты приложения - это объекты того же рода, и они делают доступной функциональность вашего приложения для создателей сценариев в QtScript. Поскольку приложение C++ написано на Qt, многие объекты уже являются объектами QObject. Самым лёгким подходом будет просто добавить все эти объекты QObject как объекты приложения в механизм сценария. Для небольших приложений это может быть достаточно, но для больших приложений это, вероятно, не правильный подход. Проблема заключается в том, что этот метод открывает слишком много внутреннего API и даёт программистам сценариев доступ к внутреннему содержимому приложения, которое не должно быть открытым.
Как правило, лучшим способом сделать функциональность приложения доступной для создателей сценариев - закодировать несколько объектов QObject, которые определяют открытые API приложения используя сигналы, слоты и свойства. Это даст вам полный контроль над функциональностью, доступной в приложении. В реализациях этих объектов просто вызывайте функции приложения, которые делают реальную работу. Итак, вместо того, чтобы сделать все ваши объекты QObject доступными для механизма сценария, добавьте только объекты-обёртки QObject.
Если у вас есть слот, который возвращает указатель на QObject, вы должны обратить внимание на то, что по умолчанию Qt Script обрабатывает только преобразование типов QObject* и QWidget*. Это означает, что если ваш слот объявлен с сигнатурой похожей на "MyObject* getMyObject()", QtScript автоматически не узнает, что MyObject* должен обрабатываться таким же образом, что и QObject* и QWidget*. Самый простой способ найти выход из этого - использовать только QObject* и QWidget* в сигнатурах методов вашего интерфейса работы со сценариями.
В качестве альтернативы, вы можете зарегистрировать вспомогательные функции для вашего пользовательского типа с помощью функции qScriptRegisterMetaType(). Таким образом, вы можете сохранить точность типов в ваших объявлениях C++, пока по-прежнему позволяя указателям на ваши пользовательские объекты плавно перетекать между C++ и сценариями. Пример:
class MyObject : public QObject { Q_OBJECT ... }; Q_DECLARE_METATYPE(MyObject*) QScriptValue myObjectToScriptValue(QScriptEngine *engine, MyObject* const &in) { return engine->newQObject(in); } void myObjectFromScriptValue(const QScriptValue &object, MyObject* &out) { out = qobject_cast<MyObject*>(object.toQObject()); } ... qScriptRegisterMetaType(&engine, myObjectToScriptValue, myObjectFromScriptValue);
В Qt Script, функции являются первоклассными значениями (first-class values); они являются объектами, которые могут иметь свойства самих себя, как и любой другой тип объектов. Они могут сохраниться в переменных и передаваться в качестве аргументов в другие функции. Знание того, как ведут себя вызовы функции в Qt Script полезно, когда вы хотите определить и использовать свои собственные функции сценария. В этом разделе обсуждается этот вопрос, а также объясняется, как вы можете реализовать "родные" функции; то есть, функции Qt Script написанные на C++, в противоположность функциям, написанным на языке сценариев. Даже если вы будете полагаться в основном на динамическое связывание QObject, предоставляемое Qt Script, знание этих мощных концепций и приёмов важно для понимания того, что действительно происходит когда выполняются функции сценария.
Вызов функции Qt Script из C++ выполняется с помощью функции QScriptValue::call(). Обычный план действий - вы вычисляете скрипт, который определяет функцию, и в некоторой точке вы хотите вызывать эту функцию из C++, возможно передав ей несколько аргументов, а затем обработать результат. В следующем сценарии определяется объект Qt Script с функцией toKelvin():
({ unitName: "Celsius", toKelvin: function(x) { return x + 273; } })
Функция toKelvin() получает в качестве аргумента температуру в Кельвинах и возвращает температуру, конвертированную в градусы Цельсия. Следующий фрагмент показывает, как можно получить функцию toKelvin() и вызывать её из C++:
QScriptValue object = engine.evaluate("({ unitName: 'Celsius', toKelvin: function(x) { return x + 273; } })"); QScriptValue toKelvin = object.property("toKelvin"); QScriptValue result = toKelvin.call(object, QScriptValueList() << 100); qDebug() << result.toNumber(); // 373
Если сценарий определяет глобальную функцию, вы можете получить доступ к функции как к свойству QScriptEngine::globalObject(). Например, в следующем сценарии определяется глобальная функция add():
function add(a, b) { return a + b; }
В коде C++ можно вызывать функцию add() как изложено ниже:
QScriptValue add = engine.globalObject().property("add"); qDebug() << add.call(QScriptValue(), QScriptValueList() << 1 << 2).toNumber(); // 3
Как уже говорилось, функции в Qt Script являются только значениями; функция сама по себе не "привязана к" конкретному объекту. Вот почему вы указываете объект this (первый аргумент в QScriptValue::call()), к которому будет применена функция.
Если предполагается, что функция действует как метод (т.е. она может быть применена только к определённому классу объектов), функции придётся самой проверять, что она была вызвана с совместимым объектом this.
Передавая неверный QScriptValue в качестве аргумента this в QScriptValue::call() указывает на то, что глобальный объект будет использован в качестве объекта this; другими словами, функция будет вызываться как глобальная функция.
Когда функция Qt Script вызывается из сценария, способ, которым вызывается функция, определяет объект this когда выполняется тело функции, как проиллюстрировано в следующем примере сценария:
var getProperty = function(name) { return this[name]; }; name = "Global Object"; // создает глобальную переменную print(getProperty("name")); // "Global Object" var myObject = { name: 'My Object' }; print(getProperty.call(myObject, "name")); // "My Object" myObject.getProperty = getProperty; print(myObject.getProperty("name")); // "My Object" getProperty.name = "The getProperty() function"; getProperty.getProperty = getProperty; getProperty.getProperty("name"); // "The getProperty() function"
Обратите внимание на важную вещь, что в Qt Script, в отличие от C++ и Java, объект this не является частью области видимости исполнения (execution scope). Это означает, что функции-члены (т.е., функции, которые оперируют с this) должны всегда использовать ключевое слов this для доступа к свойствам объекта. Например, следующий сценарий вероятно делает не то, что вы хотите:
var o = { a: 1, b: 2, sum: function() { return a + b; } }; print(o.sum()); // ошибка ссылки, или сумма глобальных переменных a и b!!
Вы получаете сообщение об ошибке ссылки, в которой говорится что 'a is not defined' или, хуже, две полностью несвязанных глобальных переменных a и b будут использованы для выполнения вычислений, если они существуют. Вместо этого сценарий должен выглядеть примерно так:
var o = { a: 1, b: 2, sum: function() { return this.a + this.b; } }; print(o.sum()); // 3
Случайно пропущенное ключевое слово this - типичная источник ошибок для программистов, которые используют правила областей видимости C++ и Java.
Qt Script предоставляет QScriptEngine::newFunction() как способ обёртывания указателя на функцию C++; это позволяет вам реализовать функцию в C++ и добавить её в окружение сценария, так что сценарии могут вызывать вашу функцию как если бы она была "обычной" функцией сценария. Вот как можно написать на C++ предыдущую функцию getProperty():
QScriptValue getProperty(QScriptContext *ctx, QScriptEngine *eng) { QString name = ctx->argument(0).toString(); return ctx->thisObject().property(name); }
Вызывайте QScriptEngine::newFunction() для обёртывания функции. В результате получится специальный тип объекта функции, который содержит внутри себя указатель на функцию C++. После того, как получившаяся обёртка будет добавлена в окружения сценария (например, установив его как свойство глобального объекта), сценарии могут вызывать функцию не зная и не беспокоясь о том, что это, фактически, "родная" функция.
Обратите внимание на то, что имя функции C++ не важно в смысле работы со сценариями; имя, по которому функция вызывается сценариями, зависит только от того, что вы вызываете свойство объекта сценария в котором вы сохранили обёртку функции.
В настоящее время невозможно обёртывать фукнции-члены; т.е., методы класса C++, которым требуется объект this.
QScriptContext содержит всё состояние, связанное с конкретным вызовом вашей функции. Посредством QScriptContext вы можете:
В следующих разделах объясняется, как воспользоваться этой функциональностью.
Две вещи нелишне отметить об аргументах функции:
Итак: Qt Script автоматически не выставляет какие-либо ограничения на количество или тип аргументов, содержащиеся в вызове функции.
"Родная" функция Qt Script похожа на функцию сценария, которая не определяет формальных параметров и использует для обработки аргументов только встроенную переменную arguments. Чтобы это увидеть, давайте сначала рассмотрим, как сценарий обычно определяет функцию add(), которая получает два аргумента, складывает их и возвращает результат:
function add(a, b) { return a + b; }
Когда функция сценария определена с формальными параметрами, чьи имена можно рассматривать как простые псевдонимы свойств объекта arguments; например, в теле определения функции add(a, b) a и arguments[0] ссылаются на одну и ту же переменную. Это означает, что эквивалент функции add() можно записать примерно так:
function add() { return arguments[0] + arguments[1]; }
Это позднее образует близкое совпадение с "родной" реализацией, обычно выглядящей так:
QScriptValue add(QScriptContext *ctx, QScriptEngine *eng) { double a = ctx->argument(0).toNumber(); double b = ctx->argument(1).toNumber(); return a + b; }
Вновь вспомним, что присутствие (или недостаток) имен формальных параметров в определении функции не влияет на то, как функция может быть вызвана; add(1, 2, 3) допускается механизмом также как и add(42). В случае с функцией add() ей действительно необходимы два аргумента для того, чтобы сделать что-нибудь полезное. Это можно выразить определением сценария, как изложено ниже:
function add() { if (arguments.length != 2) throw Error("add() takes exactly two arguments"); return arguments[0] + arguments[1]; }
Это приведёт к возбуждению ошибки если сценарий вызовет add() с количеством аргументов, отличающихся от двух. "Родную" функцию можно модифицировать для выполнения такой проверки:
QScriptValue add(QScriptContext *ctx, QScriptEngine *eng) { if (ctx->argumentCount() != 2) return ctx->throwError("add() takes exactly two arguments"); double a = ctx->argument(0).toNumber(); double b = ctx->argument(1).toNumber(); return a + b; }
Кроме того, ожидая определённое количество аргументов, функция может ожидать, что это аргументы определённых типов (например, что первый аргумент является числом, а второй - строкой). Такая функция будет явно проверять тип аргументов и/или выполнять преобразование или возбуждать ошибку, если тип аргумента является несовместимым.
В данной ситуации, "родная" реализация функции add(), показанная выше, не имеет точно такой же семантики что и прототип сценария; это потому, что поведение оператора + Qt Script зависит от типов операндов (например, если один из операндов будет строкой, то выполняется конкатенация строк). Чтобы придать функции сценария строгую семантику (то есть, она будет складывать только числовые операнды), типы аргументов можно проверять:
function add() { if (arguments.length != 2) throw Error("add() takes exactly two arguments"); if (typeof arguments[0] != "number") throw TypeError("add(): first argument is not a number"); if (typeof arguments[1] != "number") throw TypeError("add(): second argument is not a number"); return arguments[0] + arguments[1]; }
Тогда вызов подобный add("foo", new Array()) приведёт к возбуждению ошибки.
Версия на C++ может вызвать QScriptValue::isNumber() для выполнения простейших проверок:
QScriptValue add(QScriptContext *ctx, QScriptEngine *eng) { if (ctx->argumentCount() != 2) return ctx->throwError("add() takes exactly two arguments"); if (!ctx->argument(0).isNumber()) return ctx->throwError(QScriptContext::TypeError, "add(): first argument is not a number"); if (!ctx->argument(1).isNumber()) return ctx->throwError(QScriptContext::TypeError, "add(): second argument is not a number"); double a = ctx->argument(0).toNumber(); double b = ctx->argument(1).toNumber(); return a + b; }
В менее строгой реализации сценария можно разрешить выполнение явного преобразования в числа перед применением оператора +:
function add() { if (arguments.length != 2) throw Error("add() takes exactly two arguments"); return Number(arguments[0]) + Number(arguments[1]); }
В "родной" реализации, это эквивалентно вызову QScriptValue::toNumber() без выполнения сначала каких-либо проверок типов, поскольку QScriptValue::toNumber() будет, если необходимо, автоматически выполнять преобразование типов.
Чтобы проверить является ли аргумент объектом определённого типа (класса), сценарии могут использовать оператор instanceof (например, вычисление "arguments[0] instanceof Array" даст, если первый аргумент является объектом массива); "родные" функции можно вызвать QScriptValue::instanceOf().
Чтобы проверить является ли аргумент пользовательским типом C++, используйте qscriptvalue_cast() и проверяйте правильность результата. Для типов объектов это означает приведение к типу указателя и проверяет не является ли он нулевым; для типов значений класс будет иметь isNull(), isValid() или аналогичный метод. В качестве альтернативы, так как большинство пользовательских типов переводится в QVariant'ты, вы можете проверить является ли значение сценария QVariant используя QScriptValue::isVariant(), и затем проверить может ли быть конвертирован QVariant в ваш тип используя QVariant::canConvert().
Поскольку присутствует встроенный объект arguments, реализация функций, которые получают переменное количество аргументов, проста. Фактически, как мы увидели, в техническом смысле все функции Qt Script могут рассматриваться как функции с переменным количеством аргументов (variable-argument). В качестве примера, рассмотрим функцию concat(), которая принимает произвольное количество аргументов, преобразует аргументы к их строковому представлению и сцепляет полученные строки; например, concat("Qt", " ", "Script ", 101) вернёт "Qt Script 101". Определение concat() в сценарии может выглядеть так:
function concat() { var result = ""; for (var i = 0; i < arguments.length; ++i) result += String(arguments[i]); return result; }
Вот эквивалентная "родная" реализация:
QScriptValue concat(QScriptContext *ctx, QScriptEngine *eng) { QString result = ""; for (int i = 0; i < ctx->argumentCount(); ++i) result += ctx->argument(i).toString(); return result; }
Второй вариант используйте для переменного количества аргументов для реализации необязательных аргументов. Вот как обычно делается определение сценария:
function sort(comparefn) { if (comparefn == undefined) comparefn = /* the built-in comparison function */; else if (typeof comparefn != "function") throw TypeError("sort(): argument must be a function"); ... }
А вот "родной" эквивалент:
QScriptValue sort(QScriptContext *ctx, QScriptEngine *eng) { QScriptValue comparefn = ctx->argument(0); if (comparefn.isUndefined()) comparefn = /* the built-in comparison function */; else if (!comparefn.isFunction()) return ctx->throwError(QScriptContext::TypeError, "sort(): argument is not a function"); ... }
Третий вариант используйте для перемененного количества аргументов симуляции перегрузок C++. Это требует проверки количества аргументов и/или их типа в начале тела функции (как уже показано), и соответственно действовать. Следует дважды подумать перед тем как так делать, и вместо этого поддерживать уникальные имена функции; например, иметь разные функции processNumber(number) и processString(string) лучше, чем общую функцию process(anything). На стороне вызывающего это не позволит сценариям случайно вызвать неправильную перегрузку (поскольку они не знают или не понимают ваши пользовательские сложные правила разрешения перегрузки), а на стороне вызываемого вы избегаете необходимости в потенциально сложных (читай: подверженных ошибкам) проверкам для разрешения неопределённости.
Многие "родные" функции использует функцию QScriptContext::argument() для доступа к аргументам функции. Однако, можно получить доступ к самому встроенному объекту arguments (на него ссылается переменная arguments в коде сценария), вызывая функцию QScriptContext::argumentsObject(). Он имеет три основных применения:
function foo() { // Позволим bar() беспокоиться об этом. print("calling bar() with " + arguments.length + "arguments"); var result = return bar.apply(this, arguments); print("bar() returned" + result); return result; }
Например, foo(10, 20, 30) будет иметь результатом функцию foo(), выполняющую эквивалент для bar(10, 20, 30). Это полезно если вы хотите выполнить некоторую специальную предварительную или последующую обработку при вызове функции (например, чтобы протоколировать вызов bar() без изменения самой функции bar(), как в примере выше), или если вы хотите вызвать "базовую реализацию" из прототипа функции, которая имеет точно такую же "сигнатуру". В C++ пересылка функции (forwarding function) может выглядеть примерно так:
QScriptValue foo(QScriptContext *ctx, QScriptEngine *eng) { QScriptValue bar = eng->globalObject().property("bar"); QScriptValue arguments = ctx->argumentsObject(); qDebug() << "calling bar() with" << arguments.property("length").toInt32() << "arguments"; QScriptValue result = bar.apply(ctx->thisObject(), arguments); qDebug() << "bar() returned" << result.toString(); return result; }
Некоторые функции сценария являются конструкторами; они предназначены для инициализации новых объектов. Следующий фрагмент является небольшим примером:
function Book(isbn) { this.isbn = isbn; } var coolBook1 = new Book("978-0131872493"); var coolBook2 = new Book("978-1593271473");
В функциях конструкторов нет ничего особенного. Фактически, любая функция сценария может действовать как функция конструктора (т.е., любая функция может выступать в качестве операнда для new). Некоторые функции ведут себя по-разному в зависимости от того, вызываются ли они как часть выражения new или нет; например, выражение new Number(1) создаст объект Number, хотя Number("123") выполнит преобразование типов. Другие функции, наподобие Array(), всегда создают и инициализируют новый объект (например, new Array() и Array() имеют одинаковый эффект).
"Родная" функция Qt Script может вызывать функцию QScriptContext::isCalledAsConstructor() чтобы определить вызывалась ли она как конструктор или как обычная функция. Когда функция вызывается как конструктор (т.е., это операнд в выражении new), это имеет два важных вывода:
Когда QScriptContext::isCalledAsConstructor() возвращает false то, как ваш конструктор обрабатывает этот случай зависит от того какое вы поведение желаете получить. Если, аналогично встроенной функции Number(), явный вызов функция будет выполнять преобразование типов своих аргументов, тогда вы выполните преобразование и возвращаете результат. Если, с другой стороны, вы хотите, чтобы ваш конструктор вёл себя как если бы он был вызван как конструктор (с помощью new), вы должны явно создать новый объект (то есть, игнорируя объект this), инициализировать этот объект и вернуть его.
В следующем примере реализован функция конструктора, которая всегда создаёт и инициализирует новый объект:
QScriptValue Person_ctor(QScriptContext *ctx, QScriptEngine *eng) { QScriptValue object; if (ctx->isCalledAsConstructor()) { object = ctx->thisObject(); } else { object = eng->newObject(); object.setPrototype(ctx->callee().property("prototype")); } object.setProperty("name", ctx->argument(0)); return object; }
Получив этот конструктор, сценарии будут способны использовать или выражение new Person("Bob") или выражение Person("Bob") для создания нового объекта Person; оба ведут себя одинаково.
Для функции, определённой в коде сценария, нет эквивалентного способа установить вызывалась ли она из конструктора или нет.
Обратите внимание на то, что даже хотя это не считается хорошей идеей, не ничего, что остановит вас от игнорирования созданного по умолчанию объекта (this) при вызове вашей функции как конструктора и создания вашего собственного объекта в любом случае; просто имейте конструктор, возвращающий этот объект. Объект будет "перегружен" объект по умолчанию который механизм создаёт (т.е., объект по умолчанию будет просто отвергнут внутри).
Даже если функция является глобальной — т.е., не связана с каким-либо определённым (типом) объектом — вы можете по-прежнему хотите связать с ним некоторые данные, так что он становится самодостаточным (self-contained); например, функция имеет указатель на некоторый ресурс C++, к которому нужно получить доступ. Если ваше приложение использует только один механизм сценария, или тот же ресурс C++ может/будет использоваться совместно всеми механизмами сценариев, вы можете просто использовать статическую переменную C++ и получить доступ к ней изнутри "родной" функции Qt Script.
В случае, когда статическая переменная C++ или класс-одиночка (singleton) не подходит, вы можете вызвать QScriptValue::setProperty() на объекте функции, но имейте ввиду, что эти свойства также будут доступны из кода сценария. Альтернативой является использование QScriptValue::setData(); эти данные не являются доступны из сценария. Реализация может получить доступ к этим внутренним данным через функцию QScriptContext::callee(), которая возвращает вызываемый объект функции. В следующем примере показано, как это можно использовать:
QScriptValue rectifier(QScriptContext *ctx, QScriptEngine *eng) { QRectF magicRect = qscriptvalue_cast<QRectF>(ctx->callee().data()); QRectF sourceRect = qscriptvalue_cast<QRectF>(ctx->argument(0)); return eng->toScriptValue(sourceRect.intersected(magicRect)); } ... QScriptValue fun = eng.newFunction(rectifier); QRectF magicRect = QRectF(10, 20, 30, 40); fun.setData(eng.toScriptValue(magicRect)); eng.globalObject().setProperty("rectifier", fun);
Как упоминалось ранее, объект функции может быть передан как аргумент в другую функцию; естественно, это также верно для "родных" функций. В качестве примера, вот "родная" функция сравнения, которая сравнивает свои два аргумента как числа:
QScriptValue myCompare(QScriptContext *ctx, QScriptEngine *eng) { double first = ctx->argument(0).toNumber(); double second = ctx->argument(1).toNumber(); int result; if (first == second) result = 0; else if (first < second) result = -1; else result = 1; return result; }
Вышеприведённая функция может быть передана в качестве аргумента в стандартную функцию Array.prototype.sort для численной сортировки массива, как иллюстрирует следующий код C++:
QScriptEngine eng; QScriptValue comparefn = eng.newFunction(myCompare); QScriptValue array = eng.evaluate("new Array(10, 5, 20, 15, 30)"); array.property("sort").call(array, QScriptValueList() << comparefn); // печатает "5,10,15,20,30" qDebug() << array.toString();
Обратите внимание на то, что в этом случае, мы действительно рассматриваем объект "родной" функции как значение — т.е., мы не сохраняем его как свойство окружения сценария — мы просто передаём его в качестве "безымянного" аргумента в другую функцию сценария и затем забываем о нём.
Вызов каждой функции Qt Script имеет объект activation, связанный с ней; этот объект доступен через функцию QScriptContext::activationObject(). Объект activation - объект сценария, чьи свойства являются локальными переменными, связанными с вызовом (включая аргументы, для которых функция сценария имеет соответствующее имя формального параметра). Таким образом, получая, модифицируя, создавая и удаляя локальные переменные из C++ выполняется с использованием обычных функций QScriptValue::property() и QScriptValue::setProperty(). Сам объект activation непосредственно из кода сценария не доступен (но он неявно доступен всякий раз, когда читается или записывается локальная переменная).
Для кода C++, имеется две основные формы объекта activation:
QScriptContext *ctx = eng.pushContext(); QScriptValue act = ctx->activationObject(); act.setProperty("digit", 7); qDebug() << eng.evaluate("digit + 1").toNumber(); // 8 eng.popContext();
Мы создали временный контекст выполнения, создали для этого локальную переменную, вычислили сценарий и в заключение восстановили старый контекст.
Это дополнительная тема; можете спокойно пропустить её.
Вложенная функция может быть использован для "захватить" контекста выполнения, в котором создавался объект вложенной функции; на это обычно ссылаются при создании замыкания. Когда, немного позднее, вызывается вложенная функция, она может получить доступ к переменным, которые были созданы когда вызывалась охватывающая функция. Это возможно лучше проиллюстрировать с помощью небольшого примера:
function counter() { var count = 0; return function() { return count++; } }
Функция counter() инициализирует локальную переменную нулем и возвращается вложенную функцию. Вложенная функция инкрементирует "внешнюю" переменную и возвращает её новое значение. Переменная продолжает существовать между вызовами функции, как показано в следующем примере:
var c1 = counter(); // создаём новую функцию счётчика var c2 = counter(); // создаём новую функцию счётчика print(c1()); // 0 print(c1()); // 1 print(c2()); // 0 print(c2()); // 1
Функция counter() может быть реализована также как "родная" функция — точнее, как пара "родных" функций: Одна - для внешней и одна - для внутренней. Определение внешней функции следующее:
QScriptValue counter(QScriptContext *ctx, QScriptEngine *eng) { QScriptValue act = ctx->activationObject(); act.setProperty("count", 0); QScriptValue result = eng->newFunction(counter_inner); result.setScope(act); return result; }
Функция создает локальную переменную и инициализирует ее нулем. Затем она обёртывает внутреннюю "родную" функцию и устанавливает область видимости получившегося объекта функции к объекту activation, связанный с этим вызовом (внешней) функции. Внутренняя функция имеют доступ к "внутреннему" объекту activation через область видимости вызываемого:
QScriptValue counter_inner(QScriptContext *ctx, QScriptEngine *eng) { QScriptValue outerAct = ctx->callee().scope(); double count = outerAct.property("count").toNumber(); outerAct.setProperty("count", count+1); return count; }
Возможен также гибридный подход, где внешняя функция является "родной" функцией, а внутренняя функция определена сценарием:
QScriptValue counter_hybrid(QScriptContext *ctx, QScriptEngine *eng) { QScriptValue act = ctx->activationObject(); act.setProperty("count", 0); return eng->evaluate("function() { return count++; }"); }
Свойство объекта сценария может быть определено на основе функции получения/установки значения, подобно тому как связаны со свойством C++ Qt функции чтения и записи. Это делает возможным использовать в сценарии выражений наподобие object.x вместо object.getX(); функция получения/установки значения для x будет неявно вызываться всякий раз, когда получается доступ к свойству. Для сценариев свойство выглядит и ведёт себя точно так, как свойство обычного объекта.
Единственная функция Qt Script может действовать для свойства и как геттер, и как сеттер. Когда она вызывается как геттер, количество аргументов равно 0. Когда она вызывается как сеттер, количество аргументов равно 1; аргументом является новое значение свойства. В следующем примере мы определяем "родную" функцию сочетающую получение/установку значения, которая немного преобразовывает значение:
QScriptValue getSet(QScriptContext *ctx, QScriptEngine *eng) { QScriptValue obj = ctx->thisObject(); QScriptValue data = obj.data(); if (!data.isValid()) { data = eng->newObject(); obj.setData(data); } QScriptValue result; if (ctx->argumentCount() == 1) { QString str = ctx->argument(0).toString(); str.replace("Roberta", "Ken"); result = str; data.setProperty("x", result); } else { result = data.property("x"); } return result; }
В примере используются внутренние данные объекта для сохранения и получения преобразованного значения. В качестве альтернативы, свойство может быть сохранено в другом, "скрытом" свойстве самого объекта (например, __x__). "Родная" функция свободна для реализации любой желаемой схемы сохранения до тех пор, пока внешнее поведение самого свойства является согласованным (например, что сценарии не смогут отличить их от обычного свойства).
Следующий код C++ показывает, как свойство объекта может быть определено в терминах "родного" геттера/сеттера:
QScriptEngine eng; QScriptValue obj = eng.newObject(); obj.setProperty("x", eng.newFunction(getSet), QScriptValue::PropertyGetter|QScriptValue::PropertySetter);
Когда получают доступ к свойству, как в следующем сценарии, геттер/сеттер выполняют свою работу перед сценами:
obj.x = "Roberta sent me"; print(obj.x); // "Ken sent me" obj.x = "I sent the bill to Roberta"; print(obj.x); // "I sent the bill to Ken"
Замечание: Важно, что функция установки значения, а не только геттер, возвращает значение свойства; т.е., сеттер не будет возвращать QScriptValue::UndefinedValue. Это потому, что результат присвоения свойства является значением, возвращаемым сеттером, а не выражение справа. Также обратите внимание на то, что вы обычно не будете пытаться читать то же свойство, которое геттер модифицирует внутри самого геттера, так как это вызовет рекурсивный вызов геттера.
Вы можете удалить геттер/сеттер свойства с помощью вызова QScriptValue::setProperty(), и передавая неправильный QScriptValue в качестве геттера/сеттера. Не забудьте указать флаг(и) QScriptValue::PropertyGetter/QScriptValue::PropertySetter, в противном случае единственная вещь, которая произойдёт, - будет вызван сеттер с неправильным QScriptValue в качестве аргумента!
Геттеры и сеттеры свойства можно определить и установить в коде сценария также, как в следующем примере:
obj = {}; obj.__defineGetter__("x", function() { return this._x; }); obj.__defineSetter__("x", function(v) { print("setting x to", v); this._x = v; }); obj.x = 123;
Геттеры и сеттеры могут быть использованы только для реализации "априорных свойств"; т.е., приём не может быть использован для влияния на доступ к свойству, который объект имеет не всегда. Чтобы получить полный контроль над доступом к свойству этим способом, вам нужно создать подкласс QScriptClass.
В ECMAScript, наследование базируется на концепции совместно используемых объектов-прототипов; это полностью отличается от наследования на основе классов, привычного для программистов C++. С QtScript, вы можете связать пользовательский объект-прототип с типом C++ используя QScriptEngine::setDefaultPrototype(); это является ключевым для предоставления интерфейса сценария для этого типа. Так как модуль QtScript построен на основе системы мета-типов Qt, это можно сделать для любого типа C++.
Вас может это порадовать, когда вам понадобится использовать именно эту функциональность в вашем приложении; разве недостаточно автоматических привязок, предоставляемых QScriptEngine::newQObject()? Нет, не во всех случаях. Во-первых, не все типы C++ унаследованы от QObject; типы, которые не являются объектами QObject, не могут быть подвергнуты интроспекции посредством мета-объектной системы Qt (они не имеют свойств, сигналов и слотов). Во-вторых, даже если тип является унаследованным от QObject, функциональность, которую вы хотите открыть для сценариев, должна быть доступна не вся, так как будет странным определять каждую функцию как слот (и не всегда возможно/желательно изменять C++ API чтобы сделать так).
Вполне возможно для решения этой проблемы использовать приёмы "стандартного" C++. Например, класс QRect можно эффективно сделать способным работать со сценариями создав класс-обёртку C++ на основе QObject с помощью свойств x, y, width и так далее, который переадресует доступ к свойству и вызовы функции к обёрнутому значению. Однако, как мы увидели, получая преимущество объектной модели ECMAScript и сочетая её с мета-объектной системой Qt, мы можем придти к более элегантному, последовательному и лёгкому решению, поддерживаемому небольшим API.
Этот раздел объясняет базовые концепции наследования, основанного на прототипах. Поняв эти концепции, связанные приёмы работы могут быть применены по всему QtScript API для того, чтобы создать удобные, согласованные привязки к C++, которые будут хорошо подходит для области ECMAScript.
При экспериментировании с объектами QtScript и наследованием может быть полезным использование интерактивного интерпретатора, включённого в Примеры Qt Script, расположенного в examples/script/qscript.
В QtScript назначение объекта-прототипа заключается в определении поведения, которое будет совместно использоваться набором объектов QtScript. Мы говорим, что объекты которые используют совместно одни и те же объекты-прототипы принадлежат одному и тому же классу (кроме того, с технической стороны они не будут смешиваться с классами, создаваемыми языками наподобие C++ и Java; ECMAScript не имеет такой конструкции).
Базовый механизм наследования, основанного на прототипах, работает следующим образом: Каждый объект QtScript имеет внутреннюю ссылку на другой объект, его прототип. Когда в объекте ищется свойство, а сам объект не имеет свойства, то вместо этого свойство ищется в объекте-прототипе; если прототип имеет свойство, тогда возвращается это свойство. В противном случае, свойство ищется в прототипе объекта-прототипа, и так далее; эта цепочка объектов составляет последовательность прототипов. Последовательность объектов-прототипов прослеживается до тех пор, пока свойство не найдётся или не будет достигнут конец цепочки.
Например, когда вы создаёте новый объект с помощью выражения new Object(), результирующий объект будет иметь своим прототипом стандартный прототип Object, Object.prototype; посредством этих отношений прототипов, новый объект наследует набор свойств, включая функцию hasOwnProperty() и функцию toString():
var o = new Object(); o.foo = 123; print(o.hasOwnProperty('foo')); // true print(o.hasOwnProperty('bar')); // false print(o); // вызывает o.toString(), которая возвращает "[object Object]"
Сама функция toString() не определена в o (так как мы ничего не присвоили o.toString), таким образом вместо функции toString() стандартного объекта Object вызывается прототип, который возвращает сильно обобщённое строковое представление o ("[object Object]").
Обратите внимание на то, что свойства объекта не копируются в новый объект; поддерживается только ссылка из нового объекта на объект прототипа. Это означает, что изменения, произведённые в объекте-прототипе, немедленно отразятся на поведении всех объектов, для которых модифицированный объект является прототипом.
В QtScript класс не определяется явно; ключевого слова class нет. Вместо этого вы определяете новый класс в два этапа:
С таким расположением, открытое свойство prototype конструктора будет автоматически установлено в качестве прототипа объектов, созданных применением оператора new к вашей функции конструктора; например, прототипом объекта, созданного new Foo(), будет значение Foo.prototype.
Функции, которые не работают с объектом this ("статические" методы) обычно сохраняются как свойства функции конструктора, а не как свойства объекта-прототипа. Тоже самое верно для констант, например, значений перечисления.
В следующем коде определяется простая функция конструктора для класса с именем Person:
function Person(name) { this.name = name; }
Затем вы хотите настроить Person.prototype как ваш объект-прототип; т.е., определить интерфейс, который будет общим для всех объектов Person. QtScript автоматически создаёт объект-прототип по умолчанию (с помощью выражения new Object()) для каждой функции сценария; вы можете добавить свойства в этот объект, или вы можете присвоить ваш собственный пользовательский объект. (Вообще говоря, любой объект QtScript может действовать как прототип для любого другого объекта.)
Вот пример того, как вы можете перегрузить функцию toString(), которая унаследовала Person.prototype от Object.prototype, чтобы дать вашим объектам Person более подходящие строковые представления:
Person.prototype.toString = function() { return "Person(name: " + this.name + ")"; }
Это имеет сходство с процессом переопределения виртуальной функции в C++. Впредь, когда ищется прототип с именем toString в объекте Person, он будет разрешён в Person.prototype, а не в Object.prototype как до этого:
var p1 = new Person("John Doe"); var p2 = new Person("G.I. Jane"); print(p1); // "Person(name: John Doe)" print(p2); // "Person(name: G.I. Jane)"
Имеется еще несколько других интересных вещей, которые узнали об объекте Person:
print(p1.hasOwnProperty('name')); // 'name' - переменная экземпляра, поэтому это вернёт true print(p1.hasOwnProperty('toString')); // возвращает false; унаследовано от прототипа print(p1 instanceof Person); // истина print(p1 instanceof Object); // истина
Функция hasOwnProperty() не унаследована от Person.prototype, а точнее от Object.prototype, который сам является прототипом для Person.prototype; т.е., цепочка прототипов объектов Person - это Person.prototype следом за Object.prototype. Эта цепочка прототипов устанавливает иерархию классов, как продемонстрирует применение оператора instanceof; instanceof проверяет значение открытого свойства prototype функции конструктора с правой стороны - достигается ли следуя по цепочке прототипов объекта с левой стороны.
Когда определяются подклассы, имеется общий шаблон, который вы можете использовать. В следующем примере показано, как он может создать подкласс Person вызывая Employee:
function Employee(name, salary) { Person.call(this, name); // вызывает базовый конструктор this.salary = salary; } // устанавливает прототип экземпляра базового класса Employee.prototype = new Person(); // инициализирует прототип Employee.prototype.toString = function() { ... }
С другой стороны, вы можете использовать instanceof для проверки того, что отношения между классами Employee и Person были установлены правильно:
var e = new Employee("Johnny Bravo", 5000000); print(e instanceof Employee); // истина print(e instanceof Person); // истина print(e instanceof Object); // истина print(e instanceof Array); // ложь
Это показывает, что цепочка прототипов объектов Employee такая же что и у объектов Person, но с Employee.prototype добавленным в головную часть цепочки.
Вы можете использовать QScriptEngine::newFunction() для обёртывания "родных" функций. При реализации функции конструктора вы также передаёте объект-прототип в качестве аргумента в QScriptEngine::newFunction(). Вы можете вызывать QScriptValue::construct() для вызова функции конструктора, и вы можете использовать QScriptValue::call() изнутри "родной" функции конструктора если вам нужно вызвать конструктор базового класса.
Класс QScriptable предоставляет удобный способ реализации объекта-прототипа на основе слотов и свойств C++. Ознакомьтесь с Примером "Default Prototypes" чтобы увидеть, как это делается. В качестве альтернативы, функциональность прототипа может реализована на основе автономных "родных" функций, которые вы обёртываете с помощью QScriptEngine::newFunction() и устанавливаете как свойства вашего объект-прототипа вызывая QScriptValue::setProperty().
В реализации функций вашего прототипа, вы используете QScriptable::thisObject() (или QScriptContext::thisObject()) для получения ссылки на QScriptValue, с которой работаете; затем вы вызываете qscriptvalue_cast() для приведения его к вашему типу C++, и выполнению соответствующих операций используя для этого типа обычный C++ API.
Вы связываете объект-прототип с типом C++ вызывая QScriptEngine::setDefaultPrototype(). Однажды установив это отображение, QtScript будет автоматически присваивать правильный прототип когда значение типа такого типа обёрнуто в QScriptValue; когда вы явно вызываете либо QScriptEngine::toScriptValue(), либо когда значение такого типа возвращается из слота C++ и передаётся изнутри механизмом обратно в код сценария. Это означает, что вы не можете реализовать классы-обёртки, если используете этот подход.
В качестве примера давайте рассмотрим, как класс Person из предыдущего раздела может быть реализован на основе Qt Script API. Начнём с "родной" функции конструктора:
QScriptValue Person_ctor(QScriptContext *context, QScriptEngine *engine) { QString name = context->argument(0).toString(); context->thisObject().setProperty("name", name); return engine->undefinedValue(); }
Вот "родной" эквивалент функции Person.prototype.toString, которую мы видели раньше:
QScriptValue Person_prototype_toString(QScriptContext *context, QScriptEngine *engine) { QString name = context->thisObject().property("name").toString(); QString result = QString::fromLatin1("Person(name: %0)").arg(name); return result; }
Класс Person может быть затем инициализирован следующим образом:
QScriptEngine engine; QScriptValue ctor = engine.newFunction(Person_ctor); ctor.property("prototype").setProperty("toString", engine.newFunction(Person_prototype_toString)); QScriptValue global = engine.globalObject(); global.setProperty("Person", ctor);
Реализация подкласса Employee является аналогичной. Мы используем QScriptValue::call() для вызова конструктора суперкласса (Person):
QScriptValue Employee_ctor(QScriptContext *context, QScriptEngine *engine) { QScriptValue super = context->callee().property("prototype").property("constructor"); super.call(context->thisObject(), QScriptValueList() << context->argument(0)); context->thisObject().setProperty("salary", context->argument(1)); return engine->undefinedValue(); }
Класс Employee может быть затем инициализирован следующим образом:
QScriptValue empCtor = engine.newFunction(Employee_ctor); empCtor.setProperty("prototype", global.property("Person").construct()); global.setProperty("Employee", empCtor);
При реализации объекта-прототипа класса, вы можете захотеть использовать класс QScriptable, поскольку он позволяет вам определять API вашего класса сценария на основе свойств Qt, сигналов и слотов, а также автоматически обрабатывать преобразование значений между Qt Script и C++.
При реализации объекта-прототипа для типа, основанного на значениях, -- например, QPointF -- применяется тот же общий приём; вы заполняете объект-прототип функциональностью, которая будет совместно использоваться экземплярами класса. Затем связываем объект-прототип с типом с помощью вызова QScriptEngine::setDefaultPrototype(). Это гарантирует, что когда например значение соответствующего типа возвращается из слота в сценарий, ссылка прототипа на значение сценария будет правильно инициализирована.
Когда значения пользовательского типа сохраняются в объектах QVariant -- которые Qt Script использует по умолчанию --, qscriptvalue_cast() позволяет вам безопасно привести значение сценария к типу указателя C++. Это облегчает контроль типов type-checking, и, для функций прототипа, которые будут модифицировать базовое значение C++, позволяет вам модифицировать фактическое значение, содержащееся в значении сценария (а не копировать его).
Q_DECLARE_METATYPE(QPointF) Q_DECLARE_METATYPE(QPointF*) QScriptValue QPointF_prototype_x(QScriptContext *context, QScriptEngine *engine) { // Поскольку точка не изменилась, будет нормально привести к значению здесь QPointF point = qscriptvalue_cast<QPointF>(context->thisObject()); return point.x(); } QScriptValue QPointF_prototype_setX(QScriptContext *context, QScriptEngine *engine) { // Приводим к типу указателя, чтобы иметь возможность модифицировать базовое значение C++ QPointF *point = qscriptvalue_cast<QPointF*>(context->thisObject()); if (!point) return context->throwError(QScriptContext::TypeError, "QPointF.prototype.setX: this object is not a QPointF"); point->setX(context->argument(0).toNumber()); return engine->undefinedValue(); }
Вы можете реализовать функцию конструктора для типа, основанного на значениях, обернув "родную" функцию-фабрику. Например, в следующей функции реализован простой конструктор для QPoint:
QScriptValue QPoint_ctor(QScriptContext *context, QScriptEngine *engine) { int x = context->argument(0).toInt32(); int y = context->argument(1).toInt32(); return engine->toScriptValue(QPoint(x, y)); } ... engine.globalObject().setProperty("QPoint", engine.newFunction(QPoint_ctor));
В вышеприведенном коде мы немного упростили вещи, например, мы не проверяем количество аргументов, чтобы решить который C++ конструктор QPoint для использования. В ваших собственных конструкторах вы сами можете сделать этот тип разрешения, т.е. проверяя количество аргументов, переданных в "родную" функцию, и/или проверяя тип аргументов и преобразовав аргументы к требуемому типу. Если вы обнаружили проблему с аргументами, вы можете захотеть сигнализировать об этом возбудив исключение сценария; смотрите QScriptContext::throwError().
Для типов, основанных на значениях (например, QPoint), объект C++ будет разрушен при объект Qt Script попадёт под сборку мусора, поэтому управление памятью объекта C++ не представляет проблем. Для объектов QObject, Qt Script предоставляет несколько альтернатив для управления базовым временем жизни объекта C++; смотрите раздел Управление владением QObject. Однако, для полиморфных типов это не унаследовано от QObject, и когда вы не можете (или не хотите) обернуть тип в QObject, вы можете сами управлять временем жизни объекта C++.
Когда объект Qt Script обёртывает объект C++ ведёт себя надлежаще - объект C++ удаляется, когда объект Qt Script удалён сборщиком мусора; это обычный случай, когда объекты могут быть созданы сценариями, в отличие от приложения, предоставляющего сценарии с предварительно созданными объектами "окружения". Способ сделать время жизни объекта C++ отслеживает время жизни объекта Qt Script - использовать класс совместно используемого указателя, например, QSharedPointer, для хранения указателя на ваш объект; когда объект Qt Script содержащий в себе QSharedPointer удалён сборщиком мусора, базовый объект C++ будет удалён, если нет других ссылок на этот объект.
Следующий фрагмент показывает функцию конструктора, которая создаёт объекты QXmlStreamReader, которые сохраняются с использованием QSharedPointer:
typedef QSharedPointer<QXmlStreamReader> XmlStreamReaderPointer; Q_DECLARE_METATYPE(XmlStreamReaderPointer) QScriptValue constructXmlStreamReader(QScriptContext *context, QScriptEngine *engine) { if (!context->isCalledAsConstructor()) return context->throwError(QScriptContext::SyntaxError, "please use the 'new' operator"); QIODevice *device = qobject_cast<QIODevice*>(context->argument(0).toQObject()); if (!device) return context->throwError(QScriptContext::TypeError, "please supply a QIODevice as first argument"); // Создаём объект C++ QXmlStreamReader *reader = new QXmlStreamReader(device); XmlStreamReaderPointer pointer(reader); // сохраняем совместно используемый указатель в создаваемом нами объекте сценария return engine->newVariant(context->thisObject(), qVariantFromValue(pointer)); }
Функции прототипа могут использовать qscriptvalue_cast() для приведения объекта this к соответствующему типу:
QScriptValue xmlStreamReader_atEnd(QScriptContext *context, QScriptEngine *) { XmlStreamReaderPointer reader = qscriptvalue_cast<XmlStreamReaderPointer>(context->thisObject()); if (!reader) return context->throwError(QScriptContext::TypeError, "this object is not an XmlStreamReader"); return reader->atEnd(); }
Объект-прототип и объект конструктора настраиваются обычным образом:
QScriptEngine engine; QScriptValue xmlStreamReaderProto = engine.newObject(); xmlStreamReaderProto.setProperty("atEnd", engine.newFunction(xmlStreamReader_atEnd)); QScriptValue xmlStreamReaderCtor = engine.newFunction(constructXmlStreamReader, xmlStreamReaderProto); engine.globalObject().setProperty("XmlStreamReader", xmlStreamReaderCtor);
Сценарии теперь могут создавать объекты QXmlStreamReader вызывая конструктор XmlStreamReader, а когда объект Qt Script удаляется сборщиком мусора (или механизм сценариев разрушен), объект QXmlStreamReader также уничтожается.
Имеются случаи, когда недостаточно ни динамических привязок QObject, предоставляемых QScriptEngine::newQObject(), ни ручных привязок, предоставляемых QScriptEngine::newFunction(). Например, вы можете захотеть реализовать динамический прокси сценариев для базового объекта; или вы можете захотеть реализовать массивоподный класс (т.е. который предоставляет специальное обращение к свойствам, которые являются корректными индексами массива, и к свойству "length"). В таких случаях вы можете создать подкласс QScriptClass чтобы получить требуемое поведение.
QScriptClass позволяет вам обрабатывать доступ ко всем свойствам для (класса) объекта сценария посредством виртуальных функций получения/установки значения свойства. Перебор пользовательских свойств также поддерживается посредством класса QScriptClassPropertyIterator; это означает, что вы можете рекламировать свойства, сообщённые операторами for-in сценария и QScriptValueIterator.
О синтаксических ошибках в сценариях будет сообщаться во время вычисления сценария; QScriptEngine::evaluate() будет возвращать объект SyntaxError, который можно преобразовать в строку чтобы получить описание ошибки.
Функция QScriptEngine::uncaughtExceptionBacktrace() даёт вам обратную трассировку (backtrace) последнего не отловленного исключения в удобном для восприятия виде. Для того, чтобы получить полезную информацию об имени файла в обратной трассировке, вы будете передавать правильные имена файлов в QScriptEngine::evaluate() при выполнении ваших сценариев.
Часто исключение не происходит во время вычисления сценария, но происходит позднее, когда функция, определённая сценарием, действительно выполняется. Для обработчиков сигналов C++ это не будет надёжным; рассмотрите случай, когда сигнал clicked() кнопки соединён с функцией сценария, и эта функция сценария вызывает исключение сценария когда она обрабатывает сигнал. Как распространяется исключение сценария?
Решение - соединить с сигналом QScriptEngine::signalHandlerException(); это будет давать вам уведомление, когда обработчик сигнала вызовет исключение, так что вы можете узнать что случилось и/или выйти из него.
В Qt 4.4 был введён класс QScriptEngineAgent. QScriptEngineAgent предоставляет интерфейс для передачи нижнеуровневых "событий" в механизме сценариев, например, при входе в функцию или когда достигнут новый оператор сценария. Создавая подклассы QScriptEngineAgent вы можете получать уведомления об этих событиях и выполняет некоторое действие, если вы хотите. Сам QScriptEngineAgent не предоставляет какой-либо функциональности специфичной для отладки (например, устанавливая точки останова), но он является основой для таких инструментов.
Класс QScriptEngineDebugger введённый в Qt 4.5, предоставляет отладчик Qt Script, который можно встроить в ваше приложение.
Qt Script предоставляет встроенную функцию print(), которую удобно использовать для простой отладки. Встроенная функция print() пишет в стандартный вывод. Вы можете переопределить функцию print() (или добавить свою собственную функцию, например debug() или log()), которая перенаправит текст куда-нибудь ещё. В следующем коде показана пользовательская функция print(), которая добавляет текст в QPlainTextEdit.
QScriptValue myPrintFunction(QScriptContext *context, QScriptEngine *engine) { QString result; for (int i = 0; i < context->argumentCount(); ++i) { if (i > 0) result.append(" "); result.append(context->argument(i).toString()); } QScriptValue calleeData = context->callee().data(); QPlainTextEdit *edit = qobject_cast<QPlainTextEdit*>(calleeData.toQObject()); edit->appendPlainText(result); return engine->undefinedValue(); }
В следующем коде показано, как можно инициализировать и использовать пользовательскую функцию print().
int main(int argc, char **argv) { QApplication app(argc, argv); QScriptEngine eng; QPlainTextEdit edit; QScriptValue fun = eng.newFunction(myPrintFunction); fun.setData(eng.newQObject(&edit)); eng.globalObject().setProperty("print", fun); eng.evaluate("print('hello', 'world')"); edit.show(); return app.exec(); }
Указатель на QPlainTextEdit сохраняется как внутреннее свойство самой функции сценария, так что он может быть получен при вызове функция.
Функция QScriptEngine::importExtension() может быть использован для загрузки подключаемых модулей в механизм сценариев. Подключаемые модули обычно добавляют некоторую дополнительную функциональность к механизму; например, подключаемый модуль может добавить полные привязки для API рисования Qt Arthur, так что эти классы могут быть использованы из сценариев Qt Script. В настоящее время не имеется подключаемых модулей сценариев поставляемых вместе с Qt.
Если вы реализовали некоторую функциональность Qt Script, которую вы хотите сделать доступной для использования другими разработчиками, стоит изучить разработку расширения (например, создав подкласс QScriptExtensionPlugin).
Начиная с Qt 4.5, Qt Script поддерживает инетернационализацию сценариев, построенную на функциональности C++ по интернационализации (смотрите Интернационализация с Qt).
При любых обстоятельствах ваш сценарий использует "текст в кавычках" для текста, который будет представлен пользователю, гарантирующий её обработку функцией QCoreApplication::translate(). По существу все, что необходимо для достижения этого - использовать функцию сценария qsTr(). Пример:
myButton.text = qsTr("Hello world!");
Это справедливо для 99% строк видимых пользователю которые вы захотите написать.
Функция qsTr() использует базовое имя файла сценария (смотрите QFileInfo::baseName()) в качестве контекста перевода; если в ваше проекте имя файла не является уникальным, вы должны использовать функцию qsTranslate() и передать соответствующий контекст в качестве первого аргумента. Пример:
myButton.text = qsTranslate("MyAwesomeScript", "Hello world!");
Если вам нужно иметь переводимый текст полностью за пределами функции, то чтобы помочь имеется две функции: QT_TR_NOOP() и QT_TRANSLATE_NOOP(). Они незаметно помечают текст для извлечения утилитой lupdate, описанной ниже. Во время выполнения эти функции просто возвращают текст для перевода в исходном виде.
Пример QT_TR_NOOP():
FriendlyConversation.prototype.greeting = function(type) { if (FriendlyConversation['greeting_strings'] == undefined) { FriendlyConversation['greeting_strings'] = [ QT_TR_NOOP("Hello"), QT_TR_NOOP("Goodbye") ]; } return qsTr(FriendlyConversation.greeting_strings[type]); }
Пример QT_TRANSLATE_NOOP():
FriendlyConversation.prototype.greeting = function(type) { if (FriendlyConversation['greeting_strings'] == undefined) { FriendlyConversation['greeting_strings'] = [ QT_TRANSLATE_NOOP("FriendlyConversation", "Hello"), QT_TRANSLATE_NOOP("FriendlyConversation", "Goodbye") ]; } return qsTranslate("FriendlyConversation", FriendlyConversation.greeting_strings[type]); }
Функция String.prototype.arg() (которая сделана по образцу QString::arg()) предлагает простые способы для подстановки аргументов:
FileCopier.prototype.showProgress = function(done, total, currentFileName) { this.label.text = qsTr("%1 of %2 files copied.\nCopying: %3") .arg(done) .arg(total) .arg(currentFileName)); }
После того как вы использовали qsTr() и/или qsTranslate() по всем вашим сценариям, вы можете начать создавать переводы текста, видимого пользователю, в вашей программе.
Справочное руководство Qt Linguist предоставляет дополнительную информацию об инструментах перевода Qt, Qt Linguist, lupdate и lrelease.
Перевод сценариев Qt Script является трёхступенчатым процессом:
Обычно вы будете повторять эти шаги для каждого выпуска приложения. Утилита lupdate делает все возможное по повторному использованию переводов от предыдущих релизов.
При запуске lupdate вы должны указать расположение сценария(иев), и имя создаваемого файла .ts. Примеры:
lupdate myscript.qs -ts myscript_la.ts
будет извлекать переводимый текст из myscript.qs и создавать файл перевода myscript_la.qs.
lupdate -extensions qs scripts/ -ts scripts_la.ts
будет извлекать переводимый текст из всех файлов с именами, заканчивающимися на .qs, в каталоге scripts и создаёт файл перевода scripts_la.qs.
В качестве альтернативы, вы можете создать отдельный файл проекта qmake, который устанавливает переменные SOURCES и TRANSLATIONS, соответственно; затем запустите lupdate с файлом проекта в качестве ввода.
lrelease myscript_la.ts
При запуске lrelease, вы должны указать имя входного файла .ts; или, если вы используете файл проекта qmake для управления переводами сценария, вы указываете имя этого файла. lrelease создаст myscript_la.qm, двоичное представление перевода.
В вашем приложении, вы должны использовать QTranslator::load() для загрузки файлов перевода соответствующего пользовательскому языку, и установить их используя QCoreApplication::installTranslator(). В заключение, вы должны вызвать QScriptEngine::installTranslatorFunctions() чтобы сделать функции перевода сценария (qsTr(), qsTranslate() и QT_TR*_NOOP()) доступными для сценариев, которые впоследствии вычисляются QScriptEngine::evaluate(). Для сценариев, использующих функцию qsTr(), правильное имя файла должно быть передано в качестве второго параметра в QScriptEngine::evaluate().
linguist, lupdate и lrelease установлены в поддиректории bin базовой директории, в которой установлен Qt. Щелкните на Help|Manual в Qt Linguist для доступа к руководству пользователя; он содержит учебное пособие для начала работы.
Смотрите также Пример "Hello Script".
QtScript реализует все встроенные классы и функции, определённые в стандарте ECMA-262.
Функции синтаксического разбора и строковых преобразований Date реализованы с использованием QDateTime::fromString() и QDateTime::toString(), соответственно.
Класс RegExp является обёрткой вокруг QRegExp. Семантика QRegExp точно не совпадает с семантикой регулярных выражений, описанной в ECMA-262.
var o = new Object(); (o.__proto__ === Object.prototype); // это установит в истину
var o = new Object(); o.__defineGetter__("x", function() { return 123; }); var y = o.x; // 123
var o = new Object(); o.__defineSetter__("x", function(v) { print("and the value is:", v); }); o.x = 123; // будет печатать "and the value is: 123"
[Предыдущая: Модуль QtOpenGL] [Модули Qt] [Следующая: Модуль QtScriptTools]
Copyright © 2009 Nokia Corporation and/or its subsidiary(-ies) | Торговые марки | Qt 4.5.3 |
Попытка перевода Qt документации. Если есть желание присоединиться, или если есть замечания или пожелания, то заходите на форум: Перевод Qt документации на русский язык... Люди внесшие вклад в перевод: Команда переводчиков |