Bildbetrachter in C/C++ und QT

Kürzlich habe ich mit der Nutzung von QT als grafische Benutzeroberfläche begonnen. Eigentlich wundert es mich, dass ich darum immer so einen grossen Bogen gemacht habe, denn eigentlich tut es genau das, was ich von einem solchen ToolKit verlange! Wahrscheinlich liegt es daran, dass ich aus irgendeinem Grund der Meinung war, QT-Oberflächen müsste man mit dem QT–Creator bauen. Aber egal.

Nun bliebe natürlich die Frage, warum mache ich das? Das Netz ist voll solcher Beschreibungen. Das ist eigentlich ganz einfach. Ich habe es bei Nana gesehen. Das habe ich in letzter Zeit zum erstellen grafischer Benutzeroberflächen eingesetzt und war soweit auch ganz zufrieden damit. Aber da war immer mal wieder das Problem, habe ich eine Zeit nicht damit gearbeitet, ging mir viel verloren und ich durfte wieder nachschauen. Jetzt, bei QT, notiere ich eben alles hier und wer weiss, vielleicht helfe ich damit ja jemandem.

Es sollte klar sein, dass man QT installiert haben muss, damit es funktioniert. Wie man das bei der jeweiligen Distribution macht, oder unter Windows, müsst ihr selbst herausfinden. Ja, Windows sollte auch funktionieren. Habe ich noch nicht versucht, aber werde ich in diesem Projekt hier machen!

Ziel dieses Artikels

Am Ende dieses Artikels soll es möglich sein, dass Programm, welches ich dubb nennen werde (diabolus Umarov Bildbetrachter) , in der Eingabeaufforderung mit einem Parameter zu starten. Der Parameter soll, wer hätte es gedacht, die Bilddatei angeben. Natürlich soll diese dann angezeigt werden.

Der Beginn von dubb

Ich verwende als IDE Visual Studio Code. Ja, ein Microsoft-Produkt. Damit bestätige ich auch, was ich gerne behaupte. Ich bin durchaus dazu in der Lage, Produkte dieser Firma zu nutzen, wenn sie mir gefallen! Dazu verwende ich auch noch cmake und clang.

Zuerst brauche ich ein Verzeichnis, indem ich arbeiten kann. Das nenne ich einfach dubb und öffne es in VCode.

Als nächstes muss ich das Projekt anlegen. Das ist recht simpel mit shift+strg+p. Oben öffnet sich ein Drop-Down-Menü und da wähle ich CMake: Quick Start aus. Im Anschluss dann Clang 8.0.0 …, gebe dubb als Projekt-Name ein und wähle schliesslich Executable aus. Schon wird das Projekt erstellt und die Datei CMakeLists.txt geöffnet, in der folgendes drinsteht:

cmake_minimum_required(VERSION 3.0.0)
project(dubb VERSION 0.1.0)

include(CTest)
enable_testing()

add_executable(dubb main.cpp)

set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)

Ausserdem wird noch die Datei main.cpp erstellt. Ich teste einfach mal, ob alles funktioniert hat und ändere dafür diese Datei etwas ab. Einfach so, dass das berühmte Hallo Welt! erscheint. Das sieht dann so aus:

#include <iostream>

using namespace std;

int main(int argc, char* argv[]) 
{
    cout << "Hallo Welt!" << endl;
}

Im Prinzip hätte ich die Vorgabe so lassen können, denn die hat genau das gemacht. Doch ich bin da eigen! Egal, den Krempel compiliere ich jetzt und dann schaue ich, ob es auch brach funktioniert hat!

$ ./dubb 
Hallo Welt!

Hervorragend. Klappt ja soweit alles!

Klappt das aber auch mit dem Parameter?

argv[0] wäre der Name des Programms. Das heisst, ich schaue mal was argv[1] zurückgibt.

#include <iostream>

using namespace std;

int main(int argc, char* argv[]) 
{
    cout << argv[1] << endl;
}

Und das Ergebnis ist:

$ ./dubb hallo
hallo

Perfekt!

Nun ran an QT

Ich will aber eine grafische Oberfläche haben und dafür will ich QT einsetzen. Da ich es echt nicht schön finde, wenn ich alles in einer Datei hängen habe und ich auch nicht weiss, wie gross diese Geschichte überhaupt wird, fange ich mal an weitere Dateien zu erstellen und diese dann mit entsprechendem Code zu füllen.

Zuerst einmal die Datei windows.h. Da kommen alle Klassen und so rein.

#include <QMainWindow>

class HauptWindow : public QMainWindow
{
    Q_OBJECT

    public:
        HauptWindow();

    private slots:

    private:
};

Als Grundgerüst funktioniert das. Aber, so wird der Compiler ganz laut schreien! Denn, da muss noch was in der CMakeLists.txt eingetragen werden, die dann so aussieht:

cmake_minimum_required(VERSION 3.0.0)
project(dubb VERSION 0.1.0)

find_package(Qt5 COMPONENTS Widgets REQUIRED)
find_package(Qt5Core REQUIRED)

include(CTest)
enable_testing()

add_executable(dubb main.cpp)

set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)

target_link_libraries(dubb Qt5::Widgets)

Und schon klappts auch mit dem Nachbarn! Aber, so bringt das alles überhaupt nichts! Das Fenster muss noch geöffnet und gefüllt werden usw. Also erst einmal das Fenster!

Das könnte ich nun alles in die windows.h Datei schreiben, was bei diesem Programm wohl auch kein Problem wäre. Ich bevorzuge es jedoch anders und erstelle dafür wieder eine neue Datei mit dem Namen hauptwindow.h. Da kommen dann die Funktionen rein, die das Fenster betreffen und natürlich muss das auch alles noch eingebunden werden.

Für den Anfang reicht es mir, wenn sich das Fenster öffnet und ich es auch wieder schliessen kann. Da ich noch nicht genau weiss, in welche Richtung sich das Projekt entwickeln wird, werde ich die Funktion zum schliessen mit QAction definierten. Das muss natürlich zuerst in der Klasse auch definiert werden und das sieht dann so aus in der windows.h:

#include <QMainWindow>
#include <QAction>

class HauptWindow : public QMainWindow
{
    Q_OBJECT

    public:
        HauptWindow();

    private slots:
        void beenden();

    private:
        QAction *beendenAction;
};

Die Datei hauptwindow.h sieht dann so aus und tut nichts anderes, als dem Fenster die Möglichkeit einzuräumen, auch wieder geschlossen werden zu zu können.

HauptWindow::HauptWindow()
{
    beendenAction = new QAction(tr("&Beenden"));

    connect(beendenAction, SIGNAL (triggered()), this, SLOT (beenden()));   
}

void HauptWindow::beenden()
{
    qApp->quit();
}

Okay. Also muss nur noch etwas her, um das Fenster auch nach dem Programmstart zu zeigen! Das kommt in main.cpp.

#include <iostream>
#include <QApplication>

#include "windows.h"

using namespace std;

#include "hauptwindow.h"

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

    HauptWindow hw;

    hw.show();

    return app.exec();
}

Ausserdem muss ich die Datei CMakeLists.txt anpassen und die sieht dann so aus:

cmake_minimum_required(VERSION 3.0.0)
project(dubb VERSION 0.1.0)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

find_package(Qt5 COMPONENTS Widgets REQUIRED)
find_package(Qt5Core REQUIRED)

include(CTest)
enable_testing()

add_executable(dubb main.cpp windows.h hauptwindow.h)

set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)

target_link_libraries(dubb Qt5::Widgets)

So. Los Compiler, mach deine Arbeit! Hat das alles funktioniert, kommt folgendes bei raus:

Alles richtig gemacht, in nur ganz wenigen Schritten. QT gefällt mir! Erinnert mich in gewisser Hinsicht an MUI auf dem AmigaOS! Ausserdem, wie ein Test zeigt, schliesst sich das Fenster auch brav wieder, wenn man das X im Titel anklickt.

Das mit dem Bild

Nun gut. So toll das Fenster ja sein mag, es nutzt mir überhaupt nichts! Das dumme Ding soll schliesslich ein vorher ausgewähltes Bild zeigen!

Im Prinzip könnte ich jetzt einfach den Wert von argv[1] nehmen, damit das Bild laden und anzeigen lassen. Aber eben, ich habe noch keine Ahnung, wie weit dieses Projekt noch anwachsen wird und deshalb will ich vorbereitet sein. Im Prinzip heisst das nichts anderes, als dass ich eine Klasse für das anzuzeigende Bild anlege. Die ist super banal und speichert nur den Pfad und den Dateiname. Aber wer weiss, wo das noch hinführt, da bin ich lieber vorbereitet.

Also mal wieder eine neue Datei und die bekommt den überaus gelungenen Namen bild.h.

class Bild
{
    private:
        string file;

    public:

        void setFile(char *f)
        {
            file = f;
        }

        QString getFile()
        {
            return QString::fromStdString(file);
        }
};

Jede Wette, man kann es auch viel besser machen, aber ich mache es eben so weil ich weiss, dass es funktioniert!

Jetzt muss ich noch main.cpp anpassen, dass argv[1] in die Klasse gespeichert wird. Ach ja, natürlich muss ich ja auch noch auf die Klasse zugreifen können und zwar auch aus den anderen Dateien heraus. Dann sieht das so aus:

#include <iostream>
#include <QApplication>

#include "windows.h"

using namespace std;

#include "bild.h"

Bild bild;

#include "hauptwindow.h"

int main(int argc, char* argv[]) 
{
    bild.setFile(argv[1]);

    QApplication app(argc, argv);

    HauptWindow hw;

    hw.show();

    return app.exec();
}

Das nützt aber auch noch nichts, denn nur weil ich jetzt eine Datei als Variable in einer Klasse speichern und wieder abrufen kann, wird hier noch gar nichts angezeigt. Also, irgendwas muss ich auch damit machen können!

Dafür bauche ich ein paar Dinge. Zum Einen muss das Bild ja irgendwie geladen auch noch irgendwie im Fenster angezeigt werden. Also, ran ans Werk!

Zum Anzeigen verwende ich einfach ein QLabel. Ist es die beste Variante? Keine Ahnung, aber egal. Dafür ändere ich dann natürlich wieder windows.h sowie hauptwindow.h. In dieser Reihenfolge.

#include <QMainWindow>
#include <QAction>
#include <QLabel>
#include <QPixmap>

class HauptWindow : public QMainWindow
{
    Q_OBJECT

    public:
        HauptWindow();

    private slots:
        void beenden();

    private:
        QAction *beendenAction;

        QLabel *bildLabel;

        QPixmap bildPixmap;
};

und

HauptWindow::HauptWindow()
{
    bildLabel = new QLabel();

    bildPixmap.load(bild.getFile());

    bildLabel->setPixmap(bildPixmap);

    beendenAction = new QAction(tr("&Beenden"));

    connect(beendenAction, SIGNAL (triggered()), this, SLOT (beenden()));   

    setCentralWidget(bildLabel);
}

void HauptWindow::beenden()
{
    qApp->quit();
}

Lasset uns schauen, ob die Nummer auch funktioniert!

Ja da schau an, es hat funktioniert! Zumindest zum Teil. Ja, das Bild wird angezeigt. Aber, es ändert die Grösse mit demFenster. Im Prinzip ist das ja nicht schlimm, aber was wenn man ein Bild hat, was grösser ist als die Auflösung? Oder wenn man das Fenster skaliert? Also da muss noch was dran!

Ich könnte es mir nun einfach machen und das Label einfach so einstellen, dass es seinen Inhalt bei Grössenveränderungen skaliert. Dazu müsste ich nur folgendes in der hauptwindow.h einbauen:

bildLabel->setScaledContents(true);

Das wäre aber nicht das Gelbe vom Ei, denn dann würde sich das Bild unter Umständen verziehen. Es soll aber sein Verhältnis beibehalten! Das heisst also mehr Arbeit! Zum Einen muss ich ein Event einbauen, welches auf eine Veränderung der Grösse reagiert. Dort muss ich dann die Grösse des Bildes ermitteln, es unter Beibehaltung seines Verhältnisses skalieren und damit erst die Grösse von bildLabel anpassen. Ausserdem soll ja auch alles schön mittig angezeigt werden, weshalb ich bildLabel auch noch in ein QHBoxLayout packe. Das bedeutet Änderungen in windows.h und natürlich auch in haupwindow.h. Das sieht dann so aus:

#include <QMainWindow>
#include <QAction>
#include <QLabel>
#include <QPainter>
#include <QPixmap>
#include <QHBoxLayout>

class HauptWindow : public QMainWindow
{
    Q_OBJECT

    public:
        HauptWindow();
        void resizeEvent(QResizeEvent* event);

    private slots:
        void beenden();

    private:
        QAction *beendenAction;

        QHBoxLayout *mainBox;
        
        QLabel *bildLabel;

        QPainter *bildPainter;

        QPixmap bildPixmap;
};
HauptWindow::HauptWindow()
{
    mainBox = new QHBoxLayout();
    QWidget *mainLayout = new QWidget();

    bildPixmap.load(bild.getFile());

    bildLabel = new QLabel();

    bildPainter = new QPainter(bildLabel);

    bildLabel->setPixmap(bildPixmap);
    bildLabel->setScaledContents(true);
    bildLabel->setAlignment(Qt::AlignCenter);

    beendenAction = new QAction(tr("&Beenden"));

    connect(beendenAction, SIGNAL (triggered()), this, SLOT (beenden()));   

    mainBox->addWidget(bildLabel);
    mainBox->setAlignment(Qt::AlignCenter);

    mainLayout->setLayout(mainBox);

    setCentralWidget(mainLayout);
}

void HauptWindow::beenden()
{
    qApp->quit();
}

void HauptWindow::resizeEvent(QResizeEvent* event)        
{
    QSize ls = bildLabel->size();
    QSize bs;

    QPixmap neuPixmap = bildPixmap.scaled(ls, Qt::KeepAspectRatio);

    bs = neuPixmap.size();

    bildLabel->resize(bs);
}

War das erfolgreich? Das wird ein Test zeigen!

Das nenne ich dann jetzt einfach mal Erfolg! Nicht ganz perfekt, da das Bild in einigen Fenstern durchaus auch grösser sein könnte, aber für den Anfang reicht es und ich behaupte einfach mal, damit ist das Ziel dieses Artikels auch erreicht.

Aber damit ist die Sache noch nicht beendet. Weitere Teile werden folgen!

Schreib einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.