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

Правило следующее: вот так писать не надо!

property var varOfRectangle: item.varOfItem

Надо вот так!

readonly property var varOfRectangle: item.varOfItem

Или вот так!

property var varOfRectangle

Т.е. либо обязательно ставим переменную в readonly, либо не присваиваем переменной вообще ничего, и биндим через Binding.

Item {
  id: item
  property var varOfItem
}

Rectangle {
  id: rectangle
  property var varOfRectangle
}

Binding {
  target: rectangle
  property: "varOfRectangle"
  value: item.varOfItem
}

Я сказал слово "присваивание"? Именно так! В этом и заключается проблема! Оператор ":" делает тоже самое, как если бы вы присвоили биндинг через оператор "=".

Component.onCompleted: {
  varOfRectangle = Qt.binding( function () { return item.varOfItem } )
}

А это значит, что вашу переменную varOfRectangle можно перезаписать другим значением, и она больше не будет ни на что забинжена.

onSomeVarChanged: {
  ...
  parent.varOfRectangle = true
  ...
}

И вот вы усиленно меняете значение переменной varOfItem, в полной уверенности на неё забинжена varOfRectangle, а на самом деле нет.

А теперь о более сложном. О концепте QML, об идеологии, о парадигме вцелом.

Суть QML предполагает, что в программе нет времени, нет прошлого, не будущего, а значит нет настоящего нет порядка последовательности.

Зная всё написанное выше, вновь вернёмся к тому с чего начали, к примеру, как писать не надо.

property var varOfRectangle: item.varOfItem

Рассмотрим эту строчку ещё раз. Тут написано, что переменную можно как читать, так и записывать. Логично предположить, что прочитав переменную varOfRectangle мы получим значение переменной varOfItem. Так ли это? Можем ли мы гарантировать, что произойдёт первым: чтение или запись? Напомню, что после записи в эту переменную, она уже не будет забинжена varOfItem, как мы могли бы подумать, читая этот код.

И тут вновь возвращаемся к ключевой концепции QML: у программы нет времени. Мы не можем предсказать, что произойдёт первым, чтение или запись. Библиотека Qt обрабатывает события в любой последовательности, в разных условиях по разному, как посчитает наиболее оптимальным.

Ваш код на QML не должен зависеть от последовательности событий.

При использовании Binding, можно присваивать переменной другое значение в любое время - биндинг не потеряется, и останется рабочим.

Итог: используйте readonly при обьявлении переменной; если же предполагаете менять её значение через оператор "=", но хотите её на что-то забиндить - используйте Binding. Этот компонент управляем, его можно сделать активным или неактивным по условию. Это предотвратит ваш код от неожиданного поведения.

Нарушая это правило, программа становится зависимой от порядка чтения-записи, а это - нарушение идеологии QML и с вероятностью 100% станет той самой верёвкой в вашем коде, длина которой достаточна чтобы выстрелить себе в ногу.

Всех благ, и пишите в комментариях, что вы думаете об этом.

Комментарии (8)


  1. TrueRomanus
    17.08.2024 13:35

    Если хочется забиндить свойства внутреннего компонента в другой компонент то проще всего сделать это через alias который создает bi-directional binding c и в целом решает полностью описанную Вами проблему.


    1. Bombus
      17.08.2024 13:35

      Можно пример? Или ссылку на пример? Было бы здорово.


      1. TrueRomanus
        17.08.2024 13:35
        +1

        Item {
          id: item
          property var varOfItem
        }
        
        Rectangle {
          id: rectangle
          property alias varOfRectangle: item.varOfItem
        }
        


        1. f1af Автор
          17.08.2024 13:35

          Кстати, хорошее решение. Тогда зачем нужен readonly? :)


          1. TrueRomanus
            17.08.2024 13:35

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


          1. Deosis
            17.08.2024 13:35

            Для вычисляемых свойств использовать.

            Например, количество детей элемента.

            Так как нет универсальной логики, что делать при записи в такое свойство


    1. f1af Автор
      17.08.2024 13:35

      Именно по этому я поместил сей материал в раздел "Мнение". Как раз думаю в этом направлении.

      Ведь получается, что если мы где то считываем значение переменной varOfRectangle, то какая нам разница, от куда она получила это значение?

      property var varOfRectangle: item.varOfItem
      ...
      property var ourProblemVar: varOfRectangle

      В этом куске кода мы имеем проблему с ourProblemVar. Мы думали что получим значение из item.varOfItem. Но его не получаем, поскольку varOfRectangle где то в другом месте был переназначен.

      Резонно задать вопрос: а какая нам разница, от куда получено значение varOfRectangle, если мы на него биндимся? Если разница всё таки есть, то тогда так и надо писать:

      property var ourProblemVar: isCondition ? varOfRectangle : item.varOfItem

      Вот и будем получать гарантировано что хотим.

      alias который создает bi-directional binding

      Имеенно так. Именно bi-directional binding мне сейчас нужен! И я долго думал как его сделать, поскольку есть проблема: компонент, к которому надо сделать алиас, находится внутри Loader-а.

      У меня есть ListView внутри Loader-а, который грузится какое то время, и есть currentIndex из файла конфигурации, который тоже грузится не сразу. И мне как то надо их свести вместе :)

      Но это уже совсем другая история...

      А так, да, alias, сейчас смотрю в этом направлении, спасибо!

      Тем не менее, логика статьи мне кажется всё равно актуальной. Зачем писать:

      property var varOfRectangle: item.varOfItem

      ... когда varOfRectangle всё равно не будет равен item.varOfItem? А если и будет, то хрен знает когда и как надолго. Мне кажется, тут лучше поставить readonly.

      Если же мы где то хотим эту переменную переназначать, то зачем тогда ей присваивать Qt.binding( function () { return item.varOfItem } ) ? Ведь это так же лишено смысла.

      Вот и получается, что такая форма записи логически не приводит в багу, но и смысла в ней нет. Либо присваиваем Qt.binding и пишем readonly, либо нехрен присваивать если всё равно перезапишешь.

      Думаю, для полноты картины стоит ещё пройтись и по default и по required, чтобы картина присвоения значения переменных была полной.


      1. TrueRomanus
        17.08.2024 13:35

        property var varOfRectangle: item.varOfItem

        Тем не менее, логика статьи мне кажется всё равно актуальной. Зачем писать:

        Итак давайте все же начнем сначала, когда мы пишем код выше то значит что мы априори подразумеваем что нам не надо управлять varOfRectangle самостоятельно а чтобы он присвоился автоматически из item.varOfItem. Т.е. биндинг по природе своей подразумевает что мы отдаем присвоение какой-то переменной некому автоматическому механизму. Если Вы сделали так то зачем начали присваивать его вручную? Если Вам необходимо делать вручную или Вас не устраивает как работает биндинг то никто не мешает написать например так:

        Item {
          id: item
          property var varOfItem
        
          onVarOfItem: {
            rectangle.varOfRectangle = item.varOfItem
          }
        }
        
        Rectangle {
          id: rectangle
          property var varOfRectangle
          onVarOfRectangle: {
            item.varOfItem = rectangle.varOfRectangle
          }
        }

        Таким образом можно сделать биндинг просто кодом.

        У меня есть ListView внутри Loader-а, который грузится какое то время, и есть currentIndex из файла конфигурации, который тоже грузится не сразу. И мне как то надо их свести вместе :)

        Если интересно вот тут можете посмотреть как реализовано https://github.com/anilibria/anilibria-winmaclinux/blob/master/src/Views/OnlinePlayer.qml#L164 нечто похоже по описанию как Вам требуется.