Tutorial: Customise the look and feel of your app

Customise the drawing of fundamental widgets in your application. Make a custom skin for your application by drawing your own buttons, sliders, and other components.

Level: Beginner

Platforms: Windows, macOS, Linux, iOS, Android

Classes: LookAndFeel, Slider, Button, Path, AffineTransform

Getting started

Download the demo project for this tutorial here: PIP | ZIP. Unzip the project and open the first header file in the Projucer.

If you need help with this step, see Tutorial: Projucer Part 1: Getting started with the Projucer.

The demo project

The demo project creates a GUI with two buttons and two rotary sliders using the standard JUCE look-and-feel:

Standard look-and-feel buttons and sliders

The LookAndFeel class is fundamental to creating customised GUIs in JUCE. Using the LookAndFeel class you can perform simple customisations such as changing the default colours of certain components. But you can also customise the drawing of many types of component. For example, this allows you to create buttons and sliders with a custom appearance.

Customising colours

When a LookAndFeel object is applied to a component, it is applied to that component and its child components (see Tutorial: Parent and child components) unless the child components have specifically had a different look-and-feel assigned.

One thing that you can do with the look-and-feel system is to override specific colours for elements of the standard JUCE components (see Tutorial: Colours in JUCE.) For example, if you add the following line to the MainContentComponent constructor, then both dials will be red:

getLookAndFeel().setColour (Slider::thumbColourId, Colours::red);

This should look something like the following screenshot:

Overriding look-and-feel colours

To set the two dials differently we could make a new LookAndFeel instance and apply that to only one of the dials. First add a LookAndFeel_V4 object as a member [1] (this is the class that implements the default JUCE look-and-feel).

private:
LookAndFeel_V4 otherLookAndFeel; // [1]
Slider dial1;
Slider dial2;
TextButton button1;
TextButton button2;

Then change the line of code in the constructor, that we just added, to this:

Let's use this look-and-feel only for the first dial. Add this line of code to the MainContentComponent constructor:

dial1.setLookAndFeel (&otherLookAndFeel);

This should now create a UI like the following screenshot:

Using diffrent look-and-feel objects for different components

Of course, in this simple example this approach offers no benefits compared to setting the Slider::thumbColourId colour on the slider objects directly. But your app may use multiple sliders for different purposes where you want sliders for one purpose to use one set of colours and sliders for other purposes to use different sets of colours. This approach allows you to change these colours globally as long as each slider is assigned the appropriate look-and-feel for its type.

The benefits of this approach are clearer once we start to customise the actual drawing code. In particular, we need to create a custom look-and-feel class.

Custom look-and-feel

To customise the drawing of certain components we need to create a new class that inherits from the LookAndFeel class. If you inherit directly form the LookAndFeel class itself then you'll need to implement all of the pure virtual functions. It's much more practical to inherit from one of the classes that already has all of these functions defined. Then you need override only the ones you need. Let's create a simple custom look-and-feel that has only this one colour change defined compared to the default look-and-feel.

First, remove this line from the constructor, which we added earlier:

Now, add our new class, which inherits from the LookAndFeel_V4 class, before the MainContentComponent class:

class OtherLookAndFeel : public LookAndFeel_V4
{
public:
OtherLookAndFeel()
{
}
};

Before we run this code, change the class name of our otherLookAndFeel member to OtherLookAndFeel [2]:

private:
OtherLookAndFeel otherLookAndFeel; // [2]
Slider dial1;
Slider dial2;
TextButton button1;
TextButton button2;

Build and run the application and the result should appear identical to the previous screenshot.

Customising drawing

There are many functions in the LookAndFeel class for many different types of components. The functions that are designated for a specific component type are easy to find as these are all declared within a nested class named LookAndFeelMethods within the relevant component class.

Slider customisation

For example, take a look at the Slider::LookAndFeelMethods within the JUCE API documentation. In this list you will notice a function named Slider::LookAndFeelMethods::drawRotarySlider().

Let's override this in our OtherLookAndFeel class. Add the declaration to the class:

void drawRotarySlider (Graphics& g, int x, int y, int width, int height, float sliderPos,
const float rotaryStartAngle, const float rotaryEndAngle, Slider& slider) override
{
//...
}

Here you can see that we are passed the following data:

  • g: The Graphics context.
  • x: The x coordinate of the top-left of the rectangle within which we should draw our rotary slider.
  • y: The y coordinate of the top-left of the rectangle within which we should draw our rotary slider.
  • width: The width of the rectangle within which we should draw our rotary slider.
  • height: The height of the rectangle within which we should draw our rotary slider.
  • sliderPos: The position of the slider as a proportion in the range 0..1 (this is independent of the slider's actual range of values).
  • rotaryStartAngle: The start angle of the dial rotation (in radians).
  • rotaryEndAngle: The end angle of the dial rotation (in radians).
  • slider: The Slider object itself.
Note
The x, y, width, and height arguments take into account the size and position of any text box that the slider may be using. This is why we can just access the slider position and size and use those values.

Now let's write the function body such that it draws a simple dial that is just a filled circle with a line representing the pointer of the dial. First, we will need some temporary variables to help with our calculations based on the values we have been passed:

auto radius = jmin (width / 2, height / 2) - 4.0f;
auto centreX = x + width * 0.5f;
auto centreY = y + height * 0.5f;
auto rx = centreX - radius;
auto ry = centreY - radius;
auto rw = radius * 2.0f;
auto angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle);
Note
You can see that the final angle variable contains the angle at which the dial should point.

Now let's add code to fill in the colour of the dial and draw an outline:

// fill
g.fillEllipse (rx, ry, rw, rw);
// outline
g.drawEllipse (rx, ry, rw, rw, 1.0f);

To draw the pointer itself, first we'll use a Path object that we will translate and rotate into position by the required angle:

Path p;
auto pointerLength = radius * 0.33f;
auto pointerThickness = 2.0f;
p.addRectangle (-pointerThickness * 0.5f, -radius, pointerThickness, pointerLength);
p.applyTransform (AffineTransform::rotation (angle).translated (centreX, centreY));

Then we fill this path to draw the pointer:

// pointer
g.fillPath (p);
Note
The completed code for this section can be found in the LookAndFeelCustomisationTutorial_02.h file of the demo project for this tutorial.
Exercise
Modify the drawing of the pointer. You could try different lengths, a slightly thicker but rounded rectangle, or draw an arrow.

This shows you only one simple customisation of one of the Slider look-and-feel methods. But the principle applies to the other methods. Perhaps the best approach for creating other customisations is to look at the existing implementation in the LookAndFeel_V4 or LookAndFeel_V3 classes and use this as a basis for your own code.

Note
The LookAndFeel_V4 class inherits from the LookAndFeel_V3 class and some methods are not redefined in the LookAndFeel_V4 class.

Button customisation

Let's look at customising the buttons. First, let's set our OtherLookAndFeel class as the look-and-feel for our whole MainContentComponent by using this line in its constructor:

setLookAndFeel (&otherLookAndFeel);

Let's also make sure that the LookAndFeel object is not used on shutdown by the MainContentComponent anymore by supplying this line in its destructor:

setLookAndFeel (nullptr);

This will, of course, mean that both of our dials take on the appearance we customised in the previous section. Now let's add the Button::LookAndFeelMethods::drawButtonBackground() function declaration:

void drawButtonBackground (Graphics& g, Button& button, const Colour& backgroundColour,
bool isMouseOverButton, bool isButtonDown) override
{
//...
}

Here, we are passed the following data:

  • g: The Graphics context.
  • button: The Button object itself.
  • backgroundColour: The base background colour that should be used (which will have been chosen from the LookAndFeel colours based on the toggle state of the button).
  • isMouseOverButton: Whether the mouse pointer is within the bounds of the button.
  • isButtonDown: Whether the mouse button is down.

Now, let's add the function body to make a really simple button background that simply fills the button rectangle with the background colour:

auto buttonArea = button.getLocalBounds();
g.setColour (backgroundColour);
g.fillRect (buttonArea);

If you build and run this, it should look similar to the following screenshot:

Simple button

If you interact with this, you will notice that the buttons do not respond visually to mouse pointer interaction. Let's implement a simple shadow effect. Change the drawButtonBackground() function to this:

auto buttonArea = button.getLocalBounds();
auto edge = 4;
buttonArea.removeFromLeft (edge);
buttonArea.removeFromTop (edge);
// shadow
g.setColour (Colours::darkgrey.withAlpha (0.5f));
g.fillRect (buttonArea);
auto offset = isButtonDown ? -edge / 2 : -edge;
buttonArea.translate (offset, offset);
g.setColour (backgroundColour);
g.fillRect (buttonArea);

The button will now appear to move as we click the button. Unfortunately, the text stays static, so we need to override the Button::LookAndFeelMethods::drawButtonBackground() function to make this more believable. To write this function we'll start with a copy of the code from the LookAndFeel_V2 class and add it to our OtherLookAndFeel class:

void drawButtonText (Graphics& g, TextButton& button, bool isMouseOverButton, bool isButtonDown) override
{
auto font = getTextButtonFont (button, button.getHeight());
g.setFont (font);
.withMultipliedAlpha (button.isEnabled() ? 1.0f : 0.5f));
auto yIndent = jmin (4, button.proportionOfHeight (0.3f));
auto cornerSize = jmin (button.getHeight(), button.getWidth()) / 2;
auto fontHeight = roundToInt (font.getHeight() * 0.6f);
auto leftIndent = jmin (fontHeight, 2 + cornerSize / (button.isConnectedOnLeft() ? 4 : 2));
auto rightIndent = jmin (fontHeight, 2 + cornerSize / (button.isConnectedOnRight() ? 4 : 2));
auto textWidth = button.getWidth() - leftIndent - rightIndent;
if (textWidth > 0)
leftIndent, yIndent, textWidth, button.getHeight() - yIndent * 2,
}

We just need to change the offset at which the text is drawn to match the apparent movement in our drawButtonBackground() function. We need to change only the last few lines:

//...
auto textWidth = button.getWidth() - leftIndent - rightIndent;
auto edge = 4;
auto offset = isButtonDown ? edge / 2 : 0;
if (textWidth > 0)
leftIndent + offset, yIndent + offset, textWidth, button.getHeight() - yIndent * 2 - edge,
}

Build and run this and it should look similar to the following screenshot.

Buttons with shadows (Button 1 is shown "clicked")
Note
The completed code for this section can be found in the LookAndFeelCustomisationTutorial_03.h file of the demo project for this tutorial.
Exercise
Add some changes to the drawing of the button to respond to the mouse pointer being over the button. For example you could adjust the background colour slightly, change the shadow colour, or subtly change the rectangle sizes or positions.

Summary

In this tutorial we have introduced the concept of customising the look-and-feel of JUCE components using the LookAndFeel class. In particular you should now be able to:

  • Customise colours in the default look-and-feel.
  • Create a new look-and-feel class.
  • Customise slider and button drawing code.
  • Find the look-and-feel methods for other components so you can customise any JUCE component.

See also

Component::proportionOfHeight
int proportionOfHeight(float proportion) const noexcept
Returns a proportion of the component's height.
Component::setLookAndFeel
void setLookAndFeel(LookAndFeel *newLookAndFeel)
Sets the look and feel to use for this component.
TextButton
A button that uses the standard lozenge-shaped background with a line of text on it.
Definition: juce_TextButton.h:43
Path
A path is a sequence of lines and curves that may either form a closed shape or be open-ended.
Definition: juce_Path.h:69
Component::getWidth
int getWidth() const noexcept
Returns the component's width in pixels.
Definition: juce_Component.h:275
Path::applyTransform
void applyTransform(const AffineTransform &transform) noexcept
Applies a 2D transform to all the vertices in the path.
Graphics::fillPath
void fillPath(const Path &path) const
Fills a path using the currently selected colour or brush.
jmin
JUCE_CONSTEXPR Type jmin(Type a, Type b)
Returns the smaller of two values.
Definition: juce_MathsFunctions.h:110
Graphics::fillRect
void fillRect(Rectangle< int > rectangle) const
Fills a rectangle with the current colour or brush.
Graphics::drawEllipse
void drawEllipse(float x, float y, float width, float height, float lineThickness) const
Draws an elliptical stroke using the current colour or brush.
Button::getButtonText
const String & getButtonText() const
Returns the text displayed in the button.
Definition: juce_Button.h:72
Button::isConnectedOnRight
bool isConnectedOnRight() const noexcept
Indicates whether the button adjoins another one on its right edge.
Definition: juce_Button.h:335
Button
A base class for buttons.
Definition: juce_Button.h:46
Colour
Represents a colour, also including a transparency value.
Definition: juce_Colour.h:42
Component::getHeight
int getHeight() const noexcept
Returns the component's height in pixels.
Definition: juce_Component.h:278
Button::isConnectedOnLeft
bool isConnectedOnLeft() const noexcept
Indicates whether the button adjoins another one on its left edge.
Definition: juce_Button.h:330
Graphics
A graphics context, used for drawing a component or image.
Definition: juce_GraphicsContext.h:49
Component::isEnabled
bool isEnabled() const noexcept
Returns true if the component (and all its parents) are enabled.
LookAndFeel::setColour
void setColour(int colourId, Colour colour) noexcept
Registers a colour to be used for a particular purpose.
Justification::centred
Indicates that the item should be centred vertically and horizontally.
Definition: juce_Justification.h:143
Graphics::setFont
void setFont(const Font &newFont)
Changes the font to use for subsequent text-drawing functions.
Colours::yellow
static const JUCE_API Colour yellow
ARGB = 0xffffff00.
Definition: juce_Colours.h:48
TextButton::textColourOffId
The colour to use for the button's text when the button's toggle state is "off".
Definition: juce_TextButton.h:84
Graphics::drawFittedText
void drawFittedText(const String &text, int x, int y, int width, int height, Justification justificationFlags, int maximumNumberOfLines, float minimumHorizontalScale=0.0f) const
Tries to draw a text string inside a given space.
AffineTransform::rotation
static AffineTransform rotation(float angleInRadians) noexcept
Returns a new transform which is a rotation about (0, 0).
Graphics::fillEllipse
void fillEllipse(float x, float y, float width, float height) const
Fills an ellipse with the current colour or brush.
Slider
A slider control for changing a value.
Definition: juce_Slider.h:57
TextButton::textColourOnId
The colour to use for the button's text.when the button's toggle state is "on".
Definition: juce_TextButton.h:85
Button::getToggleState
bool getToggleState() const noexcept
Returns true if the button is 'on'.
Definition: juce_Button.h:112
Rectangle::removeFromLeft
Rectangle removeFromLeft(ValueType amountToRemove) noexcept
Removes a strip from the left-hand edge of this rectangle, reducing this rectangle by the specified a...
Definition: juce_Rectangle.h:503
Graphics::setColour
void setColour(Colour newColour)
Changes the current drawing colour.
Colours::orange
static const JUCE_API Colour orange
Definition: juce_Colours.h:48
Slider::thumbColourId
The colour to draw the thumb with.
Definition: juce_Slider.h:869
Colours::darkgrey
static const JUCE_API Colour darkgrey
Definition: juce_Colours.h:48
Rectangle::removeFromTop
Rectangle removeFromTop(ValueType amountToRemove) noexcept
Removes a strip from the top of this rectangle, reducing this rectangle by the specified amount and r...
Definition: juce_Rectangle.h:487
LookAndFeel_V4
The latest JUCE look-and-feel style, as introduced in 2017.
Definition: juce_LookAndFeel_V4.h:41
roundToInt
int roundToInt(const FloatType value) noexcept
Fast floating-point-to-integer conversion.
Definition: juce_MathsFunctions.h:475
Path::addRectangle
void addRectangle(float x, float y, float width, float height)
Adds a rectangle to the path.
Colours::red
static const JUCE_API Colour red
ARGB = 0xffff0000.
Definition: juce_Colours.h:48
Component::findColour
Colour findColour(int colourID, bool inheritFromParent=false) const
Looks for a colour that has been registered with the given colour ID number.
Component::getLocalBounds
Rectangle< int > getLocalBounds() const noexcept
Returns the component's bounds, relative to its own origin.