GetDunne Wiki

Notes from the desk of Shane Dunne, software development consultant

User Tools

Site Tools


juce_gui_basics

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. It's usually more work than the other two approaches.

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

The JUCE framework is huge, and although some aspects of it may seem less clean, well-designed, or complete than others, one of its great strengths is its graphics support.

JUCE's graphics API, which is rich and fully cross-platform, is accessed through a large class called juce::Graphics. You never need to instantiate this class; the framework always does it for you. All of your “drawing” your code will go into various juce::Component-derived classes that you write, specifically their paint() method, whose one argument is a reference to a juce::Graphics object.

Layout is basically top-down

Every juce::Component-derived class also implements a resized() function, which gets called automatically whenever the Component's bounds (top-left X/Y coordinates, width and height) are changed. Any Component may have “child” Components, which are normally drawn entirely inside its bounds. One of the main jobs of each Component's resized() method is to compute where each child component should go and call its setBounds() member.

Thus, JUCE implements a top-down layout approach:

  • The top-level (outermost) Component receives a setBounds() call (usually from the JUCE framework itself).
  • You would not normally override setBounds() yourself. The default implementation does a bit of internal housekeeping, and calls resized() to do the real work.
  • You DO override setBounds(), and the first thing you do there is call getLocalBounds() to get your component's assigned bounds (in the form of a juce::Rectangle). You decide how to divide up this rectangular space, and compute smaller juce::Rectangles, which you pass to your child components' setBounds() methods.

A few important aspects of layout in JUCE don't quite work this way:

  • Any time you create a juce::Component subclass for a top-level window (e.g., the main GUI window for a plug-in), you put a call to setSize() right at the bottom of your class's constructor. This fixes the width and height of the component, so the framework need only set its position on the screen.

However, as far as I know, there is no generic way to do bottom-up layout in a JUCE program. If a component needs to know the “natural” or “default” dimensions of its child components (in order to compute their positions based on some kind of layout algorithm), you have to implement that yourself; juce::Component does not provide specific methods you can override for this purpose.

JUCE does provide quite a substantial set of layout-related classes. See https://docs.juce.com/master/group__juce__gui__basics-layout.html, but be prepared to do a lot of reading, head-scratching, and experimentation, because none of these components is very well documented.

(The truth is, no part of JUCE is truly well-documented. There's quite a bit of documentation, but a hell of a lot more code. I would estimate that it is less than 5% adequately documented, perhaps closer to 1%.)

Linking GUI components to variables in your code

JUCE does not provide simple mechanisms to link, say, a juce::Slider control to a C++ float in your code. Instead, the Slider has a “current value” which you can get/set using its getValue() and setValue() methods. You can also inherit from juce::Slider::Listener, or set your Slider's onChange std::function pointer to a lambda in your code, to receive callbacks when the user changes the value.

Linking GUI components to plug-in parameters

JUCE does provide a very high-level system to link GUI controls to plug-in parameters, which goes by the ungainly class name juce::AudioProcessorValueTreeState (abbreviated AVTS).

  • //juce::AudioProcessor is the root class for your plug-in's DSP code
  • juce::ValueTree implements a hierarchical structure of “values” which are juce::var objects.
  • juce::AudioProcessorValueTreeState (AVTS) is a special kind of juce::ValueTree intended to represent the “state” of all a plug-in's parameters (the same ones it exposes to a host for automation).
  • AVTS knows all about parameters, which are all expressed as subclasses of juce::AudioProcessorParameter
  • AVTS also defines several “attachment” classes e.g. ButtonAttachment, SliderAttachment, etc. which set up automated, two-way links between a GUI control and its associated parameter. This is by far the best way to do this in a plug-in.
  • juce::AudioProcessorParameter has an associated Listener class, which you can override to receive a callback whenever the parameter value gets changed, either by the user manipulating the GUI, or through host automation, or in a few other cases (like loading presets, adjusting related parameters, etc.)

JUCE is NOT a Model-View-Controller based framework

Nearly all GUI frameworks in widespread use are based on the classic Model-View-Controller design pattern, but JUCE is not.

As you see from the above sections, GUIs for audio plug-ins are not built according to anything like an MVC pattern. You will, however, find a few spots where MVC-like ideas are applied, the main one being the ListBox/ListBoxModel pair of classes: to create a scrolling list, you use a juce::ListBox component, which is completely generic, and link it to a juce::ListBoxModel-derived object which contains the specific data to be listed.

Plugin Processor and Editor

JUCE implements a plug-in using a pair of classes:

The processor is what is instantiated by a host (e.g. DAW). It has a createEditor() method, whose job is to create an instance of the editor and return a pointer to it. The editor has a member variable which is a reference to the corresponding processor instance; through this reference, the editor can communicate with the processor, call its methods, get at public member variables, etc. (This is a convention, not something which is enforced by the class definitions.)

Because the processor can exist alone, i.e., there is no requirement that the host ever instantiate the editor, and most hosts actually delete the editor instance each time the user closes the plug-in GUI window, there is no reciprocal arrangement where the processor can get at “the” editor. (There are ways, but they are indirect.) The more common pattern is that the editor (or any of its child components) are set up as Listeners of the processor, so they get callbacks when its state changes in a way that requires the GUI to update. The JUCE classes ChangeBroadcaster and ChangeListener are most often used for this.

As mentioned above, JUCE's “attachments” mechanism allows GUI controls to be connected directly to the corresponding plug-in parameters (which themselves are embedded in a juce::AudioProcessorValueTreeState member variable of the processor). These connections are two-way and work basically automatically.

juce_gui_basics.txt · Last modified: 2021/02/08 23:52 by shane