JGU Logo JGU Logo JGU Logo JGU Logo

Institut für Informatik

Michael Wand
David Hartmann
Sommersemester 2020DIGITAL

Blatt 09

Anlage: GUIs mit Qt
Einführung in die Softwareentwicklung



Qt Basics

Was ist Qt?

Qt ist ein Toolkit, das dabei hilft Programme (und insbesondere Programmoberflächen) plattformübergreifend zu entwickeln. Bei der richtigen Verwendung kann Ihr Programm nicht nur auf Windows und gängigen POSIX-Systemen, sondern mittlerweile auch auf Android, iOS, Windows Phone und eingebetteten Systemen ausgeführt werden.


In dieser Übersicht soll ein kleiner Teil von Qt (in seiner aktuellen Version Qt5.15) vorgestellt werden, der für die Lösung der Aufgaben ausreichen sollte.



Bauen von Qt-Programmen

QtCreator:
Wählen Sie beim Erstellen eines Projektes den Projekttyp: Application (Qt) -> Qt Widgets AApplication.


CMake:
Sofern Sie bereits mit CMake vertraut sind, ist das Bauen gegen Qt einfach durch target_link_libraries möglich:

CMakeLists.txt

cmake_minimum_required(VERSION 3.0.2)
project(GuiProgramm)

find_package(Qt5Widgets REQUIRED)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)

add_executable(GuiProgramm gui.cpp)
target_link_libraries (GuiProgramm Qt5::Widgets)

Bauen kann man dann wie mit Cmake üblich mit: cmake .; make


QMake:
QMake ist wie CMake ein Makefile-Generator, der jedoch für Qt spezialisiert ist.

GuiProgramm.pro

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = GuiProgramm
TEMPLATE = app
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

CONFIG += c++17

SOURCES += gui.cpp

Bauen kann man dann ähnlich wie mit Cmake mit: qmake .; make

Verwendung von Qt

Bevor Sie die Klassen aus Qt verwenden können, müssen Sie zuvor ein QApplication-Objekt erstellen. Dieses kümmert sich darum, dass Kommandozeilenargumente an die GUI Ihres Programms richtig übergeben werden, Events/Interaktiv bearbeitet werden, Ihr Programm richtig initialisiert wird uvm.


#include <QtWidgets>

// argc bekommt die Anzahl der Kommandozeilenargumente übergeben, 
// argv bekommt die Argument selbst übergeben
// Bsp.:
// Windows:
// abc.exe blub bla
// Linux/OS X:
// ./abc blub bla
// führt zu 
// argc ist 3
// argv ist {"./abc", "blub", "bla"}
int main(int argc, char *argv[])
{
    # Initialisierung des Programms und Ihrer GUI
    QApplication app(argc, argv);

    // ...
    // Ihr Programmcode
    // ...

    // Starten der Ereignisschleife (engl. event loop; Abfangen von Klicken,
    // Tippen, etc.) und beenden des gesamten Programms beim Beenden des Qt-Teils
    // (dies geschieht standardmäßig beim Schließen aller Fenster).
    // Diese beiden Schritte zu trennen ist innerhalb dieses Praktikums nicht nötig.
    return app.exec();
}

Achtung: Vergessen Sie beim Testen nicht QApplication zu initialisieren und am Ende auszuführen (app.exec()). Falls Sie es vergessen, werden Sie unter Umständen ein schwarzes Fenster sehen, auf jeden Fall aber sind dann Ihre Interaktionsmöglichkeiten mit der Anwendung sehr beschränkt.


Hallo Welt Testen Sie den folgenden Code und versuchen Sie nachzuvollziehen, was geschieht:


#include <QtWidgets>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QWidget w;
    QLabel *l = new QLabel(&w);
    l->setText("Hallo Welt!");
    // Alternativ:
    /* QLabel *l2 = new QLabel("Nochmals hallo!",&w); */
    w.setGeometry(120,120,200,50);
    w.move(100,300);
    w.setWindowTitle("Tadaa, ein Fenster");
    w.show();

    return app.exec();
}

Die QWidget-Basisklasse

Die QWidget-Klasse ist die Basisklasse aller anderen Benutzerschnittstellen-Objekte. Das bedeutet Buttons, Textfelder, Fenster usw. liefern dieselbe Funktionalität. Wie Sie vielleicht bereits durch das „Hallo Welt“ bemerkt haben, repräsentiert QWidget() ein leeres Fenster1. Hier sind noch ein paar Funktionen von QWidget:


#include <QtWidgets>
#include <QDesktopWidget>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QWidget fenster1;
    QWidget fenster2;
    QWidget fenster3;
    QWidget fenster4;

    // Fensternamen
    fenster1.setWindowTitle("Das ist Fenster 1");
    fenster2.setWindowTitle("Das ist Fenster 2 (max)");
    fenster3.setWindowTitle("Das ist Fenster 3 (min)");
    fenster4.setWindowTitle("Das ist Fenster 4 (center)");

    // typische Fenstermanipulation
    fenster1.resize(300,500); // Größe ändern.
    fenster1.move(25,50);     // auf dem Bildschirm verschieben

    // Zentrieren auf dem Bildschirm (etwas komplizierter, aber auch möglich)
    fenster4.move(QApplication::desktop()->screenGeometry(&fenster4).center() 
                    - fenster4.rect().center());

    // Anzeigen der Fenster
    fenster1.show();
    fenster2.showMaximized();
    fenster3.showMinimized();
    fenster4.show();
    return app.exec();
}

Ableiten von QWidget

Meistens werden Sie keine „losen“ GUI-Objekte in einer Methode haben wollen. Stattdessen können Sie auch eine Klasse erstellen, die selbst von QWidget ableitet, um eigene Widgets zu erstellen (natürlich können Sie von allen QWidget-Klassen ableiten, etwa die QMainWindow-Klasse, zu der wir später kommen werden).


#include <QtWidgets>

class MyGuiProgram : public QWidget {
    public:

    // statische (optionale) Methode zum Starten
    // des GUI-Programms
    static int spawn(int argc, char *argv[]) {
        QApplication app(argc, argv);
        MyGuiProgram P;
        return app.exec();
    }

    // Konstruktor
    MyGuiProgram() : QWidget() /* dies ruft den Konstruktor der Basisklasse auf */ {

        // gute Idee: Lagern Sie Code aus, um Ihren Code
        // übersichtlich zu halten
        this->setWindowTitle("Nice Title");
        this->init_mainWindow(); //ausgelagerte GUI-Erstellung
        this->showMaximized();
    }

    void init_mainWindow() {
        // new ist wichtig, sonst wird QLabel nach der Funktion zerstört
        // delete muss dabei nicht mehr aufgerufen werden!
        QLabel *L = new QLabel("What a label!", this);
    }
};

int main(int argc, char *argv[])
{
    return MyGuiProgram::spawn(argc, argv);
}

Nützliche GUI-Elemente

Bevor wir uns spezielleren Themen widmen möchten wir Ihnen einige Elemente präsentieren, die Ihnen beim Erstellen Ihrer GUI behilflich sein kann.



Wie bereits erwähnt, sind auch GUI-Elemente wie Buttons oder Text-Eingabefelder selbst QWidgets - daher erben sie auch die oben aufgeführten Funktionen und können also selbst auch bereits alleinstehend in einem automatisch erstellten Fenster angezeigt werden. Z.B.:

QPushButton b("Drück mich!");
b.resize(150,200);
b.move(10,20);
b.show();

Im Umkehrschluss bedeutet das aber auch, dass QWidget selbst kein Fenster darstellt, sondern lediglich einen leeren "Container". Einzelne Elemente anzuzeigen ist nicht so spannend; bevor wir zur Schachtelung von Elementen kommen, sollen nun einige nützliche Elemente und ihre Funktionsweisen erklärt werden. Probieren Sie die Elemente ruhig aus. Alternativ können Sie ihr Aussehen und mehr Informationen zu diesen auch unter den eingangs genannten Seiten entnehmen. Schauen Sie sich ruhig auch die oben genannte offizielle Dokumentation an, um Genaueres zu erfahren.


Anmerkung: Alle QWidgets haben stets zusätzliche Konstruktoren die ein weiteres QWidget* als letztes Argument erhalten, das das neue Widget als Hauptelement anzeigen soll (und dessen Speicherverwaltung übernimmt, siehe oben).

QPushButton b("Drück mich!",this);
b.resize(150,200);
b.move(10,20);

Wie man strukturierte Layouts (ohne absolute Positionierung) baut schauen wir uns weiter unten an.


QPushButton & Signals

„Klickbare“ Buttons


// in .h: static void myfunction();
void MyGuiProgram::myfunction() {
    std::cout << "clicked button" << std::endl;
}

...

QPushButton* btn = new QPushButton("&Drück mich!"); // & bedeutet: Alt+D löst den Button aus
btn->setDefault(true);    // Standard-Button (wird bei Enter gedrücht)
btn->setEnabled(false);   // ausgegraut

// connect Verbindet ein Ereignis mit einem beliebigen Funktionsaufruf.
// Hier wird das Signal/Ereignis „clicked“ mit dem Slot/der Funktion
// myfunction verbunden.
connect(btn, &QPushButton::clicked, &MyGuiProgram::myfunction);

// kürzer und sauberer mithilfe von Lambdas
connect(btn, &QPushButton::clicked, [=](){
    std::cout << "clicked button lambda" << std::endl;
});

Falls connect nicht gefunden wird, kann stattdessen QObject::connect(...) verwendet werden, bzw. statt QObject kann auch QPushButton::connect(...) verwendet werden, um Type-checks auf den Slots zu erlauben.


Andere Ereignisse sind: QPushButton::released und QPushButton::pressed. QPushButton::clicked wartet bis beide Ereignisse eingetroffen sind.



QLabel

Nicht-veränderbaren (durch den User) Text oder Bilder (wird später behandelt) anzeigen.

QLabel *lbl = new QLabel("Ha! Du kannst mich nicht ändern!");
QLabel *lbl2 = new QLabel();
lbl2->setText("nachträglich verändert");  // nur nicht durch den User veränderbar
lbl2->setAlignment(Qt::AlignRight);       // Ausrichtung

QLineEdit

Veränderbarer Text

QLineEdit *le  = new QLineEdit("Initialtext");
QLineEdit *le2 = new QLineEdit(this);

// Einstellungen
le->setMaxLength(10);                   // Maximallänge
le->setAlignment(Qt::AlignRight);     // Ausrichtung
le->setFont(QFont("Arial", 20));     // Schriftart (Arial, Größe 20)

// direkte Validierung
le2->setValidator(new QIntValidator());     // nur Ganzzahlen
le2->setValidator(new QDoubleValidator());  // nur Fließkommazahlen 
le2->setValidator(new QDoubleValidator(0.99, 99.99, 2));  // (festes Format)

// signals (wie bei QPushButton)
connect(le2, &QLineEdit::inputRejected, [=](){
    std::cout << "ungültige Eingabe" << std::endl;
});
connect(le2, &QLineEdit::cursorPositionChanged, [=](int oldPos, int newPos){
    std::cout << "neue Position" << newPos << std::endl;
});
connect(le2, &QLineEdit::textEdited, [=](QString s){
    std::cout << "neuer Text:" << s.toStdString() << std::endl;
});
//      ..., &QLineEdit::textChanged, ...
//      ..., &QLineEdit::editingFinhsed, ...
//      ..., &QLineEdit::selectionChanged, ...
//      ..., &QLineEdit::returnPressed, ...

QCheckBox

Checkbox mit Text-Annotation


QCheckBox *cb = new QCheckBox("Possibility 1");
cb->setChecked(true); // Standardmäßig an oder aus?
cb->setText("abc");
cb->text();           // getter (hier: "abc")
cb->isChecked();

// Funktionsaufruf bei Statusänderung, Verwendung wie bei den anderen Signals oben.
// connect( ..., &QCheckBox::toggled, ... );

Layout Manager

Absolute Positionierung

Jedes Widget hat das optionale Argument parent. Durch Setzen dessen auf ein anderes Widget, wird das neu erstellte als Teil des Widgets parent dargestellt.


// Initialisierung wie üblich
QApplication app(argc, argv);

QWidget win;
win.setGeometry(10, 30, 300, 200); // Position: (10,30) auf dem Bildschirm
                                  // Größe:  (300,200)
QPushButton b*tn = new QPushButton("Jim",&win);
btn.move(20,20);                   // Position (20,20) in parent=win
QPushButton *btn2 = new QPushButton("Jim2",&win);
btn2.move(120,120);                // Position (120,120) in parent=win

// Anzeigen und Starten
win.show();
app.exec();

Auf diese Art und Weise können ganze Layouts definiert werden. Erstellen Sie Widgets und arrangieren Sie darin rekursiv andere Widgets. Diese Herangehensweise hat jedoch einige offensichtliche Nachteile:



Aus diesem Grund gibt es sogenannte Layout Manager, die die absolute Positionierung für Sie übernehmen und bei der dynamische Änderungen eines Layouts helfen können.


Worin sich alle Layouts ähneln, ist, dass man anstelle eines Widgets auch immer ein anderes Layout einsetzen kann. So können selbst sehr verschachtelte GUIs strukturiert werden. Das äußerste Layout muss per win.setLayout(layout) zugewiesen werden.

QHBoxLayout (horizontal) & QVBoxLayout (vertikal)

Diese Layouts arrangieren Widgets und Layouts in einer horizontalen bzw. vertikalen Reihe. Es folgt ein Beispiel, das beides verwendet.


Interessant ist hier insbesondere das Verhalten bei Größenänderungen des Fensters:


int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QWidget win;
    QPushButton *btn1 = new QPushButton("Jim");
    QPushButton *btn2 = new QPushButton("Benjamin");
    QLabel *lbl = new QLabel("Entscheide dich:");
    QPushButton *btn3 = new QPushButton("blau");
    QPushButton *btn4 = new QPushButton("rot");

    QVBoxLayout *vbox = new QVBoxLayout();
    vbox->addWidget(btn1);     // erst btn1
    vbox->addWidget(btn2);     // darunter btn2
    vbox->addStretch();        // skalierter leerer Platz
    vbox->addWidget(lbl);      // darunter ein Label

    QHBoxLayout *hbox = new QHBoxLayout();
    vbox->addLayout(hbox);     // darunter ein neues Layout (als Widget)

    hbox->addWidget(btn3);     // links:  btn3
    hbox->addStretch();        // Mitte:  gestreckte leere Fläche
    hbox->addWidget(btn4);     // rechts: btn4

    win.setLayout(vbox);
    win.show();
    return app.exec();
}

QFormLayout

Diese Layouts arrangieren Widgets und Layouts in einer typischen Formularansicht: Zeilenweise links ein Label, rechts ein Widget:


int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QWidget win;
    QFormLayout *form = new QFormLayout();
    form->addRow(new QLabel("test"), new QLineEdit());
    form->addRow(new QLabel("name"), new QPushButton());

    QHBoxLayout *hbox = new QHBoxLayout();
    hbox->addWidget(new QCheckBox("check1"));
    hbox->addWidget(new QCheckBox("check2"));
    hbox->addWidget(new QCheckBox("check3"));

    form->addRow(new QLabel("check"), hbox);

    win.setLayout(form); // Wichtig! Erst hier wird das Layout an win angefügt
    win.show();
}

QGridLayout

Das Grid-Layout ermöglicht das Arrangieren der Widgets in einem Gitter. Es können dabei sowohl einzelne Gitterzellen durch je ein Widget befüllt werden, als auch mehrere Zellen durch ein Widget:


int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QWidget win;
    QGridLayout *grid = new QGridLayout();

    // einzelne Zellen
    QString str = "Knopf %1 / %2";
    for (int i=1; i<=4; i++)
        for (int j=1; j<=3; j++)
            grid->addWidget(new QPushButton(str.arg(i).arg(j)), i,j); // Position (i,j)

    // mehrere Zellen
    grid->addWidget(new QPushButton("Knopf Quad"), 1,3, 2, 2); // Position (1,3), Größe (2,2)
    grid->addWidget(new QPushButton("Knopf Lang"), 1,5, 4, 1);
    grid->addWidget(new QPushButton("Knopf Breit"), 4,1, 1, 2);

    // Unterlayouts können übrigens auch hinzugefügt werden
    // grid.addLayout(...);

    win.setLayout(grid); // Wichtig! Erst hier wird das Layout an win angefügt
    win.show();
    return app.exec();
}

Tooltips

Um das Arbeiten mit Ihrem Programm zu erleichtern, können Sie Tooltips einsetzen. Diese werden angezeigt, sobald Sie mit der Maus über das jeweilige Element fahren und warten.

QPushButton *btn = new QPushButton("&Drück mich!"); // & bedeutet: Alt+D löst den Button aus
btn->setToolTip("Dies erscheint erst, wenn die Maus über den Button fährt und dann wartet.");
// PS: Tooltips können auch bei allen anderen QWidgets angewandt werden.

Dialoge

Angenommen Sie erwarten eine Benutzereingabe, z.B. das Auswählen einer Datei (mittels QFileDialog) oder Sie müssen dem Benutzer etwas mitteilen. Durch einen Dialog kann dies erreicht werden. Die folgenden Beispiele sollten bei der Erstellung Ihrer Benutzeroberflächen als Anreiz reichen. (Eine weitere, hier nicht aufgeführte, Dialogklasse ist QMessageBox)


Entscheidungsdialog


#include <QtWidgets>

void dialog(QPushButton *b) {
    QDialog d;
    d.setWindowTitle("Dialogtitel");

    // kann der Dialog umgangen werden um andere Fenster zu bedienen?
    d.setWindowModality(Qt::ApplicationModal); // nein (Standard)
    // d.setWindowModality(Qt::WindowModal);   // ja

    // ... wie QWidget verwendbar
    auto btnok = new QPushButton("Rot",&d);
    auto btnno = new QPushButton("Blau",&d);
    auto hbox = new QHBoxLayout();
    hbox->addWidget(btnok);
    hbox->addWidget(btnno);
    d.setLayout(hbox);

    // akzeptieren / ablehnen
    QPushButton::connect(btnok, &QPushButton::clicked, [&d]() { d.accept(); });
    QPushButton::connect(btnno, &QPushButton::clicked, [&d]() { d.reject(); });

    // bei Fehler / Erfolg an Hauptfenster melden
    QPushButton::connect(&d, &QDialog::accepted, [b]() { 
        b->setText("Willkommen in der Matrix!"); 
    });
    QPushButton::connect(&d, &QDialog::rejected, [b]() {
        b->setText("Ach ist das Wunderland schön!");
    });

    // Extras
    btnok->setDefault(true);          // btn wird bei Enter gedrückt
    d.exec();                         // Fenster ausführen
}

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QWidget w;
    QPushButton *b = new QPushButton("Die &Entscheidung", &w);
    b->resize(200,50);
    QPushButton::connect(b, &QPushButton::clicked, [b]() { dialog(b); });

    w.show();
    return app.exec();
}

Dateiauswahl-Dialog


#include <QtWidgets>

void dialog(QPushButton *b) {
    QFileDialog d;
    d.setWindowTitle("Dialogtitel");

    // bei Auswahl einer Datei
    QFileDialog::connect(&d, &QFileDialog::fileSelected, [b](QString file) { 
        b->setText(file); 
    });

    d.exec(); // Fenster ausführen
}

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QWidget w;
    QPushButton *b = new QPushButton("Die &Entscheidung", &w);
    b->resize(500,30);
    QPushButton::connect(b, &QPushButton::clicked, [b]() { dialog(b); });

    w.show();
    return app.exec();
}

Timer

Manchmal benötigen Sie Programmcode, der nach einem Zeitintervall einmalig oder periodisch aufgerufen werden soll. Dieses Beispiel zeigt Ihnen, wie Sie dies mit Qt erreichen können.


fn ist wie bei allen anderen Slots auch entweder ein Pointer auf die aufzurufende Funktion oder aber ein Lambda-Ausdruck


QTimer timer;
timer.setInterval(300);    // Timer-Reset alle 300ms

// QTimer::connect(&timer, &QTimer::timeout, fn);
// Z.B.:
QTimer::connect(&timer, &QTimer::timeout, []() {
    std::cout << "timeout" << std::endl; 
});

timer.start(3000); // 3000: Anzahl der Millisekunden, nach denen der Timer die
                   // Ausführung von fn erstmals startet. (Standardmäßig ms=0)
//timer.stop();               // Timer anhalten
//timer.setSingleShot(true);  // einmalige Ausführung von fn
//timer.isActive();           // gibt true zurück, falls der Timer an ist

Bilder

Das Arbeiten mit Bildern ist für einige Anwendungen unerlässlich. Das betrifft nicht nur das Anzeigen von bestehenden Bildern, sondern auch das Erstellen neuer Bilddaten. In Qt gibt es dabei zwei Arten von Bildrepräsentationen: QPixmap und QImage.



Als erstes schauen wir uns das Arbeiten mit reinen Pixeldaten an.


Anzeigen einer Bilddatei

Bilder sind keine Widgets in Qt und können daher nicht direkt angezeigt werden. Glücklicherweise liefert das Widget QLabel diese Möglichkeit nach, indem man diesem ein QPixmap als anzuzeigendes Objekt übergibt. (Gehen Sie davon aus, dass die Konvertierung von QImage zu QPixmap schnell von statten geht.) Erinnern Sie sich daran, dass ein Pixmap fest ist und daher beim Neuzeichnen erneut erstellt werden muss. Im folgenden Beispiel gehen wir davon aus, dass sich ein Bild mit dem Namen bild.png in demselben Ordner befindet, in dem auch das Skript gestartet wird.


int main(int argc, char *argv[]) {
    // in QLabel wird das Bild angezeigt
    QLabel display;

    // Laden der Datei in eine Pixmap (nicht direkt bearbeitbar)
    QPixmap bild("bild.png");
    display.setPixmap(bild);

    // Alternativ: QImage (muss zu QPixmap konvertiert werden)
    QImage bild("bild.png");
    display.setPixmap(QPixmap::fromImage(bild));

    // Fenster anzeigen
    display.show();

    // wie immer auch ...
    app.exec();
}

Skalieren eines Bildes

Sie können ein Pixmap folgendermaßen skalieren - beachten Sie, dass es sowohl möglich ist die Skalierung an der vorgegeben Größe eines displays (QLabel) festzumachen, aber auch die Größe direkt festzulegen:


// Möglichkeit 1
QLabel display;
display.resize(200,300);
QPixmap pixmap = ... // wie oben QPixmap(...) oder QPixmap::fromImage(...)
QPixmap scaledpixmap = pixmap.scaled(display.size(), Qt::KeepAspectRatio);
display.setPixmap(scaledpixmap);

// Möglichkeit 2
QLabel display;
QPixmap pixmap = ... // wie oben QPixmap(...) oder QPixmap.fromImage(...)
QPixmap scaledpixmap = pixmap.scaled(200,300, Qt::KeepAspectRatio);
display.setPixmap(scaledpixmap);

Erstellen und Manipulation eines Bildes

// wir erstellen zuerst das Bild
int breite = 400, hoehe = 300;

// QImage-Formate: http://doc.qt.io/qt-5/qimage.html#Format-enum
QImage img(breite, hoehe, QImage::Format_RGBA8888);
img.fill(Qt::white); // Bild mit Standardwerten füllen (Wichtig!)

// das Format der Pixelfarbe lautet 0xAARRGGBB, wobei
// RR, GG, BB jeweils die Hex-Darstellung der Farbwerte für 
// Rot, Grün und Blau zwischen 0 und 255 ist (d.h. 195 entspräche C3). 
// 0 bedeutet, dass die Farbe nicht vorhanden ist, 
// 255 ist der Maximalwert jeder Farbe.
// AA beschreibt die Transparenz (0 für Transparent, 1 für Sichtbar)
img.setPixel(10,20, 0xff009900); // Dunkelgrün (der A-/Opacity-Wert ist vorne)

// das Setzen von Hex-Werten bedarf einiger Übung und Gewöhnung.
// Glücklicherweise ist es auch möglich, die Farbwerte im 10er-System
// anzugeben (Werte liegen jeweils zwischen 0 und 255).
img.setPixelColor(20,40,QColor(20,30,155));

// Anzeigen in QLabel display
display.setPixmap(QPixmap::fromImage(img));

// neue Manipulation ist erst beim Neu-Zeichnen sichtbar!
img.setPixelColor(80,80,QColor(20,30,155));

// erneut Zeichnen
display.setPixmap(QPixmap::fromImage(img));

Speichern eines Bildes

Nicht jedes Programm, das mit Bildern arbeitet, muss eine GUI mitliefern und dieses Bild erst auf dem Bildschirm anzeigen. Hier sehen Sie eine Möglichkeit, das Bild zu speichern (kann natürlich auch zusätzlich angezeigt werden).


# wir erstellen zuerst das Bild
int breite = 400, hoehe = 300;
QImage img(breite, hoehe, QImage::Format_RGBA8888);
img.fill(Qt::white); // Bild mit Standardwerten füllen (Wichtig!)

# Ändern von Pixelinformationen
img.setPixel(10,20, 0xff009900); // Dunkelgrün (der A-/Transparency-Wert ist vorne)

img.save("neues_bild.png")

QPainter Grundfunktionen

Mit QPainter können sie Ellipsen, Kreissegmente, Rechtecke, Linien, Polygonzüge, Grafiken und Text auf einem Bild zeichnen. Beachten Sie bitte die Dokumentation für eine vollständigere Auflistung der Funktionen:


#include <QtWidgets>
#include <iostream>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    QLabel display;

    int breite = 400, hoehe = 600;
    display.resize(breite,hoehe);

    // unsere Hauptzeichenfläche
    QImage *canvas = new QImage(breite,hoehe, QImage::Format_RGBA8888);
    QPainter painter(canvas);

    // Linien
    float linienbreite = 2;
    painter.setPen(QPen(Qt::blue, linienbreite, Qt::DotLine, 
                        Qt::SquareCap, Qt::BevelJoin));
    painter.drawLine(50,50,50,100);
    painter.setPen(QPen(QColor(100,200,300,50), 5*linienbreite, 
                   Qt::SolidLine, Qt::RoundCap, Qt::BevelJoin));
    painter.drawLine(70,50,70,100);

    // Rechtecke (beachten Sie die Verwendung von Stiften (Pen) und Pinseln (Brush)
    painter.fillRect(100,50,150,200,Qt::white);
    painter.setPen(QPen(Qt::red, linienbreite, Qt::DashLine, 
                        Qt::SquareCap, Qt::BevelJoin));
    painter.drawRect(120,70,170,220);
    painter.setPen(Qt::NoPen);
    painter.setBrush(Qt::green);
    painter.drawRect(220,170,270,320);

    // Ellipsen
    painter.setBrush(Qt::NoBrush);
    painter.setPen(QPen(QColor(0,150,150,100), 20));
    painter.drawEllipse(QPoint(230,180),30,50);

    // Text
    painter.setPen(Qt::black);
    float groesse = 20;
    painter.setFont(QFont("Helvetica", groesse));
    painter.drawText(100,300, "Hallo Welt!");

    display.setPixmap(QPixmap::fromImage(*canvas));

    display.show();
    return app.exec();
}

Bildebenen

Sofern sie eine odere mehrere QPixmap aufwändig gezeichnet haben, können sie dies verwenden, um sie übereinander zu setzen. (Nicht gezeichnete Pixel sind transparent) Dies eignet sich nicht nur für Zeichenprogramme, bei denen man auf mehreren Ebenen zeichnen können möchte, (um sich etwas Arbeit zu sparen, insbesondere, wenn man nur Teile des Bildes austauchen will) sondern auch etwa für Spiele, bei denen der Hintergrund fest ist und nur der Vordergrund neu gezeichnet wird. Insbesondere dann ist diese Variante von Vorteil, da das Zeichnen selbst auch bereits Zeitintensiv sein kann:


// Qlabel zum Anzeigen des Bildes
QLabel display;
display.setGeometry(QRect(0,0,width*zoom, height*zoom));

// unsere Hauptzeichenfläche
canvas = QPixmap(1000,500); // oder QImage
painter = QPainter(canvas);

// Hintergrund Ebene
ebene1 = QPixmap(1000,500); // oder QImage
painter1 = QPainter(ebene1);
// ... langsamer code, komplexes Zeichnen der Ebene

// Objekt auf eigener Ebene
ebene2 = QPixmap(200,300); // oder QImage
painter2 = QPainter(canvas);
// langsamer code, komplexes Zeichnen der Ebene

// ... in der redraw-Schleife
// weiß färben
painter.fillRect(0,0,1000,500,Qt.white);

// Hintergrund hinzufügen, (0,0) ist die Startposition
painter.drawPixmap(0,0,ebene1); // bei QImage: .drawImage(...)

// Objekt hinzufügen
painter.drawPixmap(234,345,ebene2); // bei QImage: .drawImage(...)

// skalieren auf die Größe der Zeichenfläche
// für Canvas vom Typ: QPixmap
display.setPixmap(canvas);
// für canvas vom Typ: QImage
display.setPixmap(QPixmap.fromImage(canvas));

QMainWindow

Das QMainWindow gibt bereits eine GUI-Struktur vor, die bei der GUI-Erstellung häufig verwendet wird: Statusleiste, Menü, anheftbare Widgets (hier nicht behandelt):


#include <QtWidgets>
#include <iostream>

int main(int argc, char *argv[]) {

    // Initialisierung wie üblich
    QApplication app(argc, argv);

    QMainWindow main;
    main.resize(500,600);
    main.setWindowTitle("Hauptfenster");

    // Zentrale Teil kann wie Üblich gefüllt werden
    QWidget *win = new QWidget();
    main.setCentralWidget(win);
    QHBoxLayout *hbox = new QHBoxLayout();
    win->setLayout(hbox);
    hbox->addWidget(new QPushButton("links"));
    hbox->addWidget(new QPushButton("Mitte"));
    hbox->addWidget(new QPushButton("rechts"));

    // MainWindow hat auch eine Statusleiste
    // (diese kann selbst auch Widgets anzeigen)
    QStatusBar *stat = main.statusBar();
    // stat->addWidget(new QLabel("Statustext")) // linksbündig, wird evtl. 
                                                 // von showMessage überblendet
    stat->addPermanentWidget(new QLabel("Statustext2")); // rechtsbündig
    stat->showMessage("wird 2s lang eingeblendet", 2000);
    // stat->showMessage("wird angezeigt, bis gelöscht wird");
    // stat->clearMessage(); // löschen angezeigter Nachrichten

    // ... und auch eine Menüleiste
    QMenuBar *menu = main.menuBar();
    menu->setNativeMenuBar(false); // optional: z.B. OS X stellt das Menü anders dar

    // ... Menü-Reiter: Datei
    QMenu *m1 = menu->addMenu("&Datei");
    m1->addAction("Neu");
    // Menüeintrag mit Tastenkürzel
    m1->addAction("Öffnen");
    QMenu *m1sub = m1->addMenu("Untermenü");
    m1sub->addAction("Versteckt");
    m1->addSeparator();
    QAction *close = new QAction("Beenden", &main);
    close->setShortcut(Qt::CTRL+Qt::Key_Q);
    close->setStatusTip("Schließt das Programm");
    QAction::connect(close, &QAction::triggered, [&main](){
        main.close();
    });
    m1->addAction(close);

    // ... Menü-Reiter: Bearbeiten
    QMenu *m2 = menu->addMenu("&Bearbeiten");
    for (int i=0; i<3; i++) {
        m2->addSeparator();
        for (int j=0; j<4; j++)
            m2->addAction("Platzhalter");
    }
    menu->addSeparator();

    // ... Menü-Reiter: Info 
    QMenu *m3 = menu->addMenu("Info");
    QAction *info = m3->addAction("Info");
    info->setShortcut(Qt::Key_I);
    QAction::connect(info, &QAction::triggered, [](){
        QDialog dialog;
        dialog.resize(200,150);
        QLabel *L = new QLabel("Programmversion 3.1415",&dialog);
        dialog.exec();
    });

    // Anzeigen
    main.show();
    return app.exec();
}

Tastatur- und Mausevents

Tastaturevents

Nicht jede Eingabe soll in Text-Eingabefeldern erfolgen. Häufig benötigt man Eingaben, die erst vorverarbeitet werden müssen. Sie haben im letzten Abschnitt die Möglichkeit kennengelernt Tastenkürzel für Menüeinträge zu definieren. Die Definition war dabei lokal an das jeweilige Menü und das Kürzel selbst gebunden. Im Folgenden sind die Funktionen globaler Natur. Mit anderen Worten - alle Tasten rufen zuerst dieselbe Funktion auf, erst darin wird entschieden was mit dem „Event“ gemacht wird.


Zuerst konzentrieren wir uns hier auf das Drücken einer Taste (keyPressEvent), die vorgehensweise beim Event fürs Loslassen der Taste (keyReleaseEvent) ist dabei ganz analog.


Ähnlich zu den Signalen, bei denen Sie per connect die aufzurufende Funktion an das Objekt gebunden haben, können Sie bei den hier aufgeführten Events die entsprechende Funktion überschreiben. In dem folgenden Besipiel wird jedes mal, wenn Sie eine beliebige Taste drücken, dies in der Statusleiste kenntlich gemacht. Im Spezialfall der Escape-Taste wird das Fenster geschlossen:


#include <QtWidgets>
#include <iostream>

class CustomWindow : public QMainWindow {
    public:
    void keyPressEvent( QKeyEvent* event ) {
        QString str(event->key());
        if (event->key() == Qt::Key_Escape)
            close();
        else if (event->key() == Qt::Key_Up)
            statusBar()->showMessage("Taste mit key-code down gedrückt", 1000);
        else if (event->key() == Qt::Key_Down)
            statusBar()->showMessage("Taste mit key-code up gedrückt", 1000);
        else
            statusBar()->showMessage("Taste mit key-code "+str+" gedrückt", 1000);
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    CustomWindow main = CustomWindow();
    main.resize(500,600);
    main.show();

    return app.exec();
}

Tastenanschläge können nun abgefangen werden. Doch welcher Tasten-Code gehört zu welcher Taste? Die leichteste Möglichkeit besteht darin, die Konstanten (wie etwa bei der Escape-Taste) zu verwenden. Beachten Sie, dass an dieser Stelle nicht zwischen Groß- und Kleinschreibung unterschieden wird. Häufig benutzte Tasten können sie der offiziellen Qt-Dokumentation entnehmen. Unter anderem gibt es:


Mausevents

Ganz analog zu den Tastenanschlägen kann auch die Maus abgefangen werden. Es ändert sich einzig die Form, in der das übergebene Event-Objekt event vorliegt. Eine vollständige Liste aller Mouse-Buttons können sie wieder der offiziellen Qt-Dokumentation entnehmen.



#include <QtWidgets>
#include <iostream>

class CustomWindow : public QMainWindow {
    public:
    CustomWindow() : QMainWindow() {
        setMouseTracking(true);
    }

    void mouseMoveEvent( QMouseEvent* event ) {
        QString str = "Maus-Position (";
        statusBar()->showMessage(str
            + QString::number(event->x()) + "," 
            + QString::number(event->y())+")", 1000);

        // wird nur bei Bewegung erkannt
        // sonst mousePressEvent etc
        if (event->buttons() & Qt::LeftButton)
            statusBar()->showMessage(str 
                + QString::number(event->x()) + "," 
                + QString::number(event->y()) 
                + ") + Linke Maustaste", 1000);
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
        
    CustomWindow main = CustomWindow();
    main.resize(500,600);
    main.show();

    return app.exec();
}

Andere Events

Neben den Tastatur- und Mausevents gibt es noch einige mehr, was die Darstellung und Interaktion mit Widgets betrifft (z.B. resize Event, leaveEvent, ...). Entnehmen Sie die genauere Funktionsweise bitte der Dokumentation - im Moment sind diese für die Bearbeitung der Übungsaufgaben nicht nötig.




[1] Das stimmt nicht ganz, aber für den Moment nehmen wir das an.