Нередко перед разработчиком встает задача записи происходящего на экране в окне приложения — для целей записи демонстрационного ролика, развлечения пользователя, иногда отладки, и так далее. В этом tutorial я продемонстрирую, как это можно сделать в Qt Quick с минимальными усилиями. Итак, поехали.
Для начала создадим простейшее приложение, создав новый проект в Qt Creator. Его main.qml будет выглядеть примерно вот так:
Внутри этого окна определим сцену, которую, собственно, и будем записывать. В нашем случае это будет обычный прямоугольник белого цвета:
Далее нам нужно заполнить сцену чем-то, что можно было бы записывать. Для демонстрационных целей я решил добавить в нее анимированные картинки:
и падающие снежинки:
но в принципе тут может быть практически все, что угодно. Затем добавим в сцену реакцию на событие «клик мышкой», которое и будет у нас, собственно, запускать процесс записи:
и экран-ширму, который бы активировался на время создания записи. Задача экрана-ширмы двоякая — во-первых, показать, что «процесс идет», во-вторых, защитить сцену от просачивания лишних кликов и перезапуска процесса записи. Реализуем его следующим образом:
Теперь нам потребуется немного C++ кода для, собственно, создания GIF. Для работы непосредственно с форматом GIF в данном tutorial я буду использовать one-header GIF library от Charlie Tangora, осталось сделать ее доступной в QML. Для этого напишем оберточный класс, объявление которого будет выглядеть так:
Свойство imageFilePathMask будет предоставлять QML-коду шаблон пути для записи очередного кадра, который в сочетании QString::arg() можно будет использовать для создания файла с этим кадром:
Свойство gifFilePath у нас работает совершенно аналогично и предоставляет путь, по которому будет находиться результат нашей работы, то есть GIF-файл.
Далее реализуем создание самого GIF из кучи кадров с помощью функций из библиотеки gif.h:
и экспортируем класс-обертку в QML через добавление следующего кода в main.cpp:
Все, подготовка закончена, теперь самое интересное. Для записи отдельных кадров мы воспользуемся методом grabToImage, который есть у любого Item. Этот метод делает «скриншот» самого Item, и всех элементов, так или иначе дочерних к нему, даже динамических (в нашем случае это снежинки). Для начала наделаем нужное количество кадров, используя Timer:
а затем слепим их в GIF с помощью нашего класса-обертки, немного усложнив тот же самый таймер:
Готово! По окончании записи в консоли мы увидим путь к GIF-файлу, по которому мы сможем его найти, посмотреть, и увидеть примерно следующее:
Часики крутятся, снежинки падают, все замечательно.
Полный код этого tutorial доступен на GitHub, спасибо за внимание.
Для начала создадим простейшее приложение, создав новый проект в Qt Creator. Его main.qml будет выглядеть примерно вот так:
import QtQuick 2.12
import QtQuick.Controls 2.5
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("GifExample")
}
Внутри этого окна определим сцену, которую, собственно, и будем записывать. В нашем случае это будет обычный прямоугольник белого цвета:
Rectangle {
id: sceneRectangle
anchors.fill: parent
color: "white"
}
Далее нам нужно заполнить сцену чем-то, что можно было бы записывать. Для демонстрационных целей я решил добавить в нее анимированные картинки:
Row {
anchors.centerIn: parent
spacing: 32
Image {
source: "qrc:/resources/clock.png"
NumberAnimation on rotation {
loops: Animation.Infinite
from: 0
to: 360
duration: 1000
}
}
Image {
source: "qrc:/resources/clock.png"
NumberAnimation on rotation {
loops: Animation.Infinite
from: 360
to: 0
duration: 1000
}
}
}
и падающие снежинки:
import QtQuick.Particles 2.12
[...]
ParticleSystem {
id: particleSystem
}
Emitter {
anchors.fill: parent
system: particleSystem
lifeSpan: 1000
size: 16
emitRate: 25
velocity: AngleDirection {
angle: 90
angleVariation: 30
magnitude: 30
magnitudeVariation: 10
}
ImageParticle {
z: 1
system: particleSystem
source: "qrc:/resources/snowflake.png"
}
}
но в принципе тут может быть практически все, что угодно. Затем добавим в сцену реакцию на событие «клик мышкой», которое и будет у нас, собственно, запускать процесс записи:
MouseArea {
anchors.fill: parent
onClicked: {
captureGifTimer.start();
}
}
и экран-ширму, который бы активировался на время создания записи. Задача экрана-ширмы двоякая — во-первых, показать, что «процесс идет», во-вторых, защитить сцену от просачивания лишних кликов и перезапуска процесса записи. Реализуем его следующим образом:
Rectangle {
id: waitRectangle
z: 2
anchors.fill: parent
color: "white"
opacity: 0.75
visible: captureGifTimer.running
BusyIndicator {
anchors.centerIn: parent
running: waitRectangle.visible
}
MouseArea {
anchors.fill: parent
}
}
Теперь нам потребуется немного C++ кода для, собственно, создания GIF. Для работы непосредственно с форматом GIF в данном tutorial я буду использовать one-header GIF library от Charlie Tangora, осталось сделать ее доступной в QML. Для этого напишем оберточный класс, объявление которого будет выглядеть так:
class GifCreator : public QObject
{
Q_OBJECT
Q_PROPERTY(QString imageFilePathMask READ imageFilePathMask)
Q_PROPERTY(QString gifFilePath READ gifFilePath)
public:
explicit GifCreator(QObject *parent = nullptr) : QObject(parent) {}
virtual ~GifCreator() = default;
QString imageFilePathMask() const;
QString gifFilePath() const;
Q_INVOKABLE bool createGif(int frames_count, int frame_delay);
};
Свойство imageFilePathMask будет предоставлять QML-коду шаблон пути для записи очередного кадра, который в сочетании QString::arg() можно будет использовать для создания файла с этим кадром:
QString GifCreator::imageFilePathMask() const
{
QString tmp_dir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
if (tmp_dir != "") {
QDir().mkpath(tmp_dir);
}
return QDir(tmp_dir).filePath("image_%1.jpg");
}
Свойство gifFilePath у нас работает совершенно аналогично и предоставляет путь, по которому будет находиться результат нашей работы, то есть GIF-файл.
Далее реализуем создание самого GIF из кучи кадров с помощью функций из библиотеки gif.h:
bool GifCreator::createGif(int frames_count, int frame_delay)
{
QImage first_image(imageFilePathMask().arg(0));
if (!first_image.isNull()) {
GifWriter gif_writer;
if (GifBegin(&gif_writer, gifFilePath().toUtf8(), static_cast<uint32_t>(first_image.width()),
static_cast<uint32_t>(first_image.height()),
static_cast<uint32_t>(frame_delay))) {
for (int frame = 0; frame < frames_count; frame++) {
QImage image(imageFilePathMask().arg(frame));
if (!image.isNull()) {
if (!GifWriteFrame(&gif_writer,
image.convertToFormat(QImage::Format_Indexed8).
convertToFormat(QImage::Format_RGBA8888).constBits(),
static_cast<uint32_t>(image.width()),
static_cast<uint32_t>(image.height()),
static_cast<uint32_t>(frame_delay))) {
GifEnd(&gif_writer);
return false;
}
} else {
GifEnd(&gif_writer);
return false;
}
}
return GifEnd(&gif_writer);
} else {
return false;
}
} else {
return false;
}
}
и экспортируем класс-обертку в QML через добавление следующего кода в main.cpp:
engine.rootContext()->setContextProperty(QStringLiteral("GifCreator"), new GifCreator(&app));
Все, подготовка закончена, теперь самое интересное. Для записи отдельных кадров мы воспользуемся методом grabToImage, который есть у любого Item. Этот метод делает «скриншот» самого Item, и всех элементов, так или иначе дочерних к нему, даже динамических (в нашем случае это снежинки). Для начала наделаем нужное количество кадров, используя Timer:
Timer {
id: captureGifTimer
interval: 100
repeat: true
triggeredOnStart: true
property int frameNumber: 0
property int framesCount: 10
onRunningChanged: {
if (running) {
frameNumber = 0;
}
}
onTriggered: {
if (frameNumber < framesCount) {
var frame_number = frameNumber;
if (!sceneRectangle.grabToImage(function (result) {
result.saveToFile(GifCreator.imageFilePathMask.arg(frame_number));
})) {
console.log("grabToImage() failed for frame %1".arg(frame_number));
}
frameNumber = frameNumber + 1;
} else {
stop();
}
}
}
а затем слепим их в GIF с помощью нашего класса-обертки, немного усложнив тот же самый таймер:
Timer {
id: captureGifTimer
interval: 100
repeat: true
triggeredOnStart: true
property int frameNumber: 0
property int framesCount: 10
onRunningChanged: {
if (running) {
frameNumber = 0;
} else {
if (frameNumber >= framesCount) {
if (GifCreator.createGif(framesCount, interval / 10)) {
console.log(GifCreator.gifFilePath);
} else {
console.log("createGif() failed");
}
}
}
}
onTriggered: {
if (frameNumber < framesCount) {
var frame_number = frameNumber;
if (!sceneRectangle.grabToImage(function (result) {
result.saveToFile(GifCreator.imageFilePathMask.arg(frame_number));
})) {
console.log("grabToImage() failed for frame %1".arg(frame_number));
}
frameNumber = frameNumber + 1;
} else {
stop();
}
}
}
Готово! По окончании записи в консоли мы увидим путь к GIF-файлу, по которому мы сможем его найти, посмотреть, и увидеть примерно следующее:
Часики крутятся, снежинки падают, все замечательно.
Полный код этого tutorial доступен на GitHub, спасибо за внимание.
Комментарии (8)
Sazonov
12.03.2019 20:17Я в своё время намучился с кросс-платформенной реализацией «пипетки» с помощью Qt — взятие цвета любого пикселя на экране. Читая заголовок ожидал увидеть элегантное решение захвата экрана.
А тут всё сводится к двум вещам: 1) есть single-header-gif-writer, 2) у QtQuick компонент (впрочем как и у виджетов) есть метод grabToImage.
Спасибо за статью, но заголовок и близко не соответствует содержимому.
hardex
Кликбейт
KanuTaH Автор
Хотите сказать, 5 минут на класс-обертку и таймер не хватит? :)
hardex
Хочу сказать, что в заголовке «запись экрана», а в результате запись содержимого собственного окна.
KanuTaH Автор
Не окна, а любого визуального QML-элемента на экране.
P.S. Переформулировал «запись экрана» в «запись С экрана», так, может быть, действительно более понятно.
Chaos_Optima
Под записью экрана или записью с экрана, большинство будет понимать как запись всего что происходит на экране.
Но ведь только в рамках вашего приложения правильно?
KanuTaH Автор
Да, об этом написано во вводной части до ката.
Sazonov
Не любого, а лишь в контексте вашего процесса. «Любого» умеет GammaRay от KDAB. А ещё есть опен сорсный QBS Studio. Но там точно не на 20 минут работы.