GetDunne Wiki

Notes from the desk of Shane Dunne, software development consultant

User Tools

Site Tools


juce_gui_basics

This is an old revision of the document!


JUCE GUI basics

The JUCE Tutorials at https://juce.com/learn/tutorials are the best place to get started with JUCE programming. Unfortunately, many of the tutorials tend to jump straight into details, and there is very little in the way of high-level overview.

JUCE is a C++ framework, not a library

First, it's critical to understand that JUCE is a C++ framework, which is very different from a traditional C function library or “API”. There are two big differences:

  1. With a library, you write the program, and call the library's functions to get things done. With a framework, the overall structure of the program is provided by the framework itself, and you add your own code to do the application-specific things.
  2. Just about all the app-specific code you write will be in the form of new C++ classes, derived from one or more of the framework's classes.

For any non-trivial code you write, you can of course create your own classes and even static functions. The main way you connect these to the JUCE framework is by creating your own JUCE-derived classes, but there are also two other mechanisms: listeners and callbacks. All of these can be explained with reference to JUCE's Button class.

Subclassing JUCE GUI classes

You might choose to subclass juce::Button if you want to create your own type of button widget with unique appearance and behavior. To respond to the button being clicked, you would overload the clicked() member function. To change its appearance, you would overload paint(), and so on.

Creating a derived class like this allows you to override as much of the standard class's functionality as you like, and inherit the rest.

Listeners

Subclassing a JUCE GUI class may seem like overkill when you simply want to specify how your program should respond to input-events, like a button being clicked or a slider/knob being dragged. In these cases, you can usually choose to subclass a related Listener class.

For example, you could declare your button object as an instance of juce::Button, write a subclass of the (far simpler) juce::Button::Listener class, then attach an instance of your listener class to the button object. Because C++ supports multiple inheritance, the most common approach is to have your containing object (GUI window or part thereof) itself inherit from juce::Button::Listener, and simply pass its this pointer to the button's addListener() method.

In cases like this, the callback approach (below) is actually more commonly used. The real power of JUCE Listeners is that a given object can have multiple listeners (aka Notifier-Observer pattern). This is very helpful when two or more distinct parts of your GUI need to update in response to the same event.

Callbacks (std::function and Lambdas)

Writing your own derived class can be overkill when you only need to override one or two functions. For these cases, many of JUCE's GUI-related classes allow you to instead create a callback–a small chunk of your own code which the JUCE class is already set up to call at the appropriate moments. This is almost always done with a combination of two so-called “modern C++” techniques: std::function and lambdas.

juce::Button includes a std::function member (basically a pointer to a function) called onClick, and will call the associated function (if there is one, i.e., if the pointer is not null) when the button is clicked.

Connecting one of a class's member functions (aka methods) directly to a std::function pointer is miserably difficult, but recent C++ revisions provide a much more convenient method called lambda functions, aka “lambdas”. A lambda function is just a block of code (often just a single line) which is “wrapped” in such a way that you can treat it like a function, but you don't have to declare it separately, give it a name, etc. Instead, you can write like this:

myButton.onClick = [this]()
{
    SomeMemberFunction();
    someMemberVariable += 1;
    someOtherGuiWidgetMember.setEnabled(someMemberVariable > 0);
};

Behind the scenes, the C++ compiler creates what amounts to a C function containing the code inside the curly brackets, and does some magic to ensure that any variables you put between the square brackets (the so-called capture list) are appropriately bound to the code, so when it executes, it has access to them. By capturing this (which is a pointer to the object within whose scope the lambda is defined), you ensure that your lambda works just like a member function, with full access to other member functions (methods) and data.

The above example illustrates a typical GUI usage, where clicking on a button may cause some other GUI widget to be enabled/disabled, according to some conditions among the data members.

See e.g. https://en.cppreference.com/w/cpp/language/lambda for details, but don't try to understand it all at once.

GUIs in JUCE: Components, Layout and more

Now that you understand all the main mechanisms by which JUCE's prebuilt framework code connects to your custom code, we can start talking about how GUIs are built in JUCE.

juce::Component and "custom controls"

juce::Component is the base class for all GUI objects in JUCE. Everything you see in a JUCE GUI is an instance of some subclass of Component. The framework provides what I'd all a “minimal” collection of Component subclasses such as juce::Button, juce::Slider, etc. Perhaps “miserably meagre” might be a better term. If you want to create nice-looking GUIs for a JUCE program, you're going to end up writing a lot of classes for your own “custom controls” or “widgets”.

Whenever possible, you should try to derive your custom GUI component classes from a logically-equivalent JUCE class. For example, you could subclass juce::Slider to create your own custom “knob” class. This not only allows you to inherit a great deal of useful functionality; it will also save you from ripping your hair out by the roots when you need to connect your custom widgets to your plug-in's parameters.

It's almost beyond belief that JUCE—a framework used almost exclusively for audio plug-ins—does not already include a knob class. When working through the JUCE tutorials, you have to get all the way to the 42nd example before they show you how to make a real knob, and it's painful, because they have you do it using another complex aspect of JUCE called Look-and-Feel classes.

Look and Feel

JUCE provides a “Look and Feel” (LAF) system, which is ostensibly there to simplify the process of “skinning”, i.e., altering multiple aspects of a program's GUI in a systematic way. I find it overly complicated, and the idea of actually using a custom LAF class to re-skin a whole GUI is daunting. In practice, it tends to be used in much more limited ways.

The basic idea is that the JUCE GUI-widget classes don't actually contain code for “look and feel” aspects like drawing the actual graphics; instead they call appropriate routines from an attached juce::LookAndFeel-derived class. To change the look and feel, you simply call the widget object's setLookAndFeel() method, so it points to a different juce::LookAndFeel object.

Graphics

juce_gui_basics.1612802963.txt.gz · Last modified: 2021/02/08 16:49 by shane