How to properly manage GUI, callbacks and listeners in separate files?

Hi,

I’m having quite a bit of trouble getting the GUI for my standalone audio app to work. Trying to keep with good practice, I’m separating my GUI from my MainComponent, but here is where the troubles arise. I’ve tried looking through the tutorials on GUI and parent/child, and also searched this forum, along with external forums, but I can’t wrap my head around it…

Drawing the components is fine, but everything falls apart when I want to use listeners. I tried using the lambda functions, but for me it seemed impossible, but if anyone has suggestion with lambdas I would prefer that solution.

Anyways, my code triggers a breakpoint in juce_Component.cpp with a comment suggesting: “adding a component to itself!?”. I thought I’d done it the correct way with forward declaration, but it seems not. My code looks like this:

// =============================================================================
// MainComponent.h
#pragma once
// includes... (NOT appGUI.h)

// Forward declaration
namespace myApp // I'm using a namespace across several files, but not in MainComponent.h
{
    class TabbedComp;
};

class MainComponent  : public juce::Slider::Listener
{
public:
    MainComponent();
    ~MainComponent() override;

    // I needed to override these virtual functions of Slider::Listener
    void sliderValueChanged(juce::Slider* slider) override;
    void sliderDragStarted(juce::Slider*) override;
    void sliderDragEnd(juce::Slider*) override;

   // ...

private:
    // ...

    myApp::TabbedComp* gui;
};

// =============================================================================
// MainComponent.cpp

MainComponent::MainComponent()
{
    gui = new anyMidi::TabbedComp(this);
    addAndMakeVisible(gui);
}

void MainComponent::resized()
{ 
    gui->setBounds(getLocalBounds().reduced(4));
}

void MainComponent::sliderValueChanged(juce::Slider* slider) {}
void MainComponent::sliderDragStarted(juce::Slider*) {}
void MainComponent::sliderDragEnded(juce::Slider*) {}

Then there is the App GUI:

// =============================================================================
// appGUI.h
#pragma once

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


namespace myApp {
    class TabbedComp : public juce::TabbedComponent
    {
    public:
        TabbedComp(MainComponent* mc);
    };

    class AppSettingsPage : public juce::Component
    {
    public:
        AppSettingsPage(juce::Slider::Listener* mc);
        void resized() override;

        juce::Slider mySlider;
    };

}; // namespace myApp

// =============================================================================
// appGUI.cpp
#include "appGUI.h"

using namespace anyMidi;
TabbedComp::TabbedComp(MainComponent* mc) : TabbedComponent(juce::TabbedButtonBar::TabsAtTop)
{
    addAndMakeVisible(this);
    auto color = juce::Colour(0, 0, 0);
    addTab("App Settings", color, new AppSettingsPage(mc), true);
}

AppSettingsPage::AppSettingsPage(juce::Slider::Listener* mc)
{
    mySlider.addListener(mc);
}

Please help me understand how to do this properly! As I said before, I’d like to use lambdas, but couldn’t understand how I would acces the MainComponent callback functions I wanted them to trigger without having circular includes.

Slider callbacks

The Slider class contains two methods for receiving callbacks which are completely separate from each other. One is the Slider::Listener class, and one is the public std::function member called onValueChange. The second one uses lambdas, the first one does not, and it seems from your post that you are confusing the two.

To use the first method, you have to derive from Slider::Listener and then add that as a listener to a slider.

class MainComponent : public juce::Component, public juce::Slider::Listener
{
public:

    MainComponent()
    {
        setBounds(100, 100, 500, 500);
        addAndMakeVisible(slider);
        slider.setBounds(10, 10, 400, 50);

        slider.addListener(this);   // it seemed that you were missing this line.
        
    }

    ~MainComponent()
    {
        slider.removeListener(this);    // technically you could get away without doing this here since the
                                        // slider and the listener are destroyed together, but it's an important habit to get into
    }

    void sliderValueChanged(juce::Slider* s) override
    {
        DBG(s->getValue());
    }

private:

    juce::Slider slider;
    
};

To use the second method, you ignore the Slider::Listener class and just assign a lambda to the onValueChange member inside the constructor.

class MainComponent : public juce::Component
{
public:

    MainComponent()
    {
        setBounds(100, 100, 500, 500);
        addAndMakeVisible(slider);
        slider.setBounds(10, 10, 400, 50);

        slider.onValueChange = [this]() { DBG(slider.getValue()); };

        // in case you're unfamiliar with lambdas, the "[this]" part captures the "this" pointer to the MainComponent.
        // Without this, the lambda function wouldn't be able to call slider.getValue().
    }

private:

    juce::Slider slider;
    
};

Both methods have different strengths and weaknesses. The second one is more succinct and should probably be preferred in most instances. However, if you want your slider to have multiple listeners, the first will be better.

Dealing with circular dependencies

I remember this being a real pain when I first started out, but once you get used to it it becomes a breeze. The trick is to have as few #includes in the header files as you possibly can, and to rely on forward declarations instead. Here’s an example:


// MainComponent.h

#include <memory>

namespace myApp
{
    class SomeClass;  // forward declaration
}

class MainComponent : public juce::Component
{
public:

    MainComponent();

private:
    std::unique_ptr<myApp::SomeClass> someClass;  // notice that this will compile even though SomeClass hasn't been defined yet.
};
// MainComponent.cpp
#include "MainComponent.h"

namespace myApp
{
    class SomeClass    // full declaration here
    {
    public:
        SomeClass() = default;
    };
}

MainComponent::MainComponent()
{
    setBounds(10, 10, 500, 500);

    someClass = std::make_unique<myApp::SomeClass>();
}

You should be able to use this trick to clear up 80% of circular dependencies.

A few other things:

1: There is a memory leak in your code right here:

MainComponent::MainComponent()
{
    gui = new anyMidi::TabbedComp(this);
    addAndMakeVisible(gui);
}

gui should be a unique_ptr or some other smart pointer. Always use smart pointers when dealing with memory allocation!

2: In juce-land, the word “component” usually means “gui element”. Hence when you say…

I’m separating my GUI from my MainComponent

…it sounds like a contradiction. However, this is of course only semantics, and your general idea of separating your gui from your data model is a good one.

2 Likes

Thank you very much for taking the time to write this answer! This helped very much but I’m still struggling a bit. Now I have my MainComponent with a member gui which is a unique pointer to a TabbedComponent. This TabbedComponent contains a tab, AudioSettingsPage, in which my slider is a member. I still don’t understand how I’m to traverse this to get the listeners right. I tried with the lambda-way, and sent down my MainComponent object through my TabbedComponent, on to the slider:

class TabbedComp : public juce::TabbedComponent
{
public:
    TabbedComp::TabbedComp(MainComponent* mc) : TabbedComponent(juce::TabbedButtonBar::TabsAtTop)
    {
        addAndMakeVisible(this);
        addTab("Audio Settings", someColor, new AudioSetupPage(mc->deviceManager), true);
    }
};

class AppSettingsPage : public juce::Component
{
public:
    AppSettingsPage(MainComponent* mc)
    {
        slider.onValueChange = [mc]
            {
                // Code here
            };
    }
};

but I reckon I should pass this instead of the MainComponent mc. But how do I link the slider and MainComponent then? Do I have to make my TabbedComponent both a broadcaster and a listener, listening to the tab with the slider, and broadcasting to the MainComponent whenever this happens?

The code you’ve written looks like it will work, but passing around pointers like this makes it fragile. If the slider manages to outlive the MainComponent, then it’s holding a dangling pointer in the lambda capture, and will crash as soon as you use it. This might not happen in the current configuration, but you might run into problems at a later date if you decide to change something.

Creating an intermediate broadcaster as you suggested is another viable option, but this can get complicated also, as you have to remember to add and remove listeners at the right point, and it can be hard to reason about code when there are several layers of listeners.

In this case, I would suggest that you consider using ValueTree to store the value. This will allow you to synchronize data across the whole system without worrying about passing pointers, and you can easily attach listeners at any time. If you don’t already have it, you would have to come up with a well defined structure for your data model in order for this to work. But this is a good thing to have in any case.

Also, take a look at the PropertiesDemo in the demo runner. There are a few classes shown there that are helpful for creating property windows.

2 Likes

Once again, thank you! I will definitely look into the things you suggested.

I’m late to this thread, but imo, you are being given the wrong advice. You started I’m separating my GUI from my MainComponent, which is not the best practice. I think the best practice you are shooting for is to minimize coupling, which is often achieved by separating the data from the UI. Your current methods are just moving the coupling around, not really removing it. I did not read your post in detail, but instead of tying to slide to some other piece of UI, you could have the separate pieces of UI share the data that the slider is operating on, because that is really the shared thing. ValueTrees are an excellent solution for this, as they have the callback mechanism that can be used to sign up for receiving changed values. The basics are that when you slider changes value, it updates a property in the valuetree, and the other part of the UI that is interested in this change has set up a valuetree listener callback, which it receives each time the slide updates the value. Obviously there are many more details, but, imo, this is the separation you are shooting for.

2 Likes

Aha! Yes, I guess I’m misunderstanding the data model - MainComponent relationship. I guess I’m taking a look at the ValueTree next. Thank you!

Maybe I’ll get this answered in the tutorial, but I’ll ask anyway: How, concretely, should the structure of my code look like? My MainComponent should contain an instance of my GUI and an instance of a ValueTree, yes? Should my audio processing be separate from the MainComponent? As of now I have a dps-related class and a midi-related class, which both are in their separate files. The MainComponent has an instance of each, but some processing is still happening in the MainComponent. Is this bad practice?

MainComponent IS your top level GUI. I look at the model this way, the UI is just the view to the data (although there is UI related data that is owned by the UI too, it should mostly be local), so the data model(s) and GUI are instantiated by the application class, with references to the data models passed into the UI classes. I have adopted a global ‘root’ valuetree model, where I have ‘persistent’ (updates are auto saved to app properties file) and ‘runtime’ ValueTrees that I pass into things, and various domain specific children are added to those. Those domain specific children can be found by any code that has the roots, and so sharing data is super simple once the basic architecture is in place. I also create domain specific wrapper classes, that hide the ValueTree-ness of the data, and when used in conjunction with the roots, makes data access even easier.

So, some specifics, Should my audio processing be separate from the MainComponent?, yes. Refer to my first sentence. And, also consider the idea the UI is a view/controller. Your audio stuff should generally not interact with that code directly. As well, another good perspective is to think about what work is involved to put a new UI on to something. Have you written the code in a way where those changes would be well isolated?

Hopefully some of this helps. :slight_smile:

1 Like

Thank you very much!

In case there is any confusion here, what @cpr2323 has said is pretty much exactly the same as what I was trying to say. I don’t think we are in dispute.

2 Likes

Sorry for missing that, @LiamG. I see now that I was lazy and did not read every response. :relaxed:

1 Like