Good practice to handle look & feel of custom UI components


#1

Hi,

while working on the user interface of my current application, I wonder what’s the best practice to handle the look and feel of custom UI components.
For an example, I use some more or less realistic designed buttons, looking just like some LED-lightened on/off buttons on traditional analog hardware.

My first approach was to write a class named DrawableToggleButton, inheriting from Button, that implements some kind of toggle button with two drawable pointers passed to the constructor, one for the “on”-state image and one for the “off”-state image. Now, I create multiple subclasses, inheriting from DrawableToggleButton, e.g. YellowLEDToggleButton and GreenLEDToggleButton, which have a unique set of drawables for the on & off state as private members and simply pass those pointers to the base class at construction time.
This works but doesn’t seem to fit into the juce concept of look & feel.

Now my next idea was to create a custom look & feel subclass for every type of button, that owns the two drawables for the on & off state as private members and overrides the drawToggleButton member function. So I’d just implement e.g. YellowLEDToggleButtonLookAndFeel, YellowLEDToggleButtonLookAndFeel…, implement the usual toggle buttons and assign those special look and feels to those buttons.

But this seems to have a downside when it comes to more complex types of elements, such as rotary sliders with a textbox for their value. If I had multiple knob-types with different knob designs on my UI, but their textbox should all use the same font as the rest of the text elements on the UI uses, a custom “global” look and feel would seem to be the more suitable approach. But how should I handle the drawing of the individual knob designs here?
What do you think of an approach somehow according to this idea:

class CustomSlider : public Slider {

public:

enum {
    flatKnob,
    realisticKnob,
    evenMoreRealisticKnob
} KnobStyle;

CustomSlider (KnobStyle ks, TextEntryBoxPosition tbp) : Slider (SliderStyle::Rotary, tbp), sliderKnobStyle (ks) {};
~CustomSlider() {};

void paint (Graphics &g) override {
    lookAndFeel->drawCustomRotary(sliderKnobStyle, ... all the other arguments)
};

private:

const KnobStyle sliderKnobStyle;
};



class CustomLookAndFeel : public LookAndFeel_V4 {

public:

// override some existing member functions here

void drawCustomRotary (CustomSlider::KnobStyle sliderKnobStye, ... all the other arguments) {
    
    Drawable *knobToDraw;
    
    switch (sliderKnobStyle) {
        case flatKnob:
            knobToDraw = flatKnobDrawable.get();
            break;
            
        case realisticKnob:
            knobToDraw = realisticKnobDrawable.get();
            break;
            
        case evenMoreRealisticKnob:
            knobToDraw = evenMoreRealisticKnobDrawable.get();
            break;
    }
    
    // rotate the drawable and draw it...
}

private:

ScopedPointer<Drawable> flatKnobDrawable = Drawable::createFromImageData(BinaryData::flatKnob_svg, BinaryData::flatKnob_svgSize);
ScopedPointer<Drawable> realisticKnobDrawable = Drawable::createFromImageData(BinaryData::realisticKnob_svg, BinaryData::realisticKnob_svgSize);
ScopedPointer<Drawable> evenMoreRealisticKnobDrawable = Drawable::createFromImageData(BinaryData::evenMoreRealisticKnob_svg, BinaryData::evenMoreRealisticKnob_svgSize);
};

Please note that I wrote the code above just to explain my idea, it’s not meant to be error-free :wink:

So, how do you handle similar cases? What is the “JUCE-way” to do this clean & efficiently and to generate code that could be easially re-used for future projects?

Looking forward to your ideas!


#2

Instead of inheriting direct from Slider, use a mixin based programming

template <class BaseSlider> class MyCustomSlider : public BaseSlider
{
public:
  template <typename ...Args>
  MyCustomSlider (Args &&...args)
    : BaseSlider(std::forward<Args>(args)...)
  {
  }
virtual void paint(Graphics &g)
{
// custom draw here
}
};

so you can subclass different Slider class

my 2 cents


#3

I never subclassed from Slider at all. It’s a class that’s already far too complex.

I’m working with JUCE since almost 10 years now and the approach I settled with is to always do the drawing only in the LookAndFeel classes.

So, if you need your Sliders to have different looks, assign different LookAndFeels to them.
If those LookAndFeels have some parts in common, promote those parts to a base LookAndFeel class that’s used for inheriting all the other specific LookAndFeels that you need.

In your specific case, you could implement a base SliderLookAndFeel class which in reality only overrides the methods for drawing the text boxes.

Then, derive from that SliderLookAndFeel the specific FlatKnobLookAndFeel, RealisticKnobLookAndFeel, etc. where you will override the methods that actually draw the rotary slider as you desire.

Last point is to assign to each Slider in your UI the appropriate FlatKnobLookAndFeel, RealisticKnobLookAndFeel, etc.


#4

Thank you for your helpful approaches. I think yfede’s approach seems to be the most straightforward way of doing things - I somehow never thought of subclassing my basic LookAndFeel for the special sliders, although this really seems to be the easiest way to handle those everything :wink:


#5

+1 to @yfede’s comment.

I have another component, a LevelMeter class on github, where I dived into an architecture with a separated LookAndFeel, and how to attach it to the existing juce::LookAndFeel classes.
It might serve as inspiration, and if anybody wants to comment, I am looking forward for criticisms. I am always happy to improve my code and myself.


#6

My main grip with Juce LnF system is that it is not ref counted.
So if you assign a custom LnF to a single widget, the widget (or someone else) have to take care of the LnF deletion
So either custom widget or store the LnF in the class that includes this custom widget, which make the whole LnF for a single widget system painful.


#7

It’s a good point.

at the moment, I gather all LookAndFeels in a singleton that owns them, something like a “look and feel library”, where they can be obtained by a name associated with them upon registration in that singleton.

That way, I also avoid having to create the LookAndFeel multiple times for multiple instances of the plug-ins