Archive for the ‘QT’ Category

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

Также в первой части статьи вскользь упоминалась библиотека qglgui (https://github.com/Kvalme/qglgui). Эта библиотека предназначена для интеграции QT в свое приложение.

Передача сообщений от клавиатуры/мышки в QT

Первое желание, которое возникает после реализации рендеринга QT — потыкать в кнопочки. Первый метод которым хочется воспользоваться — запихнуть события непосредственно в  QApplication::event, что разумеется не работает. Покопавшись чуть дальше, мы увидим, что события надо подсовывать непосредственно в eventloop QT и периодически дергать его.

Для работы с eventLoop у QApplication есть два метода:

  • Можно вызвать QApplication::exec и больше ни о чем не думать. Из данного метода QApplication вернется только при закрытии. Так что этот метод не подходит, если мы хотим сохранить контроль над тредом QT, либо вообще делать что-либо кроме рисования UI в однопоточном случае.
  • QApplication::processEvents — данный метод обрабатывает все имеющиеся на данный момент события и возвращает управление. Именно его я и предлагаю использовать.

После организации цикла с QApplication::processEvents в нашем приложении смогут работать сигналы, например, от таймера.

Для передачи событий в QT из QPA есть специальный класс: QWindowSystemInterface.

QWindowSystemInterface

Данный класс предназначен для получения событий от оконной системы. К сожалению, на него, как и почти на все классы QPA, нет никакой документации, поэтому все приходится узнавать либо методом тыка, либо смотря исходники QT.

Из того, что было найдено, наиболее важная вещь — методы данного класса могут вызываться не из основного потока QT приложения.

События от клавиатуры

От клавиатуры у нас есть всего два вида событий — нажатие и отпускание кнопки. Для передачи этих событий в QT используется один метод:

static void handleKeyEvent(QWindow *w, QEvent::Type t, int k, Qt::KeyboardModifiers mods, const QString & text = QString(), bool autorep = false, ushort count = 1);

Кратко по списку параметров:

  • w — окно которому передать события. Если передать NULL — уйдет текущему активному окну
  • t — тип события. QEvent::KeyPress либо QEvent::KeyRelease
  • k — кнопка с которой произошло событие (одна из Qt::Key)
  • mods — состояние модификаторов (Qt::NoModifier, Qt::AltModifier, Qt::ShiftModifier, Qt::ControlModifier, Qt::MetaModier, есть еще экзотические типа: Qt::KeypadModifier, Qt::GroupSwitchModifier)
  • text — символ, который соответствует нажатой кнопке. Если этот параметр пустой — отрабатывает нажатие кнопки, если не пустой — кнопка игнорируется и отрабатывается только печать символа
  • autorep — по всей видимости что-то, связанное с автоповтором кнопки. Я реализую автоповтор с вызывающей стороны, поэтому не экспериментировал с этим параметром
  • count — видимо, количество нажатий

События от мышки

Данная группа несколько объемней, и имеет некоторые тонкости в обработке. Рассмотрим методы по порядку.

static void handleMouseEvent(QWindow *w, const QPointF & local, const QPointF & global, Qt::MouseButtons b, Qt::KeyboardModifiers mods = Qt::NoModifier);

Данный метод занимается работой с событиями от кнопок мыши и передвижения мыши.

  • w — окно которому нужно передать событие.
  • local — локальные (оконные) координаты точки в которой произошло событие. Можно получить используя wnd->mapFromGlobal(global)
  • global — глобальные (экранные) координаты точки в которой произошло событие.
  • b — нажатые кнопки. Если кнопка была отпущена — не передаем флаг, соответствующий отпущенной кнопке.
  • mods — состояние модификаторов клавиатуры (Qt::NoModifier, Qt::AltModifier, Qt::ShiftModifier, Qt::ControlModifier, Qt::MetaModier, есть еще экзотические типа: Qt::KeypadModifier, Qt::GroupSwitchModifier)
  • source — не трогаем. Назначение не известно.

Особенность обработки нажатий кнопок состоит в том, что необходимо после передачи события в QT, еще и вручную активировать окно (w->requestActivate).

static void handleWheelEvent(QWindow *w, const QPointF & local, const QPointF & global, QPoint pixelDelta, QPoint angleDelta, Qt::KeyboardModifiers mods = Qt::NoModifier, Qt::ScrollPhase phase = Qt::ScrollUpdate);

Данный метод занимается работой с событиями от колесиков мыши.Параметры в целом совпадают с теми, что были рассмотрены выше. Два новых параметра(pixelDelta и angleDelta) отвечают за 2 координаты колесиков. Я заполняю их так:

QPoint angleDelta(deltax, deltay);
QPoint pixelDelta(angleDelta * 10);

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

Обработка событий в QGLGUI

QGLGUI реализует как однопоточную схему работы с QT, так и многопоточную. Для передачи событий выделены несколько методов в классе GlGui:

/**
 * Injects mouse move event into QT event queue
 * 
 * @param screenId idientifyer of the screen on which event happened
 * @param positon current mouse position
 * @param buttons pressed mouse buttons
 * @param modifiers active keyboard modifiers
 */
virtual void InjectMouseMoveEvent(int screenId, QPoint position, Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers) = 0;
 
/**
 * Injects mouse button event into QT event queue
 * 
 * @param screenId idientifyer of the creen on which event happened
 * @param position current mouse position
 * @param button pressed button
 * @param modifiers active keyboard modifiers
 */
virtual void InjectMouseButtonEvent(int screenId, QPoint position, Qt::MouseButton button, Qt::KeyboardModifiers modifiers) = 0;
 
/**
 * Injects mouse wheel event into QT event queue
 * 
 * @param screenId identifyer of the screen on which event happened
 * @param position current mouse position
 * @param deltax offset of mouse wheel on X axis
 * @param deltay offset of mousewheel on Y axis
 * @param modifiers active keyboard modifiers
 */
virtual void InjectMouseWheelEvent(int screenId, QPoint position, double deltax, double deltay, Qt::KeyboardModifiers modifiers) = 0;
 
/**
 * Injects keyboard event into QT event queue
 * Always passes key to current active window
 * 
 * @param eventType Qt::KeyPress or Qt::KeyRelease
 * @param key key
 * @param modifiers modifiers state (shift, meta, ctrl)
 */
virtual void InjectKeyboardEvent(QEvent::Type eventType, Qt::Key key, Qt::KeyboardModifiers modifiers) = 0;
 
/**
 * Injects character event into QT event queue
 * Always passes character to current active window
 * 
 * @param character character to inject
 */
virtual void InjectCharacterEvent(QChar character) = 0;

Данные методы могут быть вызваны из любого треда. Также, для упрощения интеграции с GLFW, имеется вспомогательная библиотека — glfwhelper.

 

Интеграция QT приложения в OpenGL

Интеграция QT приложения в OpenGL

При разработке приложений на OpenGL (впрочем как и на D3D) постоянно возникает вопрос, на чем же делать GUI. На данный момент имеется большое количество разнообразных библиотек для построения GUI. Изначально я планировал использовать одну из этих библиотек, однако, попробовав их понял, что в моем случае они не подходят. Мне нужно делать сложный UI с разнообразными таблицами, листами и т.д, а существующие решения во-первых, предназначены для построения достаточно простых интерфейсов, а во-вторых, обычно не имеют достаточно мощных средств для графической разработки UI, связывания элементов, сложных раскладок.

  • CEGUI — хорошая библиотека для простого UI, если она у вас работает. При попытке её использовать, у меня не заработали примеры, идущие в составе с библиотекой. Не особенно удобный редактор интерфейса.
  • MyGUI — еще один аналог CEGUI, разрабатывается отечественными программистами. Одно время не работал диалог выбора файлов при использовании ОС Linux и нестандартных файловых систем (XFS). Желания фиксить чужие баги у меня не было, да и редактор интерфейса оставляет желать лучшего.
  • libRocket — возможно это отличная библиотека, которая замечательно подходит для использования в казуальных и/или мобильных приложениях. Но рассматривать ее как серьезный GUI framework невозможно.
  • Scaleform — вроде все хорошо, но надо платить деньги. К томуже надо знать ActionScript
  • QT — хорошая, зарекомендовавшая себя временем и множеством коммерческих продуктов, библиотека. К сожалению, довольно медлительная. Отличный редактор интерфейса.

Посмотрев на все это, я уже было решил писать собственную GUI систему, но вовремя обратил внимание на QT. Долгое время использовать QT для построение интерфейса в играх было неудобно из-за невозможности интегрировать его в приложение и сильного падения производительности, если строить приложение целиком на QT, однако с выходом QT5 с QPA запуск GUI на QT в отдельном потоке исполнения стал очень простым.

Что такое QPA

QPA — QT Platform Abstraction — это слой QT, который обеспечивает взаимодействие с операционной системой и оконным менеджером. В QT5 все платформы реализованы через QPA и имеется возможность заменять плагин, через который QT будет работать. Сам QT отрисовывает только содержимое окна, отрисовка заголовков окон, границ окон и других элементов окна полностью возлагается на ОС либо может быть реализована в QPA (что и сделано в некоторых платформах). Базово реализовав QT плагин, мы получим именно окно без заголовка и рамок.

Чтобы получить список поддерживаемых платформ можно запустить QT приложение с флагом -platform и в качестве аргумента передать любое несуществующее имя. На своей системе я получил следующее:

Failed to load platform plugin "list". Available platforms are:
linuxfb
minimal
xcb

Как мы видим, на данной системе есть три плагина. Запустить приложение с одним из них можно с помощью указания «-platform plugin_name».

Реализация плагина

По QPA почти полностью отсутствует документация, впринципе, все что есть можно найти на qt-project.org поиском по сокращению QPA. Для написания плагина проще всего взять за шаблон готовый плагин minimal, который можно найти в исходниках QT.

По сути, в минимальном плагине должны быть следующие необходимые вещи:

  • QPlatformIntegration — необходим для интеграции плагина. Через данный класс создаются окна, сервисы, запрашиваются поддерживаемые плагином возможности
  • QPlatformScreen — представляет собой область, на которой располагается окно. Примерно соответствует QScreen.
  • QPlatformWindow — представляет собой платформенно-зависимую реализацию для класса QWindow. Практически все вызовы к QWindow, которые влияют на поведение окна (перемещение, изменение размера, активация окна, захват указателя мыши и клавиатуры и т.д.) идут через данный класс.
  • QPlatformBackingStore —  по сути это подложка окна. Предоставляет QPaintDevice для отрисовки содержимого окна. Вместо QPlatformBackingStore может использоваться QPlatformOpenGLContext, если окно будет рисоваться встроенными в QT средствами OpenGL.

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

QPlatformIntegration

Данный класс имеет достаточно объемный интерфейс, мы же рассмотрим лишь несколько необходимых на данный момент методов:

QPlatformWindow *createPlatformWindow(QWindow *window) const;
QPlatformBackingStore *createPlatformBackingStore(QWindow *window) const;
QPlatformServices *services() const;
QPlatformFontDatabase *fontDatabase() const;
QAbstractEventDispatcher *guiThreadEventDispatcher() const;

Создание вспомогательных объектов

Как видно из списка методов, нам понадобится реализовать возврат дополнительных объектов, как то QPlatformFontDatase, QAbstractEventDispatcher и QPlatformServices. К счастью, в поставке QT (если поставить пакет qt platform support) уже есть реализация данных классов как минимум для unix систем. Детали реализации можно посмотреть в плагине minimal.

Созданные объекты нужно возвращать по соответсвующим запросам services/fontDatabase/guiThreadEventDispatcher

Создание окна

Метод createPlatformWindow отвечает за создание окна. Каждый раз, когда в приложении будет созваваться QWidget, будет вызван данный метод (создание QWidget ведет к созданию QWindow). Каждое созданное окно должно быть сразу активно.

Сразу после вызова createPlatfromWindow происходить вызов createPlatformBackingStore — чтобы создать поверхность окна, на которой оно будет рисоваться.

QPlatformScreen

Наверное, самый простой класс. Из всего интерфейса класса необходимо реализовать три метода:

  • geometry — возвращает геометрию.
  • depth — глубина цвета
  • format — используемый на этом экране формат представления пикселей

QPlatformBackingStore

У данного класса простой интерфейс и хороший пример реализации в платформе minimal:

virtual QPaintDevice *paintDevice() = 0;
virtual void flush(QWindow *window, const QRegion &region, const QPoint &offset) = 0;
virtual void resize(const QSize &size, const QRegion &staticContents) = 0;
virtual bool scroll(const QRegion &area, int dx, int dy);

Наиболее интересный здесь метод это flush. Данный метод вызывается когда закончено обновление окна, и именно из этого метода удобней всего извлекать содержимое окна, чтобы потом загрузить в текстуру.

QPlatformWindow

Самый сложный в реализации класс. В нем содержится вся логика по управлению окнами. Обладает объемным интерфейсом. Абсолютный минимум, который требуется реализовать, чтобы увидеть первое окно, примерно такой:

void setGeometry(const QRect &rect);
void setWindowState(Qt::WindowState state);
void setWindowFlags(Qt::WindowFlags flags);
QMargins frameMargins() const;
 
void setVisible(bool visible);
bool isVisible();
void requestActivateWindow();
 
WId winId() const;

Базовая реализация из плагина minimal вполне сойдет на первое время.

Интеграция с приложением

Во всей реализации платформы для QT самая сложная часть — это интеграция её с приложением. Можно конечно использовать стандартный вариант — передать через параметр командной строки в QApplication флаг -platform с именем платформы (тогда, кстати, придется оформить нашу платформу в виде плагина), но это не позволит нам инициализировать платформу в нужный нам момент, сохранить в ней дополнительные данные, например, от 3D движка нашей игры. Более практичным вариантом является ручная установка платформы в QT. К сожалению, разработчики QT не предусмотрели такой вариант и приходится это делать вручную. Для начала, кусок кода который это делает:

int argc = 0;
char **argv = nullptr;
 
qputenv("QT_QPA_FONTDIR", fontDir.c_str()); // (1)
 
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar, true);
 
QGuiApplicationPrivate::platform_name = new QString("QGL"); //(2)
mPlatform = new UIIntegration(this->gui);
QGuiApplicationPrivate::platform_integration = mPlatform;
 
guiApp = new QGuiApplication(argc, argv, QCoreApplication::ApplicationFlags); //(3)
static_cast<UIIntegration *>(QGuiApplicationPrivate::platform_integration)->init(viewport);
 
QGuiApplicationPrivate::platform_theme = new QPlatformTheme; //(4)
 
app = new QApplication(argc, argv); //(5)
app->setQuitOnLastWindowClosed(false); //(6)

А теперь рассмотрим, что мы здесь делаем.

  1. Данная строка задает место, где будут искаться шрифты. Впринципе, этого можно и не делать, но тогда для unix платформ это будет что-то вида ./lib/fonts, что выглядит не очень хорошо
  2. Говорим приватной части QGuiApplication имя платформы, создаем платформу и устанавливаем ее в качестве platform_integration. Вроде бы этого должно быть достаточно, но, к сожалению, это не так.
  3. Создаем новое QGuiApplication. Оно должно быть создано до QApplication, хоть это и выглядит нелогично. Дело в том, что внутри конструктора QApplication, если еще не создан QGuiApplication, будет вызвано создание платформы!
  4. Создание темы.
  5. Создание самого приложения.
  6. Заставляем приложение оставаться активным, даже если было закрыто последнее окно.

После того, как мы выполнили инициализацию QT, мы можем начинать создавать свои окна, но делать это требуется обязательно в томже треде, где мы создавали QApplication и QGuiApplication.

Произведя все  эти манипуляции, вполне можно получить примерно вот такой результат:

Пример простой интеграции QT

Пример простой интеграции QT (qglgui)

Что осталось

Разумеется, данная статья оставила в стороне множество вопросов. Мы просто вывели на экран окно QT приложения. Для реального использования этого мало. Поэтому в дальнейших статьях я постараюсь рассказать про:

  • Передачу событий мышки и клавиатуры в QT
  • rise/lower и почему модальные окна прячутся
  • Рисование заголовка окна
  • и остальные вопросы, про которые я забыл

Рассмотрение всего этого я буду делать на примере библиотеки qglgui (https://github.com/Kvalme/qglgui)

QGLGUI

В начале я реализовывал данный плагин как часть своего 3D движка, но посмотрев, решил выделить его в отдельную библиотеку. Задача данной библиотеки — упростить интеграцию QT в свои приложения насколько это возможно. Например, на данный момент, для того, чтобы отобразить QT окно у себя в игре, надо написать всего несколько строчек кода:

void init()
{
gui = GlGui::Create(GlGui::THREADING_MODE::SINGLE, "../../../fonts", QRect(0, 0, 800, 600)); //Инициализируем GUI
gui->RegisterWindowFactory(createWindow); //Фабрика для созданий окон. В однопоточном случае можно и без нее обойтись
gui->RegisterRenderer(CreateRenderer(RENDERER_TYPE::GL1)); //Выбираем рендерер
gui->CreateWindow(""); //Создаем окно
}
 
void render()
{
gui->Update(); //Обрабатываем события от QT
gui->Render(); //Отрисовываем окно
}

Подробнее библиотека будет рассмотрена в дальнейших статьях.

Как известно QT — это кросс-платформенная библиотека, позволяющая достаточно быстро создавать приложения. С появлением в ней встроенного веб-браузера webkit, ее стало возможно использовать для написания ботов для веб-игр, чем я и решил заняться несколько дней назад. Read the rest of this entry »