Нередко перед разработчиком встает задача записи происходящего на экране в окне приложения — для целей записи демонстрационного ролика, развлечения пользователя, иногда отладки, и так далее. В этом tutorial я продемонстрирую, как это можно сделать в Qt Quick с минимальными усилиями. Итак, поехали.

Для начала создадим простейшее приложение, создав новый проект в 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-файлу, по которому мы сможем его найти, посмотреть, и увидеть примерно следующее:

image

Часики крутятся, снежинки падают, все замечательно.

Полный код этого tutorial доступен на GitHub, спасибо за внимание.

Комментарии (8)


  1. hardex
    12.03.2019 17:16

    Кликбейт


    1. KanuTaH Автор
      12.03.2019 17:31

      Хотите сказать, 5 минут на класс-обертку и таймер не хватит? :)


      1. hardex
        12.03.2019 17:51

        Хочу сказать, что в заголовке «запись экрана», а в результате запись содержимого собственного окна.


        1. KanuTaH Автор
          12.03.2019 17:53

          Не окна, а любого визуального QML-элемента на экране.

          P.S. Переформулировал «запись экрана» в «запись С экрана», так, может быть, действительно более понятно.


          1. Chaos_Optima
            12.03.2019 18:58

            Под записью экрана или записью с экрана, большинство будет понимать как запись всего что происходит на экране.

            Не окна, а любого визуального QML-элемента на экране.

            Но ведь только в рамках вашего приложения правильно?


            1. KanuTaH Автор
              12.03.2019 19:04

              Да, об этом написано во вводной части до ката.


          1. Sazonov
            12.03.2019 20:21

            Не любого, а лишь в контексте вашего процесса. «Любого» умеет GammaRay от KDAB. А ещё есть опен сорсный QBS Studio. Но там точно не на 20 минут работы.


  1. Sazonov
    12.03.2019 20:17

    Я в своё время намучился с кросс-платформенной реализацией «пипетки» с помощью Qt — взятие цвета любого пикселя на экране. Читая заголовок ожидал увидеть элегантное решение захвата экрана.
    А тут всё сводится к двум вещам: 1) есть single-header-gif-writer, 2) у QtQuick компонент (впрочем как и у виджетов) есть метод grabToImage.
    Спасибо за статью, но заголовок и близко не соответствует содержимому.