Tutorial: Look-and-feel customisation

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, Mac OS X, Linux

Classes: LookAndFeel, Slider, Button, Path, AffineTransform

Getting started

Download the demo project for this tutorial here: tutorial_look_and_feel_customisation.zip. Unzip the project and open it in your IDE.

If you need help with this step, see Tutorial: 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:

tutorial_look_and_feel_customisation_screenshot1.png
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: The Component class, parents, and children) 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:

tutorial_look_and_feel_customisation_screenshot2.png
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:

tutorial_look_and_feel_customisation_screenshot3.png
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:

const float radius = jmin (width / 2, height / 2) - 4.0f;
const float centreX = x + width * 0.5f;
const float centreY = y + height * 0.5f;
const float rx = centreX - radius;
const float ry = centreY - radius;
const float rw = radius * 2.0f;
const float 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;
const float pointerLength = radius * 0.33f;
const float 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 MainComponent_02.h file within the Source directory 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_V2 classes and use this as a basis for your own code.

Note
The LookAndFeel_V4 class inherits from the LookAndFeel_V2 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);

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:

Rectangle<int> buttonArea = button.getLocalBounds();
g.setColour (backgroundColour);
g.fillRect (buttonArea);

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

tutorial_look_and_feel_customisation_screenshot4.png
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:

Rectangle<int> buttonArea = button.getLocalBounds();
const int edge = 4;
buttonArea.removeFromLeft (edge);
buttonArea.removeFromTop (edge);
// shadow
g.setColour (Colours::darkgrey.withAlpha (0.5f));
g.fillRect (buttonArea);
const int 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
{
Font font (getTextButtonFont (button, button.getHeight()));
g.setFont (font);
.withMultipliedAlpha (button.isEnabled() ? 1.0f : 0.5f));
const int yIndent = jmin (4, button.proportionOfHeight (0.3f));
const int cornerSize = jmin (button.getHeight(), button.getWidth()) / 2;
const int fontHeight = roundToInt (font.getHeight() * 0.6f);
const int leftIndent = jmin (fontHeight, 2 + cornerSize / (button.isConnectedOnLeft() ? 4 : 2));
const int rightIndent = jmin (fontHeight, 2 + cornerSize / (button.isConnectedOnRight() ? 4 : 2));
const int 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:

//...
const int textWidth = button.getWidth() - leftIndent - rightIndent;
const int edge = 4;
const int 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.

tutorial_look_and_feel_customisation_screenshot5.png
Buttons with shadows (Button 1 is shown "clicked")
Note
The completed code for this section can be found in the MainComponent_03.h file within the Source directory 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