Создание GUI приложений на PHP

  • Tutorial

Темой о разработке GUI приложений на PHP сегодня, пожалуй, уже никого не удивишь. Для этого существует не одно решение, есть как развивающиеся проекты, так и умершие. Но этот пост будет не о тех и не о других, а о новом расширении для PHP — библиотеке PHPQt5, а точнее о её более продвинутой реинкарнации — о PQEngine.

P.S. PHPQt5 не имеет ничего общего с более известной библиотекой php-qt!

Предисловие


По сути, PQEngine — это SAPI модуль, но позиционируется он как движок, реализующий интерфейс для исполнения php-скриптов при помощи Zend API и предоставляющий доступ к части фреймворка Qt.
Пока, к сожалению, к очень малой его части. Можно выделить основные из них: визуальные компоненты Qt (Widgets), компоновщики (Layouts), система соединений сигнал->слот (connect()), управление событиями (QEvents) и многопоточность, предоставляемая классом QThread.

Создание проекта


Для упрощения создания и сборки проектов существует очень простая в использовании утилита — PQBuilder. Это приложение на 99% написано на PHP и демонстрирует скромные возможности движка PQEngine.
Скачать утилиту можно на официальном сайте библиотеки PHPQt5: http://phpqt.ru/download/pqbuilder

Интерфейс сборщика проектов:


Все что от нас требуется — это указать название проекта, путь его размещения и шаблон. В зависимости от выбранного шаблона, сборщик создаст php-файл с базовым кодом, которого будет достаточно для запуска будущего приложения, затем откроет проводник Explorer в папке проекта, где мы увидим всего два файла и одну папку:
  • main.php — основной файл проекта с исходным кодом приложения;
  • %projectname%.pqb — файл проекта PQBuilder, в котором храниться некоторая информация о созданном проекте;
  • build — каталог сборки проекта.

Созданный проект с шаблоном QWidget Application


Код который мы видим на скриншоте был скопирован из шаблона и при выполнении покажет пустую форму.
Примечание 1
На самом деле для запуска формы совсем необязательно наследоваться от QWidget, предлагаемый код — это лишь пример. Единственный обязательный участок кода тут — qApp::exec();, где qApp — это ссылка на экземпляр класса QApplication, а функция exec() обеспечивает переход в главный цикл обработки событий Qt, тем самым предотвращая завершение работы PHP до тех пор, пока не будет вызвана функция exit() или quit(), либо пока не будут закрыты все видимые формы приложения.

Самый элементарный способ показать форму Qt выглядит примерно так:
<?php
$widget = new QWidget;
$widget->show();

qApp::exec();


Помимо того, что Сборщик переместил нас в директорию проекта, он активировал вкладку сборки, где нам остаётся лишь выбрать иконку для будущего приложения и шаблон сборки.
Примечание 2
На момент написания этой статьи существовало 3 шаблона сборки проектов. Вкратце о каждом:
  • Simple (app)
    — сборка простого приложения, без использования шифрования и упаковки. Основной PHP-файл проекта копируется в рабочую директорию приложения, при этом исходный код PHP-файла остаётся открытым.
  • Simple + manifest (app-manifest)
    — сборка простого приложения с подключением файла Manifest. О файле Manifest, для чего он нужен и как его оформлять, можно почитать на сайте Microsoft.
  • Packed (packedapp)
    — самый интересный метод сборки приложения, при котором основной PHP-файл зашифровывается и упаковывается в ресурсы приложения путём компиляции. Это позволяет защитить исходный код программы. Большой минус этого шаблона в том, что он позволяет упаковывать только файл main.php, другие ресурсы (картинки, текст и п.р.) остаются в папке с проектом.


Для своего проекта я выбрал шаблон Simple — он очень удобен на этапе разработки в плане того, что для дебага проекта не придётся каждый раз пересобирать приложение.

Нажав на кнопку сборки проекта, PQBuilder запустит компилятор, работа которого продлится от 10 до 30 секунд, затем откроется директория собранного проекта и, если процесс компиляции прошел успешно, мы увидим свежеиспеченный исполняемый файл.
Примечание 3
Если же exe-файл не появился, это означает, что во время компиляции произошли ошибки. Сборщик не предоставляет информации об ошибках компиляции, но она доступна в log-файле, который лежит в директории на уровень выше: make.log. Как правило, сообщение об ошибке можно найти в конце файла. Возможно изучение этого лога поможет вам определить ошибку и устранить её.

Для релиза приложения нам нужны только 4 файла: exe-файл, pqengine.dll, php5ts.dll и main.php. Остальные файлы в директории проекта — это временные файлы созданные компилятором, их можно смело удалить.

Разработка приложения


Примечание 4
Благодаря тому, что все зарегистрированные в движке PQEngine классы реализуют интерфейсы стандартных классов Qt, движок, теоретически, способен проглотить часть примеров с официальной документации Qt, достаточно лишь убрать типы переменных и подписать к ним знак $. Но тем не менее, некоторые функции остаются недоступными, либо отличаются набором и типами входных и выходных значений.

Посмотреть полный список методов того или иного класса можно в заголовочных файлах PQEngine, находятся они в директории с установленным сборщиком:
%путь_установки%\PQBuilder\pqenginedll\pqclasses
Все методы начинающиеся с макроса Q_INVOKABLE доступны для вызова из PHP-кода, а выполняют они ровно то, что написано в официальной документации Qt.

Разработку приложения мы продолжим с предложенным шаблонным кодом, непосредственно в директории с собранным проектом. Открываем файл build\app\release\main.php в любимом редакторе, переходим в тело функции initComponents() и приступаем:
  1. Для начала создадим и установим компоновщик для нашей формы.
    Примечание 5
    PQEngine предоставляет три вида компоновки:
    1. QVBoxLayout — вертикальное расположение виджетов;
    2. QHBoxLayout — горизонтальное расположение виджетов;
    3. QGridLayout — расположение виджетов в сетке.


    private function initComponents() {
        $layout = new QGridLayout;
        $this->setLayout($layout);
    }
    

  2. Затем добавим несколько кнопок:
    private function initComponents() {
        $layout = new QGridLayout;
        $this->setLayout($layout);
        
        $button1 = new QPushButton($this); // $this - это родитель кнопки. В качестве родителя устанавливаем нашу форму
        
        /* Устанавливаем текст кнопки. Это можно сделать как через свойство text, 
         * так и с помощью метода setText(); 
         * см. Примечание 4
         */
        $button1->text = 'Показать сообщение';
    
        /* Объекты поддерживают анонимные функции для установки событий.
         * На вход анонимной функции всегда передается 2 параметра: ссылка на
         * объект вызвавший функцию и ссылка на объект события (QEvent)
         */
        $button1->onClicked = function($sender, $action) {
            QMessageBox::information($this,
                                      'Первое сообщение',
                                      'Привет, мир!');
        };
        
        $button2 = new QPushButton($this);
        $button2->text = 'О PQEngine';
        $button2->onClicked = function($sender, $action) {
            aboutPQ(); // это функция предоставляется движком PQEngine
        };
        
        $button3 = new QPushButton($this);
        $button3->text = 'Выход';
        /* Для разнообразия покажу пример использования функции connect().
         * Всё очень просто: вы указываете какой сигнал виджета с каким слотом или функцией 
         * необходимо соединить. Я выбрал соединение с функцией php
         * Подробнее можно почитать в документации к PHPQt5:
         * http://phpqt.ru/documentation/functions/connect
         */
        $button3->connect(SIGNAL('clicked(bool)'), $this, SLOT('quitButtonClicked(bool)'));
    }
    
    // Функция-слот обязательно должна быть объявлена как публичный метод 
    public function quitButtonClicked($sender, $checked) {
        $this->close();
    }
    

  3. И, наконец, добавим все эти кнопки в созданный компоновщик:
        /* ... */
        $button3->connect(SIGNAL('clicked(bool)'), $this, SLOT('quitButtonClicked(bool)'));
    
        /* Функция addWidget принимает 3 или 5 параметров:
         * addWidget($widget, $row, $column, $rowSpan = 1, $columnSpan = 1), где:
         * $widget - ссылка на добавляемы виджет;
         * $row - номер строки размещения виджета;
         * $column - номер столбца размещения виджета;
         * $rowSpan - количество строк, объединяемых виджетом;
         * $columnSpan - количество столбцов, объединяемых виджетом;
         */
        $layout->addWidget($button1, 0, 0);
        $layout->addWidget($button2, 0, 1);
        $layout->addWidget($button3, 1, 0, 1, 2);
        
        /* Для того чтобы при растягивании формы наши кнопки оставались прижатыми 
         * к верху окна, можно добавить виджет-пустышку в качестве распорки
         */
        $layout->addWidget(new QWidget, 2, 0);  
    


Теперь для того чтобы посмотреть результат совершенно необязательно снова пересобирать проект.
Просто сохраняем исходный код и запускаем исполняемый exe-файл, который был скомпилирован ранее.
Будьте внимательны!
Выбранный в данном примере шаблон (Simple) позволяет работать именно таким образом, ведь по сути сборка приложения нужна лишь для того, чтобы прикрепить к приложению иконку, выбранную пользователем во вкладке «Сборка проекта».
НО! Будьте внимательны и осторожны! Не забывайте, что все изменения мы производили в директории с собранным проектом, поэтому если в PQBuilder'e вы снова соберёте этот проект, весь код будет затёрт!

P.S.
Такой фокус не прошел бы, если бы мы собирали приложение с шаблоном "Packed", ведь там исходный PHP-код прикрепляется в ресурсы исполняемого файла, поэтому при каждом изменении исходника приходилось бы заново пересобирать проект.


И вот что должно было получиться:


Архив проекта (распаковать и открыть файл untitled.pqb)
Поделиться публикацией

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

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 34

    +46
    Что только не придумают, чтоб на плюсах не писать! =)
      0
      Плюс php из коробки много удобных функций на все случаи жизни. И для написания достаточно мануала.
      Минус же плюса, это чтобы начать писать что-то полезное нужно иметь больший опыт, да и одним мануалом тут не отделаешся.

      Вопрос по решению, сколько потребляют ресурсов приложения? Ну и да весом приложением мы пренебрегем :)
        +9
        У плюсов есть фатальный недостаток — чтобы программировать на плюсах нужно уметь программировать.
          +3
          Не совсем так.

          Чтобы программировать на плюсах нужно уметь программировать на плюсах. Умение программировать «вообще» только отчасти облегчает задачу. Примерно так же, как умение программировать на плюсах облегчает задачу программирования на Haskell каком-нибудь.

          Их фатальный недостаток в том, что у них очень высокий порог входа, а в том, что предоставляемые ими преимущества не настолько перекрывают трудозатраты, за исключением редких специфических случаев. А вот на каком-нибудь Qt или WPF чтобы по-быстрому рычаг себе и друзьям для чего-нибудь запилить — самое оно.
            0
            Экий вы зануда :)
        +4
        Код на гитхаб переложить не хотите?

        Внутри как оно работает, вы построили проксю к объектам из Qt или обёртку как в PyQt? Какие подводные камни?
          +4
          Не буду обманывать на счет того «как в PyQt» оно или нет, честно — не знаю. Я скачивал PyQt в надежде почерпнуть что-то полезное для себя, но не смог этого сделать в силу своих незнаний Питона.
          Все классы — это действительно обёртки над Qt-шными, которые предназначены для того, чтобы расшарить доступ к не-INVOKABLE методам и за одно привести их входные и выходные значения к примитивным типам, с которыми работает PHP (int, char, bool...). Доступ к объектам со стороны PHP осуществляется через магические методы (__call, _set...), предоставляемые Zend'ом, а финальный вызов методов происходит через метосистему Qt. Если посмотреть заголовочные файлы движка PQEngine, можно увидеть, что в обертках все методы объявлены с макросом Q_INVOKABLE.

          Насчет подводных камней вопрос очень интересный и двоякий.
          Если говорить о разработке приложений с использованием этой библиотеки, то тут пожалуй самое страшное то, что возможны утечки. Именно при работе с объектами фреймворка Qt. Боролся я с ними долго и усердно, но не факт что искоренил их во всех местах.
          Если говорить о разработке самой библиотеки, то тут в голову приходят сразу 2 момента:
          1. Непереносимость библиотеки на другие ОС. Слишком тесно все завязалось вокруг WinAPI, но в принципе, при желании «окросплатформеннить» её вполне реально.
          2. Отсутствие расширяемости библиотеки в пользу её компактности. Из-за использования статической линковки с Qt написать какое-то дополнение достаточно проблематично. На счет этого уже было несколько идей, например, вынести из движка некоторые классы в подключаемые по требованию пользователя модули. Ну как в самом Qt — нужна работа с виджетами? Подключай QWidgets, нужна работа с сетью? Подключай QNetwork и т.д.
            Думаю, если библиотека будет развиваться дальше, то её компактность рано или поздно перестанет быть оправданной, именно тогда она и будет разбита на составные части подключаемые по желаю разработчика.

            +2
            Очень жаль что библиотека только для Windows.
            В статье не увидел упоминания об этом.
            А почему было принято решение жёстко привязываться к WinAPI? Всё таки одно из главных преимуществ Qt это его кроссплатформенность.
              0
              Такого решения не было, все получилось само собой.
              В основном к WinAPI привязан только модуль, позволяющий упаковывать и вытаскивать php-код из ресурсов приложения. Я вижу только один способ решить эту проблему — нужно лишь отказаться от статической линковки. Тогда можно будет пользоваться системой ресурсов Qt (qrc).
              Если сделать так, то, как мне кажется, усугубится ситуация с возможностью создания кроссплатформенной библиотеки. Это проблему тоже можно было бы решить путем открытия исходных кодов, но тогда тайна упаковки php-кода будет раскрыта и достать исходник проекта из мало-мальски защищенного приложения ни у кого не составит труда.
              Вот такой замкнутый круг получается.
                +2
                Ну для обфускации php кода можно использовать сторонние библиотеки специально для этого предназначенные (ZendGuard, ionCube, etc). Раз есть возмножность подключать php экстеншены, то можно использовать их.
                Плюс открытые исходники возможно привлекут сторонних разработчиков, так же этому поспособствует кроссплатформенность.

                На мой взгляд у проекта с открытыми исходными кодами больше шансов взлететь в подобной нише.
                  +1
                  Security through obscurity по идее не так уж и ценна. А вот кроссплатформенность — очень даже.
                  Такая библиотека лично мне привлекательна тем, что теоретически можно писать приложения которые могут жить как в браузере так и на десктопе лишь частично переработав вьювы, но сохраняя всю бизнеслогику и живя в привычном фреймворке.
                  Если я захочу использовать YII в GUI-приложении, то сохранение кода прилинкованным к экзешнику будет несколько странным и прожорливым по памяти…
                    +1
                    Хм, а в каком месте qrc мешает динамической или статической линковке? У меня всё работает и так и так, про Q_INIT_RESOURCE не забыли?
                    Тайна упаковки PHP кода — несостоятельна, я беру, делаю DLL которая является проксёй к dll ке php и смотрю какой же вы php код кидаете в eval. Если статическая линковка — то же самое только под отладчиком. Это очень легко.
                    Так же обращаю ваше внимание на то, что Qt лицензируется под LGPL (статическая линковка с не LGPL кодом запрещена), поэтому я сейчас пользуясь этой лицензией требую выслать мне исходные тексты программы. Заранее уведомляю что размещу их на GitHub поэтому приемлем вариант что их разместите ВЫ и вышлете мне ссылку на гитхаб.
                      0
                      Задействовать qrc мне ничего не мешает. Но если его подключить, приложение получается Qt-зависимым, а значит либо требует наличия shared-библиотек Qt, либо требует статической линковки, а это в свою очередь означает, что программа увеличивается в размерах в два раза (ведь библиотека и так уже собрана статикой). В настоящее время при компиляции на выходе получается нативное win-приложение, которые использует возможности движка для работы с php файлами.

                      (статическая линковка с не LGPL кодом запрещена), поэтому я сейчас пользуясь этой лицензией требую выслать мне исходные тексты программы

                      Использование лицензии не наделяет вас или кого либо другого правом владеть исходным кодом. Продукт поставляется с заголовочными и объектными файлами библиотеки — делайте с ними что хотите: хоть на гитхабе публикуйте, хоть на луну отправляйте :)
                      Более того вместе с библиотекой поставляются исходники и объектные файлы статической сборки Qt, которые как бы намекают, что разработчик не вносил изменения в исходный код фреймворка. Какой не-LGPL-код вы имеете ввиду я не понимаю.

                      Тайна упаковки PHP кода — несостоятельна

                      Ну так это вы :) любой Ванька может открыть текстовый файл, который лежит в папке с приложением, и посмотреть его содержимое. А чтобы сделать прокси и посмотреть что отправляется в eval (который там, кстати, не используется) нужно как минимум знать, что приложение написано на php.
                    +1
                    Доступна для тестов сборка для линукса: ubuntu 14.04, Qt 5.2.1, PHP 5.6.13. Пока только так.
                    Если есть желание пощупать, то вот информация: http://phpqt.ru/pqengine/pqengine-on-linux
                    +5
                    Пока не очень понятно мне как у вас соотносится объект в PHP и объект в Qt.
                    В PyQt получили проблемы вида: Qt удалило объект а питон нет и наоборот. Во втором случае мы имеем проблему с потенциальной утечкой памяти, в первом — попытку работать с удалённым объектом.

                    Использование WinAPI — это не вин, серьёзно, вы сроднили два кроссплатформеных инструмента и получили Win-only приложение. Не круто вышло.
                      0
                      В этом плане утечек быть не должно. В движке реализовано собственное хранилище объектов. Каждый раз когда php обращается к объекту qt, это хранилище проверяется на предмет уничтоженных объектов как со стороны php, так и со стороны qt. Оверхед при этом не стучается, максимум что может произойти — пых вместо объекта получит null и выдаст соответствующее сообщение.
                        0
                        А вообще для «безопасного» удаления любого qt-объекта имеется метод $object->free(). По идее само ничего удаляться не должно :) Да и движок очень узконаправлен, работает по большей части только с виджетами. Оберток над такими классами как QRect, QIcon, QVector и прочими нет и не предвидится, мне кажется что это уже лишнее. Все-таки движок предназначен больше для работы именно с GUI, а не с фреймворком Qt.
                          +1
                          Окей, не гарантирую но предполагаю проблемы в таком коде:
                          $w = new QWidget();
                          $layout = new QGridLayout;
                          $w->setLayout($layout);      // владение пререходит к $w
                          $button1 = new QPushButton($w);
                          $layout->addWidget($button1);
                          // пока всё хорошо
                          $w->free()  // но это же не единственный способ :)
                          // unset($w);  // можно добавить больше веселья или вообще заставить GC собрать её
                          echo $layout->count(); 
                          /* прикол в том что у $layout parent стал равен $w, который мы удалили и он удалит всех детей, удаление через deleteLater не является решением проблемы т.к. только откладывает её (хотя для этого кода он сработает). 
                          В принципе выше вы писали про хранилище объектов, если оно реализовано грамотно то проблем не будет, но тогда интересно что вернёт count , логичнее всего было бы исключение.
                          */
                          

                            0
                            Я не зря написал про free(), потому что это действительно безопасный метод удаления объектов. Сейчас постараюсь рассказать что же тут произойдет. Когда мы вызываем free(), движок удаляет не только сам объект, но и все его дочерние объекты. А если объект — это виджет, то удаляется и установленный Layout, если таковой имелся. В вашем коде мы бы ничего не получили, в ответ. Даже сообщения об ошибке. Движок PQEngine в этом случае бы просто проигнорировал вызов метода, но сама переменная $layout все еще существует в памяти Zend Engine (если можно так выразиться). Существовать эта переменная будет до тех пор, пока она не «исчезнет» из области видимости, либо пока мы не запишем в нее что-нибудь другое.
                            Тестировать наличие утечек в подобных ситуациях я привык с помощью таймера:
                            $timer = new QTimer;
                            $timer->interval = 1;
                            $timer->onTimer = function() {
                                $w = new QWidget();
                                $layout = new QGridLayout;
                                $w->setLayout($layout);      
                                $button1 = new QPushButton($w);
                                $layout->addWidget($button1,0,0);
                                $w->free();
                            };
                            
                            $timer->start();
                            // запускаем код, открываем диспетчер задач и видим, что потребление памяти не возрастает ни на килобайт. Весьма непрофессиональный метод, но довольно эффективный :)
                            


                            Теперь о unset(). unset() удалит лишь переменную, но qt объект останется в памяти. При этом мы с легкостью сможем получить доступ к этому объекту по его имени, либо через метод другого компонента, который бы возвратил ссылку на наш объект, например
                            $stack = new QStackWidget;
                            $widget = new QWidget;
                            $widget->objectName = 'myWidget';
                            
                            $stack->addWidget($widget);
                            
                            unset($widget);
                            
                            $widget->free(); // возвратит ошибку, ведь мы удалили переменную, но
                            $stack->currentWidget->free(); // удалит наш виджет из памяти. Навсегда. 
                            
                            // кроме того к объектам можно обращаться через специальную функцию c($objectName):
                            $objectName = 'myWidget';
                            c($objectName)->free();
                            


                            За код спасибо. Очень не хватает подобных тестов «с подковыркой».
                    +7
                    А если серьезно, каково целевое применение?
                      +12
                      Этот пост должен был нести добро и радость php-шникам, но тут пришли вы и перекрасили всё в серый цвет. Зачем же вы так? :)
                      Рассказывать о достоинствах PHP и его преимуществах перед другими языками будет не уместно, да и я рискую быть закиданным помидорами (а то и чем потяжелее), но все же — быстро склепать какую-нибудь безделушку с возможностью выхода в интернет без мозголомки, да еще и обрисовать вокруг этой безделушки вполне себе функциональную форму вполне реально.
                      Конечно, что-то достаточно серьезное на этом не напишешь, хотя попытки были… В любом случае это не мне решать, думаю тот, кого заинтересует данная библиотека найдет ей применение.
                        –4
                        Я бы почитал «о достоинствах PHP и его преимуществах перед другими языками», было бы интересно.
                          +5
                          Быстрая разработка, лучшее (или одно из лучших) на рынке соотношение цена/качество у разработчиков, большое комьюнити, довольно качественная экосистема, в последние несколько лет — еще и всё для написания качественного кода. Камон, бро, пора проснуться, мы не в 2007.
                            +2
                            Писал как-то довольно тяжеловатую консольную тулзу на пхп. Основной аргумент был в том, что 90% кода было переиспользовано из родственного проекта сайта написанного на пхп. Тот же апи, те же функции… Запускалось под виндой. Собственно оно до сих пор работает. Висит консолька у людей, с которой изредка общаются, да отслеживают ее сообщения. Если бы я тогда имел опыт с данной библиотекой, то консолька была бы не консолькой а более красивым окошком.
                            Переписывать библиотеки на другой язык? Вспоминать другой язык? А зачем? тулза работает на трех компах, используется только сисадминами и кодерами…
                        +6
                        Закопайте уже стюардессу.
                        • НЛО прилетело и опубликовало эту надпись здесь
                            +8
                            PHP и Flash можно закапывать вечно :)
                              –2
                              Простите за оффтоп, но в защиту PHP хочется отметить умеренные цены на хостинг чисто с PHP (что особенно интересно для персональных сайтов).
                          0
                          Интересно будет ли дальнейшее разве проекта. было бы крайне приятно иметь что то типа PyQt.
                          А то все предыдущие библиотеки подобной направленности мертвы.
                            0
                            В силу возможностей — будет :) по крайней мере пара-тройка нереализованных идей еще имеется.
                            0
                            Скажите, а есть возможность работать с веб-сокетами? Если да, то неплохо бы примерчик.
                              0
                              Возможность такая есть, движок поддерживает подключение php-расширений. Что для этого нужно:
                              1. в директории с приложением необходимо создать папку ext и скопировать туда нужную библиотеку. В вашем случае это будет php_sockets.dll. Библиотеку можно найти в дистрибутиве php, версия должна быть либо 5.4.45, либо 5.6.11 (в зависимости от того какой версией PQBuilder вы собирали приложение);
                              2. затем, для того чтобы эта библиотека подключилась, в директории с приложением создаем текстовый файл с именем pqengine.ini и со следующим содержимым:
                                extension="php_sockets.dll"
                                pqengine.ini — это тот же самый php.ini
                                0
                                Что должно получиться:

                                +3
                                Никогда такого не было, и вот опять.

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

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