- PVSM.RU - https://www.pvsm.ru -
QtQuick и QML образуют по-настоящему хороший язык для разработки пользовательских интерфейсов. Привязки QML очень производительны и удобны. Декларативный синтаксис действительно приятен в работе. Возможно ли сделать то же самое на C++? В этом посте я покажу рабочую реализацию привязки свойств на чистом C++.
Внимание: это было сделано для забавы, а не для использования в реальном проекте.
Привязки служат для создания свойств, зависящих от других свойств. При изменении зависимостей значение свойства обновляется автоматически.
Вот пример, на написание которого меня вдохновил код из документации QML [1].
int calculateArea(int width, int height) {
return (width * height) * 0.5;
}
struct rectangle {
property<rectangle*> parent = nullptr;
property<int> width = 150;
property<int> height = 75;
property<int> area = [&]{ return calculateArea(width, height); };
property<std::string> color = [&]{
if (parent() && area > parent()->area)
return std::string("blue");
else
return std::string("red");
};
};
Если вы не знакомы с синтаксисом [&]{ ... }
, то это лямбда-функции [2]. Также я использую тот факт, что в C++11 можно инициализировать члены структуры или класса прямо во время объявления.
Теперь посмотрим, как работает класс свойства. В конце я покажу пример его использования.
В коде используется множество конструкций C++11, он был протестирован с GCC 4.7 и Clang 3.2.
Я использовал свои знания QML и мета-объектной системы Qt для того, чтобы создать что-то похожее в виде C++ привязок.
Цель состоит лишь в демонстрации proof of concept. Код не оптимизирован, я старался сделать его как можно более понятным.
Идея в том, что можно написать класс property
, поведение которого будет аналогичным свойствам в QML. Каждое свойство будет хранить список своих зависимостей. При обработке привязки все входящие в неё property
будут отмечены как зависимости.
property<T>
— шаблонный класс. Общая часть будет помещена в родительский класс property_base
.
class property_base
{
/* Множество свойств, зависящих от текущего
Когда свойство изменится, все зависимые свойства будут обновлены */
std::unordered_set<property_base *> subscribers;
/* Множество свойств, от которых зависит текущее */
std::unordered_set<property_base *> dependencies;
public:
virtual ~property_base()
{ clearSubscribers(); clearDependencies(); }
// это свойство должно быть переопределено
virtual void evaluate() = 0;
// [...]
protected:
/* Эта функция вызывается производным классом после того, как свойство было изменено
Стандартная реализация переопределяет все свойства, зависящие от текущего */
virtual void notify() {
auto copy = subscribers;
for (property_base *p : copy) {
p->evaluate();
}
}
/* Эта функция вызывается производным классом при получении доступа к свойству
Здесь происходит регистрация всех зависимостей */
void accessed() {
if (current && current != this) {
subscribers.insert(current);
current->dependencies.insert(this);
}
}
void clearSubscribers() {
for (property_base *p : subscribers)
p->dependencies.erase(this);
subscribers.clear();
}
void clearDependencies() {
for (property_base *p : dependencies)
p->subscribers.erase(this);
dependencies.clear();
}
/* Вспомогательный класс */
struct evaluation_scope {
evaluation_scope(property_base *prop) : previous(current) {
current = prop;
}
~evaluation_scope() { current = previous; }
property_base *previous;
};
private:
friend struct evaluation_scope;
/* thread_local */ static property_base *current;
};
Далее мы реализуем класс property
.
template <typename T>
struct property : property_base {
typedef std::function<T()> binding_t;
property() = default;
property(const T &t) : value(t) {}
property(const binding_t &b) : binding(b) { evaluate(); }
void operator=(const T &t) {
value = t;
clearDependencies();
notify();
}
void operator=(const binding_t &b) {
binding = b;
evaluate();
}
const T &get() const {
const_cast<property*>(this)->accessed();
return value;
}
// автоматическое приведение
const T &operator()() const { return get(); }
operator const T&() const { return get(); }
void evaluate() override {
if (binding) {
clearDependencies();
evaluation_scope scope(this);
value = binding();
}
notify();
}
protected:
T value;
binding_t binding;
};
Желательно также получать уведомления, когда свойство изменяется, поэтому мы можем, например, вызывать update()
. Класс property_hook [3] позволяет указать функцию, которая будет вызываться при изменении свойства.
Теперь у нас есть класс свойств, на основе которых мы можем построить что угодно. Я буду использовать их в связке с Qt Widgets.
Далее я ввожу property_qobject [4], который является базовой обёрткой property
в QObject
. Он инициализируется передачей указателя на QObject
и строкой свойства, которую вы хотите отслеживать.
Реализация не является эффективной и может быть оптимизирована путём обмена QObject
вместо создания по одному для каждого свойства. С Qt 5 я бы мог использовать лямбда-функции вместо этого хака, но здесь я использовал Qt 4.8.
Теперь я создаю обёртки вокруг каждого класса, в котором будут использованы свойства property_qobject
.
Давайте посмотрим, на что мы способны:
Этот маленький пример содержит line edit, который позволяет вам задать цвет, и два слайдера, задающие поворот и прозрачность графического элемента.
Пусть код говорит сам за себя.
Нам нужен прямоугольник с соответствующими привязками:
struct GraphicsRectObject : QGraphicsWidget {
// привязываем свойства QObject
property_qobject<QRectF> geometry { this, "geometry" };
property_qobject<qreal> opacity { this, "opacity" };
property_qobject<qreal> rotation { this, "rotation" };
// добавляем свойство цвета с привязкой для обновления, когда оно изменяется
property_hook<QColor> color { [this]{ this->update(); } };
private:
void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget*) override {
painter->setBrush(color());
painter->drawRect(boundingRect());
}
};
Теперь мы объявляем объект окна со всеми подвиджетами:
struct MyWindow : Widget {
LineEdit colorEdit {this};
Slider rotationSlider {Qt::Horizontal, this};
Slider opacitySlider {Qt::Horizontal, this};
QGraphicsScene scene;
GraphicsView view {&scene, this};
GraphicsRectObject rectangle;
::property<int> margin {10};
MyWindow() {
// компоновка элементов; реализано не так хорошо, как через настоящие Layout, зато продемонстрированы привязки
colorEdit.geometry = [&]{ return QRect(margin, margin,
geometry().width() - 2*margin,
colorEdit.sizeHint().height()); };
rotationSlider.geometry = [&]{ return QRect(margin,
colorEdit.geometry().bottom() + margin,
geometry().width() - 2*margin,
rotationSlider.sizeHint().height()); };
opacitySlider.geometry = [&]{ return QRect(margin,
rotationSlider.geometry().bottom() + margin,
geometry().width() - 2*margin,
opacitySlider.sizeHint().height()); };
view.geometry = [&]{
int x = opacitySlider.geometry().bottom() + margin;
return QRect(margin, x, width() - 2*margin, geometry().height() - x - margin);
};
// зададим значения по умолчанию
colorEdit.text = QString("blue");
rotationSlider.minimum = -180;
rotationSlider.maximum = 180;
opacitySlider.minimum = 0;
opacitySlider.maximum = 100;
opacitySlider.value = 100;
scene.addItem(&rectangle);
// дальше - наши привязки
rectangle.color = [&]{ return QColor(colorEdit.text); };
rectangle.opacity = [&]{ return qreal(opacitySlider.value/100.); };
rectangle.rotation = [&]{ return rotationSlider.value(); };
}
};
int main(int argc, char **argv)
{
QApplication app(argc,argv);
MyWindow window;
window.show();
return app.exec();
}
Вы можете клонировать репозиторий [5] и попробовать использовать всё это сами.
На данный момент библиотека представляет собой лишь небольшой прототип; возможно, когда-нибудь она будет дописана до приемлемого состояния.
Автор: epicfailguy93
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/28520
Ссылки в тексте:
[1] документации QML: http://qt-project.org/doc/qt-4.8/propertybinding.html#property-binding
[2] лямбда-функции: http://stackoverflow.com/questions/7627098/what-is-a-lambda-expression-in-c11
[3] property_hook: http://woboq.com/blog/property-bindings-in-cpp/code/src/property.h.html#property_hook
[4] property_qobject: http://woboq.com/blog/property-bindings-in-cpp/code/src/property_qobject.h.html#property_qobject
[5] клонировать репозиторий: https://github.com/woboq/property_bindings
[6] Источник: http://habrahabr.ru/post/171295/
Нажмите здесь для печати.