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
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!