Qt + QML на простом примере

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

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

    Пользовательский интерфейс будет полностью реализован на QML, программная часть на Qt.

    Для тех, у кого Qt еще не установлен, заходим на страницу загрузки qt.nokia.com/downloads и в разделе "Qt SDK: Complete Development Environment" скачиваем бинарники для своей платформы. И, собственно, устанавливаем.

    Запускаем Qt Creator, выбираем пункт меню Файл -> Новый файл или проект. В открывшемся окне Проект Qt C++ -> Gui приложение Qt, далее кнопка Выбрать.

    Qt

    В новом окне вводим название проекта, указываем путь к проекту, жмем Далее.

    В следующем окне снимаем галочку Создать форму, она нам не пригодиться, Далее. И в последнем просто нажимаем кнопку Завершить. Все, каркас нашего приложения создан.

    Qt

    Первым делом добавим модуль qt-declarative к нашему проекту, для этого, в файле проекта (4Toddler.pro), к строке

    QT += core gui

    добавим declarative
    QT += core gui declarative

    Далее изменим базовый класс для нашего главного окна, заменим QMainWindow на QDeclarativeView и включим заголовочный файл QDeclarativeView
    #include <QDeclarativeView>

    class MainWindow : public QDeclarativeView
    {
      ...
    }


    От реализации конструктора отрежем, ставшую ненужной, инициализацию базового класса QMainWindow(parent).

    Если сейчас собрать и запустить проект то мы увидим пустое окно. Так и должно быть, т.к. мы еще не создали и не инициализировали Qml интерфейс.

    Добавим в проект новый QML файл, для этого щелкаем правой клавишей по проекту

    image

    Добавить новый..., далее выбираем раздел Qt, шаблон Файл Qt QML. Даем ему имя main, затем Далее и Завершить.

    image

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

    Rectangle
    {
      // Идентификатор, по нему будет происходить
      // обращение к свойствам этого элемента
      id: canvas;

      // Цвет фона, черный
      color: "black"

      // Изменять размер под размеры
      // родительского элемента
      anchors.fill: parent

      // Будет получать фокус ввода
      focus: true
    }


    Пока ничего особенного, просто черный фон. Добавим код загрузки QML файла, чтобы посмотреть, что у нас получилось. Для этого добавим нашему окну новый метод
    void MainWindow::Init()
    {
      // Путь к папке, содержащей QML файлы
      QString contentPath;

    #ifdef QT_DEBUG
      // В отладочной версии это абсолютный путь к папке проекта
      contentPath = "D:/MyProjects/QT/4Toddler";
    #else
      // В релизе это путь к папке, в которой расположено приложение
      contentPath = QApplication::applicationDirPath();
    #endif

      setFocusPolicy(Qt::StrongFocus);
      // Изменять размеры QML объекта под размеры окна
      // Возможно делать и наоборот,
      // передавая QDeclarativeView::SizeViewToRootObject
      setResizeMode(QDeclarativeView::SizeRootObjectToView);

      // Загрузить QML файл
      setSource(QUrl::fromLocalFile(contentPath + "/main.qml"));
    }

    Теперь заменим в файле main.cpp строку
    int main(int argc, char *argv[])
    {
      ...
      w.show();
      ...
    }

    на
    int main(int argc, char *argv[])
    {
      ...
      w.showFullScreen();
      ...
    }

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

    Элемент WindowButton мы будем использовать повторно, изменяя лишь определенные свойства у каждого экземпляра. Кнопки у нас будут выполнены в виде иконок, каждой из них мы будем задавать путь к файлу иконки и изменять обработчик нажатия левой клавишей мыши. Ниже приведен готовый код элемента с комментариями
    Image
    {
      // Идентификатор элемента
      id: button

      // Область, обрабатывающая "мышиные" сообщения
      MouseArea
      {
        // Действует в пределах всего
        // элемента Image
        anchors.fill: parent

        id: mouseArea

        // При нажатии вызвать метод callback
        onClicked: callback()
      }
    }

    Добавим пару кнопок к нашему окну
    // Элемент позволяющий
    // распологать элементы горизонтально
    Row
    {
      // Правый край элемента выравнивается
      // по правому краю родительского элемента
      anchors.right: parent.right;
      // Отступ справа, 4 пикселя
      anchors.rightMargin: 4;
      // Верхний край эелемента выравнивается
      // по верхнему краю родительского элемента
      anchors.top: parent.top;
      // Отступ сверху, 4 пикселя
      anchors.topMargin: 4;

      // Отступ между элементами
      spacing: 4

      WindowButton
      {
        // Кнопка возова диалога "О программе"
        id: about;

        // Путь к файлу с изображением
        // в данном случае иконка лежит в той же папке,
        // что и QML файл
        source: "about.png";

        // Метод, который будет вызываться
        // при нажатии на кнопку левой клавишей мыши
        // onClicked: callback()
        function callback()
        {
        }
      }

      WindowButton
      {
        // Кнопка закрытия окна
        id: exit;

        source: "exit.png";

        function callback()
        {
        }
      }
    }


    Чтобы то, что мы сделали заработало нам осталось реализовать оба метода callback для каждой кнопки. Для закрытия окна мы вызовем метод Quit, который реализуем в классе окна. Для этого в объявление класс добавим
    Q_INVOKABLE void Quit();

    Затем реализуем этот метод
    void MainWindow::Quit()
    {
      QApplication::quit();
    }

    Осталось сделать этот метод видимым из QML. В метод Init добавим одну единствунную строку, которая сделает экземпляр нашего окна видимым в QML
    rootContext()->setContextProperty("window", this);

    Обращаться к этому объекту мы сможем по имени — window, имя это произвольное. Добавим реализацию для кнопки закрытия окна
    function callback()
    {
      window.Quit();
    }

    Обратите внимание, что вызывать можно только те методы, которые объявлены как Q_INVOKABLE, т.е. от же метод Init, главного окна вызвать не удастся.

    Готово, запускаем, видим черный экран, все, что сейчас мы можем сделать это закрыть окно, нажав на кнопку exit. Нажали и видим, что состояние кнопки при наведении курсора и при нажатии никак не меняется, выглядит как «неживая». Оживим ее, добавив состояния:
    Image
    {
      ...
      states:[
        State
        {
          // Произвольное название
          name: "hovered";
          // Указание на то, когда элемент переходит в это состояние
          // в данном случае когда нажата левая кнопка мыши
          when: mouseArea.pressed;
          // Какие свойства будут изменяться в этом состоянии
          // в данном случае это будет прозрачность
          PropertyChanges { target: button; opacity: 1;}
        },
        State
        {
          name: "normal"
          // В это состояние элемент будет переходить
          // когда левая кнопка мыши не нажата
          when: mouseArea.pressed == false;
          PropertyChanges { target: button; opacity: 0.7; }
        }
       ]
    }

    Элемент может переходить в определенное состояние как автоматически при выполнении условия, указанного в when, так и вручную, путем изменения свойства state.

    Запустили, нажали, прозрачность изменяется, уже лучше, но не хватает плавности. Добавим следующий код:
    Image
    {
      ...
      Behavior on opacity
      {
        // Анимация с шагом в 100 миллисекунд
        // Раз в 100 миллисекунд прозрачность будет изменяться
        // на 0,1
        NumberAnimation { duration: 100 }
      }
    }

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

    Запускаем и смотрим, совсем другое дело, плавный переход от полупрозрачного к непрозрачному состоянию.


    Окно о программе будет реализовано полностью на QML. Это будет модальное окно, которое будет появляться при нажатии кнопки about. При щелчке левой клавишей мыши в любом месте окна оно будет исчезать. Добавим новый QML файл About.qml в проект.



    Я приведу сразу весь код этого окна с пояснениями
    // Главный элемент для диалогового окна
    Rectangle
    {
      id: about

      // Функция для отображения окна
      // изменяет прозрачность главного элемента
      function show()
      {
        about.opacity = 1;
      }

      // Функция для закрытия окна
      function hide()
      {
        about.opacity = 0;
      }
      
      // Прозрачный задний фон
      color: "transparent"
      // Полностью прозрачен по умолчанию
      opacity: 0
      
      // Ширина и высота устанавливаются равными
      // ширине и высоте родительского элемента
      // в данном случае это элемент с id: canvas
      width: parent.width
      height: parent.height

      // Видимым элемент будет считаться если выполняется условие
      // opacity > 0
      visible: opacity > 0

      // Дочерний элемент, создающий полупрозрачный фон
      Rectangle
      {
        anchors.fill: parent

        opacity: 0.5

        color: "gray"
      }

      // Дочерний элемент создающий который является диалогом
      // "О программе..."
      Rectangle
      {
        id: dialog
        
        // Ширина и высота являются фиксированными
        width: 360
        height: 230

        // Координаты верхнего левого угла вычисляются
        // исходя из размеров самого диалога и родителя
        // так, чтобы окно располагалось в центре
        x: parent.width / 2 - dialog.width / 2;
        y: parent.height / 2 - dialog.height / 2;
        // Задаем z индекс таким, чтобы он был
        // больше z тех элементов, которые должны остаться
        // за диалоговым окном
        z: 10

        border.color: "gray"

        Text
        {
          text: "4 Toddler"

          font.bold: true

          font.pixelSize: 22
          
          // Выравнивание элемента по центру
          anchors.horizontalCenter: parent.horizontalCenter
          anchors.verticalCenter: parent.verticalCenter
        }
      }

      Behavior on opacity
      {
        NumberAnimation { duration: 100 }
      }

      MouseArea
      {
        // Элемент полностью заполняет родительский элемент
        anchors.fill: parent;
        
        // При клике в любом месте прячем окно
        onClicked: hide();
      }
    }

    Для начала хотелось бы обратить внимание на свойство
    width: parent.width

    Это не просто присвоение ширины, если в процессе отображения будет меняться ширина родительского элемента, то и ширина дочернего будет перерасчитана. Не знаю как вас, а меня в процессе «ковыряния» QML эта особенность приятно удивила. Так же интересна следующая строка:

    visible: opacity > 0

    Свойство может быть не только задано, но и вычислено.

    Осталось добавить диалог и код для его отображения при нажатии кнопки about. В файл Main.qml добавим код, в конце элемента canvas

    Rectangle
    {
        id: canvas
      ..
      About
      {
        id: aboutDlg
      }
    }

    Для того, чтобы окно отображалось добавим строку

    aboutDlg.show();

    в функцию callback кнопки about

    WindowButton
    {
      id: about;
      ...
      function callback()
      {
        aboutDlg.show();
      }
    }

    Теперь добавим, собственно основной функционал. Начнем с отображения случайной картинки при нажатии любой клавиши. Картинка будет являться эелементом Image, определим этот элемент в отдельном файле. Добавим в проект файл Block.qml
    Image
    {
        id: block;

      // Новое свойство объекта, необходимое
      // для изменения состояния при удалении объекта
        property bool remove: false
      // При добавлении объекта
        property bool show: false
      
        opacity: 0;
        fillMode: Image.Stretch;

      states: [
        State
        {
          // Состояние, в которое переходит объект
          // тогда, когда нам нужно его удалить
          name: "remove"; when: remove == true;
          PropertyChanges { target: block; opacity: 0 }
          StateChangeScript { script: block.destroy(1000); }
        },
        State
        {
          // Состояние, в которое переходит объект
          // тогда, когда нам нужно его отобразить
          name: "show"; when: show == true;
          PropertyChanges { target: block; opacity: 1 }
        }
      ]
      
        Behavior on opacity { NumberAnimation { duration: 300 } }
    }

    При нажатии любой клавиши на клавиатуре будет отображаться блок с произвольной картинкой. Добавим в проект новый файл main.js. В нем, мы определим обработчик нажатия клавиши на клавиатуре.
    // Шаблон для создания новых элементов
    var component = Qt.createComponent("block.qml");

    // Максимальное количество элементов
    var maxBlocksCount = 10;

    // Массив, в котором будут храниться все эелементы
    var blocksArray    = new Array();

    // Функция обработчик нажатия клавиши
    function handleKey()
    {
      // Координата x - случайно число от 0 до ширины окна
      var x = Math.floor(Math.random() * canvas.width);
      // Координата y - случайно число от 0 до ширины окна
      var y = Math.floor(Math.random() * canvas.height);

      // Вызов функции, которая создаст новый элемент
      // с указанными координатами
      createNewBlock(x, y);
    }

    // Создание нового элемента
    function createNewBlock(x, y)
    {
      if(component.status != Component.Ready)
      {
        return false;
      }

      // Удалить лишние элементы
      if(blocksArray.length > maxBlocksCount)
      {
        removeAllBlocks();
      }

      var newBlock = component.createObject(canvas);

      if(newBlock == null)
      {
        return false;
      }

      // Путь к файлу иконки доступен через свойство главного
      // окна randomIcon
      var iconFile = window.randomIcon;

      newBlock.source = ("Icons/" + iconFile);

      newBlock.x = x;
      newBlock.y = y;

      // Переводим элемент в состояние show
      newBlock.show = true;

      blocksArray.push(newBlock);

      // Проигрываем случайный звуковой эффект
      window.PlaySound();

      return true;
    }

    // Удаление всех добавленных элементов
    function removeAllBlocks()
    {
      for(var i = 0; i < blocksArray.length; ++i)
      {
        blocksArray[i].remove = true;
      }

      while(blocksArray.length != 0)
      {
        blocksArray.pop();
      }
    }

    Как видно из кода нам еще следует реализовать свойство randomIcon и функицию PlaySound главного окна.

    Добавим свойство в объявление класса MainWindow

    Q_PROPERTY(QString randomIcon READ RandomIcon)

    И объявление функции

    QString RandomIcon();

    Затем реализацию:

    QString MainWindow::RandomIcon()
    {
      QStringList iconFilesList;
      QString searchPath = m_ContentPath + "/Icons/";

      QDir directory = QDir(searchPath);
      QStringList filters;
      filters << "*.png";
      directory.setNameFilters(filters);
      // Получаем список файлов с расширением png
      iconFilesList = directory.entryList(QDir::AllEntries);

      // Получаем случайный индекс элемента
      int fileIdx = qrand() % iconFilesList.count();
      
      // Возвращаем название файла
      return iconFilesList.at(fileIdx);
    }

    Теперь добавим в заголовочный файл функцию для проигрывания звукового эффекта
    Q_INVOKABLE void PlaySound();

    и реализацию
    void MainWindow::PlaySound()
    {
      QStringList soundFilesList;
      QDir directory = QDir(m_ContentPath + "/Sounds/");
      QStringList filters;
      filters << "*.wav";
      directory.setNameFilters(filters);

      // Получаем список файлов с расширением wav
      soundFilesList = directory.entryList(QDir::AllEntries);
      // Получаем случайный индекс элемента
      int fileIdx = qrand() % soundFilesList.count();

      // Получаем название файла
      QString soundFile = m_ContentPath + "/Sounds/" + soundFilesList.at(fileIdx);
      // Проигрываем файл
      QSound::play(soundFile);
    }

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

    import Qt 4.7
    import "main.js" as Main


    и сам обработчик внутри элемента canvas

    Rectangle
    {
        id: canvas
      ...
      Keys.onPressed: { if(event.isAutoRepeat == false) { Main.handleKey(); } }

    На этом все — можем запускать и любоваться.



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

    Архив с проектом можно скачать здесь
    Поделиться публикацией

    Похожие публикации

    Комментарии 14
      0
      Интересно. Было бы здорово услышать в чем преимущества над WPF или их сравнение.
        0
        К сожалению пока не пробовал работать с WPF, поэтому сказать ничего не могу. Qt + QML понравились за гибкость и кросс-платформеность. Немного расстроил оверхед, который дает Qt. В моем проекте при размере самой утилиты в 1,5 Мб дистрибутив «весит» 24 Мб.
          +2
          Так .NET рантайм весит отнють не меньше, если не больше, просто он предустановлен на машинке чаще всего.
          Зато в Линуксовых дистрах обратная ситуация, и даже на Symbian QML программы вскоре будут без бубна ставится.
            0
            С этой стороны да, путь и 24 зато «все включено».
          0
          В своей практике я сделал выбор в сторону WPF. Стояла задача сгенерировать из одного описания GUI какое-либо другое (дабы потом показать диалог). Из-за практически идентичности языков был выбран QML. Однако наличие id только в дизайн-тайм (нельзя найти контрол по id, так как в рантайм его уже просто нет) и отсутствие стандартных контролов (я знаю про обходной способ вставки QWidget в QML, но элементы автоматом не лайаутится, надо обрамлять ещё одним элементом, до и попробуйте вставить туда QWidget'ный список, содержащий в себе другие QML-элементы; описано всё предельно невнятно), убило всю идею. На WPF же всё перенеслось за день. Не хочу говорить о QML только плохое, но создалось ощущение какой-то сырой технологии, в отличие от WPF. От него впечатления куда лучше.
            0
            QML только совсем недавно появилась, пока еще QtComponents для QML не доделаны, а там как раз и есть стандартный набор управляющих элементов. А вообще пытаться юзать QWidget'ы в QML это моветон и оверкилл. Гораздо правильнее использовать QGraphicsScene элементы.
            Насчет id я так и не понял в чем проблема, из javascript'а же можно манипулировать объектами с выбранным id.
              –1
              Должна быть реализована (не QML-ем, а мной) возможность выставить обработчик по ключу. Приходилось генерировать дополнительно property key, хотя казалось бы.

              пока еще QtComponents для QML не доделаны, а там как раз и есть стандартный набор управляющих элементов

              Что ж, хорошо. Значит скоро дела будут намного лучше.
                0
                qt.gitorious.org/qt-components

                Есть еще пара подобных проектов. Где-то линки видел. В общем дело пока развивается и весьма быстро. Но в целом я бы так сказал WPF больше на desktop аппликухи ориентирован, а QML на мобильные.

                –1
                Вот честно говоря долго пытался понять, зачем этот самый QML нужен, если в нём нет стандартизованной библиотеки графических компонентов. Есть такое нехорошее внутреннее ощущение, что Trolltech после покупки их Нокией решили изобрести велосипедновый Adobe Flash, только с блекджеком и так далее. При этом забросив массу разных интересных идей, которые предполагалось реализовать в ближайшее время.
                  0
                  >Вот честно говоря долго пытался понять, зачем этот самый QML нужен, если в нём нет стандартизованной библиотеки графических компонентов.

                  Линк выше. Библиотека делается уже, обещали как раз возможность использовать нативные для системы стили оформления, но пока всё делается в первую очередь для MeeGo и Symbian, остальные платформы потом будут добавляться.

                  > Есть такое нехорошее внутреннее ощущение, что Trolltech после покупки их Нокией решили изобрести велосипедновый Adobe Flash, только с блекджеком и так далее

                  А что в этом плохого? Благо подобной технологии, да нормально реализованной и плюс ко всему opensource не существует.

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

                  Это какие же?
            +1
            >Обратите внимание, что вызывать можно только те методы, которые объявлены как Q_INVOKABLE, т.е. от же метод Init, главного окна вызвать не удастся.

            Не только, таким же образом без проблем вызываются и обычные slot'ы. Кстати, не советую помечать как Q_INVOKABLE или как Q_SLOT виртуальные функции. Доступ к ним будет заметно медленнее.
              +1
              И еще: в QtCreator 2.1RC поддержка QML заметно лучше.
                –3
                А авто тестирование такой интерфейс позволяет проводить?
                Если я правельно понял, то весь интерфейс это просто картинка — без возможности выделить конкретные елементы?
                  +2
                  Хорошо! Только маленькое замечание: для выхода из программы в QML более правильно вызывать в сцене уже готовый метод Qt.quit(), это приведет к срабатыванию соответствующего сигнала движка, на который можно повесть обработчиком тот же слот close() главного окна.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое