Screenshots erstellen mit C++/Qt
Veröffentlicht: 18. August 2019
Markus Meyer
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 Screenshot-Example von Qt selbst nimmt leider nur den gesamten Bildschirm auf.
- Die Idee auf StackOverflow nutzt ein transparentes Widget zum Auswählen eines Bereichs.
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

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.