4. Prozessbeispiel: Bildumwandlung

In diesem Beispiel geht es um das Einbinden externer Programme in das eigene Qt-Programm mittels QProcess. Und damit das Ganze optisch noch ein wenig aufgepeppt wird, spendieren wir unserem Programm auch noch einen Fortschrittsbalken mittels QProgressBar. Das Programm soll in der Lage sein eine Liste von Bitmap-Dateien in das JPG-Format zu konvertieren. Dazu bedienen wir uns des Programms convert. Wie man mit einer QTableWidget umgeht und einen Open-Dialog erstellt, haben wir schon in den vorangegangenen Kapiteln besprochen. Nun erweitern wir diese beiden Komponenten um das Einbinden externer Prozesse mittels QProcess. Zuerst muß wieder die Oberfläche im Qt-Desigern erstellt werden. Sie soll in etwa so aussehen (die Art des verwendeten Widgets und die jeweilige Bezeichnung stehen jeweils daneben):

Bild: bildumwandlung-ui.jpg

Wie man sieht, muß der processButton inaktiv geschaltet werden, da er erst aktiviert werden soll, wenn man Files geöffnet hat und somit die Tabelle mit Dateien für die Umwandlung gefüllt hat. Einen Button können wir schon im Vorfeld mit Funktionalität belegen. Es handelt sich dabei um den closeButton, welchem wir das Signal clicked() mit dem Slot close() gleich direkt im Qt-Designer zuweisen können. Sämtliche andere Funktionalität wird später mittels unseren beiden Dateien bildumwandlung.cpp und bildumwandlung.h implementiert. Daher benötigen wir wie schon beim Anfangsbeispiel lediglich vier Dateien:

  • bildumwandlung.ui - die grafische Oberfläche vom Qt-Designer erstellt (siehe oben).
  • main.cpp - das Hauptprogramm.
  • bildumwandlung.h - enhält die Deklarierung eigener Variablen, Klassen und Slots.
  • bildumwandlung.cpp - enthält die Verbindungen und Funktionen der Signale und Slots.
Beginnen wir mit der main.cpp, die an ihrer Einfachheit nichts verloren hat:

main.cpp

#include <QApplication>

#include "bildumwandlung.h"

int main(int argc, char *argv[])
{
QApplication app(argc, argv);
bildumwandlung *dialog = new bildumwandlung;
dialog->show();
return app.exec();
}

Wie man sieht, keine Besonderheiten und daher gibt es nichts weiteres dazu zu sagen. Machen wir weiter mit unserer Header-Datei:

bildumwandlung.h

# ifndef BILDUMWANDLUNG_H
# define BILDUMWANDLUNG_H

# include <QDialog>
# include <QProcess>

# include "ui_bildumwandlung.h"

class bildumwandlung : public QDialog, public Ui::bildumwandlung
{
Q_OBJECT

public:
bildumwandlung(QWidget *parent = 0);

private slots:
void openDialog();
void process();
void ProcBeendet(int exitCode, QProcess::ExitStatus exitStatus);
void updateProgressbar();
void converttest();

private:
QProcess proc;
int anzahl;
int position;

};

# endif

Wer ein gutes Gedächtnis hat und unsere allererste Standard-Datei noch im Kopf, dem fällt sofort die neue Include-Anweisung auf, namens <QProcess>, die es uns erst ermöglicht, QProcess zu verwenden. Mittels des slots openDialog() belegen wir den openButton später mit Funktionalität und process() ist für das Abstarten des externen Prozesses. Mittels des Slots ProcBeendet(int exitCode, QProcess::ExitStatus exitStatus) bekommen wir den Rückgabe-Wert des externen Prozesses und können so eine Abfrage einbauen, ob der externe Prozess korrekt beendet wurde und unsere Umwandlung überhaupt erfolgreich war. Da wir pro Bild einen Prozess starten, dient der Slot außerdem noch als Zähler durch unsere Dateien. Man hätte die Liste der Bitmap-Files auch mit einer for-Schleife durchwandern können, müßte dann aber für jeden Prozess die Funktion waitForFinished aufrufen, weil sonst sämtliche Prozesse für die jeweiligen Bilder in schneller Folge hintereinander aufgerufen würden und das würde dazu führen, daß nur der erste Prozess (also das erste Bild) umgewandelt wird und die anderen Prozesse mit einem Process is already running gar nicht erst gestartet werden. Leider hat die Funktion waitForFinished den unangenehmen Nebeneffekt, daß sie die ganze Anwendung für die Dauer der Verarbeitung der Bilder blockiert. Sogar so sehr blockiert, daß man sie nicht mal mehr mit dem X in der rechten Ecke schließen könnte. Daher ist die Kombination aus einem Zähler (int position) verbunden mit jeweils einem Slot für den Prozess und einen für die Weiterschaltung wesentlich besser. Die Anwendung bleibt bedienbar und kann somit jederzeit geschlossen werden, falls man den Prozess abbrechen möchte. Der slot updateProgressbar() setzt später unseren Fortschrittsbalken und mittels converttest() überprüfen wir, ob das Programm convert überhaupt installiert ist, denn ohne convert funktioniert unser kleines Programm ja nicht. Es folgt unser QProcess-Objekt das wir schlicht proc nennen und die Variablen für die Gesamtanzahl der umzuwandelnden Bilder (int anzahl) und der Position bei welchem Bild man sich befindet (int position).

bildumwandlung.cpp

# include <QtGui>

# include "bildumwandlung.h"

bildumwandlung::bildumwandlung(QWidget *parent): QDialog(parent)
{
setupUi(this);

QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8"));
QTimer::singleShot(0, this, SLOT(converttest()));

connect(openButton, SIGNAL(clicked()), this, SLOT(openDialog()));
connect(processButton, SIGNAL(clicked()), this, SLOT(process()));

connect(&proc, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(ProcBeendet(int, QProcess::ExitStatus)));
}

bool caseInsensitiveLessThan(const QString &s1, const QString &s2)
{
return s1.toLower() > s2.toLower();
}

void bildumwandlung::openDialog()
{
QString initialDir = openLineEdit->text();
if(initialDir.isEmpty())
initialDir = QDir::homePath();

QStringList InputFiles = QFileDialog::getOpenFileNames(
this,
"Open File(s)",
initialDir,
"Bitmap (*.bmp);;All Files(*.*)");

qSort(InputFiles.begin(),InputFiles.end(),caseInsensitiveLessThan);

if(!InputFiles.isEmpty())
{
QFileInfo file(InputFiles[0]);
QString FilePath = InputFiles[0];
FilePath.remove(file.fileName());
openLineEdit->setText(FilePath);

TableWidget->clear();
TableWidget->setRowCount(0);
TableWidget->setColumnCount(2);
TableWidget->horizontalHeader()->resizeSection(0,250);
TableWidget->horizontalHeader()->resizeSection(1,60);
TableWidget->setHorizontalHeaderItem(0, new QTableWidgetItem("Files"));
TableWidget->setHorizontalHeaderItem(1, new QTableWidgetItem("Status"));

foreach (QString str, InputFiles)
{
str.remove(FilePath);
TableWidget->insertRow(0);
TableWidget->setItem(0,0, new QTableWidgetItem(str));
}
processButton->setEnabled(true);
progressBar->setValue(0);
position=0;
}
}

void bildumwandlung::process()
{
openButton->setEnabled(false);
processButton->setEnabled(false);
QApplication::setOverrideCursor(Qt::WaitCursor);

QString Input,Output;
anzahl = TableWidget->rowCount();

Input = openLineEdit->text()+TableWidget->item(position,0)->text();
Output = openLineEdit->text()+QFileInfo(Input).completeBaseName()+".jpg";

QStringList arg;
arg << "-format" << "jpg" << "-quality" << "90" << Input << Output;
proc.start("convert", arg);
}

void bildumwandlung::ProcBeendet(int exitCode, QProcess::ExitStatus exitStatus)
{
if (exitStatus==QProcess::CrashExit || exitCode!=0)
{
openButton->setEnabled(true);
processButton->setEnabled(true);
QApplication::restoreOverrideCursor();
QMessageBox::critical(this, tr("Bildumwandlung"),
tr("Fehler bei der Konvertierung!"
"Fehlende Schreibrechte?"),
QMessageBox::Ok);
}
else    {
TableWidget->setItem(position,1, new QTableWidgetItem(tr("Fertig.")));
if(position<anzahl-1)
{
QApplication::restoreOverrideCursor();
position++;
updateProgressbar();
process();
}
else if(position==anzahl-1)
{
progressBar->setValue(100);
openButton->setEnabled(true);
QApplication::restoreOverrideCursor();
}
}
}

void bildumwandlung::updateProgressbar()
{
double prozent = (double)position/(double)anzahl*100;
int value = (int)prozent;
progressBar->setValue(value);
}

void bildumwandlung::converttest()
{
QFileInfo test("/usr/bin/convert");
if(!test.exists())
{
QMessageBox msgBox;
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.setIcon(QMessageBox::Critical);
msgBox.setWindowTitle(tr("Bildumwandlung - Fehler"));
msgBox.setText(tr("<b>Fehler: convert nicht gefunden!</b><br><br>"
"Bitte installieren Sie convert<br>"
"und starten sie das Programm neu.<br><br>"
"Das Programm wird geschlossen."
));
msgBox.exec();
QCoreApplication::exit(2);
}
}

Eigentlich kommt das tr() von translation und bedeutet, daß Strings die zwischen den Klammern stehen in andere Sprachen übersetzt werden können. Möchte man sein Programm International auslegen, ist es wichtig, alle Texte mittels tr einzuklammern. Es gibt aber noch einen weiteren Grund warum man seine Texte mittels tr einklammern sollte, um wie z.B. in unserem Fall den korrekten Zeichencode UTF-8 einzustellen. Die Zeile QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8")) macht genau das. So werden Sonderzeichen wie ä,ö,ü und ß in unserer aufpoppenden Fehler-Message-Box korrekt dargestellt. Mittels QTimer::singleShot(0, this, SLOT(converttest())) wird die Überprüfung aufgerufen, die im Falle des Fehlens von convert besagte MessageBox erzeugt und das Programm beendet. Manche werden sich nun fragen, warum mittels eines QTimer, warum kann ich die Funktion nicht direkt aufrufen einfach durch den Befehl converttest()? Das Problem ist, daß die Funktion vorher aufgerufen werden würde, noch bevor unser Dialog überhaupt fertig erstellt wurde. Und da er noch nicht existiert, kann man ihn auch nicht beenden. Das würde dazu führen, daß die MessageBox alleine aufpoppt mit dem freundlichen Hinweis, daß das Programm geschlossen wird, aber nach dem Drücken des OK-Knopfs das Programm trotzdem ganz normal abstartet und läuft. Der QTimer ist sozusagen ein Umweg über den Slot, der ja erst angesprochen werden kann, wenn das Objekt gebildet wurde und so das gewünschte Ergebnis - das Schließen der Anwendung - überhaupt erst ermöglicht.
Es folgen die üblichen Signal-Slot-Verbindungen, einmal für unsere zwei übrig gebliebenen Buttons openButton und processButton und eine weitere Verbindung für den Rückgabe-Wert des externen Programms. Der Rückgabe-Wert besteht aus zwei Variablen, einer int-Variablen und einer QProcess::ExitStatus.

Die Funktion caseInsensitiveLessThan ist nützlich um die Elemente einer QStringList zu sortieren. Damit sortieren wir in alphabetischer Reihenfolge unsere File-Liste vor dem Eintragen in das QTableWidget. Als nächstes folgt der Open-Dialog der besagte QStringList mit Dateiennamen inkl. Pfad befüllt. Mittels qSort ruft man die Sortierung auf. Nun beginnt das Eintragen der sortierten QStringList in die entsprechenden Elemente, einmal der Pfad in das openLineEdit und einmal die Dateinamen in das QTableWidget. Auf ein einzelnes Element einer QStringList kann man mittels des Array-Bezeichners zugreifen. Da alle Dateien aus demselben Verzeichnis stammen, ist es eigentlich egal, welche Datei wir für die Pfadangabe hernehmen. Ich habe mich für das erste Element, also für den Array-Bezeichner [0] entschieden. Mittels FilePath.remove(file.fileName()) wird der Filename vom Pfad entfernt und selbiger kann in das openLineEdit eingetragen werden. Es folgt die Formatierung der QTableWidget-Tabelle. Mittels clear wird sie erst einmal gelöscht, damit zuvor geöffnete Files verschwinden. Es werden zwei Spalten gebildet, die eine feste Breite von jeweils 250 und 60 Pixeln haben und die Überschriften "Files" und "Status" besitzen. Zeilen gibt es im Moment noch keine, denn die werden erst erzeugt, wenn wir unsere Files eintragen. Das erledigt die Schleife mit foreach. Wie die Bezeichnung es schon verrät, wird für jedes Element der QStringList ein QString mit der Bezeichnung str gebildet und diesem das Element übergeben. Anschließend wird unser zuvor ermittelter Verzeichnispfad entfernt, eine neue Zeile in der QTableWidget eingefügt und in dieser den so ermittelten Dateinamen eingetragen. Nachdem wir den processButton aktiviert haben und die Position auf das erste Bild gesetzt haben, ist das Öffnen der Files auch schon abgeschlossen.

Es folgt der Slot process(), welcher für das Abstarten der Umwandlungsprozesse mittels convert zuständig ist. Zu Beginn setzen wir erst einmal den openButton und processButton auf Disabled, damit man weder Änderungen an unserer Datei-Liste vornehmen kann (durch öffnen anderer Files), noch den Konvertierungsprozess ein zweites Mal abstarten, da beide Vorgänge während der Dateierstellung im günstigsten Fall nur zu falschen Dateien führen würden, im ungünstigsten Fall einen Programm-Crashs verursachen würden. Anschließend setzen wir noch mit QApplication::setOverrideCursor(Qt::WaitCursor) den Mauscursor auf ein beschäftigtes Symbol, damit man auch sieht, daß unser Programm arbeitet. Es folgt die Deklarierung der Input und Output-Files und die Zählung der zu erledigenden Konvertierungen. Dazu zählen wir einfach die Anzahl der Zeilen (rowCount()) unserer Tabelle. Das Input-File wird aus dem Inhalt des openLineEdit -was unseren Pfad darstellt- und dem Inhalt des an der Position befindlichen Tabellen-Eintrags, also unserem Dateinamen erstellt. Das Output-File wird ebenfalls aus der Pfadangabe und dem kompletten Basisnamen (completeBaseName = der Dateiname bis zum letzten Punkt ohne Dateiendung) plus der Endung .jpg erstellt. Mittels der QStringList arg wird eine Liste von Argumenten für den Befehl convert zusammengestellt. Hier die Argumente -format jpg um das jpg-Format zu wählen und -quality 90 um die Komprimierungsqualität für jpg-Files auf 90 zu stellen. Als letzte Argumente kommen die Input- und die Output-File-Angabe. Mittels proc.start(Programmname, Argumente) wird der externe Prozess abgestartet und convert beginnt seine Arbeit.

Hat convert seine Aufgabe erfüllt und beendet sich, wird der QProcess ebenfalls beendet und sendet das Signal finished(), mit welchem wir unseren Slot ProcBeendet aufrufen. Als Übergabe-Parameter bekommt dieser Slot den exitCode und den exitStatus des Prozesses. Auf diese beiden Werte können wir unsere Abfragen aufbauen. Hat der exitStatus den Wert QProcess::CrashExit, ist das Programm also abgestürzt, oder einen anderen Zahlenwert beim exitCode als Null (Programme geben Null bei erfolgreicher Beendigung und irgendeine andere Zahl als Fehler wieder), also hat das Programm einen Fehler gemeldet, lassen wir lediglich eine QMessageBox aufpoppen, die uns über den Mißerfolg benachrichtigt. Vorher stellen wir natürlich noch die Buttons wieder her und den Cursor zurück, damit wir nicht mit einer Sanduhr als Cursor weiterarbeiten müssen. Im anderen Fall, wenn unser externer Prozess erfolgreich verlaufen ist, schreiben wir in die Status-Spalte an der entsprechenden Datei-Position die Information "Fertig" rein und erfragen mit position<anzahl-1 ob noch Konvertierungen zu machen sind (-1 deswegen, weil wir mit den Zeilen bei 0 anfangen zu zählen). Ist dies der Fall, setzen wir den Mauscursor auf normal zurück, zählen die Position um eins hoch und rufen den Slot process() erneut auf, damit er das nächste File abstarten kann. Den Slot updateProgressbar() brauchen wir für die Fortschritts-Anzeige. Dazu gleich im entsprechenden Slot mehr. Viele werden jetzt fragen, Moment!, wieso setzen wir den Mauscursor auf Normal zurück, wir sind doch noch nicht fertig? Das schon, aber am Anfang unseres process()-Slots stellen wir den Cursor auf wait. Würden wir hier den Cursor nicht zurück nehmen, würde er für jeden Prozess erneut auf wait gesetzt werden, was sich im Speicher aufstapelt. Jeder waitCursor() muß auch wieder mit einem restoreOverrideCursor() zurück gesetzt werden. Sonst hätten wir nach Abarbeitung sämtlicher Dateien immer noch einen waitCursor, da ein einziger restore-Aufruf nicht sämtliche waitCursor wieder zurück nimmt, die jeder Aufruf von process() verursacht hätte. In der Praxis stört das aber nicht weiter, da der Wechsel von restore auf den erneuten waitCursor so schnell vonstatten geht, daß man das eigentlich nicht sieht und somit ein dauerhafter waitCursor zu sehen ist. Sind sämtliche Dateien abgearbeitet und somit die Positionszahl gleich der Dateienanzahl minus eins (position==anzahl-1), setzen wir die progressBar auf den Wert 100 Prozent, aktivieren den openButton (der processButton bleibt deaktiviert, da ein nochmaliges Umwandeln der Dateien keinen Sinn hätte und man somit gezwungen ist, neue Files zu öffnen) und stellen unseren Mauscursor zurück.

Es folgt der Slot für unseren Fortschritts-Balken, der den Gesamtfortschritt der Umwandlungen anzeigen soll. Dazu müssen wir die Prozentangabe errechnen aus der Gesamtanzahl der Dateien und der gerade aktuellen Position. Beide Werte stehen uns ja mit den Variablen position und anzahl zur Verfügung, jedoch leider nur als int-Werte, was für eine korrekte Prozent-Berechnung nicht funktioniert. Um den korrekten Wert zu ermitteln, müssen wir unsere int-Variablen mittels des Cast-Operators in double umwandeln. Da unsere progressBar aber nur int-Werte als Eingabe aktzeptiert, müssen wir unser Ergebnis von double wieder zurück nach int konvertieren. Bei dieser Konvertierung werden die mit double erzeugten Nachkommastellen einfach abgeschnitten, was aber für unsere Anzeige vollkommen ausreicht.

Zum Abschluß folgt noch der Slot der durch unseren QTimer aufgerufen wird und überprüft, ob das Programm convert überhaupt vorhanden ist. Das erreichen wir in dem wir mit der ausführbare Datei unter /usr/bin/ ein QFileInfo-Objekt erzeugen und mittels exists() abfragen, ob das Objekt existiert. Das Ausrufezeichen bewirkt die Negation unserer Abfrage, also lautet die Verzweigung im Falle daß convert NICHT existiert, wird eine MessageBox mit dem Standard-Button Ok ausgegeben und das Programm mittels QCoreApplication::exit(2) beendet. Die 2 in Klammern steht für den Rückgabewert unseres Programms. Möchte man den Text vom Zeilenumbruch oder Aussehen her in der QMessageBox etwas anpassen, kann man das mittels HTML-Tags erreichen.

Damit sind wir am Ende unseres Prozessbeispiel-Programms und wer sich das Programm runterladen will um es selbst zu kompilieren, kann sich hier ein komprimiertes tar-Archiv mit den benötigten Dateien runterladen: bildumwandlung.tar.gz