Qt: Benutzer­definiertes Widget

Vitalij Mast · 2 Februar 2013

Wer mit Qt arbeitet der hat sich früher oder später schon mal gefragt wie erstelle ich ein eigenes Steuerelement und/oder wie zeige ich meine eigene Grafik in meiner GUI an.
Eins vorweg ich arbeit mit Visual Studio und dem Plugin dafür inklusive des Qt-Designers. Mit dem Qt-Creator sollten die hier Beschriebenen Schritte jedoch ähnlich ablaufen.
In diesem Artikel möchte ich euch zeigen wie einfach es sein kann sowas zu realisieren und wie man das Steuerelement direkt im Qt Designer platziert. Im Bild rechts sieht man einen Schalter der ein Volumenmeter, welches auch von Windows verwendet wird um die Sound- ausgabe zu visualisieren, steuert. Die Programmierung von diesem wird im folgendem erklärt.

Zunächst erstellen wir uns eine von QWidget abgeleitete Klasse namens QVolumeMeter und deklarieren uns die virtuelle Funktion paintEvent. Diese Funktion wird von der Elternklasse immer dann aufgerufen wenn das Steuerelement gezeichnet werden soll. Die Header-Datei sollte dann in etwa so aussehen:

#ifndef QVOLUMEMETER_H
#define QVOLUMEMETER_H
#include <QWidget>
class QVolumeMeter : public QWidget
{
Q_OBJECT
public:
QVolumeMeter(QWidget *parent);
~QVolumeMeter();
protected:
virtual void paintEvent(QPaintEvent *event);
};

Nun sollte man sich zunächst Gedanken machen wie man das was Dargestellt werden soll am besten intern Beschreibt. Ich habe wie es für die meisten Fortschrittsanzeigen gemacht wird mir drei Variablen angelegte mit Start, Stop und Position, jeweils mit dem Typ float. An dieser Stelle ist es relativ egal welchen Typ ihr nehmt ihr solltet nur beachten dass ihr die Verwendete Typen richtig für eure Funktion interpretiert. Nun definieren wir für diese Variablen wie es sich gehört jeweils drei sogenannte Getter und Setter Funktionen mit denen man die Werte auslesen und beschreiben kann. Die Getter Funktionen sollten in diesem Fall klar sein, sie geben einfach den aktuell eingestellten Wert wieder zurück. Die Setter Funktionen führen jedoch in meinem Fall noch die Prüfung der übergebenen Werte durch, zb. damit der Stop-Wert sich nicht vor dem Start-Wert befindet und umgekehrt. Eine mögliche Implementierung könnte dann so aussehen (ich habe für stop und start hier min un max verwendet):

void QVolumeMeter::setLevelMin(float min)
{
this->pLevelMin=min;
if (this->pLevelMax<this->pLevelMin)
qSwap(this->pLevelMax, this->pLevelMin);
this->update();
};
void QVolumeMeter::setLevelMax(float max)
{
this->pLevelMax=max;
if (this->pLevelMax<this->pLevelMin)
qSwap(this->pLevelMax, this->pLevelMin);
this->update();
};
void QVolumeMeter::setLevel(float lvl)
{
this->pLevel=lvl;
if (this->pLevel>this->pLevelMax)
this->pLevel=this->pLevelMax;
else
if (this->pLevel<this->pLevelMin)
this->pLevel=this->pLevelMin;
this->update();
};

Man beachte bei jeder Funktion den letzten Befehl update. Was macht nun dieser Befehl? Es gibt neben update noch den Befehl repaint. Der unterschied zwischen beiden ist, dass repaint das Steuerelement sofort neu zeichnet, update hingegen das Neuzeichnen in „Auftrag“ gibt, sodass wenn mehrere update Befehle eingehen das Steuerelement nur einmal neu gezeichnet wird. Warum benutz ich hier nun update anstelle von repaint? Ganz einfach, wenn man mehrmals diese Funktionen innerhalb einer Aktion aufruft, soll sich das Steuerelement möglichst erst nach dem letzten update Aufruf neu zeichnen um alle neuen Informationen anzuzeigen. Würde man den Befehl repaint verwenden würde dies das Steuerelement jedesmal zum Neuzeichnen veranlassen und das Programm zum stocken bringen, da das Neuzeichnen eines Steuerelementes im vergleich viel Rechenleistung beansprucht. Von daher sollte man darauf achten, dass man das Steuerelement auch nur dann neue Zeichnet wenn es nötig ist.

Kommen wir endlich zum Zeichnen des Steuerelements. Da ich an dieser Stelle ein fertiges Bild auf das Steuerelement male ist die Zeichenroutine auch relativ simple. Ich habe das Bild als Qt-Ressource eingebunden und lade dieses im Konstruktor des eigenen Steuerelements:

pTemplate = new QImage(":/QTestExampleWidget/Resources/volumeMeter.png");

Ich möchte an diese Stelle hinweisen, dass wenn man mehrere Steuerelemente der selben eigenen Klasse verwendet sich dann eine Hilfsklasse schreiben sollte um die Bilder besser zu verwalten, da sonst für jedes Steuerelement das selbe Bild immer wieder geladen wird.

Schließlich wird das Bild in der Zeichenfunktion mit Hilfe eines QPainter Objekts auf das Steuerelement gezeichnet:

void QVolumeMeter::paintEvent(QPaintEvent *event)
{
if (!pTemplate)
return;
QPainter painter(this);
painter.drawImage(0, 0, *pTemplate, width(), 0);
float range = (this->pLevelMax - this->pLevelMin);
float pos = (range-(this->pLevel - this->pLevelMin)/range)* float(this->height()) ;
QRect rect(0, pos, this->width(), this->height());
painter.drawImage(rect, *pTemplate, rect);
painter.end();
}

Mein Bild besteht aus zwei Hälften, die eine Hälfte zeigt den gefüllten Status und die andere den Ungefühlten Status. Die Berechnung oben in der Zeichenfunktion mixt die zwei Hälften jeweils zu einem zusamen sodass man eine Trennung der Hellen und dunklen Flächen erhält und damit eine Art Höhenanzeige.

Um nun das Steuerelement im Qt Designer verwenden zu können deklariere ich noch ein paar Metaeigenschaften und Slotfunktionen:

Q_PROPERTY(float levelMin READ levelMin WRITE setLevelMin)
Q_PROPERTY(float levelMax READ levelMax WRITE setLevelMax)
Q_PROPERTY(float level READ level WRITE setLevel)
...
void setPercent(int value);

Mit diesen Eigenschaften kann nun im Qt Designer die Eigenschafts-Werte vor der Laufzeit festgelegt werden. Dafür erstellen wir zunächst im Qt Designer ein einfaches QWidget Objekt und klicken mit der rechten Maustaste darauf und dann auf Promote to … im nächsten Fenster geben wir unter new die Daten von unserem eigene Steuerelement an (Bassisklasse, Unser neuer Klassenname sowie den Headerdateinamen) und fügen diese hinzu. Anschließend können wir mit einem klick auf Promote dem Designer sagen dass das ausgewählte Widget das von uns erstellte QVolumeMeter-Widget ist. Es wird jedoch leer angezeigt, das ist normal da der Qt Designer ohne ein Plugin nicht wissen kann wie wir dieses programmiert haben. Im Propertyeditor sehen wir dann auch nichts können jedoch die Eigenschaften die wir für den Metainterpreter hinterlegt haben selbst hinzufügen und verwenden.
Die Zusätzliche setPercent-Funktion hab ich noch hinzugefügt um ein Signal eines QSlider-Objekts direkt mit meinem zu Verknüpfen.

Die fertige Klasse sieht letzten endlich dann so aus:

Header:

#ifndef QVOLUMEMETER_H
#define QVOLUMEMETER_H
#include <QWidget>
#include <QImage>
class QVolumeMeter : public QWidget
{
Q_OBJECT
Q_PROPERTY(float levelMin READ levelMin WRITE setLevelMin)
Q_PROPERTY(float levelMax READ levelMax WRITE setLevelMax)
Q_PROPERTY(float level READ level WRITE setLevel)
public:
QVolumeMeter(QWidget *parent);
~QVolumeMeter();
float levelMin() const;
float levelMax() const;
float level() const;
public slots:
void setLevelMin(float min);
void setLevelMax(float max);
void setLevel(float lvl);
void setPercent(int value);
protected:
virtual void paintEvent(QPaintEvent *event);
private:
QImage *pTemplate;
float pLevel;
float pLevelMin;
float pLevelMax;
};
inline float QVolumeMeter::levelMin() const
{ return this->pLevelMin; };
inline float QVolumeMeter::levelMax() const
{ return this->pLevelMax; };
inline float QVolumeMeter::level() const
{ return this->pLevel; };
inline void QVolumeMeter::setPercent(int value) {
setLevel(this->pLevelMin + (this->pLevelMax - this->pLevelMin) * (float(value) / 100.f));
}
#endif // QVOLUMEMETER_H

Und der Sourcecode der Klasse:

#include "qvolumemeter.h"
#include <QPainter>
QVolumeMeter::QVolumeMeter(QWidget *parent)
: QWidget(parent), pTemplate(0), pLevel(0.5f), pLevelMin(0.f), pLevelMax(1.f)
{
this->pTemplate = new QImage(":/QTestExampleWidget/Resources/volumeMeter.png");
if (pTemplate)
{
QSize nsize(pTemplate->width()/2, pTemplate->height());
setMinimumSize(nsize);
setMaximumSize(nsize);
}
}
QVolumeMeter::~QVolumeMeter()
{
delete this->pTemplate;
}
void QVolumeMeter::setLevelMin(float min)
{
this->pLevelMin=min;
if (this->pLevelMax<this->pLevelMin)
qSwap(this->pLevelMax, this->pLevelMin);
this->update();
};
void QVolumeMeter::setLevelMax(float max)
{
this->pLevelMax=max;
if (this->pLevelMax<this->pLevelMin)
qSwap(this->pLevelMax, this->pLevelMin);
this->update();
};
void QVolumeMeter::setLevel(float lvl)
{
this->pLevel=lvl;
if (this->pLevel>this->pLevelMax)
this->pLevel=this->pLevelMax;
else
if (this->pLevel<this->pLevelMin)
this->pLevel=this->pLevelMin;
this->update();
};
void QVolumeMeter::paintEvent(QPaintEvent *event)
{
if (!pTemplate)
return;
QPainter painter(this);
painter.drawImage(0, 0, *pTemplate, width(), 0);
float range = (this->pLevelMax - this->pLevelMin);
float pos = (range-(this->pLevel - this->pLevelMin)/range)* float(this->height()) ;
QRect rect(0, pos, this->width(), this->height());
painter.drawImage(rect, *pTemplate, rect);
painter.end();
}

Im Prinzip ist es also nicht sonderlich schwer sein eigenes Steuerelement zu erstellen. Wer möchte kann nun hingehen und für sich weitere event-Funktinen implementieren um zB. auf Mausklicks etc zu reagieren, siehe dazu auch QWidget Reference. Das Projekt bzw. den Source findet ihr wieder unter meinem github Account: QTestExampleWidget

Schreibt mir doch für was Ihr euch ein eigenes Steuerelement geschrieben habt.

Twitter, Facebook