Последнее обновление:
January 20, 2020

Есть мысль... Жми, напиши!
Что имеем: Постов : 169 Авторов: 1 Категорий: 28

Qt QML OpenGL(es) application on Linux without X11/Wayland/etc (kiosk mode)

Потребовалось мне сделать управляющий софт под один девайс. То есть при загрузке он должен сразу стартовать и отображаться всё время на экране. По сути типо терминала. Зовётся это в народе «режим киоска».

Можно было бы не париться и установить обычную, десктопную систему, но очень не хотелось тащить оконный менеджер, оболочку и прочее за собой, дабы избавиться от посредников прорисовки, ну и что бы дистр с софтом весил как можно меньше. За основу был взят Ubuntu Server 19.10 (можно и ещё более облегчённый дистр).

Разумеется при попытке сразу запустить QML приложение (QML выводит всё через OpenGL) он выдал про невозможность запуска с платформой «xcb», а если указать «-platform eglfs» (то есть принудительно укажем через что выводить), выдаёт «Could not initialize egl display», то есть не может открыть дисплей (потому что eglsfs скомпилирован только для «X11»), нус, будем компилировать для работы напрямую, то есть пересобирать Qt из исходников.

Будет две машины, одна «рабочая» (на которой и будет происходить компиляция, стоит Ubuntu 19.10 Desktop), другая «тестовая» (на которой будем запускать приложение, стоит Ubuntu Server 19.10, при установке включите «OpenSSH Server» для удалённого доступа с рабочей машины).

Кстати, на тестовой машине должна быть хоть какая-нибудь видеокарта с поддержкой OpenGL 3.1. Проверить, можно поставив kmscube (заодно ставим доп. пакеты, скорее всего все они не нужны, но чтоб наверняка):

sudo apt install cmscube
sudo apt install mesa-utils
sudo aptt install libglu1-mesa freeglut3 mesa-common
cmscube

Должна запуститься демка c вращающимся кубом. Возможно, нужно будет поставить ещё драйвера (об этом в конце поста).

Переходим на рабочую.
Нужно загрузить все либы для компиляции (наверняка тут слишком много лишних, но я точно уже не помню какие именно, так что вот весь список, чтоб наверняка)

sudo apt install libglu1-mesa libglu1-mesa-dev build-essential libgl1-mesa-dev freeglut3 freeglut3-dev mesa-common-dev libglapi-mesa libosmesa6 mesa-utils libdrm-dev libgbm-dev libgbm1 libgegl-0.3-0 libgegl-dev mesa-utils-extra gegl libglfw3-dev libgles2-mesa-dev libglew1.5 libglew1.5-dev libgl1-mesa-glx

Качаем исходники

BASEPATH=/home/pavelk/QtOpenGL
mkdir $BASEPATH
cd $BASEPATH
git clone https://github.com/qt/qt5 Qt5Sources
cd Qt5Sources
perl init-repository #Успеем фильмак глянуть, пока делается
git checkout 5.14
git submodule update --recursive

Настраиваем сборку

export QT_QPA_EGLFS_INTEGRATION=eglfs_kms
./configure -platform linux-g++-64 -skip wayland -skip script -skip webengine -no-pch -no-xcb -no-xcb-xlib -no-gtk -nomake tests -nomake examples -reduce-exports -kms -eglfs -opengl es2 -opensource -release -confirm-license -make libs -prefix $BASEPATH/qt5 -v

Возможно, нужно будет добавить ещё «-qpa eglfs».
По завершению главное в выхлопе что бы было вот это:

QPA backends:
EGLFS ................................ yes
EGLFS details:
  EGLFS i.Mx6 ........................ no
  EGLFS i.Mx6 Wayland ................ no
  EGLFS EGLDevice .................... yes !!!
  EGLFS GBM .......................... yes
  EGLFS Mali ......................... no
  EGLFS Rasberry Pi .................. no
  EGL on X11 ......................... no

Та самая заветная платформа и интеграция.

Если нужно перенастроить/пересобрать, то перед запуском нужно обязательно очистить старые результаты командой: «git clean -dxf»

Ну а дальше компилируем, ставим.

make
make install

Собираться будет дооолго.

По завершению подготавливаем QtCreator, идём в «инструменты»->»параметры».

Что бы иметь возможность сразу запускать на тестовой машине:
Устройства -> Устройства, Добавить, «Обычное Linux устройство»
Название по вкусу, у меня «Test»
IP адрес устройства — айпишник тестовой машины (узнать можно залогинившись на ней и ввести «ifconfig»)
Имя пользователя — соответственно имя пользователя, которое вводили при установке.

Дальше устанавливаем ключ:
жмём «создать новую пару ключей» и запоминаем путь «файла открытого ключа» (что-то вроде «/home/pavelk/.ssh/qtc_id.pub»), прожимаем «создать и сохранить».
Далее открытый ключ нужно скопировать на тестовую машину, для этого в консольке рабочей прописываем:
«ssh-copy-id -i /home/pavelk/.ssh/qtc_id.pub user@192.168.0.193»
Только замените значения на свои.
Завершаем, проверка должна пройти без ошибок.

Комплекты -> Профили Qt, жмём Добавить
Путь: /home/pavelk/qt5/bin/qmake
Название по вкусу, у меня «OpenGLKMS»

Комплекты -> Комплекты, жмём Добавить
Компиляторы ставим те, которыми собирали.
В «Профиль Qt» выбираем созданный на предыдущем шаге
Название по вкусу, у меня «QOpenGLKMS»
Тип устройства выбираем «Обычное Linux устройство»
Устройство выбираем добавленное на предыдущем шаге.

Логинимся на тестовую машину, ставим доп. пакеты, создаём необходимые директории для библиотек Qt и переменные окружения:

sudo su
apt install mesa-utils
apt install libgles2-mesa
apt install libglfw3
apt install libharfbuzz0b
apt install libfontconfig1
apt install libmtdev1
apt install libinput10
apt install libts0

usermod -aG input user

mkdir /usr/local/lib/Qt/
mkdir /usr/local/lib/Qt/lib
chown -R user:root /usr/local/lib/Qt/

echo "/usr/local/lib/Qt/lib" > /etc/ld.so.conf.d/qt.conf
echo 'QML2_IMPORT_PATH="/usr/local/lib/Qt/qml"' >> /etc/environment
echo 'QT_PLUGIN_PATH="/usr/local/lib/plugins"' >> /etc/environment

reboot

Только имя пользователя «user» замените на своё, которое на тестовой машине.

Возвращаемся на рабочую, необходимо на тестовую скопировать либы Qt:

IP=192.168.0.193
USER=user
scp -r $BASEPATH/qt5/plugins $USER@$IP:/usr/local/lib/Qt/
scp -r $BASEPATH/qt5/qml $USER@$IP:/usr/local/lib/Qt/

scp $BASEPATH/qt5/plugins/egldeviceintegrations/libqeglfs-kms-egldevice-integration.so $USER@$IP:/usr/local/lib/Qt/plugins/egldeviceintegrations
scp $BASEPATH/qt5/lib/libQt5Core.so.5 $USER@$IP:/usr/local/lib/Qt/lib 
scp $BASEPATH/qt5/lib/libQt5Gui.so.5 $USER@$IP:/usr/local/lib/Qt/lib
scp $BASEPATH/qt5/lib/libQt5Qml.so.5 $USER@$IP:/usr/local/lib/Qt/lib
scp $BASEPATH/qt5/lib/libQt5Network.so.5 $USER@$IP:/usr/local/lib/Qt/lib
scp $BASEPATH/qt5/lib/libQt5EglFSDeviceIntegration.so.5 $USER@$IP:/usr/local/lib/Qt/lib
scp $BASEPATH/qt5/lib/libQt5DBus.so.5 $USER@$IP:/usr/local/lib/Qt/lib
scp $BASEPATH/qt5/lib/libQt5EglFsKmsSupport.so.5 $USER@$IP:/usr/local/lib/Qt/lib
scp $BASEPATH/qt5/lib/libQt5Quick.so.5 $USER@$IP:/usr/local/lib/Qt/lib
scp $BASEPATH/qt5/lib/libQt5QmlModels.so.5 $USER@$IP:/usr/local/lib/Qt/lib
scp $BASEPATH/qt5/lib/libQt5QmlWorkerScript.so.5 $USER@$IP:/usr/local/lib/Qt/lib

Имя пользователя user и айпишник замените на свои, которые для тестовой машины. Либы копируются только самые необходимые для запуска «HelloWorld», в дальнейшем Вам необходимо будет скопировать остальные.

Дальше нужно снова подключиться к тестовой и обновить список либ:

sudo ldconfig

Переходим обратно на рабочую.

Создаём тестовый проект «Приложение Qt Quick — Пустое»,
комплект выбираем который недавно создали («QOpenGLKMS «).
В *.pro файле дописываем:

INSTALLS        = target
target.path     = /home/user
CONFIG += release

Это что бы запускалось сразу на тестовой машине и релизная версия.

В main.qml

import QtQuick 2.12
import QtQuick.Window 2.12

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    Rectangle {
        id: rect1
        property var colors: ["red", "blue", "orange", "gray", "green"]
        property int curColor: 0
        color: colors[curColor]
        anchors.centerIn: parent
        width: 150
        height: 150

        MouseArea {
            anchors.fill: parent
            onClicked: {
                rect1.curColor++;
                if ( rect1.curColor>=rect1.colors.length ) { 
                   rect1.curColor = 0; 
                }
                rect1.color = rect1.colors[rect1.curColor];
            }
        }
    }
}

В настройках проекта обязательно ставим «Release»(Выпуск) сборку.
Ну и запускаем =) На тестовой машине на весь экран должно запуститься приложение.

Возможно, нужно будет поставить драйвера на видюху (хз что она потащит за собой), но навсякий случай если не работает:

sudo su
apt install mesa-utils
apt install ubuntu-drivers-common
ubuntu-drivers autoinstall
apt purge gdm3
reboot

Так же, если не запускается, то на тестовой машинке попробовать прописать:

export QT_LOGGING_RULES=qt.qpa.*=true

Грузится должен qt.qpa.eglfs.kms, если грузится «emu» то проще удалить эту интеграцию: «rm /usr/local/lib/Qt/plugins/egldeviceintegrations/libqeglfs-emu-integration.so»

Так же для диагностики стоит проверить зависимости. На тестовой:

ldd ./myApp

Если все есть, то проверяем зависимости eglfs плагина:

cd /usr/local/lib/Qt/plugins/platforms/
ldd ./libqeglfs.so

Если чего-то нет, гуглим либу и как её ставить (скорее всего из разряда sudo apt isntall lib……, пару раз tab нажмите должен появится список возможных)

И запустить приложение вручную «./myApp» (оно должно быть по пути «target.path» из *.pro файла Вашего приложения при компиляции )

При запуске приложения через ssh подключение вывод будет на тестовой машине, т.е. бегать до тестовой машины всё равно придётся.

Если наблюдаются проблемы с разрешением, или есть несколько видеокарточек (дискретная и встроенная) и выводит не на ту, либо подключено несколько мониторов и нужно вывести на конкретный, то:

cd /dev/dri
ls

В списке смотрим, какие есть видеокарточки, запоминаем.
Что бы указать Qt интеграции, как и через что выводить, содаём конфигурационный файл:

nano /usr/local/lib/Qt/eglfs.json
{
 "device": "/dev/dri/card0",
 "hwcursor": true,
 "pbuffers": true,
 "outputs": [
  {
   "name": "HDMI1", "mode": "1024x768"
  }
 ]
}

Думаю, интуитивно понятно что куда и за что.
Так же полезным будет почитать документацию: https://doc.qt.io/qt-5/embedded-linux.html

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

sudo su
echo 'QT_QPA_EGLFS_KMS_CONFIG="/usr/local/lib/Qt/eglfs.json"' >> /etc/environment
reboot

Если не работает ввод/курсор, то нужно пользователя добавить в группу input:

sudo usermod -aG input user

Ну а теперь осталось лишь сделать автозапуск приложения при включении. Для этого просто создадим свой systemd сервис:

sudo nano /etc/systemd/system/myApp.service

Description=myApp autostart
Wants=network.target
After=syslog.target network-online.target

[Service]
Type=simple
EnvironmentFile=/etc/environment
ExecStart=/home/user/myApp
Restart=on-failure
RestartSec=10
KillMode=process

[Install]
WantedBy=multi-user.target

Включаем в автозагрузку и запускаем:

sudo systemctl daemon-reload
sudo systemctl enable myApp
sudo systemctl start myApp

Ахтунг! Сервис запускается с рутовыми правами! Как сделать от имени пользователя гугл подскажет, там просто.

Вот как-то так…

Views :

6

Qt, QQuickPaintedItem отрисовка в отрицательных координатах (paint outside bounds).

Потребовалась мне тут на днях отрисовка сложных графиков в QML.
Увы, но возможностей существующих графиков не хватило — пришлось изобретать свои.

Проблема возникла с тем, что по умолчанию всё, что имеет отрицательные координаты x и y будет подвергнуто кастрации.
Да же если поставим:

setClip(false);
setFlag(QQuickItem::ItemClipsChildrenToShape, false);

И попробуем вывести круг:

void MyCircle::paint(QPainter * painter)
{
   QPainterPath path;
   path.addEllipse(-200, -200, 400, 400);

   painter->setPen(Qt::NoPen);
   painter->fillPath(path, QBrush(QColor("orange")));
}

Всё, что меньше (0, 0) будет обрезано:

Можно было бы задать размер по размеру основного окна, но мне нужно было чтобы итем холста имел конкретные размеры и положение, т.к. он на сцене будет не один.

Самое простое решение на данный момент — задать QPainter глобальную матрицу трансформации со смещением в противоположную сторону (что бы не высчитывать новые координаты при отрисовке примитивов). А компоненту задать соответственно в координатах X и Y это же значение смещения. Единственный минус — придётся делать обёртку над компонентом.
Вот так примерно:

painter->setWorldTransform(QTransform::fromTranslate(200, 200), true);
setPosition(QPoint(-200, -200));

И в QML соответственно обёртку сделать:

import QtQuick 2.12
import QtQuick.Window 2.12
import MyCircle 1.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("QQuickPaintedItem outside bounds")

    Rectangle {
        anchors.fill: parent
        color: "black"
    }

    Item {
        id: myCircleWrapper
        anchors.centerIn: parent
        width: 200
        height: 200

        MyCircle {
            id: myCircle
            width: parent.width
            height: parent.height
            clip: false
        }
    }
}

И результат:

Как сделать более правильно я, к сожалению, после нескольких дней раздумий и ковырянии исходников Qt не придумал…

Views :

10

Использование интерфейсов классов в Qt и QML

Привет!

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

Разумеется простым способом в «лоб» не получится, т.к. Qt в QML работает с QObject, а мы от него не унаследовались и никакой информации для метасистемы не дали.

Долго я копался в недрах метасистемы Qt, уж собирался делать костыли, но наткнулся на макрос Q_DECLARE_INTERFACE.

И так, допустим мы хотим сделать интерфейс класса для работы с одним свойством myProperty, тем самым заставив определить функции получения, установки значения и сигнала (который сам по себе то же функция) об изменении значения. 

Пишем:

#ifndef IINTERFACE_H
#define IINTERFACE_H

#include <QObject>


class IInterface {
public:
    virtual QString myProperty() const =0;
    virtual void setMyProperty(QString myProperty) =0;
    virtual void myPropertyChanged(QString myProperty) =0;
};

Q_DECLARE_INTERFACE(IInterface, "pavelk.iinterface")


#endif // IINTERFACE_H

Обратите внимание на строку с «Q_DECLARE_INTERFACE» — тем самым мы даём понять метаобъектной системе Qt, что это интерфейс, что бы он прописал необходимые функции по получению исходного класса.

Ну и сам класс:

#ifndef MYCLASS_H
#define MYCLASS_H

#include <QObject>

#include "iinterface.h"

class MyClass : public QObject, public IInterface
{
    Q_OBJECT
    Q_INTERFACES(IInterface)
    Q_PROPERTY(QString myProperty READ myProperty WRITE setMyProperty NOTIFY myPropertyChanged)
public:
    explicit MyClass(QObject *parent = nullptr);

    QString myProperty() const;

signals:
    void myPropertyChanged(QString myProperty);

public slots:
    void setMyProperty(QString myProperty);

private:
    QString m_myProperty;
};

#endif // MYCLASS_H

Обратите внимание на строку с «Q_INTERFACES(IInterface)» — тем самым мы даём знать Qt как именно преобразовывать этот класс к указанному интерфейсу. Кстати, можно наследоваться сразу от нескольких интерфейсов — просто пропишите через пробел их все.

Ну и теперь самое интересное. Преобразовываем класс к интерфейсу и передаём этот интерфейс в QML

   MyClass * myClass = new MyClass(0);
   IInterface * interface = myClass;
   engine.rootObjects().at(0)->setProperty("myClassInterface", qVariantFromValue( dynamic_cast<QObject*>(interface) ));

Напрямую в QVariant интерфейс засовывать нельзя, т.к. он не знает необходимой информации, поэтому преобразовываем сначала в QObject*, а благодаря тому, что мы прописал Q_DECLARE_INTERFACE метасистема знает как работать с этим интерфейсом.

Полный код примера тут:  https://github.com/Riflio/QMLInterfaces

Вот как-то так =)

Views :

518

Layout.fillWidth: true и Layout.preferredWidth/Layout.minimumWidth зависимость (очередная хитрость)

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

Так вот, задача: нужно три колонки одинаковой ширины.

Делаем:

import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Layouts 1.3

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("QMLTips&Tricks24")

    RowLayout {
        anchors.fill: parent
        spacing: 0

        Rectangle {
            color: "blue"
            Layout.fillHeight: true
            Layout.fillWidth: true            
        }

        Rectangle {
            color: "orange"
            Layout.fillHeight: true
            Layout.fillWidth: true
        }


        Rectangle{
            color: "green"
            Layout.fillHeight: true
            Layout.fillWidth: true            
        }

    }
}

Соответственно на выходе получаем:

Получилось как и задумывали.

Обновим задачу:  Оранжевая колонка должна иметь предпочтительную ширину 200 пикселей.
Делаем:

Rectangle {
    color: "orange"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 200
}

Получаем:

Что-то не то, да..? 

Вот тут начинается самое интересное.

Дело в том, что Layout.prefferedWidth (а так же Layout.minimumWidth) управляет пропорционально шириной относительно соседей, когда задано Layout.fillWidth: true

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

Делаем:

Rectangle {
    color: "blue"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 200
}

Rectangle {
    color: "orange"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 200
}

Rectangle{
    color: "green"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 200
}

Получаем:

То есть как и задумывали, три одинаковые колонки.

Теперь наглядный пример насчёт пропорционально соседям изменяемости размеров. Если мы у синего прямоугольника зададим желаемую ширину в 50, а у зелёного 100, то соответственно синий будет в 4 раза меньше оранжевого (200/50=4), а зелёный буде в два раза меньше оранжевого (200/100=2). 

То есть родитель Layout (RowLayout/ColumnLayout/GridLayout) берёт от потомков наибольший желаемый размер и относительно него пропорционально выставляет ширину всем потомкам.
Повторюсь, что так QML себя ведёт, только когда задано Layout.fillWidth: true

Делаем:

Rectangle {
    color: "blue"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 50
}

Rectangle {
    color: "orange"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 200
}

Rectangle{
    color: "green"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 100
}

Получаем:

Короче, считаем, что при указании preferredWidth, minimumWidth мы работаем с пропорциями относительно остальных в одном контейнере и всё. 

Почему именно так, а не иначе?  

По-моему как раз для того, что бы можно было задать поведение при растягивании/сжимании…

 

Аналогичная ситуация произойдёт, если мы оранжевой колонке захотим указать minimumWidth: 100 при fillWidth:true

Что делать, если нужно, что бы все колонки имели одинаковую ширину и заполняли всё пространство (т.е. fillWidth: true), но при этом у них (у всех или у некоторых) должна быть задана разная минимальная ширина? 

Я в таком случае прописываю у всех:  preferredWidth: 1000;  что бы желательная ширина у всех была одинаковая и обязательно больше, чем минимальная, иначе учитываться не будет (логично, да?)

Надеюсь, теперь больше с этим проблем не возникнет.

Кстати, такое же поведение будет если у компонента задано свойство implicitWidth или implicitHeight, потому что если мы не задали явно Layout.preferredWidth или  Layout.fillHeight, то берутся как раз они. Наверное вы спрашиваете: как быть, если это не просто прямоугольник, а какой-то компонент? То всё просто — оберните его в Item или Rectangle или задайте явно Layout.preferredWidth

Кстати, вот документация на лэйауты: тынц.

Вот как-то так =)

Views :

285