Introduction
At some point you might want to create your own component. This can be just to create some uniquely styled button but also a complex new component for displaying data or anything.
To do this, you need to subclass Control or other classes from guiSET. Here I'll give
your a short guide through the main aspects, methods to implement or override and we'll also mention (some)
basic concepts of this library. You might also want to read this article
which goes deeper into core ideas and implementation of guiSET.
Alright, let's just dive in and start with examples. I recommend to read them in order to get all the necessary information for creating your own component. In the last part there is an overview of things to beware when creating certain types of components.
Outline:First own component
To learn the basics, first we want to create a button that just displays its background color.
We create a new class and let it extend Control - the base class for all guiSET
components.
Also let's add a constructor that allows setting the background and foreground color (NOTE: in Processing we
get the impression there
is a color type, but it is literally really just an int).
class ColorButton extends Control {
ColorButton(int backgroundColor, int foregroundColor) {
setBackgroundColor(backgroundColor);
setForegroundColor(foregroundColor);
}
}
Now the IDE should complain that we do not implement a certain abstract render() method. The
render() method is the key method that determines how our button will look like and all drawing
takes place here. In the Control class there is a PGraphics object called
pg on which we can draw. In case you are used to using off-screen PGraphics: don't
worry about calling pg.beginDraw() or pg.endDraw() as this is already dealt with
by the library. Best is to start with:
// It is not necessary to write "@Override" but better, because so you a) know you overrode
// something here and b) can check if you got the name of the method right. You
// can't compile if this method does not override any method from its parent class.
@Override
void render() {
drawDefaultBackground();
}
drawDefaultBackground() is already implemented in guiSET and will draw:
- The background/hover/pressed color set for this element
- The background image or gradient if set
The border along with its color, radius and width is always drawn automatically somewhere else.
We could now come up with ideas to change the look of the button and add some stuff:
@Override
void render() {
drawDefaultBackground();
if (enabled) {
pg.fill(getForegroundColor()); // set fill color to foregroundColor
} else {
pg.fill(color(0)); // set fill color to black if the component is disabled.
}
pg.noStroke(); // no border
pg.rect(4, 4, getWidth()-8, getHeight()-8); // draw a rectangle in the center with foregroundColor
}
Now there will be a (somewhat random) rectangle inside the button that fills most of the area and is colored
with the foregroundColor of the element. If the button is disabled however - the rectangle will
be black.
Also, as this shall be a button, we might want to change the color when the mouse hovers over the component
or
presses it. To achieve this, we just set the hoverColor and pressedColor attribute
to a different color. We can do this either manually or use the built-in method
setStatusBackgroundColorsAutomatically(int backgroundColor) which not only sets background
color but also hover and pressed color according to the given value. The latter two will be set a bit darker
than the
background (or lighter if the background is too dark).
class ColorButton extends Control {
ColorButton(int backgroundColor, int foregroundColor) {
setStatusBackgroundColorsAutomatically(backgroundColor);
setForegroundColor(getForegroundColor());
}
@Override
void render() {
drawDefaultBackground();
if (enabled) {
pg.fill(foregroundColor);
} else {
pg.fill(color(0));
}
pg.noStroke();
pg.rect(4, 4, getWidth()-8, getHeight()-8);
}
}
Vertical label
This example can also be found in the library examples (as "Custom_Component") and here.
Now, next I want to create a fancy label that displays text not horizontally but vertically. For this
purpose we
subclass Label instead of Control because the latter has no functionality for text
implemented. Instead, there is the (abstract) class TextBased which can be subclassed for any
components
that have text. Label also subclasses TextBased.
The easiest (or maybe only) way to achieve the rotated text is to use 2D-Transformations (nice article about that on the Processing website). We do:
class FancyLabel extends Label {
FancyLabel(String text) {
// this.text = text; // bad -> use setter (!):
setText(text);
}
@Override
void render() {
drawDefaultBackground();
pg.pushMatrix(); // saves the current position of the coordinate system
pg.translate(getWidth(), 0); // move by width to the left so rotation is right
pg.rotate(radians(90)); // rotate by 90 degrees (around left top corner)
drawDefaultText(); // guiSET default text drawing method (sets foregroundColor, textSize etc.)
pg.popMatrix(); // restore the transformation to before when we called pushMatrix()
}
}
Ok, now that kind of works but the text is just cut off. In the constructor we would better call
setText(text) which in turn calls an autosize routine within the class. You can specify
special rules for automatic resizing when text, font size or paddings have changed. All you
need to do is implement the method autoWidth() and/or autoHeight() and
return the desired width/height.
@Override
int autoWidth() {
return (int)textHeight(getText()) + getPaddingLeft() + getPaddingRight();
}
@Override
int autoHeight(){
return (int)textWidth(getText()) + getPaddingTop() + getPaddingBottom();
}
We set the width to the text height (might be multiple lines) and the height to the text width. Also, the
paddings are added. The method setWidth(int) however calls update(), telling the component to
render again next frame which might be unnecessary. Therefore we call setWidthNoUpdate(int).
Same for height.
We might get one strange behavior though and that is min/maxWidth/Height: all setters for
width and height constrain the given value between the set min and max value. These are reversed now for
width and height and for the ultimate vertical label we also should swap minWidth and
maxWidth etc. or even override some of the setters/getters.
The only problem is that drawDefaultText() expects the text to be drawn horizontally and uses
the elements width and height to calculate the best position. Therefore, we need
to swap width and height temporarily.
The full code:
class FancyLabel extends Label {
FancyLabel(String text) {
setText(text); // will call autosize
}
@Override
void render() {
drawDefaultBackground();
pg.pushMatrix(); // saves the current position of the coordinate system
pg.translate(getWidth(), 0); // move by width to the left so rotation is right
pg.rotate(radians(90)); // rotate by 90 degrees (around left top corner)
int tempWidth = getWidth();
int tempHeight = getHeight();
setHeightNoUpdate(tempWidth); // Set height to width without updating
setWidthNoUpdate(tempHeight); // Set width to height without updating
drawDefaultText(); // guiSET default text drawing method (sets foregroundColor, textSize etc.)
pg.popMatrix(); // restore the transformation to before when we called pushMatrix()
setHeightNoUpdate(tempHeight);
setWidthNoUpdate(tempWidth);
}
// autosize is also called by setPadding(), setFontSize() and various others
@Override
int autoWidth() {
return (int)textHeight(getText()) + getPaddingLeft() + getPaddingRight();
}
@Override
int autoHeight(){
return (int)textWidth(getText()) + getPaddingTop() + getPaddingBottom();
}
}
Vertical-Flow-Container
We'll now take a look at containers - components that group other elements.
This example uses the actual implementation of VFlowContainer. This container ignores the
position specified for its items (namely the x and y attributes) and just rows
them up vertically one below the other.
We start off by creating constructors and overriding void render(). We override the method
needsSortingByZ() because as just mentioned we apply some sort of a new
layout and ignore the items x and y (This is important!). Items shall not be
sorted because they will not overlap anyway and that could change the order. In render()
we start off by calling drawDefaultBackground() which is always a good idea.
class VFlowContainer extends Container {
public VFlowContainer() {
this(100, 100);
}
public VFlowContainer(int width, int height) {
super(width, height);
}
@Override
protected boolean needsSortingByZ(){
return false;
}
@Override
protected void render() {
drawDefaultBackground();
}
}
Now the interesting part: to place one item below the other we keep track of the "used" space and initialize
it with the containers paddingTop. Then iteratively we go through all items, render them and
add the height of the item (and the margins) to usedSpace:
protected void render() {
drawDefaultBackground();
int usedSpace = getPaddingTop();
for (Control c : items) {
if (c.isVisible()) {
renderItem(c, c.getMarginLeft() + getPaddingLeft(), usedSpace + c.getMarginTop());
usedSpace += (c.getHeight() + c.getMarginTop() + c.getMarginBottom());
}
}
drawDefaultDisabled();
}
A container should always use renderItem(Control item, int x, int y) to render its items
because this prepared function applies a bunch of necessary settings. With x and y
we specify the position of the item relative to the top left corner of the container.
Some further details include taking care of paddings and margins as well as calling
drawDefaultDisabled() to gray out the container in case enabled is set to false.
Crucial things to be aware of
In general, when changing visual properties (like text or width), use the setter.
I.e. the setters for width and height take care that neither exceeds maximum size
nor is less than minimum size. Also some concepts like resizing and updating the graphics are handled by
setters. Only sometimes you will want to change width/height without calling also
update() and then you can use setWidthNoUpdate(int) and same for height.
And also (!), always, when writing a setter for a new property that affects how the component looks
like on the screen, call update() in the setter !!
Implementing a new container
Your really should subclass the class Container or another container class! A lot of things a
different for containers (such as event handling). Plus, containers need to render their items in some way.
In render(), use the provided method renderItem(Control item, int x, int y)
Also, if your container ignores the x and y coordinates of its items and places
them in some different way (like the flow- and scroll containers do), be sure to override
needsSortingByZ() and return false.
You also might want to override the fitContent() method which resizes the container to fit its
content.
When overriding or creating new methods for adding items, use the already provided
add(Control...c) method. Do not directly add to the
ArrayList<Control> items!
Using mouse events
You can go ahead and override one or more of these internal class-specific methods:
void enter(MouseEvent e)void exit(MouseEvent e)void press(MouseEvent e)void release(MouseEvent e)void move(MouseEvent e)void drag(MouseEvent e)void wheel(MouseEvent e)
It is recommended to call the super method in case the superclass does something important here:
@Override
protected void press(MouseEvent e) {
super.press(e);
// do your stuff
}
Mind, you can call stopPropagation() in a mouse event routine to prevent further elements of
the GUI to possibly receive the current mouse event.
Using key events
Just override:
void keyPress(KeyEvent e)void keyRelease(KeyEvent e)void keyTyped(KeyEvent e)
Maybe you also to call the super method here too (especially when subclassing a textbox).
What really might be useful occasionally is the overridesFrameShortcuts() method. The standard
return value for this is false (except for textboxes). If it returns true, then shortcuts registered
at Frame will not be handled if an instance of your component is focused at the moment. I.e. it
would be irrating when typing text into a textbox and every time you hit 'E', a shortcut that is by chance
Shift+E is executed.
Methods you might want to override
render(), initialize(), int autoWidth(),
int autoHeight(),
fitContent(), int getAvailableWidth(), int getAvailableHeight(),
overridesFrameShortcuts(), needsSortingByZ()
internal event methods.
