TIP: How to include value and label on RotaryKnob without declaring a label!

I wish there was a “Tips & Tricks” section.

ValueLabelLookAndFeel

The button in the screenshot was made without having to declare a “Label”. This may only be an enlightenment for beginners, but here it goes;

As I started work on a synthesizer with 100+ knobs, I found it tedious to declare and implement a “Label” for each knob, plus I wanted the value to be shown right in the middle of the knob.

Here is my custom LookAndFeel class, which some might find useful. Feel free to omit what you do not need.

class ValueLabelLookAndFeel : public LookAndFeel_V4 {
	public:

		void drawRotarySlider (Graphics& g, int x, int y, int width, int height, float sliderPos,
			const float rotaryStartAngle, const float rotaryEndAngle, Slider& slider) override
		{
			auto radius = jmin (width / 2.25, height / 2.25) - 4.5;
			auto centreX = x + width * 0.4975f;
			auto centreY = y + height * 0.44f;
			auto rx = centreX - radius;
			auto ry = centreY - radius;
			auto rw = radius * 2;
			auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
			auto isMouseOver = slider.isMouseOverOrDragging () && slider.isEnabled ();

			// Box and outline of knob area
			g.setColour (slider.findColour (Slider::backgroundColourId));
			g.fillRect (x, y, width, height);

			g.setColour (Colour::fromRGB(16, 16, 16));
			g.drawRect (x, y, width, height);

			// Knob fill
		    g.setColour (slider.findColour (Slider::rotarySliderFillColourId).
            withAlpha (isMouseOver ? 1.0f : 0.7f));
			g.fillEllipse (rx, ry, rw, rw);

			// Knob outline
			g.setColour (slider.findColour (Slider::rotarySliderOutlineColourId).
            withAlpha (isMouseOver ? 1.0f : 0.7f));
			g.drawEllipse (rx, ry, rw, rw, 1.0f);

			// Knob pointer
			Path p;
			auto pointerLength = radius * 0.5f;
			auto pointerThickness = width * 0.05f;
			p.addRectangle (-pointerThickness * 0.5f, -radius, pointerThickness, pointerLength);
			p.applyTransform (AffineTransform::rotation (angle).translated (centreX, centreY));

			g.setColour (slider.findColour (Slider::thumbColourId));
			g.fillPath (p);

			// Value
			g.setColour (Colours::black);
			g.setFont (radius * 0.4f);
			g.drawSingleLineText (String (slider.getValue ()), centreX, height * 0.5f,
            Justification::centred);

			// Label
		g.setColour (slider.findColour (Slider::textBoxTextColourId));
		g.setFont (radius * 0.55f);
		g.drawSingleLineText (slider.getProperties ()[slider.getComponentID()], centreX,
 height * 0.985f, Justification::centred);
		}
	};

The key to getting a label to the knob, is using component properties, thanks @Xenakios for this suggestion.

So I set my knob as this;

setLookAndFeel (&valueLabelLF);

	level.setComponentID ("LEVEL");     
    levelKnob[module].getProperties ().set (levelKnob[module].getComponentID(), "LEVEL");
	level.setSliderStyle (Slider::SliderStyle::Rotary);
	level.setColour (Slider::rotarySliderFillColourId, Colours::blue);
	level.setColour (Slider::rotarySliderOutlineColourId, Colours::white);
	level.setRange (0, 1, 0.01f);
	level.setValue (0.5f);
	level.setTextBoxStyle (Slider::TextEntryBoxPosition::NoTextBox, 0, 0, 0);
	addAndMakeVisible (level);

To me, changing the look and feel isn’t the most customizable way to do it.
I would suggest just have a Component that contains and positions the Label and the Slider:

#pragma once

#include "JuceHeader.h"

constexpr float sliderHeight = 0.8f;
constexpr float labelHeight = 1.f - sliderHeight;

class SliderAndLabel : public Component
{
public:

    explicit SliderAndLabel(const String& labelText)
    {
        addAndMakeVisible(slider);
        addAndMakeVisible(label);

        //setup slider any way you like here:
        slider.setSliderStyle(Slider::LinearVertical);

        //setup label any way you like here:
        label.setText(labelText, dontSendNotification);
        label.setJustificationType(Justification::centred);
    }

    void resized() override
    {
        label.setBoundsRelative(0.f, 0.f, 1.f, labelHeight);
        slider.setBoundsRelative(0.f, labelHeight, 1.f, sliderHeight);
    }

    Slider& getSlider() { return slider; }
    Label& getLabel() { return label; }

private:

    Slider slider;
    Label label;
};

I think you missed my point, why I created the custom LF class. I did it mainly so I did not have to create 100+ Labels for 100+ Knobs, as it seemed to much code just to display one string. But secondary to completely customize “label”, fontsize, color, position. Plus I also wanted to display the value on the knob.

If you want to have custom data per component instance, you can use the Component dynamic properties, instead of embedding it in the Component’s ID.

// to set the property
knob.getProperties().set("knoblabel","Level");
// to get the property
String labeltext = knob.getProperties()["knoblabel"];
3 Likes

Surely you don’t have 100 separate Slider/knob instances declared manually in your code…? You should use some kind of container object for those.

2 Likes

Oh nice one, I did not know about that, thanks!

Still very early in my development, but I use simple for loops to create those that are the same.

I think you missed my point, why I created the custom LF class. I did it mainly so I did not have to create 100+ Labels for 100+ Knobs, as it seemed to much code just to display one string. But secondary to completely customize “label”, fontsize, color, position. Plus I also wanted to display the value on the knob.

Yes, and my code has a place for you to do just that, in the constructor. :slight_smile

However, my suggested code also allows you to have many variations of that slider/label if you need to, since you’re not stuck with just one hard coded type.

2 Likes

With my example, I do not have to create a “Label”, in yours you do. That is the difference I am trying to point out.

You did create a Label, by writing your own paint function which is exactly the same as a Label, only that you hard coded it, and missed out on many nice Label features (which you might not be using, but other people may use).

1 Like

Also, you made it pretty hard to create another type of “SliderAndLabel” which doesn’t use the same naming style… So I’d suggest a more customizable approach which will work for more applications, I think.

I did not have to do this;

Label label;

nor this;

levelLabel.setText ("Level", dontSendNotification);
levelLabel.setFont (knobFontSize);
levelLabel.setJustificationType (Justification::centred);
levelLabel.setColour (Label::ColourIds::textColourId, Colour::fromRGB (255, 255, 0));
addAndMakeVisible (levelLabel);

For each of my 100+ labels.

With my custom LF class, I ONLY had to do it ONCE!

But you did have to do

			// Label
			g.setColour (slider.findColour (Slider::textBoxTextColourId));
			g.setFont (radius * 0.4f);
			g.drawSingleLineText (String (slider.getComponentID ().
            initialSectionNotContaining("#")), 
            centreX, height * 0.985f, Justification::centred);

Which is identical to what a Label does, with more lines of code, less features and hard coded style (imagine you had a different kind of Label along with the Slider somewhere…)

Also, I think you missed out on what my code example does.
It allows you to do:

std::vector<std::unique_ptr<SliderAndLabel>> sliders;

for (int index = 0; index < 100; ++index)
    sliders.push_back(std::make_unique<SliderAndLabel>(String(index));

So, I agree that you shouldn’t write the Label 100 times… :slight_smile:

Ok so my example it not helpful to you, I get it.

Let me rephrase “This may only be an enlightenment for beginners, but here it goes;” from my first post to this;

My example may only be useful for some, but here it goes;

Your use case is actually very useful, I just think there are other ways to implement it with:

  1. Less code
  2. More flexibility (say you need some of the sliders to look a little different…)
  3. less hard coding of things that already exist in JUCE, such as position a text in the middle of a label.

Well @Xenakios already made a helpful suggestion;

" Xenakios

1h

If you want to have custom data per component instance, you can use the Component dynamic properties, instead of embedding it in the Component’s ID.

// to set the property
knob.getProperties().set("knoblabel","Level");
// to get the property
String labeltext = knob.getProperties()["knoblabel"];

Anyways my example is just a starting point as users can then customize further. In my specific case, I needed every single knob to the exact same style.

Also I just updated my original post, that is code to reflect his suggestion.

He’s right, but instead of setting it and getting it via a generic Component way using a string, you can just store it right when you create your class.

So in my case:

//Resizes the Label and slider automatically, 
  //and sets custom slider style in the constructor
SliderAndLabel mySlider {"Level"};

//later:
//no need to setup anything else
addAndMakeVisible(mySlider);

the point of @eyalamir , is that your code specifically draws the caption in a given position and with a certain typeface that is hardcoded.
The moment you want to make it more customizable and change the alignment/justification, the typeface, etc., you will end up replicating the code that already exists in Label and that is well-tested.

If your point is “but in my way there will be half child Components” in my editor, that’s true, but before that becomes a performance bottleneck, that must be profiled and I guess most modern computers will display hundreds such Labels with no problem, also taking into account that the graphic primitives that are executed in your version vs. the one with Labels is almost the same

1 Like

Yes and that is exactly what I needed for my project, that is all knobs to be the same style. Sure it can be further worked on, to accommodate a broader use.