A multi-button component (for beginners)


#1

Here is a little component I put together this afternoon. It may be of some interest to (other) beginners.

It displays a panel of 4 buttons and fires a callback when one is hit, giving the index.

Coding tips (always) appreciated!

π

PS It might be an idea to use/adapt this code on the buttons pane in the demo, which has a similar GUI construct.

Code: (Create an a GUI app from ProJucer, paste the code into mainComponent.h, and remove everything apart from the #include from mainComponent.cpp).


// ABCD_Button
// π 24.02.16

#ifndef MAINCOMPONENT_H_INCLUDED
#define MAINCOMPONENT_H_INCLUDED

#include "../JuceLibraryCode/JuceHeader.h"

class ABCD_Button : public Component
                  , private Button::Listener
{
public:
    struct Listener {
        virtual void ABCD_Clicked(int index) = 0;
    };
private:
    ListenerList<Listener> listeners;
    OwnedArray<TextButton> btnGrades;
public:
    ABCD_Button() {
        for (int i = 0; i < 4; ++i) {
            auto btn = new TextButton(String::charToString('A'+i));
            btn->addListener(this);
            addAndMakeVisible(btn);
            btnGrades.add(btn);
        }
    }

    void resized() final {
        auto R = getLocalBounds();
        int w = getWidth() / 4;
        for (int i = 0; i < 4; ++i)
        {
            TextButton* btn = btnGrades[i];
            auto r = R.removeFromLeft(w);
            btn->setBounds(r);
            btn->setConnectedEdges(
                ((i != 0) ? Button::ConnectedOnLeft : 0) |
                ((i != 3) ? Button::ConnectedOnRight : 0));
        }
    }

    void addListener(Listener* _) {
        listeners.add(_);
    }

    void buttonClicked(Button* _) final {
        for (int i = 0; i < 4; i++)
            if (_ == btnGrades[i])
                listeners.call(&Listener::ABCD_Clicked, i);
    }
};

class MainContentComponent   : public Component
                            , public ABCD_Button::Listener
{
public:
    MainContentComponent() {
        abcdButton = new ABCD_Button();
        abcdButton->addListener(this);
        addAndMakeVisible(abcdButton);
        abcdButton->toFront(true);
        setSize(600, 400);
    }

    void resized() {
        abcdButton->setBounds(50,50,200,50);
    }

    void ABCD_Clicked(int index) final {
        DBG(index);
    }

private:
    ScopedPointer<ABCD_Button> abcdButton;
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

#endif  // MAINCOMPONENT_H_INCLUDED

 


#2

Wouldn't it make sense to take a string array of inputs, and create N controls for them?


#3

That would definitely make it more flexible. It wouldn't have to be an ABCD button then. It could be numbers, different letters, or any arbitrary text. It would be easy to do too. Not much code would need to be changed. That might be a good exercise for the OP. You could just use a std::initalizer_list in the constructor and create the number of buttons based on its size and set the text with its contents. I'd probably just create a std::vector member and initialize it with the std::initializer_list in the constructor and use that. That way, you could have the contents stick around in case you need it. Of course, I'd also create a setter in addition to the constructor in case you want to change the text and number of buttons at any time. 


#4

The demo has an example of how to use Button::setRadioGroupId() which seems similar to this.  


#5

Yes, generalising to accept an arbitrary string array is a good idea.

A StringArray can be constructed from an initializer list of const char* (https://www.juce.com/doc/classStringArray#aeabb8817e379dc5581241d34c9a1603f)

Another simple generalisation might be an isRadio flag which means that one of the buttons is always 'pressed'.

Another idea I was considering is sizing each button depending on the text width it requires. But I can't see any way of getting the pixel width for a particular string in non fixedwidth font.

Or icons!  Hah this is why I tend to only generalise when I need to. It's easy to get carried away :)

π


#6

But I can't see any way of getting the pixel width for a particular string in non fixedwidth font.

There's Font::getStringWidth() (or getStringWidthFloat()) for that!


#7

@any beginners: Please don't follow the example in the code above of using '_' as a variable name! Ugh! That might be a thing in Haskell, but in C++ it's definitely considered... eccentric!


#8

Totally failed to notice I still had that '_' in there... No idea where I picked that up from :)

void buttonClicked(Button* _) final {
    for (int i = 0; i < 4; i++)
        if (_ == btnGrades[i])
            listeners.call(&Listener::ABCD_Clicked, i); 
}

Having said, I quite like it.  I would certainly keep it in my own code. Rationale: require minimum mental exertion to comprehend + if you know what you're doing, you can break all the rules to that end.

However, I agree that if aimed at a beginner, it may not be the best choice.

'b' would be my second (first?) choice. 'button' and 'pButton' would be a poor third&fourth, even though I believe they are in line with accepted style conventions.

Argument against '_' I suppose would be that if you are reading it mentally you have to say 'underscore' which is unwieldy , 'b' much punchier.  Parry would be 'Does a programmer typically comprehend code by verbalising it mentally?' I think the simple stuff like that we just stare at it and grok.  Like .. we don't mentally say 'open brace' whenever we see '{'.

I would btw contend '_ is not self descriptive' to be a strawman, as it is perfectly described by the surrounding context. To use e.g. 'pButton' would be repeating oneself, as the preceding symbols are 'Button*'

So .. I don't think it's inherently bad. Just unfamiliar.  A couple of years ago I considered starting an opening '{' on the same line to be grotesque. It created a visceral reaction. Like scratching a chalkboard. But now I prefer it for the majority of situations. My original logic of 'visually match the braces' must have been repatterned to 'indent determines nesting' though Python exposure.

π


#9

Bear in mind this paragraph from the C++ standard:

17.4.3.2.1 Global names [lib.global.names]

Certain sets of names and function signatures are always reserved to the implementation:

  • Each name that contains a double underscore (_ _) or begins with an underscore followed by an uppercase letter (2.11) is reserved to the implementation for any use.
  • Each name that begins with an underscore is reserved to the implementation for use as a name in the global namespace.
  • Such names are also reserved in namespace ::std (17.4.3.1).

If you use a variable name falling under those rules in your code, it's undefined behaviour. In this particular case your code is technically fine because you're not violating any of those rules. But because those rules are non-obvious, I always strictly avoid anything starting with an underscore so I never even have to think about the above. I'd strongly recommend this coding guideline!


#10

I'd say that a lone underscore begins with an underscore? E.g. juce::String.startsWith ("_") == true?

But besides all that using an underscore feels very wrong to me. It wreaks of hidden, internal meaning and is difficult to see in text. What's wrong with "b" for button? It's short, descriptive and easier to read?

I have a general rule that variable names should be proportional to the scope they occupy. This means it's perfectly acceptable to use single letters in scopes of a few lines. The amount of code you have to reason about in this case is small so you can easily remember b == button.


#11

I'd say that a lone underscore begins with an underscore? E.g. juce::String.startsWith ("_") == true?

Yes but his lone underscore is not in the global namespace or namespace std and does not have an uppercase letter or another underscore following it. (proving the point that the language rules are non-obvious so you shouldn't even go anywhere near that territory ;-)


#12

@dave96, my experience is that '_' is extremely easy to make out in text -- even easier than 'b'!  I would probably prefer 'b', but I'm not entirely sure!

@timur, I think the standard has mastered the art of making simple things sound complicated.

I think it could just say "You are not allowed to create any name which: 1) contains two consecutive underscores, 2) begins with an underscore followed by an uppercase letter or 3) begins with an underscore and is in the global namespace ([lib.requirements]/[requirements])."

I just use "_ and _lowercase are ok for non-globals".  That gives me sufficient freedom, and 100% safety! It's also dead easy to remember.

My most common use of _foo is initialising members from constructor parameters:

class X {
    int y;
    X(int _y) : y(_y)  { }
};

As regards single letters versus words, maybe it is just my background in maths that makes me prefer single letters -- although I feel there is something fundamental about single letters -- they are ultra-easy for eyes & brains to process.

I would usually prefer:

for (auto& v : myContainerInst) { ... }

over...

for (DoWeReallyNeedTypename& cumbersomeToken : myContainerInst') { ... }

... unless there is some clarity to be gained by elaborating.

π


#13

@timur Good point. I must remember the golden rule: don't rush reading the standard!

 

@pi I think we agree on variable name length to be proportional to the scope. Things like for loops, lambdas, small structs etc. are all good examples of where it is a good idea to keep scope narrow. I think this is good practice in general really as it makes code much more manageable to read. And yes, particularly when writing maths codes small names improve readability, but then scope is usually small in maths functions.

However, I completely disagree that "_" is more readable in code. I think it might be that underscores are often used where spaces would normally go which makes me just skip over them. (I also hate the underscore separation naming convention in the standard, to me it's much clearer that a new word starts with a capital but I'm not going to argue too hard with those geniuses or I'll get taught a lesson!)

Also for any other noobs reading this I would strongly advise against using leading or trailing underscores for passing arguments to methods. Firstly, it's easy to avoid and just creates an eyesore but more importantly it's very easy to introduce bugs because you left off an underscore and ended up initialising a member variable with itself. This may seem obvious but it's extremely easy to do and very difficult to track down if you don't have the Wuninitialized warning on (I don't think IJ projects generate this by default).

Personally, if your declaration and definition are split between header/cpp files I'd use the full variable name in the declaration where necessary (omit names completely if it's obvious e.g. (Graphics&)) and then a short name in the definition. If you've only got a header file, then pick a slightly different name.

That's just my 2 cents though ;)


#14

Single-letter variables have their purposes, especially in certain domains. Underscore is not really one of them, it's very to miss depending on syntax highlighting and carries no specific semantic meaning (imo), unlike other classics like 'i' for loop variables and so forth. I did a double-take when I saw that code.


#15

Yes I use camelCase everywhere, foo_bar reminds me of old C code.

Here is an illustration of what I'm getting at: Look at your last post, and see how long it takes you to find the following characters: z ) G @ _

_ is the only one instantly visible to me, it reveals itself even before I focus my eyes!

The visual system becomes predisposed towards discerning horizontal and vertical lines (http://www.psypress.co.uk/mather/resources/topic.asp?topic=ch13-tp-03#ch13-it-05 <-- Annis & Frost 1973)

So it should really stand out -- as if it were written in bold! That is my perception.

Of course if the code contains other variables using foo_bar, that might dissuade me.  But in a camelCase project, it's an almost unused symbol!

I am playing Devil's advocate here. I am not attached to using '_' as a variable, in fact this code is the first time I've ever done it in C++. But I see no sound reason against it, in fact the more I think about it the more it appeals.

π

PS @Mayae. _ syntax-highlights okay! And the semantic meaning is given by the type at the point of declaration. Like 'x' in maths. Although I can see an argument for using 'b' for a button, 'v' for a vector etc. But in a really tiny method, I'm not sure I would find any added benefit.


#16

I guess my main problem with it is that it has zero context. Usually with single character variable names I use, they're the first letter of the type or longer name e.g. "track -> t", "Button* b", "Rectangle<int> r" etc. These can all be used together. You can only use _ once.

Also, with code there are a lot of other strange symbols e.g. []{}()?:; etc. all these get in the way of recognising underscores for me.


#17

Yes, whatever situation you are dealing with, you need an appropriate response!

In my situation I had a 4-line method containing a single parameter and a loop variable, so it seems reasonable.

But generally I agree, I like to use the first letter. That seems optimal. _ seems to be maybe pushing it a bit far. Eccentric as Jules points out. Maybe I will drop the idea. But a little eccentricity adds to the flavour, keeps us awake!

π