Das letzte mal haben wir uns eine minimale GUI Anwendung geschrieben, die zunächst ein einfaches Fenster darstellt. Wir haben etwas mit einigen Einstellungen herumgespielt, aber mehr auch nicht. Heute wollen wir diese Anwendung einmal mit Inhalt füllen.

Auch hier möchte ich mich wieder an den offiziellen JUCE Tutorials orientieren und nenne unser heutiges JUCE-Projekt MainComponentTutorial. Also starte ich wieder den Projucer, erstelle eine GUI Applikation, benenne mein Projekt und lasse wieder nur die Main.cpp generieren (create a Main.cpp only) und öffne meine IDE (Visual Studio).

Genau wie das letzte mal erstelle ich mir zunächst eine MainWindow Klasse und fülle die initialise() und shutdown() Methode mit Leben. So dass meine Main.cpp jetzt so aussieht:

#include <JuceHeader.h>

//==============================================================
class MainComponentTutorialApplication  : public JUCEApplication
{
public:
    //==========================================================
    MainComponentTutorialApplication() {}

    const String getApplicationName() override { 
        return ProjectInfo::projectName; }
    const String getApplicationVersion() override { 
        return ProjectInfo::versionString; }
    bool moreThanOneInstanceAllowed() override { 
        return true; }

    //==========================================================
    class MainWindow : public DocumentWindow
    {
    public:
        MainWindow(String name) : DocumentWindow(name,
            Colours::lightgrey,
            DocumentWindow::allButtons)
        {
            setUsingNativeTitleBar(true);
            centreWithSize(300, 200);
            setVisible(true);
        }

        void closeButtonPressed() override
        {
            JUCEApplication::getInstance()->systemRequestedQuit();
        }

    private:
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainWindow)

    };//==========================================================
    void initialise (const String& commandLine) override
    {
        mainWindow.reset(new MainWindow(getApplicationName()));
    }

    void shutdown() override
    {
        mainWindow = nullptr;
    }

    //============================================================
    void systemRequestedQuit() override
    {
        quit();
    }

    void anotherInstanceStarted (const String& commandLine) override
    {
       
    }

private:
    std::unique_ptr<MainWindow> mainWindow;
};

//================================================================
// This macro generates the main() routine that launches the app.
START_JUCE_APPLICATION (MainComponentTutorialApplication)

Während jede GUI Anwendung ein MainWindow benötigt, um ein GUI darzustellen, benötigt sie auch ein MainComponent, um darin Inhalte darzustellen. Darum soll es heute gehen.

Die wichtigste Klasse für jede JUCE GUI Anwendung ist die Component Klasse. Alle sichtbaren Elemente in einen grafischen Benutzerschnittstelle sind Komponenten, die Erben dieser Klasse sind. Aus diesem Grund benötigt man zunächst eine Hauptkomponente (MainComponent), um alle die erbenden Komponenten zu beherbergen.

Eine Hauptkomponenten-Klasse hinzufügen

Um unserem Projekt eine eigene Komponenten-Klasse hinzuzufügen müssen wir diese zunächst im Projucer erstellen.

Wichtig: Wenn man neue Klassen erstellt, dann sollte das immer in einer eigenen Datei passieren und der Name dieser Dateien sollte dem Namen der Klasse entsprechen. Und es ist besser immer den Projucer hierfür zu nutzen und das nicht direkt in der IDE zu machen. Wenn ich nämlich das nächste mal das Projekt im Projucer abspeichere, würden die in der IDE erstellten Dateien eventuell überschrieben!

Links im Projucer, im File-Browser, machen wir einen Rechtsklick auf den Source-Ordner und wählen „Add new Component class (split between CPP & header„.

Der Projucer fragt nun nach einem Namen für die Klasse. Diese benennen wir MainComponent. Der Projucer hat nun zwei neue Quellcode Dateien für uns erstellt. Jetzt speichern wir das Projekt wieder und öffnen erneut die IDE.

Der Projucer hat automatisch etwas Code für uns generiert. Die neue Klasse ist ein Erbe der Component Klasse und hat die folgende Klassendeklaration (MainComponent.h):

class MainComponent    : public Component
{
public:
    MainComponent();
    ~MainComponent();

    void paint (Graphics&) override;
    void resized() override;

private:
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};

Die Component Klasse verfügt über zwei wichtige virtuelle Methoden die wir überschreiben sollten. Der Projucer hat schon automatisch diese beiden Overrides erstellt:

  • Component::paint() – Diese Methode bestimmt, wie unsere Komponente auf dem Bildschirm dargestellt wird. Sie sollte von jeder Komponenten-Klasse überschrieben werden.
  • Component::resized() – Diese Methode bestimmt, was mit unserer Komponente passieren soll, wenn ihre Größe verändert wird. Auch diese Methode sollte für jede Komponente überschrieben werden.

Auch die Implementierung der paint()-Methode (in MainComponent.cpp) hat der Projucer schon mit Beispielcode gefüllt. Wir werden aber etwas eigenen Code einfügen:

void MainComponent::paint (Graphics& g)
{
    g.fillAll(Colours::darkgrey);
    g.setColour (Colours::orange);
    g.setFont (28.0f);
    g.drawText ("Hello World!", getLocalBounds(),
                Justification::centred, true);  // draw "Hello World!"
}

Ich werde jetzt nicht allzu sehr auf die einzelnen Zeilen eingehen. In den nächsten Tutorials werden mehr darüber erfahren. Man kann aber schon erahnen, dass der Hintergrund dunkelgrau gefärbt wird und der „Hello World“ Text wird dann orange und in der Mitte des Fensters geschrieben.

Damit man die Komponente auch sieht, muss sie natürlich sichtbar gemacht werden. Dafür müssen wir zunächst die Header-Datei in unserer MainWindow-Datei inkludieren…

#include "MainComponent.h"

Als nächstes müssen wir ein MainComponent Objekt erstellen und diese dem Hauptfenster hinzufügen. Dazu nehmen wir die DocumentWindow::setContentOwned() Methode. Somit ist das MainWindow Objekt für unsere Komponente verantwortlich. Wir fügen also folgende Zeile zu unserem MainWindow Konstruktor hinzu:

setContentOwned (new MainComponent(), true);

Unser Konstruktor sieht nun so aus:

MainWindow (String name)  : DocumentWindow (name,
                                            Colours::lightgrey,
                                            DocumentWindow::allButtons)
{
    setUsingNativeTitleBar (true);
    setContentOwned (new MainComponent(), true);
    centreWithSize (getWidth(), getHeight());
    setVisible (true);
}

Wie man sieht, haben wir auch die Component::centreWithSize() Methode etwas geändert. Die Größe des Fensters wird nicht mehr mit Zahlen festgelegt, sondern anhand der Content-Größe. Aus diesem Grund müssen wir dem Content, also der Komponente, noch eine Größe zuweisen. Das machen wir im Konstruktor unserer MainComponent Klasse:

setSize (400, 300);

Jetzt können wir unser Projekt kompilieren und das Ergebnis bewundern:

So sehen unsere Quellcode Dateien nun aus:

#include <JuceHeader.h>
#include "MainComponent.h"

//==============================================================================
MainComponent::MainComponent()
{
    setSize(400, 300);

}

MainComponent::~MainComponent()
{
}

void MainComponent::paint (Graphics& g)
{
    g.fillAll(Colours::darkgrey);
    g.setColour (Colours::orange);
    g.setFont (28.0f);
    g.drawText ("Hello World!", getLocalBounds(),
                Justification::centred, true);  // draw "Hello World!"
}

void MainComponent::resized()
{
    // This method is where you should set the bounds of any child
    // components that your component contains..

}
#include <JuceHeader.h>
#include "MainComponent.h"

//==============================================================
class MainComponentTutorialApplication  : public JUCEApplication
{
public:
    //==========================================================
    MainComponentTutorialApplication() {}

    const String getApplicationName() override { 
        return ProjectInfo::projectName; }
    const String getApplicationVersion() override { 
        return ProjectInfo::versionString; }
    bool moreThanOneInstanceAllowed() override { 
        return true; }

    //==========================================================
    class MainWindow : public DocumentWindow
    {
    public:
        MainWindow(String name) : DocumentWindow(name,
            Colours::lightgrey,
            DocumentWindow::allButtons)
        {
            setUsingNativeTitleBar(true);
            setContentOwned(new MainComponent(), true);
            centreWithSize(getWidth(), getHeight());
            setVisible(true);
        }

        void closeButtonPressed() override
        {
            JUCEApplication::getInstance()->systemRequestedQuit();
        }

    private:
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainWindow)

    };//==========================================================
    void initialise (const String& commandLine) override
    {
        mainWindow.reset(new MainWindow(getApplicationName()));
    }

    void shutdown() override
    {
        mainWindow = nullptr;
    }

    //============================================================
    void systemRequestedQuit() override
    {
        quit();
    }

    void anotherInstanceStarted (const String& commandLine) override
    {
       
    }

private:
    std::unique_ptr<MainWindow> mainWindow;
};

//================================================================
// This macro generates the main() routine that launches the app.
START_JUCE_APPLICATION (MainComponentTutorialApplication)

Die resize() Methode implementieren

Nachdem wir nun die paint() Methode behandelt haben, sollten wir noch schauen, wie unsere Komponente auf Änderungen der Fenstergröße reagiert. Erstmal sollten wir dem Hauptfenster sagen, dass man es in der Größe verändern kann, das haben wir das letzte mal schon gemacht.

Wenn man jetzt das Projekt kompiliert und das Fenster größer zieht, sieht man das der Inhalt automatisch mit dem Fenster vergrößert wird. Das Verhalten ist in der Component Klasse schon automatisch implementiert.

Was aber, wenn man ein anderes Verhalten wünscht? Der Code, der dies bestimmt kommt in die resized() Methode der MainComponent Klasse. Im Moment haben wir den „Hello World!“ Text als String Literal übergeben, das können wir natürlich auch mithilfe einer Variablen tun. Wir legen also eine Member-Variable an, die später die tatsächliche Größe unseres Fensters anzeigt.

class MainComponent    : public Component
{
    // ...
 
private:
    String currentSizeAsString;
    // ...
};

Jetzt müssen wir noch das gewünschte Verhalten der Variablen implementieren. Zunächst muss der Inhalt der Variablen auf dem Bildschirm dargestellt werden und dieser Inhalt muss sich jeder Größenänderung des Fensters anpassen.

Zunächst stellen wir die Variable auf dem Bildschirm dar. Dazu ändern wir den Aufruf der g.drawText() Funktion dementsprechend:

void MainComponent::paint (Graphics& g)
{
    //...
    g.drawText (currentSizeAsString, getLocalBounds(),
                Justification::centred, true);  
}

… dann müssen wir innerhalb der resized() Methode den Inhalt der Variablen bei jedem Aufruf der Methode updaten:

void MainComponent::resized()
{
    currentSizeAsString = String(getWidth()) + " x " 
                                             + String(getHeight());
}

Wenn wir unser Projekt jetzt nochmal kompilieren und ausführen bekommen wir Folgendes:

Mit jeder Änderung der Fenstergröße wird auch der Inhalt der dargestellten String-Variablen geändert.

Im nächsten Tutorial geht es dann um die Graphics Klasse…