Skip to content

Example: A Button Class#

Let's say we need something like a button in our code. And of course it needs to be visible and it needs to be clickable and it should probably have text on it. It would also be cool if it knew about its position.

That's a great use case for our newly learned skill of creating classes! Let's think back to our first simple ways of creating buttons. We did a check if the mouse was clicked within a rectangle. Let's make our next button a little bit more comfortable!

Follow Along!

It is a good idea to follow along with this article. While this is nothing you need to hand in, and I will not check up on you, it will help understanding of classes tremendously. Also you'll probably need buttons in a few places from now on, so this is as good a place as any to create a Button-class.

Writing Down Our Requirements#

As usual, we first want to make clear what we're actually setting out to do. What do we want? A button. What does a button actually do?

  • Needs to have a clickable area
    • This tells us it needs to know its position and size
    • This also tells us that we need some kind of check for its clickable are
  • Needs to contain a text
    • This tells us that we need to store a text
  • Bonus goal: should have a hover-effect
  • Bonus goal: could have a configurable color for normal and hover state

Thinking About the Attributes and Methods#

How would all the different things be called? We can think about this beforehand. That way we can play common operations through in our heads before we need to write the first line of code!

classDiagram
class Button{
    +String label
    +float x
    +float y
    +float width
    +float height
    +color normalColor
    +color hoverColor
    +Button(label, x, y, width, height)
    +boolean collisionCheck(x, y)
    +void paint()
}

A button without its position and label is not really useful, so I chose to include this information directly in its constructor. I also added something I call .collisionCheck(x, y) to our methods — we need to check whether the mouse is "within" the bounds of our button. That method will then return true ✅ or false ❌ as a result.

Writing Stuff Down Helps!

Please do this extra step, especially in the beginning. It is a stupid idea to keep everything in your head. Use your head for thinking, not for remembering. Writing stuff down frees some brain capacity. It also enables you to see the bigger picture and stops you from jumping directly into code.

.paint()!?

I'm using a different name here, to show that it is an arbitrarily chosen name. I could have called this function .show() like in the last example, I could also call it .draw(). I could come up with a list of synonyms for it. As long as people understand it, it's fine. (It's probably a good idea to stick to one name in your application. It is very annoying if one class calls it .show() and the other class calls it .pixelOutput() — but especially in large projects where you might have different third-party libraries included, these things do happen all the time.)

First Draft: Just Barely Getting it to Work#

There are a few things that will be fairly quickly implemented, so let's start with those. We're creating a class that we can instantiate and draw to our screens. We don't need much for that, but it's a useful step in building our whole class. Open up a new sketch and create a second tab, call it "Button". Add a few very simple lines to the first tab — juuuuust enough to draw your upcoming button. Add the first few lines for your Button class to the Button tab.

Button b = new Button("click me!");

void setup() {
}

void draw() {
    background(0);
    b.paint();
}
class Button {
    String label;

    Button(String buttonLabel) {
        label = buttonLabel;
    }

    void paint() {
        text(label, 20, 20);
    }
}

Can it do everything we set out to do? No! But it's a start! Next, let's give it a position, so we don't always draw it in the same place.

Success? Commit!

Make it a habit to do a git commit whenever you completed something. Even if it's a tiny step forward.

Second Draft: Add More Attributes#

Button b = new Button("click me!", 50, 50, 200, 50);

void setup() {
    size(800, 300);
}

void draw() {
    background(0);
    b.paint();
}
class Button {
    String label;
    float x;
    float y;
    float width;
    float height;

    Button(String buttonLabel, float buttonX, float buttonY, float buttonWidth, float buttonHeight) {
        label = buttonLabel;
        x = buttonX;
        y = buttonY;
        width = buttonWidth;
        height = buttonHeight;
    }

    void paint() {
        fill(255);
        rect(x, y, width, height);
        fill(0);
        text(label, x + 10, y + 30);
    }
}

That looks like a lot of change at once, and even though this is only the second draft, we already threw away a little bit of code we wrote before. This is fine. We overwrote how our constructor looked before (line 8) and extended it quite a bit. We also completely rewrote how this button is being drawn in .paint() (lines 17-20).

This now has a very clearly visible position on our screen, so let's get to the collision check next.

Commit

Works → Commit!

Collision

"are these coordinates within these boundaries?" is a very common question, especially in games. We call this a Collision Test or Hit Test, and they can get quite complicated. For now, we will only check for a single point (mouse coordinates) within a rectangular box (our button). That is fairly easy and is still a very useful and quick first estimate that is often done before performing more complicated hit tests.

Graduating to Usefulness#

Our next draft will use the check we already introduced with the boolean operators - we introduce a collisionCheck method that returns a boolean.

Button b = new Button("click me!", 50, 50, 200, 50);

void setup() {
    size(800, 300);
}

void draw() {
    println(b.collisionCheck(mouseX, mouseY));
    background(0);
    b.paint();
}
class Button {
    String label;
    float x;
    float y;
    float width;
    float height;

    Button(String buttonLabel, float buttonX, float buttonY, float buttonWidth, float buttonHeight) {
        label = buttonLabel;
        x = buttonX;
        y = buttonY;
        width = buttonWidth;
        height = buttonHeight;
    }

    void paint() {
        fill(255);
        rect(x, y, width, height);
        fill(0);
        text(label, x + 10, y + 30);
    }

    // checks if otherX and otherY are in our boundaries
    // returns `true` if inside, `false` otherwise
    boolean collisionCheck(float otherX, float otherY) {
        boolean collidedOnX = x <= otherX && otherX <= x + width;
        boolean collidedOnY = y <= otherY && otherY <= y + height;
        return collidedOnX && collidedOnY;
    }
}

A few lines added to the class, one line added to the main sketch. We're just println()ing if our check works so we can quickly move on. For the collisionCheck, we're comparing the object's attributes (x, y, width, height) with something we get from the outside as a parameter.

The Class Does Not Need To Know The Mouse!

It would be tempting to just put mouseX and mouseY here. This would also work. It is much less flexible, though. Maybe we want to check for other things, too? In my opinion it is much cleaner to have these "outside" x and y coordinates as parameters to your collisionCheck.

You know what this means

We're one step further, even a tiny success should be committed to git.

Making it hover#

Now that this part does something (check your console!), we can move on to actually making the button do something. Here we have to make a decision: Does the button manage itself as much as possible, or is this something our outside code needs to handle? There isn't always a clear answer to this We have at least these two options:

The main code could check for the collision (as we were doing in the previous example) and then tells the button, which color it should be drawn in. The Button does not need to know about the mouse, we can use our collisionDetection as much as we want. The downside is, that our main code gets more complicated and we need to remember this for each new button we add to our project.

The main code checks for collision, and the button remembers the last result of this check on its own. It can then decide for itself which color it should have. This makes adding more buttons much easier, and they will "just work". The only downside is, that collisionCheck now has a "side-effect" that might not be a great fit for all situations. The check also influences how the button is rendered.

In my opinion, for this button, Option B fits better to our situation. Let's go with that. That also means we will need a new Attribute for our class, because as mentioned in Option B, we now need to remember the result of the last collisionCheck1. So, even though we thought about our button in step one, we might still need to modify it slightly. That is ok.

classDiagram
class Button{
    +String label
    +float x
    +float y
    +float width
    +float height
    +boolean lastCollisionCheckResult;
    +color normalColor
    +color hoverColor
    +Button(label, x, y, width, height)
    +boolean collisionCheck(x, y)
    +void paint()
}

Let's put this into code:

Button b = new Button("click me!", 50, 50, 200, 50);

void setup() {
    size(800, 300);
}

void draw() {
    b.collisionCheck(mouseX, mouseY);
    background(0);
    b.paint();
}
class Button {
    String label;
    float x;
    float y;
    float width;
    float height;
    color normalColor;
    color hoverColor;
    boolean lastCollisionCheckResult;

    Button(String buttonLabel, float buttonX, float buttonY, float buttonWidth, float buttonHeight) {
        label = buttonLabel;
        x = buttonX;
        y = buttonY;
        width = buttonWidth;
        height = buttonHeight;
        normalColor = color(170, 170, 170);
        hoverColor = color(255, 255, 255);
        lastCollisionCheckResult = false;
    }

    void paint() {
        if (lastCollisionCheckResult) {
            fill(hoverColor);
        } else {
            fill(normalColor);
        }
        rect(x, y, width, height);
        fill(0);
        text(label, x + 10, y + 30);
    }

    // checks if otherX and otherY are in our boundaries
    // returns `true` if inside, `false` otherwise
    boolean collisionCheck(float otherX, float otherY) {
        boolean collidedOnX = x <= otherX && otherX <= x + width;
        boolean collidedOnY = y <= otherY && otherY <= y + height;
        lastCollisionCheckResult = collidedOnX && collidedOnY;
        return lastCollisionCheckResult;
    }
}

We Barely touched the main code, in fact we would not have needed any change, but I did remove the println() part again. We're still doing our .collisionCheck() of course.

In Button, we're introducing a few new attributes, and we're also filling them with default values in our constructor (lines 17-19). Please note that we did not add them to the parameters of our constructor. This is a choice, and you might disagree and add them to your constructor. In my opinion, changing the colors is something we'll do rarely, so a default value is good enough for now.

We updated .paint() to make use of our newly created colors, and also of the .lastCollisionCheckResult to actually choose the correct color. And finally we made sure our .collisionCheck() actually stores the information in our variable.

Congratulations, you now have a hovering button.

You Know What This Box Means!

Making it Clickable#

Our Button does not really know what it does, yet. In most languages (including Java) it is possible to attach a piece of code to an object. We did not talk about that, yet (and we're likely not going to). For now we will leave the "mouseClicked() management" to our main sketch, which is probably what you were already doing.

We'll also include a bunch of other buttons! Because we can.

Button leftButton = new Button("Left", 50, 50, 100, 50);
Button rightButton = new Button("Right", 650, 50, 100, 50);
Button exitButton = new Button("Exit", 375, 200, 50, 50);

void setup() {
    size(800, 300);
    exitButton.normalColor = color(200, 0, 0);
    exitButton.hoverColor = color(255, 0, 0);
}

void draw() {
    leftButton.collisionCheck(mouseX, mouseY);
    rightButton.collisionCheck(mouseX, mouseY);
    exitButton.collisionCheck(mouseX, mouseY);

    background(0);
    leftButton.paint();
    rightButton.paint();
    exitButton.paint();
}

void mouseClicked() {
    if (leftButton.collisionCheck(mouseX, mouseY)) {
        println("LEFT CLICKED");
    }
    if (rightButton.collisionCheck(mouseX, mouseY)) {
        println("RIGHT CLICKED");
    }
    if (exitButton.collisionCheck(mouseX, mouseY)) {
        println("EXIT CLICKED");
        exit();
    }
}
class Button {
    String label;
    float x;
    float y;
    float width;
    float height;
    color normalColor;
    color hoverColor;
    boolean lastCollisionCheckResult;

    Button(String buttonLabel, float buttonX, float buttonY, float buttonWidth, float buttonHeight) {
        label = buttonLabel;
        x = buttonX;
        y = buttonY;
        width = buttonWidth;
        height = buttonHeight;
        normalColor = color(170, 170, 170);
        hoverColor = color(255, 255, 255);
        lastCollisionCheckResult = false;
    }

    void paint() {
        if (lastCollisionCheckResult) {
            fill(hoverColor);
        } else {
            fill(normalColor);
        }
        rect(x, y, width, height);
        fill(0);
        text(label, x + 10, y + 30);
    }

    // checks if otherX and otherY are in our boundaries
    // returns `true` if inside, `false` otherwise
    boolean collisionCheck(float otherX, float otherY) {
        boolean collidedOnX = x <= otherX && otherX <= x + width;
        boolean collidedOnY = y <= otherY && otherY <= y + height;
        lastCollisionCheckResult = collidedOnX && collidedOnY;
        return lastCollisionCheckResult;
    }
}

A lot has changed in our main code. But please note how our Button class has not changed at all. We now have three buttons in our code, and they all look very different but they are all powered by the same piece of code. It is now an abstract representation of a button. An idea of a button. You can now have any button you want. As long as it's rectangular. And single-colored. Aaaaand has off-center text on it. But any button within that set of restrictions.

Looking at our main code, we now instantiate three separate buttons, all with their own label and coordinates (lines 1-3). We also set a special button color for our exitButton in lines 7 and 8 — as I mentioned earlier, this is still possible, even though we set a default value in the Button's constructor. In draw() we now first check all buttons for collisions. This is basically one of the inputs from our input-process-output principle. We then proceed to drawing all three buttons (the output from our input-process-output principle) on lines 17-19.

Finally, I added a whole new function to our main code, that handles mouseClicked() by checking for collisions on all three buttons again - but this time we're in a mouseClicked()-handler, so we know the mouse has been clicked. If any of the collisions are true ✅ , we know that this button has been clicked. For now we're just using prinln() to see output in the console. Except for the exit-button, which actually exits our program now.

You Know What This Box Means!

Checking our Requirements Again#

So, looking at our requirements from the beginning, we accomplished all of them!

  • It has a clickable area
  • It contains a text
  • Bonus goal: hover-effect accomplished
  • Bonus goal: configurable color accomplished

Taking it Further#

Our buttons all work fine, but they all look very basic. This would be a great time to improve their look. Since it is very clear who is in charge of what, it should also be clear that all we need to do is change our .paint() method. Let's make it shine!

Button leftButton = new Button("Left", 50, 50, 100, 50);
Button rightButton = new Button("Right", 650, 50, 100, 50);
Button exitButton = new Button("Exit", 375, 200, 50, 50);

void setup() {
    size(800, 300);
    exitButton.normalColor = color(200, 0, 0);
    exitButton.hoverColor = color(255, 0, 0);
}

void draw() {
    leftButton.collisionCheck(mouseX, mouseY);
    rightButton.collisionCheck(mouseX, mouseY);
    exitButton.collisionCheck(mouseX, mouseY);

    background(0);
    leftButton.paint();
    rightButton.paint();
    exitButton.paint();
}

void mouseClicked() {
    if (leftButton.collisionCheck(mouseX, mouseY)) {
        println("LEFT CLICKED");
    }
    if (rightButton.collisionCheck(mouseX, mouseY)) {
        println("RIGHT CLICKED");
    }
    if (exitButton.collisionCheck(mouseX, mouseY)) {
        println("EXIT CLICKED");
        exit();
    }
}
class Button {
    String label;
    float x;
    float y;
    float width;
    float height;
    color normalColor;
    color hoverColor;
    boolean lastCollisionCheckResult;

    Button(String buttonLabel, float buttonX, float buttonY, float buttonWidth, float buttonHeight) {
        label = buttonLabel;
        x = buttonX;
        y = buttonY;
        width = buttonWidth;
        height = buttonHeight;
        normalColor = color(170, 170, 170);
        hoverColor = color(255, 255, 255);
        lastCollisionCheckResult = false;
    }

    void paint() {
        push();
        translate(x + width/2, y + height/2);
        if (lastCollisionCheckResult) {
            scale(1.1);
            rotate(sin((float)millis() / 300)/5);
            fill(hoverColor);
        } else {
            fill(normalColor);
        }
        rect(-width/2, -height/2, width, height, 10);
        fill(0);
        textAlign(CENTER, CENTER);
        textSize(25);
        text(label, 0, 0);
        pop();
    }

    // checks if otherX and otherY are in our boundaries
    // returns `true` if inside, `false` otherwise
    boolean collisionCheck(float otherX, float otherY) {
        boolean collidedOnX = x <= otherX && otherX <= x + width;
        boolean collidedOnY = y <= otherY && otherY <= y + height;
        lastCollisionCheckResult = collidedOnX && collidedOnY;
        return lastCollisionCheckResult;
    }
}

No changes at all to our main code, but the .paint() function now has slightly more to offer than before. Rounded corners (line 32). Larger, centered text (line 34-35). Hover zoom and ANIMATION (lines 26 and 27). Without a single change to the main code. Just because we updated our class. And every button in our program gets those changes immediately.

(by now, I hope you formed the habit of committing even without green boxes.)


  1. This attribute could be a private property, because this will usually not be needed from the outside. If you're already more familiar with object oriented programming, looking into the different visibilities would be a good idea. Basically you want to be as private as possible.