Building Nested GroupComponents & FlexBoxes for a Flexible GUI

I am trying to set up a flexible GUI design that is based on FlexBox and allows for two tiers of FlexBox organization - one for all the elements within each “group”, and one for all the groups on the panel. I’ve been stumped on it for over a month.

This is what I have:

This is what I want to have:

The principle is that:

  • Every knob should be created as also belonging to a group (or somehow assigned to a group).
  • All the knobs that belong to any given group should be automatically added to an array for that group so they can be automatically FlexBoxed within the group.
  • MainContentComponent can then add all groups to another array and arrange that with Flexbox so the groups are arranged on the panel.

I cannot figure out the proper way to do this though.

Here is the working code I have that will create the first screenshot in this thread as a single JUCE CPP file:

/*
==============================================================================

This file was auto-generated and contains the startup code for a PIP.

==============================================================================
*/

#include "../JuceLibraryCode/JuceHeader.h"

///============================
///BEGINNING OF USUAL PROJECT HEADER


#pragma once

class LabeledSlider : public GroupComponent

{
public:
	LabeledSlider(const String& name)
	{
		setText(name);
		setTextLabelPosition(Justification::centredTop);
		addAndMakeVisible(slider);
	}

	void resized() override
	{
		slider.setBounds(getLocalBounds().reduced(10));
	}

	Slider slider
	{
		Slider::RotaryHorizontalVerticalDrag, Slider::TextBoxBelow
	};


};




//==============================================================================
//MAINCONTENTCOMPONENT
//==============================================================================

class MainContentComponent : public Component

{
public:
	MainContentComponent()
	
	{
	
		LabeledSlider *control = new LabeledSlider("Level");
		control->slider.setRange(0.0, 1.0);
		control->slider.onValueChange = [this] { targetLevel = (float)level.slider.getValue(); };
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy1");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy2");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy3");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy4");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy5");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy6");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy7");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy8");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy9");
		addAndMakeVisible(knobs.add(control));


		setSize(600, 600);
	}

	~MainContentComponent()
	{
//		shutdownAudio();
	}


	void resized() override
	{

		FlexBox knobBox;
		knobBox.flexWrap = FlexBox::Wrap::wrap;
		knobBox.justifyContent = FlexBox::JustifyContent::flexStart;
		knobBox.alignContent = FlexBox::AlignContent::flexStart;


		for (auto* k : knobs)
			knobBox.items.add(FlexItem(*k).withMinHeight(80.0f).withMinWidth(80.0f).withFlex(1));

		FlexBox fb;
		fb.flexDirection = FlexBox::Direction::column;
		fb.items.add(FlexItem(knobBox).withFlex(2.5));
		fb.performLayout(getLocalBounds().toFloat());


	}

	

	
private:

	float currentLevel = 0.1f, targetLevel = 0.1f;
	
	LabeledSlider frequency{ "Frequency" };
	LabeledSlider level{ "Level" };
	LabeledSlider dummy1{ "Dummy 1" };
	LabeledSlider dummy2{ "Dummy 2" };
	LabeledSlider dummy3{ "Dummy 3" };
	LabeledSlider dummy4{ "Dummy 4" };
	LabeledSlider dummy5{ "Dummy 5" };
	LabeledSlider dummy6{ "Dummy 6" };
	LabeledSlider dummy7{ "Dummy 7" };
	LabeledSlider dummy8{ "Dummy 8" };
	LabeledSlider dummy9{ "Dummy 9" };

	OwnedArray<LabeledSlider> knobs;


	JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainContentComponent)
};





///============================
///BEGINNING OF USUAL MAIN.CPP
class Application : public JUCEApplication
{
public:

	Application() {}

	const String getApplicationName() override { return "SineSynthTutorial"; }
	const String getApplicationVersion() override { return "3.0.0"; }

	void initialise(const String&) override { mainWindow.reset(new MainWindow("SineSynthTutorial", new MainContentComponent(), *this)); }
	void shutdown() override { mainWindow = nullptr; }

private:
	class MainWindow : public DocumentWindow
	{
	public:
		MainWindow(const String& name, Component* c, JUCEApplication& a)
			: DocumentWindow(name, Desktop::getInstance().getDefaultLookAndFeel()
				.findColour(ResizableWindow::backgroundColourId),
				DocumentWindow::allButtons),
			app(a)
		{
			setUsingNativeTitleBar(true);
			setContentOwned(c, true);

#if JUCE_ANDROID || JUCE_IOS
			setFullScreen(true);
#else
			setResizable(true, false);
			setResizeLimits(300, 250, 10000, 10000);
			centreWithSize(getWidth(), getHeight());
#endif

			setVisible(true);
		}

		void closeButtonPressed() override
		{
			app.systemRequestedQuit();
		}

	private:
		JUCEApplication & app;

		//==============================================================================
		JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainWindow)
	};

	std::unique_ptr<MainWindow> mainWindow;
};

//==============================================================================
START_JUCE_APPLICATION(Application)

?

I spent a few hours chatting with law and matkatmusic on Discord (thanks guys), and I’ve gotten a little farther in my conceptualization. I gotta go to bed, but this is what I have so far. Again this is all one single standalone .cpp file:

/*
==============================================================================

This file was auto-generated and contains the startup code for a PIP.

==============================================================================
*/

#include "../JuceLibraryCode/JuceHeader.h"

///============================
///BEGINNING OF USUAL PROJECT HEADER


#pragma once

class LabeledSlider : public GroupComponent

{
public:
	LabeledSlider(const String& name)
	{
		setText(name);
		setTextLabelPosition(Justification::centredTop);
		addAndMakeVisible(slider);
	}

	void resized() override
	{
		slider.setBounds(getLocalBounds().reduced(10));
	}

	Slider slider
	{
		Slider::RotaryHorizontalVerticalDrag, Slider::TextBoxBelow
	};


};

class LabeledGroup : public GroupComponent

{
public:
	LabeledGroup(const String& name)
	{
		setText(name);
		setTextLabelPosition(Justification::centredTop);

		addAndMakeVisible(dummy1);
		addAndMakeVisible(dummy2);
		addAndMakeVisible(dummy3);
		addAndMakeVisible(dummy4);

	}

	void resized() override
	{
		//setBounds(getLocalBounds().reduced(10));

		FlexBox knobBox1;
		knobBox1.flexWrap = FlexBox::Wrap::wrap;
		knobBox1.justifyContent = FlexBox::JustifyContent::flexStart;
		knobBox1.alignContent = FlexBox::AlignContent::flexStart;

		Array<LabeledSlider*> knobs1;
		knobs1.add(&dummy1, &dummy2, &dummy3, &dummy4);

		for (auto *k : knobs1)
			knobBox1.items.add(FlexItem(*k).withMinHeight(80.0f).withMinWidth(80.0f).withFlex(1));

		FlexBox fb1;
		fb1.flexDirection = FlexBox::Direction::column;
		fb1.items.add(FlexItem(knobBox1).withFlex(2.5));
		fb1.performLayout(getLocalBounds().toFloat());

	}


private:
	LabeledSlider dummy1{ "Dummy 1" };
	LabeledSlider dummy2{ "Dummy 2" };
	LabeledSlider dummy3{ "Dummy 3" };
	LabeledSlider dummy4{ "Dummy 4" };

};


//==============================================================================
//MAINCONTENTCOMPONENT
//==============================================================================

class MainContentComponent : public Component

{
public:
	MainContentComponent()

	{
		addAndMakeVisible(group1);
		group1.setSize(499, 400);
		group1.setTopLeftPosition(0, 0);

		level.slider.setRange(0.0, 1.0);
		level.slider.setNumDecimalPlacesToDisplay(1);
		level.slider.onValueChange = [this] { targetLevel = (float)level.slider.getValue(); };
		addAndMakeVisible(level);
		
		
		
		setSize(600, 600);
	}

	~MainContentComponent()
	{
		//		shutdownAudio();
	}


	void resized() override
	{
		/*
		FlexBox knobBox1;
		knobBox1.flexWrap = FlexBox::Wrap::wrap;
		knobBox1.justifyContent = FlexBox::JustifyContent::flexStart;
		knobBox1.alignContent = FlexBox::AlignContent::flexStart;

		Array<LabeledSlider*> knobs1;
		knobs1.add(&dummy1, &dummy2, &dummy3, &dummy4);

		for (auto *k : knobs1)
			knobBox1.items.add(FlexItem(*k).withMinHeight(80.0f).withMinWidth(80.0f).withFlex(1));

		FlexBox fb1;
		fb1.flexDirection = FlexBox::Direction::column;
		fb1.items.add(FlexItem(knobBox1).withFlex(2.5));
		fb1.performLayout(getLocalBounds().toFloat());
		*/
	}




private:

	float currentLevel = 0.1f, targetLevel = 0.1f;

	LabeledGroup group1{ "Group 1" };

/*
    	LabeledSlider level{ "Level" };
    	LabeledSlider dummy5{ "Dummy 5" };
    	LabeledSlider dummy6{ "Dummy 6" };
    	LabeledSlider dummy7{ "Dummy 7" };
    	LabeledSlider dummy8{ "Dummy 8" };
    	LabeledSlider dummy9{ "Dummy 9" };
*/


	JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainContentComponent)
};





///============================
///BEGINNING OF USUAL MAIN.CPP
class Application : public JUCEApplication
{
public:

	Application() {}

	const String getApplicationName() override { return "SineSynthTutorial"; }
	const String getApplicationVersion() override { return "3.0.0"; }

	void initialise(const String&) override { mainWindow.reset(new MainWindow("SineSynthTutorial", new MainContentComponent(), *this)); }
	void shutdown() override { mainWindow = nullptr; }

private:
	class MainWindow : public DocumentWindow
	{
	public:
		MainWindow(const String& name, Component* c, JUCEApplication& a)
			: DocumentWindow(name, Desktop::getInstance().getDefaultLookAndFeel()
				.findColour(ResizableWindow::backgroundColourId),
				DocumentWindow::allButtons),
			app(a)
		{
			setUsingNativeTitleBar(true);
			setContentOwned(c, true);

#if JUCE_ANDROID || JUCE_IOS
			setFullScreen(true);
#else
			setResizable(true, false);
			setResizeLimits(300, 250, 10000, 10000);
			centreWithSize(getWidth(), getHeight());
#endif

			setVisible(true);
		}

		void closeButtonPressed() override
		{
			app.systemRequestedQuit();
		}

	private:
		JUCEApplication & app;

		//==============================================================================
		JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainWindow)
	};

	std::unique_ptr<MainWindow> mainWindow;
};

//==============================================================================
START_JUCE_APPLICATION(Application)

The end goal is that each LabeledGroup class member should automatically have the FlexBox code built into it so that each LabeledGroup automatically FlexBoxes all of its members.

Then I can in MainContentComponent automatically FlexBox all the LabeledGroups to make a final panel.

The biggest problem I’m having conceptually is I’ve put the labeledSlider definitions and addandmakevisible commands right inside the LabeledGroup class definition, and obviously that won’t work.

I need to define those in MainContentComponent so that I can specify them for each Group.

Can anyone offer any help or ideas here? I’m still stuck on the code in post 2 of this thread.

There are three places that the names of the knobs must be specified as variables within the LabeledGroup Class definition:

  • addAndMakeVisible(dummy1)
  • labeledSlider dummy1{ “Dummy 1”}
  • knobs1.add(&dummy1, …)

My idea was to just build an array for each group and then run a range-limited for loop for them in each of those three spots.

I tried for example testing in LabeledGroup instead of the individual addAndMakeVisible declarations:

String group1string[] = { "dummy1", "dummy2", "dummy3", "dummy4", "dummy5" };
for (String& n : group1string) 
    {
    addAndMakeVisible(n);
    }

But even this simple code did not work. It gives me this error:

I posted on StackOverflow for help but no one was much help:
https://stackoverflow.com/questions/52580394/running-a-range-based-for-loop-from-an-array/52580745

I then asked in a more general sense for solutions to the problem and got a few ideas come back of arrays which were along the same lines of the solution I was trying to implement:

The first solution seemed promising but ran into the same problem as my basic array attempt.

So I have a concrete problem but I am stumped on how to solve it. Basically I just need to be able to set an array (or arrays) in MainContentComponent for each LabeledGroup. The array(s) will contain the names of the labeledSliders that will then be used inside the LabeledGroup class definition in the three places listed above to create an appropriate LabeledGroup object for those arrays.

It seems to me like this should be a simple C++/Juce issue. Any help?

@daniel you helped me get this going in the first place. Any chance you could throw me another bone? :slight_smile:

From what I can see you are calling addAndMakeVisible on a String, not on a Label instance…

Thanks leehu! That gets rid of the error at least. I’m still clumsy about learning how to write C++.

But it still doesn’t seem to be accomplishing the goal. None of the sliders appear to be “made visible”.

With the manual code this works:

	addAndMakeVisible(dummy1);
	addAndMakeVisible(dummy2);
	addAndMakeVisible(dummy3);
	addAndMakeVisible(dummy4);

But not with the test loop code in its place:

	    LabeledSlider group1string[] = { "dummy1", "dummy2", "dummy3", "dummy4" };
		for (LabeledSlider& i : group1string)
		{
			addAndMakeVisible(i);
		}

Any further ideas? Shouldn’t these two be interchangeable? That’s the goal …