Связываемые свойства Qt
Qt предоставляет связываемые свойства. Связываемые свойства — это свойства, которые либо имеют значение, либо задаются с помощью любой C++ функции, как правило, лямбда-выражения C++. В случае, если они задаются с помощью C++ функции, они обновляются автоматически всякий раз, когда изменяются их зависимости.
Связываемые свойства реализованы в классе QProperty, который состоит из объекта данных и указателя на структуру данных управления, и в классе QObjectBindableProperty, который состоит только из объекта данных и использует охватывающий QObject для хранения указателя на структуру данных управления.
Вводный пример
Выражение связывания вычисляет значение, считывая другие значения QProperty. За кулисами эта зависимость отслеживается. Всякий раз, когда обнаруживается изменение зависимости любого свойства, выражение связывания переоценивается, и новый результат применяется к свойству. Например:
QProperty<QString> firstname("John");
QProperty<QString> lastname("Smith");
QProperty<int> age(41);
QProperty<QString> fullname;
fullname.setBinding([&]() { return firstname.value() + " " + lastname.value() + " age: " + QString::number(age.value()); });
qDebug() << fullname.value(); // Prints "John Smith age: 41"
firstname = "Emma"; // Triggers binding reevaluation
qDebug() << fullname.value(); // Prints the new value "Emma Smith age: 41"
// Birthday is coming up
age.setValue(age.value() + 1); // Triggers re-evaluation
qDebug() << fullname.value(); // Prints "Emma Smith age: 42" Когда новое значение присваивается свойству firstname, выражение связывания для fullname переоценивается. Таким образом, когда последнее qDebug() утверждение пытается прочитать значение имени свойства fullname, возвращается новое значение.
Поскольку связывания являются C++ функциями, они могут выполнять всё, что возможно в C++. Это включает вызовы других функций. Если эти функции обращаются к значениям, хранящимся в QProperty, они автоматически становятся зависимостями связывания.
Выражения связывания могут использовать свойства любого типа, поэтому в приведённом выше примере возраст — это целое число, и он включён в строковое значение с помощью преобразования в целое число, но зависимость полностью отслеживается.
Связываемые свойства: геттеры и сеттеры
Когда класс имеет связываемое свойство, используя QProperty или QObjectBindableProperty, необходимо соблюдать особую осторожность при формулировании геттеров и сеттеров для этого свойства.
Связываемые свойства: геттеры
Для обеспечения правильной работы системы автоматического отслеживания зависимостей, каждый возможный путь кода в геттере должен считывать данные из базового объекта свойства. Кроме того, свойство не должно записываться внутри геттера. Шаблоны проектирования, которые пересчитывают или обновляют что-либо в геттере, несовместимы со связываемыми свойствами.
Поэтому рекомендуется использовать только тривиальные геттеры со связываемыми свойствами.
Связываемые свойства: сеттеры
Для обеспечения правильной работы системы автоматического отслеживания зависимостей, каждый возможный путь кода в сеттере должен записывать данные в базовый объект свойства, даже если значение не изменилось.
Любой другой код в сеттере имеет высокую вероятность быть неправильным. Любой код, выполняющий обновления на основе нового значения, скорее всего, является ошибкой, поскольку этот код не будет выполняться при изменении свойства через связывание.
Поэтому рекомендуется использовать только тривиальные сеттеры со связываемыми свойствами.
Запись в связываемое свойство
Связываемые свойства информируют свои зависимые свойства о каждом изменении. Это может вызвать обработчики изменений, которые, в свою очередь, могут вызвать произвольный код. Таким образом, каждую запись в связываемое свойство необходимо тщательно проверять. Могут возникнуть следующие проблемы.
Запись промежуточных значений в связываемые свойства
Связываемые свойства не должны использоваться как переменные в алгоритмах. Каждое записанное значение будет сообщено зависимым свойствам. Например, в следующем коде другие свойства, зависящие от myProperty, будут сначала проинформированы об изменении на 42, а затем об изменении на maxValue.
myProperty = somecomputation(); // returning, say, 42
if (myProperty.value() > maxValue)
myProperty = maxValue; Вместо этого выполните вычисление в отдельной переменной. Правильное использование показано в следующем примере.
int newValue = someComputation();
if (newValue > maxValue)
newValue = maxValue;
myProperty = newValue; // only write to the property once Запись связываемых свойств в переходных состояниях
Когда связываемое свойство является членом класса, каждая запись в это свойство может раскрыть текущее состояние снаружи. Таким образом, связываемые свойства не должны записываться в переходных состояниях, когда инварианты класса не соблюдаются.
Например, в классе, представляющем круг, который содержит два члена radius и area, согласованные, сеттер может выглядеть так (где radius — связываемое свойство):
void setRadius(double newValue)
{
radius = newValue; // this might trigger change handlers
area = M_PI * radius * radius;
emit radiusChanged();
} Здесь код, запущенный в обработчиках изменений, может использовать круг, в то время как у него новый радиус, но всё ещё старая площадь.
Связываемые свойства с виртуальными сеттерами и геттерами
Сеттеры и геттеры свойств обычно должны быть минимальными и не делать ничего, кроме установки свойства; поэтому обычно неподходяще для таких сеттеров и геттеров быть виртуальными. Нет ничего, что должно делать производный класс.
Однако некоторые классы Qt могут иметь свойства с виртуальными сеттерами. При наследовании такого класса Qt переопределение таких сеттеров требует особой осторожности.
В любом случае, базовая реализация должна вызываться для корректной работы связывания.
Следующее иллюстрирует этот подход.
void DerivedClass::setValue(int val)
{
// do something
BaseClass::setValue(val);
// probably do something else
} Все общие правила и рекомендации относительно записи в связываемые свойства также применяются здесь. Как только вызывается реализация базового класса, все наблюдатели уведомляются об изменении свойства. Это означает, что инварианты класса должны быть выполнены перед вызовом базовой реализации.
В редких случаях, когда такие виртуальные геттеры или сеттеры необходимы, базовый класс должен документировать требования, которые он накладывает на переопределения.
Формирование связывания свойства
Любое C++ выражение, вычисляющее правильный тип, может использоваться как выражение связывания и передаваться методу setBinding(). Однако для формулирования корректного связывания необходимо соблюдать некоторые правила.
Отслеживание зависимостей работает только для связываемых свойств. Разработчик несёт ответственность за то, чтобы все свойства, используемые в выражении связывания, были связываемыми свойствами. При использовании несвязываемых свойств в выражении связывания изменения этих свойств не вызывают обновлений привязанного свойства. Никакие предупреждения или ошибки не генерируются ни во время компиляции, ни во время выполнения. Привязанное свойство будет обновлено только при изменении связываемых свойств, используемых в выражении связывания. Несвязываемые свойства могут использоваться в связывании, если можно гарантировать, что markDirty вызывается для привязываемого свойства при каждом изменении несвязываемой зависимости.
Привязанное свойство может несколько раз вычислять своё связывание в течение своего жизненного цикла. Разработчик должен убедиться, что все объекты, используемые в выражении связывания, живут дольше, чем связывание.
Система связываемых свойств не является потокобезопасной. Свойства, используемые в выражении связывания в одном потоке, не должны считываться или изменяться никаким другим потоком. Объект, производный от класса QObject, который имеет свойство со связыванием, не должен перемещаться в другой поток. Также объект, производный от класса QObject, который имеет свойство, используемое в связывании, не должен перемещаться в другой поток. В этом контексте не имеет значения, используется ли оно в связывании свойства в том же объекте или в связывании свойства в другом объекте.
Выражение связывания не должно считывать данные из свойства, для которого оно является связыванием. В противном случае существует цикл вычисления.
Выражение связывания не должно записывать данные в свойство, для которого оно является связыванием.
Функции, используемые в качестве связываний, а также весь код, вызываемый внутри связывания, не должны использовать co_await. Это может сбить с толку систему отслеживания зависимостей свойств.
Связываемые свойства и многопоточность
Связываемые свойства не являются потокобезопасными, если не указано иное. Связываемое свойство не должно считываться или изменяться никаким потоком, кроме того, в котором оно было создано.
Отслеживание связываемых свойств
Иногда отношения между свойствами нельзя выразить с помощью связываний. Вместо этого вам может потребоваться выполнить пользовательский код всякий раз, когда изменяется значение свойства, и вместо присваивания значения другому свойству, передать его другим частям вашего приложения. Например, запись данных в сетевой сокет или вывод отладочной информации. QProperty предоставляет два механизма для отслеживания.
Вы можете зарегистрироваться на обратный вызов функции, которая будет вызываться всякий раз, когда изменяется значение свойства, используя onValueChanged(). Если вы хотите, чтобы обратный вызов также вызывался для текущего значения свойства, зарегистрируйте свой обратный вызов с помощью subscribe() вместо этого.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-6.2/bindableproperties.html