Which is best for my use: Grid or FlexBox? Simplest way to get some knobs organized on the screen?

I am trying to convert some complex Reaktor modal synthesizers I have built over to Juce. The overwhelming majority of the controls I need are simple rotary sliders. Although my synths are near complete they are not completely so (was running out of resources in Reaktor due to its inefficiency so couldn’t finish them). So this is still in the “90% done prototyping” phase.

This means I want flexibility and simplicity in my layout method.

Basically all I need is the simplest way to let 50 or 100 knobs be aligned and not overlapping on a given project size.

I’d like to be able ideally to just let them be generated from left to right in rows based on their order in the project so it’s very easy/brainless to reorganize them while testing.

Would the Grid function allow me to do this? I’m not worrying about “flipping the orientation” like FlexBox seems designed for. I just want the simplest way to get some knobs laid out on the screen, and the most flexible way to reorder them once they’re there.

But perhaps FlexBox is best, because for prototyping, I could even just keep everything on one “row” and let it wrap it into columns for me?

What do you think?

Also, is there any more detailed tutorial on Grid use than this:
https://docs.juce.com/master/tutorial_flex_box_grid.html

Or pointers you can provide for how I’d want to do this?

Thanks

What I don’t get in particular about the FlexBox tutorial is all the knobs and sliders were generated by “for” loops, which is fine if they’re just dummy knobs and sliders that don’t each need their own properties and ranges etc. But that is not useful.

How do you make a FlexBox with actual real knobs in it, where each one needs its own specifications (range, default, label, etc.)?

I don’t understand how to get from the tutorial with these pointless auto-generated dummy knobs/sliders to using the same FlexBox with REAL knobs/sliders.

The loops in the tutorial are only creating the FlexItems, that control the layout.
The knobs are already in the array, so you are expected to have them already set up with parameters and connections like SliderAttachments.
However I would agree, that a knob without description is not very useful to be layouted automatically. I usually have a custom GroupComponent with the parameter name that aggregates a Slider to be displayed inside.

Hope that helps

Thanks Daniel.

I understand this part of the example project:

    MainContentComponent()
   #if ! JUCE_PROJUCER_LIVE_BUILD
    : rightPanel (Colours::lightgrey),
      leftPanel  (Colours::lightblue)
   #endif
{
    addAndMakeVisible (rightPanel);
    addAndMakeVisible (leftPanel);
    addAndMakeVisible (mainPanel);

    setSize (600, 400);
}

~MainContentComponent() {}

void paint (Graphics& g) override
{
    g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
}

void resized() override
{
    FlexBox fb;

    FlexItem left  (getWidth() / 4.0f, getHeight(), leftPanel);
    FlexItem right (getWidth() / 4.0f, getHeight(), rightPanel);
    FlexItem main  (getWidth() / 2.0f, getHeight(), mainPanel);

    fb.items.addArray ( { left, main, right } );
    fb.performLayout (getLocalBounds().toFloat());
}

It makes a flexbox called fb with three flexitems, that are then added to an array so the flex box can organize them.

But I don’t understand how this works with the knobs and sliders. I don’t see the same method used. It’s just for loops. There’s no other “addarray” anywhere else. And I don’t even see the knobs/sliders defined anywhere except as (I think) auto-generated by for loops.

eg.

 for (int i = 0; i < 6; ++i)
        {
            auto* slider = new Slider();
            slider->setSliderStyle (Slider::SliderStyle::Rotary);
            slider->setTextBoxStyle (Slider::NoTextBox, true, 0, 0);

            addAndMakeVisible (knobs.add (slider));

As I said I think this is useless from a demonstration purpose because you never just want to generate 6 random sliders with nothing specific about any of them.

So let’s say in the simplest form, I want to individually define ~30-50 knobs each with parameters each like this:

addAndMakeVisible (frequencySlider);
frequencySlider.setSliderStyle(Slider::RotaryHorizontalVerticalDrag);
frequencySlider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20);
frequencySlider.setRange (50.0, 5000.0);
frequencySlider.setSkewFactorFromMidPoint (500.0);
frequencySlider.setNumDecimalPlacesToDisplay(1);

addAndMakeVisible(frequencyLabel);
frequencyLabel.setText("Frequency", dontSendNotification);
frequencyLabel.attachToComponent(&frequencySlider, false);
frequencyLabel.setJustificationType(Justification::horizontallyCentred);

And then I want to put those in a simple array and have it automatically assign each one an equal width/height and wrap it into rows, how would I do that?

I think that would make a much more useful application of the tutorial, even if it was demonstrated with just 3-5 knobs, since that’s what we actually have to try to do in real life.

I think I can do this by just using the same method as the panels above by manually defining each knob as a flex item (but with what width/height?) and then manually entering them to an array in the same way. But with all the “for” loops in the tutorial I’m not sure if this is the most efficient or correct way.

Any further help?

Thanks

Thanks for your critique on the tutorial. The reason, why this is not covered here is probably, because this way it easier to focus on the layout, since adding the said functionality would add a considerable amount of code, that would dilute the topic.

For your purpose, you could simply remove that loop, and instead add your sliders to the knobs array (make sure to either use an OwnedArray and create the sliders as pointers with new, or using a simple Array, that doesn’t own the sliders):

Array<Slider*> knobs;
// ...
knobs.add (&frequencySlider);

Like I said before, I would create a little class like this:

class AnnotatedSlider : public GroupComponent
{
public:
    AnnotatedSlider (AudioProcessorValueTreeState& state, const String& paramID)
    : slider (Slider::RotaryHorizontalVerticalDrag, Slider::TextBoxBelow),
      attachment ( state, paramID, slider)
    {
        if (auto* param = state.getParameter (paramID))
        {
            setText (param->getName());
        }
        else
            jassertfalse;
    }
    void resized() override
    {
        slider.setBounds (getLocalBounds().reduced (10));
    }
    Slider slider;
private:
    AudioProcessorValueTreeState::SliderAttachment attachment;
};

Now you can add all parameters to your editor:

// member
OwnedArray<AnnotatedSlider> knobs;

// in editor constructor, assuming you have access to the AudioProcessorValueTreeState
auto& state = processor.getTreeState();
for (auto* param : processor.getParameters())
{
    if (auto* paramWithID = dynamic_cast<AudioProcessorParameterWithID*> (param))
        knobs.add (new AnnotatedSlider (state, paramWithID->paramID));
}

untested to get you inspiration…

Thanks Daniel.

I think I understand the principle of what you’re suggesting:

  • Define a new class called “AnnotatedSlider” which inherits from GroupComponent to represent a unit containing both a slider and a label linked to that slider.
  • Define my knobs from this “AnnotatedSlider” class.
  • Create an Array called “knobs” and add to it one by one all the AnnotatedSlider elements.
  • Define a FlexBox and use a range-based for loop to create a FlexItem for each member of the “knobs” array.
  • Let the FlexBox “performLayout” of these FlexItems and thus organize the knobs and their attached labels.

However, I cannot seem to understand how to put this together. For example, I just tried copying your class declaration for “AnnotatedSlider” into the existing FlexBox tutorial, and I get errors just from that alone:

I really wish there could be a tutorial on this, since I gather from your posts that this is what we ought to be doing in almost any real GUI design situation.

If this is the proper modern way to make and organize labels/sliders in a JUCE design, isn’t this an essential skill we should all be getting in a tutorial? Doesn’t almost every design need to organize labelled sliders? And isn’t FlexBox the best approach?

Is there any chance you could write something simple that would demonstrate this with 2-3 knobs so I can see how it would work?

It would just be helpful to get something working so I can see how it’s supposed to come together. Without ever seeing an example, I can’t find enough information to deduce how it should be done. I would be very willing to PayPal you for your time if needed.

For example, the only information I have on “AudioProcessorValueTreeState” is the few sentences at: https://docs.juce.com/master/classAudioProcessorValueTreeState.html

I wish there were books on JUCE so I didn’t have to ask these sorts of things because I’m not opposed to reading or doing work, but I feel like there’s just not enough info out there on this type of practical usage.

I just want the simplest way to get some knobs laid out on the screen, and the most flexible way to reorder them once they’re there.

I find FlexBox a pain. Here’s some layout code for a grid you could modify. This sets out a bunch of drum pads in a 4x4 grid. But you could just play with the numbers to do whatever you want. It make a box ‘b’ that is the size of the thing you are laying out and then just left-to-right lays them out till it’s got a row, then translates the box to the start of the next row.

void resized() override
{
    auto b = getLocalBounds();
    auto bh = b.getHeight() / 4;
    auto bw = b.getWidth() / 4;

    b.setSize(bh, bw);

    for (int i = 0; i < pads.size(); ++i)
    {
        pads[i]->setBounds(b);

        if (i % 4 == 3) // i.e. we have just put the last item on the row.
        {
            b.setX(0);
            b.translate(0, bh);
        }
        else
        {
            b.translate(bw, 0);
        }
    }
}

In case it’s any use, this is what I use for temporary labelling of knobs when prototyping. It attaches a label to a component, displays it, positions it with one line of code, and deletes it when the component is deleted. I think it might have been inspired by something Dave said …

class JLabel : public Component, ComponentListener, public ReferenceCountedObject
{
public:
    /** Create a new JLabel and attached it as a reference counted object to the provided components properties. */
    static JLabel* createAndAttach (Component* component, const String& text)
    {
        auto label = new JLabel (component, text);
        component->getProperties().set ("_label_object", label);
        return label;
    }

    ~JLabel()
    {
        componentToAttachTo->removeComponentListener (this);
    }

    void paint (Graphics& g) override
    {
        Font f (float (getHeight()), Font::FontStyleFlags::bold);
        g.setFont (f);
        g.setColour (Colour::greyLevel (0.8f));
        g.drawText (text.toUpperCase(), getLocalBounds(), Justification::centred, false);
    }

private:
    void updatePosition()
    {
        auto b = componentToAttachTo->getBounds();
        auto padding = 3;
        setBounds (b.withHeight (12).withY (b.getBottom() + padding));
    }

    void updateVisibility()
    {
        setVisible (componentToAttachTo->isVisible());
    }

    void attachToParent()
    {
        auto parent = componentToAttachTo->getParentComponent();

        if (parent)
        {
            parent->addAndMakeVisible (this);
            updatePosition();
            updateVisibility();
        }
    }

    void componentMovedOrResized (Component&, bool, bool) override
    {
        updatePosition();
    }
    void componentParentHierarchyChanged (Component&) override
    {
        attachToParent();
    }
    void componentVisibilityChanged (Component&) override
    {
        updateVisibility();
    }

    JLabel (Component* componentToAttachTo, const String& text) : componentToAttachTo (componentToAttachTo), text (text)
    {
        componentToAttachTo->addComponentListener (this);
        attachToParent();
        updatePosition();
        updateVisibility();
    }

    Component* componentToAttachTo;
    String text;
};

Example use:

JLabel::createAndAttach(&myKnob, "Stereo");

It couldn’t get any easy without being telepathic.

Hey jimc, thanks for the feedback. I think we developed an even better method using FlexBox over here thanks to daniel’s help:

Here is the code at present (working from the SineSynth tutorial):

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 MainContentComponent : public AudioAppComponent
{
public:
    MainContentComponent()
	
    {
    /*
	addAndMakeVisible(frequency);
    addAndMakeVisible(level);
	addAndMakeVisible(dummy1);
	addAndMakeVisible(dummy2);
	addAndMakeVisible(dummy3);
	addAndMakeVisible(dummy4);
	addAndMakeVisible(dummy5);
	addAndMakeVisible(dummy6);
	addAndMakeVisible(dummy7);
	addAndMakeVisible(dummy8);
	addAndMakeVisible(dummy9);
	*/
		/*
		frequency.slider.setRange(20.0, 20000.0);
		frequency.slider.setSkewFactorFromMidPoint(500.0);
		frequency.slider.setNumDecimalPlacesToDisplay(1);
		frequency.slider.setValue(currentFrequency, dontSendNotification);
		frequency.slider.onValueChange = [this] { targetFrequency = frequency.slider.getValue(); };
		frequency.slider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20);
		frequency.slider.setRange(50.0, 5000.0);
		frequency.slider.setSkewFactorFromMidPoint(500.0);
		frequency.slider.setNumDecimalPlacesToDisplay(1);

		level.slider.setRange(0.0, 1.0);
		level.slider.onValueChange = [this] { targetLevel = (float)level.slider.getValue(); };
		*/

		LabeledSlider* control = new LabeledSlider("Frequency");
		control->slider.setRange(20.0, 20000.0);
		control->slider.setSkewFactorFromMidPoint(500.0);
		control->slider.setNumDecimalPlacesToDisplay(1);
		control->slider.setValue(currentFrequency, dontSendNotification);
		control->slider.onValueChange = [this] { targetFrequency = frequency.slider.getValue(); };
		control->slider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20);
		control->slider.setRange(50.0, 5000.0);
		control->slider.setSkewFactorFromMidPoint(500.0);
		control->slider.setNumDecimalPlacesToDisplay(1);
		addAndMakeVisible(knobs.add(control));

		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);
    setAudioChannels (0, 2); // no inputs, two outputs
    }

    ~MainContentComponent()
    {
        shutdownAudio();
    }

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

		//Array<LabeledSlider*> knobs;
		//knobs.add(&frequency, &level, &dummy1, &dummy2, &dummy3, &dummy4, &dummy5, &dummy6, &dummy7, &dummy8, &dummy9);

		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());
	

    }

    inline void updateAngleDelta()
    {
        auto cyclesPerSample = currentFrequency / currentSampleRate;
        angleDelta = cyclesPerSample * 2.0 * MathConstants<double>::pi;
    }

    void prepareToPlay (int, double sampleRate) override
    {
        currentSampleRate = sampleRate;
        updateAngleDelta();
    }

    void releaseResources() override {}

    void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
    {
        auto* leftBuffer  = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
        auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);

        auto localTargetFrequency = targetFrequency;

        if (targetFrequency != currentFrequency)
        {
            auto frequencyIncrement = (targetFrequency - currentFrequency) / bufferToFill.numSamples;

            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentFrequency += frequencyIncrement;
                updateAngleDelta();
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }

            currentFrequency = localTargetFrequency;
        }
        else
        {
            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }
        }

        auto localTargetLevel = targetLevel;
        bufferToFill.buffer->applyGainRamp (bufferToFill.startSample, bufferToFill.numSamples, currentLevel, localTargetLevel);
        currentLevel = localTargetLevel;
	}

private:
	double currentSampleRate = 0.0, currentAngle = 0.0, angleDelta = 0.0;
	double currentFrequency = 500.0, targetFrequency = 500.0;
	float currentLevel = 0.1f, targetLevel = 0.1f;
	int rotaryDiam = 100;
		
	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)
};

I commented out some parts rather than deleting them as I think those may be a necessary approach in other situations.

Now, I need to work out a good way to nest a layer of Flexboxes over top of these Flexboxes, so I can have “groups” of knobs.

I think what I will have to do is:

  • Define a new GroupComponent called LabeledGroup.
  • Manually assign the knobs to a bunch of arrays (one for each “group”).
  • Set up a FlexBox for each array manually also so each group gets FlexBoxed within it.
  • Then use the OwnedArray approach above for automatically sorting the groups of knobs all out.

That’s my challenge now. Otherwise it’s working very well as is and it’s pretty simple to use.

Ah wicked - well if you’ve got it working with FlexBox all well and good. I suspect I’m just adverse to new things myself :wink:

Well I’m new so it’s easier - EVERYTHING is new. :slight_smile: Plus daniel did all the work. lol.

Next step is to try to get a LabeledGroup class set up so that the LabeledSliders can be set to belong to a given group, and put the FlexBox code into the LabeledGroup code so it auto-arranges them.

The we can have a panel of auto-arranged sliders within auto-arranged groups. ie. Two layers of automatic FlexBoxing.

This would be the ultimate flexible, no hassle configuration for prototyping and even likely final design (with some tweaks).

If you’re keen to give it a shot, I posted the question of how to do this here, as well as the most current working code for the existing approach:

I don’t understand enough about C++ to know how to execute what I’m trying to, and when I post these questions on general C++ forums they scratch their heads because they don’t know JUCE.

So any help is appreciated. I think you’ll find this structure quite favorable for your own use even as is. It’s very easy to work with.