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.

Derived 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;
};

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.

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

juce_gui_basics.1612799269.txt.gz · Last modified: 2021/02/08 15:47 by shane