Screenshots erstellen mit C++/Qt

Veröffentlicht: 18. August 2019
0 Kommentare
Enthalten in einer Kategorie: Programmierung

Einen Screenshot des Bildschirms zu erstellen ist einfach. Wenn es aber darum geht, dem Benutzer die Möglichkeit zu geben nur einen selbst gewählten Bereich des Bildschirms aufzunehmen, wird es komplizierter. In diesem Beitrag stelle ich eine Klasse vor, die das ermöglicht.

Die Klasse stammt aus einem Tool zum Erstellen von Tickets über die API der Projektverwaltung Redmine. Neben den üblichen Angaben und der Beschreibung des Fehlers hat der Nutzer auch die Möglichkeit, einen oder mehrere Screenshots an das Ticket anzuhängen. Da der Nutzer verständlicherweise nicht seinen ganzen Bildschirm als Screenshot hochladen will, habe ich nach einer Möglichkeit gesucht, dem Nutzer einen Auswahlrahmen anzubieten. Auf meiner Recherche nach dieser Funktion fand ich zwei Ansätze, die ich miteinander kombiniert und zusätzlich mit dem Support für mehrere Bildschirme erweitert habe.

Das Erstellen von Screenshots wird in der Klasse ScreenCaptureTool gebündelt. Diese ist ein <a class="extern" href="https://doc.qt.io/qt-5/qobject.html">QObject</a> mit zwei Slots und einem Signal. Das Signal newCapture() wird immer dann ausgesendet, wenn ein neuer Screenshot bereit steht. In den Parametern befindet sich neben dem Screenshots als <a class="extern" href="https://doc.qt.io/qt-5/qpixmap.html">QPixmap</a> auch eine Liste der Bildschirme, auf denen der Screenshot erstellt worden sind, sowie der Bereich des Screenshots. Der Bereich wird als <a class="extern" href="https://doc.qt.io/qt-5/qrect.html">QRect</a> übergeben, wobei die Pixel-Koordinaten über die gesamte Bildschirmfläche mehrerer Bildschirme hinweg zu verstehen sind. Die oberste linke Ecke hat dabei immer die Koordinate (0,0).

class ScreenCaptureTool : public QObject
{
    Q_OBJECT
public:
    explicit ScreenCaptureTool(QObject *parent = Q_NULLPTR);
    void setWindowsToHide(const QList<QWindow *> &hideWindowList);
    bool getScreenCapture(QList<QScreen* > &screens, QRect &rect, QPixmap &screenshot);
    static QByteArray convertPixmapToByteArray(const QPixmap &pixmap, const char *format = Q_NULLPTR);

private:
    QList<QWindow*> m_hideWindowList;

public slots:
    void onOpenAreaSelection();
    void onCaptureFullscreen();

signals:
    void newCapture(const QList<QScreen* >screens, const QRect &area, const QPixmap &pixmap);
};

Screenshot der gesamten Bildschirmfläche

Einen Screenshot der gesamten Bildschirmfläche kann mit dem Slot onCaptureFullscreen() erstellt werden. Der fertige Screenshot wird über das Signal ausgegeben. Dabei werden alle eigenen Fenster bzw. QWidgets kurz ausgeblendet, die über setWindowsToHide() übergeben wurden. Beispielhaft kann das aufrufende QWidget sich selbst über <a class="extern" href="https://doc.qt.io/qt-5/qwidget.html#windowHandle">window()->windowHandle()</a> ausblenden lassen. Möchte man, dass das Fenster bleibt, weil dort z.B. eine mögliche Fehlerquelle liegt, so übergibt man einfach nichts, eine leere Liste oder Nullpointer. Möchte man hingegen alle Fenster des gesamten Programms ausblenden, kann einfach <a class="extern" href="https://doc.qt.io/qt-5/qguiapplication.html#allWindows">QApplication::allWindows()</a> der Liste übergeben werden.

m_screenCaptureTool->setWindowsToHide(QList<QWindow*>() << window()->windowHandle());

In der Methode getScreenCapture() ist die eigentliche Funktion zur Herstellung des Vollbildscreenshots implementiert. Die Übergabeparameter sind Referenzen, die von der Methode gefüllt werden. Sie kann auch direkt ohne onCaptureFullscreen() und dem resultierenden Signal newCapture() verwendet werden. Die Methode nimmt für jeden Bildschirm eine QPixmap mit <a class="extern" href="https://doc.qt.io/qt-5/qscreen.html#grabWindow">QScreen::grabWindow()</a> auf und kombiniert die einzelnen Pixmaps dann zu einer QPixmap indem sie die Geometrie und Anordnung jedes Bildschirms beachtet. Beispielsweise werden 4 Bildschirme, die jeweils 2×2 angeordnet sind in der Pixmap zu einem großen 2×2 Bild zusammengelegt. Dieses umgebene Rechteck (“Bounding Rect”) wird über <a class="extern" href="https://doc.qt.io/qt-5/qrect.html#united">QRect::united()</a> aller Bildschirmgeometrien berechnet.

// capture all available screens
screens = QApplication::screens();
QList< QPair<QPixmap, QRect> > capturedPixmaps;
for(QScreen *screen : qAsConst(screens))
{
    QRect g = screen->geometry();
    QPixmap pix = screen->grabWindow(QApplication::desktop()->winId(), 
                                     g.x(), g.y(), g.width(), g.height());
    capturedPixmaps.append(QPair<QPixmap,QRect>(pix, g));
}

// calculate bounding rectangle of all screens
for(const QPair<QPixmap,QRect> &pixmap : qAsConst(capturedPixmaps))
{
    rect = rect.united(pixmap.second);
}
// remove negative offsets, e.g. if the main monitor is right and not left
// or any other monitor is higher than the main monitor
// the (0,0) position is always in the top left of the main monitor!
if(rect.top() < 0 || rect.left() < 0)
{
    for(QPair<QPixmap,QRect> &pixmap : capturedPixmaps)
    {
        // we have to translete every rect to keep the relative positions
        pixmap.second.translate(qAbs(rect.left()), qAbs(rect.top()));
    }
}

// setup screenshot size
screenshot = QPixmap(rect.width(), rect.height());

// combine all captured pixmaps based on the geometry to the full screen
QPainter painter(&screenshot);
screenshot.fill(Qt::black);
for(const QPair<QPixmap,QRect> &pixmap : qAsConst(capturedPixmaps))
{
    painter.drawPixmap(pixmap.second, pixmap.first);
}

Zu beachten sind dabei die verschiedenen Möglichkeiten, wie der Nutzer seine Bildschirme anordnen kann. Der Ursprungspunkt ist immer links oben am Hauptmonitor. Befindet sich dieser aber beispielsweise rechts neben anderen Monitoren, so hat die linke obere Ecke des umgebenden Rechtecks eine negative Koordinate. Genauso verhält es sich in der Höhe, wenn der Hauptmonitor nicht am weitesten oben ist. Die resultierende Pixmap hat jedoch keine negativen Koordinaten. Daher müssen in diesen Fällen alle Rechtecke korrigiert werden, indem man sie um den Betrag der negativen Koordinate links oben in den ersten Quadranten verschiebt. Die resultierenden Rechtecke sind immer positiv und können direkt zum Zeichnen der zusammengefügten Pixmap verwendet werden.

Das zusammengeführte QPixmap wird auf diese Größe skaliert, komplett schwarz eingefärbt und dann mit den aufgenommenen Pixmaps übermalt.

Overlay zur Auswahl eines Bereichs für den Screenshot

Das Overlay erlaubt das Ziehen eines Auswahlrahmens zur Aufnahme des Screenshots.Vergrößern
Das Overlay erlaubt das Ziehen eines Auswahlrahmens zur Aufnahme des Screenshots.

Um den Nutzer nur einen Teil seiner gesamten Bildschirmfläche muss der Slot onOpenAreaSelection() aufgerufen werden. Dieser blendet erneut alle angegebenen Fenster aus und zeigt dann ein leicht abgedunkeltes Overlay. Bei diesem Overlay handelt es sich um einen modalen <a class="extern" href="https://doc.qt.io/qt-5/qdialog.html">QDialog</a> ohne Fensterrahmen oder Titelzeile, der einen halbtransparenten Hintergrund hat und in Vollbild sowie im Vordergrund angezeigt wird. Außerdem wird der Maus-Cursor mit <a class="extern" href="https://doc.qt.io/qt-5/qguiapplication.html#setOverrideCursor">QApplication::setOverrideCursor()</a> in ein Fadenkreuz geändert, solang man sich auf der halbtransparenten Fläche bewegt. Diese Klasse heißt TransparentCaptureWindow.

Da sich die Funktion “Vollbild” nur auf einen Bildschirm beziehen kann, gibt es die Möglichkeit durch Betätigung der <Tab>-Taste auf der Tastatur durch die einzelnen Bildschirme durchzuschalten. D.h. aber auch, dass der Auswahlrahmen nicht über mehrere Bildschirme hinweg gezogen werden kann.

TransparentCaptureWindow::TransparentCaptureWindow(QWidget *parent)
    : QDialog(parent)
{    
    setWindowModality(Qt::WindowModal);
    show();
    raise();
    activateWindow();
    setWindowFlags(Qt::SplashScreen | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint );
    setWindowOpacity(0.3);

    m_rubberBand = new QRubberBand(QRubberBand::Rectangle, this);
    QPalette pal = m_rubberBand->palette();
    pal.setBrush(QPalette::Highlight, QBrush(QColor(255,0,0,0)));
    m_rubberBand->setPalette(pal);

    setGeometry(QApplication::desktop()->screenGeometry(this));
    QApplication::setOverrideCursor(QCursor(Qt::CrossCursor));
}

Der eigentliche Trick besteht darin, dass mit dem QDialog nun ein <a class="extern" href="https://doc.qt.io/qt-5/qwidget.html">QWidget</a> bereit steht, auf dem Mausklicks abgefangen werden können. Der Nutzer zieht also einen Rahmen mit <a class="extern" href="https://doc.qt.io/qt-5/qrubberband.html">QRubberband</a> und wenn die linke Maustaste losgelassen wird, nimmt wieder QScreen::grabWindow() den Screenshot auf. Diesmal jedoch nur der Bereich, der ausgewählt wurde. Um das QRubberband zu realisieren, müssen drei Event-Methoden von QWidget überschrieben werden: <a class="extern" href="https://doc.qt.io/qt-5/qwidget.html#mousePressEvent">mousePressEvent()</a>, <a class="extern" href="https://doc.qt.io/qt-5/qwidget.html#mouseMoveEvent">mouseMoveEvent()</a> und <a class="extern" href="https://doc.qt.io/qt-5/qwidget.html#mouseReleaseEvent">mouseReleaseEvent()</a>. Ersteres reagiert auf die Betätigung der linken Maustaste um den Startpunkt des Auswahlrahmens zu speichern und den Rahmen selbst sichtbar zu schalten. Bei jeder Bewegung der Maus wird das mouseMoveEvent() aufgerufen, in dem über einen <a class="extern" href="https://doc.qt.io/qt-5/qtooltip.html">QTooltip</a> die aktuelle Mausposition in Pixeln angezeigt wird. Ist der Auswahlrahmen sichtbar, wird zusätzlich noch der Anfangspunkt angezeigt. Erst sobald die linke Maustaste wieder losgelassen wird, werden die Abmaße des Auswahlrahmens zur Erstellung des Screenshots genutzt. Wird irgendeine andere Maustaste oder Taste auf der Tastatur betätigt, wird das Overlay-Widget ohne Ergebnis geschlossen.

void TransparentCaptureWindow::mouseReleaseEvent(QMouseEvent *event)
{
    if(event->button() == Qt::LeftButton)
    {
        if(m_rubberBand)
        {
            m_rubberBand->hide();

            QWindow *window = windowHandle();
            if(window)
            {
                QScreen *screen = window->screen();
                if(screen)
                {
                    // adjust the capture rect if (0,0) is not on the top left (may moved if there is a taskbar on top or something))
                    QRect g = screen->availableGeometry();

                    // normalize the origin point to be always top left of the rubberband regardless where the user started to select
                    QPoint origin = m_rubberOrigin;
                    if(m_rubberOrigin.x() > event->x() && m_rubberOrigin.y() > event->y())
                    {
                        origin = event->pos();
                    }
                    else if(m_rubberOrigin.x() > event->x())
                    {
                        origin.setX(event->x());
                    }
                    else if(m_rubberOrigin.y() > event->y())
                    {
                        origin.setY(event->y());
                    }
                    QRect captureRect = QRect(origin.x() + g.x(), origin.y() + g.y() ,
                                          m_rubberBand->rect().width(), m_rubberBand->rect().height()).normalized();
                    setWindowOpacity(0); // make this window fully transparent to grab everything behind
                    QPixmap pix = screen->grabWindow(QApplication::desktop()->winId(),
                                                     captureRect.x(), captureRect.y(), captureRect.width(), captureRect.height());
                    emit selectedPixmap(QList<QScreen* >() << screen, captureRect, pix);
                }
            }
            QApplication::restoreOverrideCursor();
            accept();
        }
    }
}

Natürlich muss hier auch beachtet werden, dass der Nutzer den Auswahlrahmen von links oben nach rechts unten oder umgekehrt zeichnen kann. Die Methode QScreen::grabWindow() geht immer von einem Ursprungspunkt links oben aus. Der erstellte Screenshot wird über das Signal selectedPixmap() übermittelt, das aber direkt mit dem Signal newCapture() der Klasse ScreenCaptureTool verbunden ist.

Der Quellcode, den Sie am Ende des Artikels herunterladen können, enthält neben den hier beschriebenen zwei Klassen noch ein QWidget namens AttachmentList. Dieses bietet nicht nur einige Schaltflächen um die beschriebene Slots auszulösen, sondern es sammelt und zeigt die erstellten Screenshots auch in einem <a class="extern" href="https://doc.qt.io/qt-5/qlistwidget.html">QListWidget</a> an. Dieses erlaubt, sofern gewünscht, auch das Hinzufügen von Dateien aus dem Dateisystem. Es kann damit z.B. in einem Tool zum Melden von Fehlern aber auch in einem E-Mail Programm verwendet werden.

Download des Quellcodes

Der Download ist als ZIP-Archiv möglich. In diesem befinden sich die Dateien mit den beschriebenen Klassen und eine Qt-Projektdatei, die mit dem QtCreator geöffnet werden kann. Ich habe die Kompilierung und Funktion auf Windows 7 und Windows 10 mit MinGW 7.3.0 64 Bit und Scientific Linux 7.4 mit gcc_64-4.8.5 erfolgreich getestet. Die getestete Qt-Version ist 5.12.2. Support für C++11 ist Pflicht. Der Code ist LGPL lizenziert, d.h. er kann frei verändert und weiterverbreitet werden. Wenn Sie Fehler finden, hinterlassen Sie gern einen Kommentar, damit ich diesem im Download beheben kann.


zurück

Teile deinen Kommentar oder Feedback zu diesem Beitrag.

Die Angabe der E-Mail-Adresse ist optional und wird nicht veröffentlicht. Sie wird zur Einbindung deines Gravatars und ggf. zur Kontaktaufnahme verwendet. Erforderliche Felder sind mit * markiert.

*

*

*

Durch Betätigen von "Kommentar abschicken", stimmen Sie der Veröffentlichung der eingegebenen Daten zu. Weitere Informationen finden Sie in der Datenschutzerklärung.
Sie können Quellcode über <pre><code>Quellcode</code></pre> in Kommentare einfügen.