Graphics, layout, and animation
Draw with Graphics, build interfaces from nested components, place them with Rectangle, FlexBox, and Grid, colour and restyle them, and animate them.
Level: Beginner to intermediate
Set up the project
The project below draws a small scene: a sky and a ground line painted by MainComponent, and two houses. Each house is a HouseComponent, built from a WallComponent and a RoofComponent, which the "Nested components" section explains. The snippets that follow are code to try inside it.
How to use the snippets below:
- A snippet that starts with
void paint (juce::Graphics& g) overrideis the body of a component'spaint(). Try it inMainComponent::paint(), or inWallComponentorRoofComponentinHouse.hto change how the houses look. - Likewise,
resized()snippets go in a component'sresized(). - Whole classes (such as
MyLookAndFeelorSpinner) go aboveMainComponentinMainComponent.h, or in their own header likeHouse.h. Add an instance as a member ofMainComponent, calladdAndMakeVisible()on it in the constructor, and position it inresized(), as the project does for its houses. - Build and run after each change (see Build and run).
This guide builds on Getting started, which explains the CMake setup and Main.cpp in detail. To skip the typing, download the starter project, GraphicsDemo.zip, and unzip it. It contains exactly the five files below:
GraphicsDemo/
├── CMakeLists.txt
├── Main.cpp
├── MainComponent.h
├── MainComponent.cpp
└── House.hIf you prefer, create the GraphicsDemo folder yourself and copy each file from this page. Either way, CMakeLists.txt expects JUCE to be cloned to ~/JUCE. If yours is elsewhere, change the path on the file (REAL_PATH ...) line. This CMakeLists.txt names the target GraphicsDemo and links the modules this guide needs.
CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "macOS architectures") # must come before project()
project(GRAPHICSDEMO VERSION 0.0.1)
file(REAL_PATH "~/JUCE" JUCE_DIR EXPAND_TILDE) # JUCE cloned to ~/JUCE; or use find_package (JUCE CONFIG REQUIRED)
add_subdirectory(${JUCE_DIR} JUCE)
juce_add_gui_app(GraphicsDemo PRODUCT_NAME "Graphics Demo")
target_sources(GraphicsDemo PRIVATE Main.cpp MainComponent.cpp)
target_compile_definitions(GraphicsDemo PRIVATE
JUCE_WEB_BROWSER=0
JUCE_USE_CURL=0
JUCE_APPLICATION_NAME_STRING="$<TARGET_PROPERTY:GraphicsDemo,JUCE_PRODUCT_NAME>"
JUCE_APPLICATION_VERSION_STRING="$<TARGET_PROPERTY:GraphicsDemo,JUCE_VERSION>")
target_link_libraries(GraphicsDemo
PRIVATE juce::juce_gui_extra
PUBLIC juce::juce_recommended_config_flags
juce::juce_recommended_warning_flags)Main.cpp
#include "MainComponent.h"
class HelloJuceApplication final : public juce::JUCEApplication
{
public:
const juce::String getApplicationName() override { return JUCE_APPLICATION_NAME_STRING; }
const juce::String getApplicationVersion() override { return JUCE_APPLICATION_VERSION_STRING; }
void initialise (const juce::String&) override
{
mainWindow.reset (new MainWindow (getApplicationName()));
}
void shutdown() override { mainWindow = nullptr; } // deletes the window
void systemRequestedQuit() override { quit(); }
class MainWindow final : public juce::DocumentWindow
{
public:
explicit MainWindow (juce::String name)
: DocumentWindow (name, juce::Colours::lightgrey, allButtons)
{
setUsingNativeTitleBar (true);
setContentOwned (new MainComponent(), true);
setResizable (true, true);
centreWithSize (getWidth(), getHeight());
setVisible (true); // required, or the window never appears
}
void closeButtonPressed() override
{
juce::JUCEApplication::getInstance()->systemRequestedQuit();
}
};
private:
std::unique_ptr<MainWindow> mainWindow;
};
START_JUCE_APPLICATION (HelloJuceApplication)This is the Main.cpp from tutorial 1, unchanged, and it stays unchanged for the rest of this guide.
MainComponent.h
#pragma once
#include "House.h"
class MainComponent final : public juce::Component
{
public:
MainComponent();
void paint (juce::Graphics& g) override;
void resized() override;
private:
HouseComponent house, cottage; // two instances of the same component
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};MainComponent.cpp
#include "MainComponent.h"
MainComponent::MainComponent()
{
addAndMakeVisible (house);
addAndMakeVisible (cottage);
setSize (600, 400);
}
void MainComponent::paint (juce::Graphics& g)
{
g.fillAll (juce::Colours::lightblue);
g.setColour (juce::Colours::darkblue);
g.setFont (juce::FontOptions (20.0f));
g.drawText ("Hello, World!", 20, 40, 200, 40, juce::Justification::centred, true);
g.setColour (juce::Colours::green);
g.drawLine (10, 350, 590, 350, 5.0f); // the ground
}
void MainComponent::resized()
{
// Each house is placed by its bounds. Its parts position themselves.
house.setBounds (60, 150, 260, 200);
cottage.setBounds (380, 240, 140, 110);
}House.h
#pragma once
#include <juce_gui_extra/juce_gui_extra.h>
// A wall: a checkerboard that fills whatever bounds it is given
class WallComponent final : public juce::Component
{
public:
void paint (juce::Graphics& g) override
{
g.fillCheckerBoard (getLocalBounds().toFloat(), 30.0f, 10.0f,
juce::Colours::sandybrown, juce::Colours::saddlebrown);
}
};
// A roof: a red triangle that fits its bounds
class RoofComponent final : public juce::Component
{
public:
void paint (juce::Graphics& g) override
{
auto w = (float) getWidth();
auto h = (float) getHeight();
juce::Path roof;
roof.addTriangle (0.0f, h, w, h, w * 0.5f, 0.0f);
g.setColour (juce::Colours::red);
g.fillPath (roof);
}
};
// A house is a roof on top of a wall. It knows nothing about where it will be placed.
class HouseComponent final : public juce::Component
{
public:
HouseComponent()
{
addAndMakeVisible (wall); // add as child and show, in one call
addAndMakeVisible (roof);
}
void resized() override
{
auto area = getLocalBounds();
roof.setBounds (area.removeFromTop (area.getHeight() / 3));
wall.setBounds (area);
}
private:
WallComponent wall;
RoofComponent roof;
};Drawing with Graphics
A component draws itself in paint (juce::Graphics&). JUCE calls it when needed; call repaint() to request one, and never call paint() yourself. Use the Graphics object only inside paint() (or when drawing into an Image). Coordinates are measured from the component's own top-left corner, (0, 0), and may be int or float; fractional positions are anti-aliased.
void paint (juce::Graphics& g) override
{
g.fillAll (juce::Colours::lightblue);
// Text: set the font, then draw into a rectangle
g.setColour (juce::Colours::darkblue);
g.setFont (juce::FontOptions ("Times New Roman", 20.0f, juce::Font::italic));
g.drawText ("Hello, World!", 20, 40, 200, 40, juce::Justification::centred, true);
// Lines, rectangles, ellipses: draw* outlines, fill* fills
g.setColour (juce::Colours::green);
g.drawLine (10, 300, 590, 300, 5.0f); // x1, y1, x2, y2, thickness
juce::Rectangle<float> house (300, 120, 200, 170);
g.fillCheckerBoard (house, 30, 10, juce::Colours::sandybrown, juce::Colours::saddlebrown);
g.setColour (juce::Colours::yellow);
g.drawEllipse (530, 10, 60, 60, 3.0f); // bounding box, not centre
// Anything else: build a Path
juce::Path roof;
roof.addTriangle (300, 110, 500, 110, 400, 70);
g.setColour (juce::Colours::red);
g.fillPath (roof);
}setColour()applies to every subsequent drawing call until changed.- Fonts are described by
juce::FontOptions(name, height, style). The olderFont (name, height, style)constructors are deprecated. A typeface that is not installed silently falls back, which is the usual reason a font looks wrong. - The last argument of
drawText()enables an ellipsis when the text does not fit. For several lines usedrawMultiLineText()ordrawFittedText(). - A line is centred on its coordinates, so a border of thickness
tdrawn on the component's edge losest / 2outside the bounds. Inset the shape by half the thickness, or callsetPaintingIsUnclipped (true)(read the caveats first). - There is no
drawTriangle()ordrawPolygon(). UsePath, which also supports curves, strokes, and transforms. Graphicscan also draw images, gradients, transparency layers, and transformed content.
Nested components
A UI is a tree. Each Component may draw itself, contain children, or both. Each child has exactly one parent at a time.
The project above builds a house this way. WallComponent and RoofComponent each draw one thing, and HouseComponent has them as children and positions them (the full classes are in House.h):
class HouseComponent final : public juce::Component
{
public:
HouseComponent()
{
addAndMakeVisible (wall); // add as child and show, in one call
addAndMakeVisible (roof);
}
void resized() override
{
auto area = getLocalBounds();
roof.setBounds (area.removeFromTop (area.getHeight() / 3));
wall.setBounds (area);
}
private:
WallComponent wall;
RoofComponent roof;
};MainComponent then uses a HouseComponent like any other widget. This is the payoff of nesting: one member, one addAndMakeVisible(), and one setBounds() per house, and the house sizes its own parts to fit. That is why the project has two houses of different sizes:
// MainComponent.h
HouseComponent house, cottage;
// MainComponent.cpp
MainComponent::MainComponent()
{
addAndMakeVisible (house);
addAndMakeVisible (cottage);
setSize (600, 400);
}
void MainComponent::resized()
{
house.setBounds (60, 150, 260, 200);
cottage.setBounds (380, 240, 140, 110);
}Try adding a third house, or giving HouseComponent a door child.
- Child bounds are relative to the parent's top-left corner, so a component can be reused anywhere without knowing where it is.
- A component paints itself first, then its children in the order they were added (change with
toFront(),toBack(), ortoBehind()). OverridepaintOverChildren()to draw on top of children. - Drawing is clipped to the component's bounds. If a child is invisible, check that its bounds were set, normally in the parent's
resized(). - A new component has zero size.
setSize()on the parent triggersresized(), which is where children get theirs.
Point, Line, and Rectangle
These templates (usually <int> or <float>) make geometry readable, in both paint() and resized().
juce::Rectangle<int> area (10, 10, 40, 40); // x, y, w, h
juce::Rectangle<int> fromCorners ({ 10, 10 }, { 50, 50 }); // any two opposite corners
juce::Rectangle<int> sized (10, 10); // width and height only
area.setCentre (juce::Point<int> (100, 80)); // position by centre
area.translate (30, 30); // or: area += juce::Point<int> (30, 30);
bool hit = area.contains (juce::Point<int> (105, 82));
auto overlap = area.getIntersection (fromCorners);
juce::Line<float> a ({ 0, 0 }, { 50, 50 }), b ({ 0, 50 }, { 50, 0 });
juce::Point<float> where;
if (a.intersects (b, where)) // true only if the segments cross
g.fillEllipse (juce::Rectangle<float> (8, 8).withCentre (where));Line::getIntersection() treats the lines as infinite, so it also returns points outside the segments. Convert between int and float versions with toFloat() and toNearestInt(). Path takes Point<float>.
Layout by subdividing rectangles
Prefer subdividing getLocalBounds() over hard-coded coordinates. Each removeFrom*() call cuts a strip off a Rectangle and returns it, leaving the remainder, so the last item can simply take what is left:
void resized() override
{
auto area = getLocalBounds().reduced (8);
header.setBounds (area.removeFromTop (40));
footer.setBounds (area.removeFromBottom (24));
sidebar.setBounds (area.removeFromLeft (160));
// Fill the remainder with equal-height rows
auto rowHeight = area.getHeight() / (int) items.size();
for (auto* item : items)
item->setBounds (area.removeFromTop (rowHeight).reduced (2));
}Reordering the strips, changing a size, or adding an item is a one-line edit. reduced() adds padding, and withTrimmedTop() and its siblings return a trimmed copy without changing the original. When you need to lay items out in a grid of non-uniform cells, subdivide the row first and the row's cells second.
FlexBox and Grid
For layouts that must adapt (wrapping rows, proportional columns), describe items and let JUCE place them. Both are used from resized().
juce::FlexBox fb;
fb.flexDirection = juce::FlexBox::Direction::row;
fb.flexWrap = juce::FlexBox::Wrap::wrap;
fb.justifyContent = juce::FlexBox::JustifyContent::spaceAround;
for (auto* c : controls)
fb.items.add (juce::FlexItem (*c).withMinWidth (80.0f).withMinHeight (40.0f).withFlex (1.0f));
fb.performLayout (getLocalBounds());using Track = juce::Grid::TrackInfo;
using Fr = juce::Grid::Fr;
juce::Grid grid;
grid.templateRows = { Track (Fr (1)), Track (Fr (2)) };
grid.templateColumns = { Track (Fr (1)), Track (Fr (1)), Track (Fr (1)) };
grid.items = { juce::GridItem (a), juce::GridItem (b), juce::GridItem (c) };
grid.performLayout (getLocalBounds());Use FlexBox for one-dimensional flow and Grid for two-dimensional structure. Both are more declarative than manual setBounds() and handle resizing well, but they cost more setup than a removeFrom*() chain for a simple fixed layout. Rectangle-based code is often the shortest option for toolbar-and-panel layouts.
Colours
Colour stores red, green, blue, and alpha.
juce::Colours::orange // named constants (HTML colours)
juce::Colours::findColourForName ("DarkRed", juce::Colours::black); // case-insensitive, no inner spaces
juce::Colour (255, 0, 0) // 8-bit RGB, opaque
juce::Colour (0xffff0000) // ARGB hex: always include the alpha
juce::Colour::fromFloatRGBA (1.0f, 0.0f, 0.0f, 1.0f)
juce::Colour::fromHSV (0.0f, 0.5f, 0.7f, 1.0f) // hue, saturation, brightness, alphaDerive palettes rather than picking each colour by hand: brighter(), darker(), withAlpha(), interpolatedWith (other, 0.5f), contrasting (amount), Colour::contrasting (a, b) for a colour clear of two backgrounds, and getHue() to keep a hue while varying brightness. Colour (0xff...) without the leading ff is fully transparent.
Built-in widgets expose named colour IDs in enums such as Label::ColourIds and Slider::ColourIds. Set them per component or per look-and-feel:
label.setColour (juce::Label::textColourId, juce::Colours::black); // one component
getLookAndFeel().setColour (juce::Slider::thumbColourId, juce::Colours::red); // all in scopeLook and feel
LookAndFeel decides how every built-in widget draws. Subclass one of the built-in styles (LookAndFeel_V4 is the current default) and override only the drawing functions you need:
class MyLookAndFeel final : public juce::LookAndFeel_V4
{
public:
MyLookAndFeel()
{
setColour (juce::Slider::thumbColourId, juce::Colours::red);
}
void drawRotarySlider (juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float startAngle, float endAngle,
juce::Slider&) override
{
auto bounds = juce::Rectangle<int> (x, y, width, height).toFloat().reduced (4.0f);
auto radius = juce::jmin (bounds.getWidth(), bounds.getHeight()) / 2.0f;
auto angle = startAngle + sliderPos * (endAngle - startAngle);
g.setColour (juce::Colours::darkgrey);
g.fillEllipse (bounds.withSizeKeepingCentre (radius * 2, radius * 2));
juce::Path pointer;
pointer.addRectangle (-2.0f, -radius, 4.0f, radius * 0.6f);
g.setColour (juce::Colours::white);
g.fillPath (pointer, juce::AffineTransform::rotation (angle)
.translated (bounds.getCentre()));
}
};Own the instance as a member and register it with setLookAndFeel (&lf) on a component (it then applies to that component's children). Call setLookAndFeel (nullptr) in the destructor, before the member is destroyed. Buttons work the same way through drawButtonBackground() and drawButtonText(). For app-wide styling use juce::LookAndFeel::setDefaultLookAndFeel().
Animation
Derive from juce::AnimatedAppComponent for a component that repaints on a timer. Implement update() to advance state and paint() to draw it:
class Spinner final : public juce::AnimatedAppComponent
{
public:
Spinner() { setFramesPerSecond (60); setSize (200, 200); }
void update() override
{
// Prefer elapsed time to frame counts so speed is independent of frame rate
angle += 0.002f * (float) getMillisecondsSinceLastUpdate();
}
void paint (juce::Graphics& g) override
{
auto c = getLocalBounds().getCentre().toFloat();
g.fillAll (juce::Colours::black);
g.setColour (juce::Colours::orange);
g.fillEllipse (juce::Rectangle<float> (20, 20).withCentre (
c + juce::Point<float> (std::sin (angle), std::cos (angle)) * 60.0f));
}
private:
float angle = 0.0f;
};The same idea animates a Path (rebuild or transform it each frame) or a composite shape: compute positions from time, then draw. Use getFrameCounter() for frame-based effects. For animation inside an existing component, Timer, VBlankAttachment, or ComponentAnimator are the alternatives; see examples/GUI/AnimationAppDemo.h.
Build and run
With all the files in place, configure and build from the project folder:
cmake -B build
cmake --build buildThe build puts the finished app in build/GraphicsDemo_artefacts/. With CMake's default generator (Makefiles on macOS and Linux):
| Platform | Run it with |
|---|---|
| macOS | open "build/GraphicsDemo_artefacts/Graphics Demo.app" |
| Linux | ./build/GraphicsDemo_artefacts/Graphics\ Demo |
The Xcode generator is multi-config and adds a configuration folder, for example build/GraphicsDemo_artefacts/Debug/Graphics Demo.app; build with cmake --build build --config Debug.
"The application cannot be opened because its executable is missing"? CMake creates the empty
.appbundle at the start of the build and only fills in the executable when compiling and linking succeed. Ifcmake --build buildreported errors, fix them and build again, then re-runopen. Check that the last lines of the build output sayBuilt target GraphicsDemo.
"use of undeclared identifier 'juce'"? CMake projects have no
JuceHeader.h, so every source file must include the module headers it uses.MainComponent.hincludesHouse.h, which includes them, andMain.cppincludesMainComponent.h. Without those includes the compiler does not know whatjuce::,std::, orSTART_JUCE_APPLICATIONmean.
Sources
Condensed from the JUCE tutorials The Graphics class, Parent and child components, The Point, Line, and Rectangle classes, Advanced GUI layout techniques, Responsive GUI layouts using FlexBox and Grid, Colours in JUCE, Customise the look and feel of your app, and Animating geometry, Copyright (c) Raw Material Software Limited, ISC licence. See the attribution notice.