© Stephen Chin, Johan Vos and James Weaver 2019
S. Chin et al.The Definitive Guide to Modern Java Clients with JavaFXhttps://doi.org/10.1007/978-1-4842-4926-0_5

5. Mastering Visual and CSS Design

Stephen Chin1 , Johan Vos2 and James Weaver3
(1)
Belmont, CA, USA
(2)
Leuven, Belgium
(3)
Marion, IN, USA
 

Written by Eugene Ryzhikov

Cascading Style Sheets or CSS is a stylesheet language which was created to describe the presentation of a document written in XML and its dialects such as HTML, SVG, and MathML. It became a de facto standard in web development for describing all of the presentation aspects of the web pages or web applications. It is one of the core languages of the open Web and is standardized across browsers according to the W3C specification.

So it was only natural that such a mature presentation language was implemented as part of JavaFX to simplify the description of all the presentation aspects of the framework such as fonts, colors, padding, effects, and so on.

In this chapter, we will cover the following topics:
  • Introduction to Cascading Style Sheets in JavaFX

  • Applying CSS techniques

  • Advanced CSS API

  • Benefits of using CSS in your JavaFX application

Introduction to Cascading Style Sheets

At the most basic level, CSS has only two building blocks, as shown in Figure 5-1:

Properties: Identifiers, which indicate a feature (font, color, etc.)

Values: Each property has a value, which indicates how the feature, described by the property, has to change.
../images/468104_1_En_5_Chapter/468104_1_En_5_Fig1_HTML.jpg
Figure 5-1

Properties and values, the building blocks of CSS

The pair of a property and a value is called a CSS declaration. CSS declarations exist within CSS declaration blocks, which in turn are paired with selectors. The pairs of selectors and declaration blocks produce CSS rulesets (or simply rules).

JavaFX dialect of CSS uses the -fx prefix for the properties to clearly distinguish them from web ones and avoid any compatibility issues, since many of them share the same names. Here is how such stylesheet may look:
/* resources/chapterX/introduction/styles.css */
.root {
   -fx-background-color: white;
   -fx-padding: 20px;
}
.label {
   -fx-background-color: black;
   -fx-text-fill: white;
   -fx-padding: 10px;
}
The .root selector refers to the root of the scene, and the .label selector refers to an instance of Label class. This is as simple as it can get – let’s use this CSS in the small application:
// chapterX/introduction/HelloCSS.java
public void start(Stage primaryStage) {
   Label label = new Label("Stylized label");
   VBox root = new VBox(label);
   Scene scene = new Scene( root, 200, 100 );
   scene.getStylesheets().add(      getClass().getResource("styles.css").toExternalForm());
   primaryStage.setTitle("My first CSS application");
   primaryStage.setScene(scene);
   primaryStage.show();
}

The key feature of the preceding code, highlighted, is a part that dynamically loads the stylesheet and applies it to the application scene. Figure 5-2 shows the resulting application next to the one without styling.

As expected, the scene background color and padding are different. Also our label has a different background and text color in addition to its padding.
../images/468104_1_En_5_Chapter/468104_1_En_5_Fig2_HTML.jpg
Figure 5-2

Application with CSS styling (left) and without (right)

Full description of JavaFX 11 CSS rules can be found in the JavaFX CSS Reference Guide at https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/doc-files/cssref.html. All aspects of JavaFX CSS are documented there.

Styles are applied to the nodes of the scene graph very similarly to the way CSS is applied to elements of the HTML DOM – they are first applied to the parent and then to its children. The code to do this is highly optimized and applies CSS only to the branches of the scene graph which require such changes. The nodes are only styled when they are part of the scene graph and reapplied on the following conditions:
  • Change to the node's pseudo-class state, style class, ID, inline style, or parent.

  • Stylesheets are added to or removed from the scene.

Selectors are responsible for matching styles to scene graph nodes and can be based on JavaFX class name, object ID, or just a style class assigned to a specific node. Let’s take look at each use case.

Selectors Based on Class Name

All the top-level JavaFX classes have their selector counterparts, with naming convention being lowercase class name, with words separated by hyphens. Special selector .root is reserved to style a root node of the scene. Here is an example of styling a ListView control:
.list-view {
   -fx-background-color: lightgrey;
   -fx-pref-width: 250px;
}

As you see, same hyphen-based approach is applied to properties. Here -fx-pref-width CSS property is automatically interpreted as prefWidth property of a ListView control.

It is also possible to address nodes by using their short class names as selectors, but it is not recommended.

Selectors Based on Custom Style Classes

Custom style classes can also be used to style scene graph nodes. In this case, the style class has to be assigned to the node manually. Multiple style classes can be assigned to the same node:
/* Stylesheet */
.big-bold-text {
   -fx-font-weight: bold;
   -fx-font-size: 20pt;
}
// JavaFX code
label.getStyleClass().add("big-bold-text");

Selectors Based on Object ID

Sometimes there is a need to address a specific instance on the node. This is done the same way as web CSS way by using the # symbol. The ID has to be manually assigned to the node instance which requires special styling and has to be unique in the scene graph:
/* Stylesheet */
#big-bold-label {
   -fx-font-weight: bold;
   -fx-font-size: 20pt;
}
// JavaFX code
label.setId("big-bold-label");

Applying CSS Styles

Loading CSS Stylesheets

A stylesheet is usually used as a resource in your application, and thus the Java resource API is the best way to load it. The most common way is to add resources to a “stylesheets” property of a scene:
// best way of loading stylesheets
  scene.getStylesheets().add(
     getClass().getResource("styles.css").toExternalForm()
  );
  // the following works, but not recommended
  // since it is prone to problems with refactoring
  scene.getStylesheets().add( "package/styles.css");

The preceding code loads the stylesheet as resource from the folder, where the current class is located.

Since the CSS resource is a URL, it is also possible to load remote CSS resources:
// remote stylesheet
  scene.getStylesheets().add( "http://website/folder/styles.css" );

Note that it is also possible to apply multiple stylesheets to a scene. Those styles will be combined behind the scenes by JavaFX CSS engine.

In many cases, it is desired to apply global stylesheets to the whole application, that is, all scenes simultaneously. This can be done by calling Application.setUserAgentStyleSheet API. Passing null will return your application to the default stylesheet. Currently, JavaFX provides two default stylesheets, which are defined as constants:
// original stylesheet ( JavaFX 2 and before )
  Application.setUserAgentStylesheet( STYLESHEET_CASPIAN );
  // default stylesheet since JavaFX 8
  Application.setUserAgentStylesheet( STYLESHEET_MODENA );

Starting with JavaFX 8u20, it is also possible to set user agent stylesheets for the Scene and SubScene. This allows Scene and SubScene to have styles distinct from the platform default. When user agent is set on SubScene, its styles are used instead of the styles from the default platform or any user agent stylesheets set on the Scene.

Applying CSS Styles to JavaFX Nodes

In addition to applying the stylesheets to the whole scene, you can apply them to any node, inherited from javafx.scene.Parent. The API is exactly the same as the one of the Scene class. When you apply CSS to a specific node, it is only applied to the node itself and all the nodes in its children hierarchy.

It is also possible to style the node using its setStyle API . This approach has its own pros and cons, but, before discussing them, let’s see how it works:
  // chapterX/applying/ApplyingStyles.java
  public class ApplyingStyles extends Application {
    private Label label = new Label("Stylized label");
    // Simplistic implementation of numeric field
    private TextField widthField = new TextField("500") {
        @Override
        public void replaceText(int start, int end, String text) {
            if (text.matches("[0-9]*")) {
                super.replaceText(start, end, text);
            }
        }
        @Override
        public void replaceSelection(String replacement) {
            if (replacement.matches("[0-9]*")) {
                super.replaceSelection(replacement);
            }
        }
    };
    private void updateLabelStyle() {
        label.setStyle(
                "-fx-background-color: black;" +
                "-fx-text-fill: white;" +
                "-fx-padding: 10;" +
                "-fx-pref-width: " + widthField.getText() + "px;"
        );
    }
    @Override
    public void start(Stage primaryStage) {
        updateLabelStyle();
        widthField.setOnAction( e -> updateLabelStyle());
        VBox root = new VBox(10, label, widthField);
        root.setStyle(
            "-fx-background-color: lightblue;" +
            "-fx-padding: 20px;");
        Scene scene = new Scene( root, 250, 100 );
        primaryStage.setTitle("My first CSS application");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
In the preceding code, styles are applied directly to the root and label controls. In the case of the root, we are applying a static style, that is, the root is always going to be of the light-blue color with the padding of 20 pixels. For the label, we chose to apply a dynamic styling, the preferred width of which comes from the value we enter into widthField. It changes as soon as we press Enter after editing the number. Figure 5-3 shows what the updated UI looks like.
../images/468104_1_En_5_Chapter/468104_1_En_5_Fig3_HTML.jpg
Figure 5-3

Dynamic style update using setStyle

This method is only recommended in cases where your styling has to be very dynamic, usually based on your own UI changes, like the preceding example. It is also very useful for quick prototyping.

In all other cases, having external CSS styles is a best choice, since they do not require code changes and thus recompilation and can be edited externally. They also have better performance characteristics.

In many cases, instead of using setStyle, it is possible to call a corresponding API method. This is where you get the ultimate performance since there is not CSS processing involved. Here is how we can replace our preceding dynamic CSS property:
// CSS way
  label.setStyle("-fx-pref-width: 500px");
// JavaFX API way
  label.setPrefWidth(500);
CSS styles can also be applied in a similar way in FXML. Any component can be styled in one of the following three ways:
  • By assigning a style class defined in the external stylesheet

  • By setting styles directly using style property

  • By assigning a stylesheet

  <!-- assign a style class -->
  <Label styleClass="fancy-label" />
  <!-- assign a style directly -->
  <Label style="-fx-pref-width: 500px" />
  <!-- assign a stylesheet -->
  <Label stylesheets="@styles.css" />
You just saw several methods of applying styles to JavaFX nodes. Even though you can apply them interchangeably, JavaFX defines the priority rules for each of them in the following sequence:
  1. 1.

    Apply user agent stylesheets.

     
  2. 2.

    Apply value set by a JavaFX API call.

     
  3. 3.

    Apply styles set by scene or node stylesheets property.

     
  4. 4.

    Apply style from node’s style property.

     

As you can see, the node’s style will override any previous style settings. This is a common source of confusion, but the rules are clear – if you set your style using setStyle API, all the other methods will be ignored by JavaFX.

Advanced CSS Techniques

Using Descendant Selectors

In a way similar to a web CSS, JavaFX CSS engine supports selectors to match styles to scene graph nodes. Since the CSS selectors are a widely known subject, we will simply show a few examples of how they are used in JavaFX CSS:
/* all labels */
.label,
.text {
   -fx-font-weight: bold;
   -fx-font-size: 20pt;
}
/* all children of #big-bold */
#big-bold .label {
   -fx-font-weight: bold;
   -fx-font-size: 20pt;
}
/* only direct children of #big-bold */
#big-bold > .label {
   -fx-font-weight: bold;
   -fx-font-size: 20pt;
}

Using Pseudo-classes

JavaFX CSS also supports pseudo-classes , which allow you to define styles corresponding to a different state of the JavaFX node. JavaFX does not implement a full range of pseudo-classes specified in the CSS standard. Instead, each Node defines a set of supported pseudo-classes, which can be found in the JavaFX CSS Reference Guide.

For example, Button supports the following pseudo-classes:

armed

Applies when the armed variable is true.

cancel

Applies if this Button receives VK_ESC if the event is not otherwise consumed.

default

Applies if this Button receives VK_ENTER if the event is not otherwise consumed.

Here is how we can take advantage of them :
/* all buttons will have a font size of 1.1 em */
.button {
   -fx-font-size: 1.1em;
}
/* default buttons will have bigger font size and color*/
.button:default {
   -fx-font-size: 1.2em;
   -fx-font-fill: blue;
}

Using Imports

Starting with JavaFX 8u20, CSS @import rule is partially supported. Currently, only unconditional imports are allowed (media type qualifier is not supported). Also import statements should appear at the top of your stylesheet.

This feature greatly simplifies development of complex styles, allowing separation of concerns.

Styles can be imported from a local or remote stylesheet:
@import "styles.css"
@import url ("http://website/folder/styles.css")

Font Loading in the Stylesheet

Since JavaFX 8, there is also a partial support for @font-face rule, which allows custom font loading:
@font-face {
   font-family: 'sample';
   font-style: normal;
   font-weight: normal;
   src: local('sample'),    url('http://font.samples/resources/sample.ttf';) format('truetype');
}

Note that remote font loading using URL is also supported.

The font-family property defines the name, which can now be used throughout the stylesheet.

Reusing Styles

To allow for higher flexibility, JavaFX CSS supports constants – a nonstandard CSS feature. Currently, only colors can be defined as constants. Besides a lot of predefined named colors, custom constants can be defined, which in the reference guide are called “looked-up colors.” With the looked-up colors, you can refer to any other color property that is set on the current node or any of its parents. This power feature allows for generic color theming in your application. The looked-up colors are “live” and react to any style changes since they are not looked up until they are applied.

Here is the simple example of this awesome feature:
.root { -my-button-background: #f00 }
.button { -fx-background-color: -my-button-background }
There is also another way of reusing styles somewhere else in the stylesheet, which is using the inherit keyword. It simply allows for child elements to reuse the styles defined in their parents:
.root {
   -fx-font-fill: green;
.button {
   -fx-font-size: 1.1em;
}
.button:default {
   -fx-font-size: 1.2em;
   -fx-font-fill: inherited;
}

In the preceding example, the default button will inherit the color from the root element.

Using Advanced Color Definitions

JavaFX specifies multiple ways to define paint values. Those are of the following:
  • Linear gradient

  • Radial gradient

  • Image pattern with optional repetition

  • Solid color

Using Linear Gradients

Linear gradient syntax is defined as follows:
linear-gradient( [ [from <point> to <point>] | [ to <side-or-corner>], ]? [ [ repeat | reflect ], ]? <color-stop>[, <color-stop>]+)
where <side-or-corner> = [left | right] || [top | bottom]

Linear gradient creates a gradient going through all the stop colors along the line between “from” point and “to” point. If the points are percentages, they are relative to the size of the area being filled. Percentages and lengths cannot be mixed in a single gradient.

If neither repeat nor reflect is given, then the CycleMethod defaults "NO_CYCLE."

If neither [from <point> to <point>] nor [ to <side-or-corner> ] is given, then the gradient direction defaults to “to bottom.” Stops are per W3C color-stop syntax and are normalized accordingly. Here are some of the examples :
/*
  *  gradient from top left to bottom right
  * with yellow at the top left corner and red in the bottom right
  */
 -fx-text-fill: linear-gradient(to bottom right, yellow, red);
/* same as above but using percentages */
-fx-text-fill: linear-gradient(from 0% 0% to 100% 100%, yellow 0%, green 100%);
/*
  * create a 50px high bar at the top with a 3 color gradient
  * white with underneath for the rest of the filled area.
/*
-fx-text-fill: linear-gradient(from 0px 0px to 0px 50px, gray, darkgray 50%, dimgray 99%, white);

Using Radial Gradients

Radial gradient syntax is defined as follows:
radial-gradient([ focus-angle <angle>, ]? [ focus-distance <percentage>, ]? [ center <point>, ]? radius [ <length> | <percentage> ] [ [ repeat | reflect ], ]? <color-stop>[, <color-stop>]+)

Radial gradient creates a gradient going through all the stop colors radiating outward from the center point to the radius. If the center point is not given, the center defaults to (0,0). Percentage values are relative to the size of the area being filled. Percentage and length sizes cannot be mixed in a single gradient function.

If neither repeat nor reflect is given, then the CycleMethod defaults "NO_CYCLE."

Stops are per W3C color-stop syntax and are normalized accordingly:
-fx-text-fill: radial-gradient(radius 100%, red, darkgray, black);
-fx-text-fill: radial-gradient(focus-angle 45deg, focus-distance 20%, center 25% 25%, radius 50%, reflect, gray, darkgray 75%, dimgray);

Using Image Pattern

This gives the ability to use image pattern as paint. The following is the syntax for image pattern:
image-pattern(<string>, [<size>, <size>, <size>, <size>[, <boolean>]?]?)
Parameters, in order, are defined as follows:

<string>

The URL of the image.

<size>

The x origin of the anchor rectangle.

<size>

The y origin of the anchor rectangle.

<size>

The width of the anchor rectangle.

<size>

The height of the anchor rectangle.

<boolean>

The proportional flag which indicates whether start and end locations are proportional or absolute.

Here are a few examples of using image patterns:
-fx-text-fill: image-pattern("images/wood.png");
-fx-text-fill: image-pattern("images/wood.png", 20%, 20%, 80%, 80%);
-fx-text-fill: image-pattern("images/wood.png", 20%, 20%, 80%, 80%, true);
-fx-text-fill: image-pattern("images/wood.png", 20, 20, 80, 80, false);
The image pattern can also be used for producing tiled image-based fills, which are an equivalent of the
image-pattern("images/wood.png", 0, 0, imageWith, imageHeight, false);
The syntax for tiled or repeating image patterns is
repeating-image-pattern(<string>)
The only parameter is the URI of the image. Here is an example of the repeating image pattern:
repeating-image-pattern("images/wood.png")

Using RGB Color Definitions

The RGB color model is used for numerical color applications. It has a number of different supported forms:
#<digit><digit><digit>
| #<digit><digit><digit><digit><digit><digit>
| rgb( <integer>, <integer>, <integer> )
| rgb( <integer> %, <integer>%, <integer>% )
| rgba( <integer>, <integer>, <integer>, <number> )
| rgba( <integer>%, <integer>%, <integer> %, <number> )
Here are examples of the different RGB formats to set the text fill for a label:
.label { -fx-text-fill: #f00              } /* #rgb */
.label { -fx-text-fill: #ff0000           } /* #rrggbb */
.label { -fx-text-fill: rgb(255,0,0)      }
.label { -fx-text-fill: rgb(100%, 0%, 0%) }
.label { -fx-text-fill: rgba(255,0,0,1)   }
As you can see, there are three types of RGB formatting :

RGB Hex

The format of an RGB value in the hexadecimal notation is a “#” immediately followed by either three or six hexadecimal characters. The three-digit RGB notation (#rgb) is converted into the six-digit form (#rrggbb) by replicating digits, not by adding zeros. For example, #fb0 expands to #ffbb00. This ensures that white (#ffffff) can be specified with the short notation (#fff) and removes any dependencies on the color depth of the display.

RGB Decimal or Percent

The format of an RGB value in the functional notation is “rgb(” followed by a comma-separated list of three numerical values (either three decimal integer values or three percentage values) followed by “).” The integer value 255 corresponds to 100% and to F or FF in the hexadecimal notation: rgb(255,255,255) = rgb(100%,100%,100%) = #FFF. White space characters are allowed around the numerical values.

RGB + Alpha

This is an extension of the RGB color model to include an “alpha” value that specifies the opacity of a color. This is accomplished via a functional syntax of the form rgba(...) that takes a fourth parameter which is the alpha value. The alpha value must be a number in the range 0.0 (representing completely transparent) and 1.0 (completely opaque). As with the rgb() function, the red, green, and blue values may be decimal integers or percentages.

The following examples specify the same color :
.label { -fx-text-fill: rgb(255,0,0) } /* integer range 0 — 255*/
.label { -fx-text-fill: rgba(255,0,0,1) /* the same, with explicit opacity of 1 */
.label { -fx-text-fill: rgb(100%,0%,0%) } /* float range 0.0% — 100.0% */
.label { -fx-text-fill: rgba(100%,0%,0%,1) } /* the same, with explicit opacity of 1 */

Using HSB Color Definitions

Colors can also be specified using the HSB (sometimes called HSV) color model as follows:
hsb( <number>, <number>%, <number>% ) |
hsba( <number>, <number>%, <number>%, <number> )

The first number is hue, a number in the range 0–360 degrees.

The second number is saturation, a percentage in the range 0–100%.

The third number is brightness, also a percentage in the range 0–100%.

The hsba(...) form takes a fourth parameter at the end which is an alpha value in the range 0.0–1.0, specifying completely transparent and completely opaque, respectively.

Using Color Functions

JavaFX CSS engine provides support for some color computation functions . These functions compute new colors from input colors at the same time the color style is applied. These enables a color theme to be specified using single based color and to other variants computed from it. There are two color functions: derive and ladder.
derive( <color>, <number>% )
The derive function takes a color and computes a brighter or darker version of that color. The second parameter is the brightness offset, representing how much brighter or darker the derived color should be. Positive percentages indicate brighter colors, and negative percentages indicate darker colors. A value of -100% means completely black, 0% means no change in brightness, and 100% means completely white:
ladder(<color>, <color-stop> [, <color-stop>]+)

The ladder function interpolates between colors. The effect is as if a gradient is created using the stops provided, and then the brightness of the provided <color> is used to index a color value within that gradient. At 0% brightness, the color at the 0.0 end of the gradient is used; at 100% brightness, the color at the 1.0 end of the gradient is used; and at 50% brightness, the color at 0.5, the midway point of the gradient, is used. Note that no gradient is actually rendered . This is merely an interpolation function that results in a single color.

Stops are per W3C color-stop syntax and are normalized accordingly.

For example, you could use the following if you want the text color to be black or white depending upon the brightness of the background:
background: white;
-fx-text-fill: ladder(background, white 49%, black 50%);

The resulting -fx-text-fill value will be black, because the background (white) has a brightness of 100% and the color at 1.0 on the gradient is black. If we were to change the background color to black or dark gray , the brightness would be less than 50%, giving an -fx-text-fill value of white.

Using Effect Definitions

JavaFX CSS currently supports the DropShadow and InnerShadow effects from the JavaFX platform. See the class documentation in javafx.scene.effect for further details about the semantics of the various effect parameters.

Drop Shadow

DropShadow is a high-level effect that renders a shadow of the given content behind it:
dropshadow( <blur-type>, <color>, <number>, <number>, <number>, <number> )

blur-type

[ gaussian | one-pass-box | three-pass-box | two-pass-box ].

color

The shadow Color.

number

The radius of the shadow blur kernel, in the range [0.0 ... 127.0], typical value 10.

number

The spread of the shadow. The spread is the portion of the radius where the contribution of the source material will be 100%. The remaining portion of the radius will have a contribution controlled by the blur kernel. A spread of 0.0 will result in a distribution of the shadow determined entirely by the blur algorithm. A spread of 1.0 will result in a solid growth outward of the source material opacity to the limit of the radius with a very sharp cutoff to transparency at the radius. Values should be in the range [0.0 ... 1.0].

number

The shadow offset in the horizontal direction, in pixels.

number

The shadow offset in the vertical direction, in pixels.

Inner Shadow

Inner shadow is a high-level effect that renders a shadow inside the edges of the given content:
innershadow( <blur-type>, <color>, <number>, <number>, <number>, <number> )

blur-type

[ gaussian | one-pass-box | three-pass-box | two-pass-box ].

color

The shadow Color.

number

The radius of the shadow blur kernel, in the range [0.0 ... 127.0], typical value 10.

number

The choke of the shadow. The choke is the portion of the radius where the contribution of the source material will be 100%. The remaining portion of the radius will have a contribution controlled by the blur kernel. A choke of 0.0 will result in a distribution of the shadow determined entirely by the blur algorithm. A choke of 1.0 will result in a solid growth inward of the shadow from the edges to the limit of the radius with a very sharp cutoff to transparency inside the radius. Values should be in the range [0.0 ... 1.0].

number

The shadow offset in the horizontal direction, in pixels.

number

The shadow offset in the vertical direction, in pixels.

Useful Tips and Tricks

Study Modena Stylesheet

As was discussed before, Modena is the default user agent stylesheet introduced with JavaFX 8.

It contains a trove of useful definitions and should be studied by anyone who wishes to use CSS styling in JavaFX applications.

Define Themes Based on Modena

Modena stylesheet color definitions are based on few attributes, which you can find in its root section. The most important one is -fx-base, which is the based colors for all the objects:
.root {
    /**********************************************************************
     *                                    *
     * The main color palette from which the rest of the colors are derived.   *
     *                                    *
     *********************************************************************/
    /* A light grey that is the base color for objects.  Instead of using
     * -fx-base directly, the sections in this file will typically use -fx-color.
     */
    -fx-base: #ececec;
    /* A very light grey used for the background of windows.  See also
     * -fx-text-background-color, which should be used as the -fx-text-fill
     * value for text painted on top of backgrounds colored with -fx-background.
     */
    -fx-background: derive(-fx-base,26.4%);
    /* Used for the inside of text boxes, password boxes, lists, trees, and
     * tables.  See also -fx-text-inner-color, which should be used as the
     * -fx-text-fill value for text painted on top of backgrounds colored
     * with -fx-control-inner-background.
     */
    -fx-control-inner-background: derive(-fx-base,80%);
    /* Version of -fx-control-inner-background for alternative rows */
    -fx-control-inner-background-alt: derive(-fx-control-inner-background,-2%);
    ....
}
This allows us to easily redefine the overall color theme of our stylesheet. For example, create a theme, which closely resembles the famous IntelliJ IDEA “Darcula” theme:
.root {
    -fx-base: rgba(60, 63, 65, 255);
}

Not only it sets all appropriate object colors but the text color is presented correctly too since the Modena stylesheet uses the ladder method to compute appropriate contrasting color.

Define Icons Using CSS

Instead of loading and assigning images using Java code, we can do it much simpler using CSS definitions. Let’s look at the example of the label, which is given the style class of “image-label”:
.image-label {
  -fx-graphic: url("icon.jpg");
}

Using URL, we simply assign the appropriate resource the -fx-graphic property. This removes unnecessary styling code from our application while giving a clean separation between styling and the code.

CSS Reusability by Using Color Constants

As we discussed above, JavaFX CSS engine supports a nonstandard feature, called color constants. Those constants can only be defined in the root section of your stylesheet, but then can be reused throughout your application. This not only promotes the great reusability but gives your application a nice consistent look.

Using Transparent Colors

In many cases, the design of the application calls for use of specific colors with full control of the background ones. For example, you are styling your custom control, but have no idea where it will be used. You want to style it in such a way that your control’s colors blend nicely with any background. Color opacity to the rescue!

Let’s see how such technique allows us to blend the colors.
../images/468104_1_En_5_Chapter/468104_1_En_5_Fig4_HTML.jpg
Figure 5-4

Blending colors using opacity

In Figure 5-4, you can see how transparent colors are nicely blending with practically any background. In contrast, colors with 100% opacity do not blend and often look out of place.

To appreciate such a design, let’s take a look at Trello Boards.
../images/468104_1_En_5_Chapter/468104_1_En_5_Fig5_HTML.jpg
Figure 5-5

Trello Boards user interface leveraging advanced CSS styling

In Figure 5-5, you can see how buttons, search fields, and even boards themselves nicely blend with any background chosen by the user.

Advanced CSS API

It is possible to extend standard JavaFX CSS with new custom style classes, properties, and pseudo-classes. Those techniques are especially useful while developing new custom controls and require thorough understanding of CSS API.

To illustrate the features of JavaFX CSS API, we will create a simple custom control. This control will represent the weather type, showing associated icon and text. For simplicity, we will extend this control from standard JavaFX label. In addition, we will also add additional, custom pseudo style to represent dangerous type of weather, which will allow us to style those differently.

First, we define the enum, representing the types of weather we care about. Since we will show our icons using icon font called “Weather Icons Regular,” we will pass related font characters into each enum. An additional enum parameter will allow us to define which weather is dangerous:
// chapterX/cssapi/WeatherType.java
import javafx.scene.text.Text;
public enum WeatherType {
    SUNNY("uf00d", false),
    CLOUDY("uf013", false),
    RAIN("uf019", false),
    THUNDERSTORM("uf033", true);
    private final boolean dangerous;
    private final String c;
    WeatherType(String c, boolean dangerous) {
        this.c = c;
        this.dangerous = dangerous;
    }
    public boolean isDangerous() {
        return dangerous;
    }
    Text buildGraphic() {
        Text text = new Text(c);
        text.setStyle("-fx-font-family: 'Weather Icons Regular'; -fx-font-size: 25;");
        return text;
    }
}

The icons will be represented by Text control. Our icon’s characters will be set as text. We will also style it, making sure that it uses appropriate font family and size. The method buildGraphic will build the Text control for a specific enum.

Time to build our custom control!

First, we want to define constants representing our control’s style class, weather property, and our new pseudo-class:
private static final String STYLE_CLASS       = "weather-icon";
private static final String WEATHER_PROP_NAME = "-fx-weather";
private static final String PSEUDO_CLASS_NAME = "dangerous";
Next, we will define our styleable property. This is a special type of property, which can be styled from CSS. This property will take the value of our WeatherType and will change the icon and text of our control appropriately:
private StyleableObjectProperty<WeatherType> weatherTypeProperty = new StyleableObjectProperty<>(WeatherType.SUNNY) {
        @Override
        public CssMetaData<? extends Styleable, WeatherType> getCssMetaData() {
            return WEATHER_TYPE_METADATA;
        }
        @Override
        public Object getBean() {
            return WeatherIcon.this;
        }
        @Override
        public String getName() {
            return WEATHER_PROP_NAME;
        }
        @Override
        protected void invalidated() {
            WeatherType weatherType = get();
            dangerous.set( weatherType.isDangerous());
            setGraphic(weatherType.buildGraphic());
            setText(get().toString());
        }
    };

Since the value of our property is of enum type, we are using StyleableObjectProperty<WeatherType>. The implementation of the invalidate method defines what happens when our property changes. Here we use the newly instantiated weather type to set the graphic and text of our control. We also set the pseudo-class here, which will be described later.

The property also returns something called WEATHER_TYPE_METADATA. In JavaFX, CSSMetadata instance provides information about the CSS style and the hooks that allow CSS to set a property value. It encapsulates the CSS property name, the type into which the CSS value is converted, and the default value of the property.

CssMetaData is the bridge between a value that can be represented syntactically in a .css file and a StyleableProperty. There is a one-to-one correspondence between a CssMetaData and a StyleableProperty. Typically, the CssMetaData of a Node will include the CssMetaData of its ancestors.

To greatly reduce the amount of boilerplate code needed to implement the StyleableProperty and CssMetaData, we will use StyledPropertyFactory class . This class defines a lot of methods to create instances of StyleableProperty with corresponding CssMetaData:
private static final StyleablePropertyFactory<WeatherIcon> STYLEABLE_PROPERTY_FACTORY = new
             StyleablePropertyFactory<>(Region.getClassCssMetaData());
    private static CssMetaData<WeatherIcon, WeatherType> WEATHER_TYPE_METADATA =
            STYLEABLE_PROPERTY_FACTORY.createEnumCssMetaData(
                    WeatherType.class, WEATHER_PROP_NAME, x -> x.weatherTypeProperty);
@Override
    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
        return List.of(WEATHER_TYPE_METADATA);
    }

We also implement the getControlCssMetaData method , which allows JavaFX CSS engine to know everything about the control’s CSS metadata by returning a list of control styleable properties.

What is left is to implement our pseudo-class. Since we have only two states in our control, dangerous and normal, we can implement our pseudo-class as boolean property. Whenever property changes, we call a special method pseudoClassStateChanged to let CSS engine know that state has changed:
private BooleanProperty dangerous = new BooleanPropertyBase(false) {
        public void invalidated() {
            pseudoClassStateChanged(DANGEROUS_PSEUDO_CLASS, get());
        }
        @Override public Object getBean() {
            return WeatherIcon.this;
        }
        @Override public String getName() {
            return PSEUDO_CLASS_NAME;
        }
    };
There are only few cosmetic changes left now. Let’s see the full state of our control:
// chapterX/cssapi/WeatherIcon.java
public class WeatherIcon extends Label {
    private static final String STYLE_CLASS       = "weather-icon";
    private static final String WEATHER_PROP_NAME = "-fx-weather";
    private static final String PSEUDO_CLASS_NAME = "dangerous";
    private static PseudoClass DANGEROUS_PSEUDO_CLASS = PseudoClass.getPseudoClass(PSEUDO_CLASS_NAME);
    private static final StyleablePropertyFactory<WeatherIcon> STYLEABLE_PROPERTY_FACTORY =
            new StyleablePropertyFactory<>(Region.getClassCssMetaData());
    private static CssMetaData<WeatherIcon, WeatherType> WEATHER_TYPE_METADATA =
            STYLEABLE_PROPERTY_FACTORY.createEnumCssMetaData(
                    WeatherType.class, WEATHER_PROP_NAME, x -> x.weatherTypeProperty);
    public WeatherIcon() {
        getStyleClass().setAll(STYLE_CLASS);
    }
    public WeatherIcon(WeatherType weatherType ) {
        this();
        setWeather( weatherType);
    }
    private BooleanProperty dangerous = new BooleanPropertyBase(false) {
        public void invalidated() {
            pseudoClassStateChanged(DANGEROUS_PSEUDO_CLASS, get());
        }
        @Override public Object getBean() {
            return WeatherIcon.this;
        }
        @Override public String getName() {
            return PSEUDO_CLASS_NAME;
        }
    };
    private StyleableObjectProperty<WeatherType> weatherTypeProperty = new StyleableObjectProperty<>(WeatherType.SUNNY) {
        @Override
        public CssMetaData<? extends Styleable, WeatherType> getCssMetaData() {
            return WEATHER_TYPE_METADATA;
        }
        @Override
        public Object getBean() {
            return WeatherIcon.this;
        }
        @Override
        public String getName() {
            return WEATHER_PROP_NAME;
        }
        @Override
        protected void invalidated() {
            WeatherType weatherType = get();
            dangerous.set( weatherType.isDangerous());
            setGraphic(weatherType.buildGraphic());
            setText(get().toString());
        }
    };
    @Override
    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
        return List.of(WEATHER_TYPE_METADATA);
    }
    public WeatherType weatherProperty() {
        return weatherTypeProperty.get();
    }
    public void setWeather(WeatherType weather) {
        this.weatherTypeProperty.set(weather);
    }
    public WeatherType getWeather() {
        return weatherTypeProperty.get();
    }
}

Let’s test the control by creating a small application which creates the control in various different states, which set our weather type from using CSS as well as Java code.

First, let’s define our CSS:
/* resources/chapterX/cssapi/styles.css */
@font-face {
    font-family: 'Weather Icons Regular';
    src: url('weathericons-regular-webfont.ttf');
}
.root {
    -fx-background-color: lightblue;
    -fx-padding: 20px;
}
.thunderstorm {
    -fx-weather: THUNDERSTORM;
}
.rain {
    -fx-weather: RAIN;
}
.weather-icon {
    -fx-graphic-text-gap: 30;
    -fx-padding: 10;
}
.weather-icon:dangerous {
    -fx-background-color: rgba(255, 0, 0, 0.25);
}
  • We load our font first. JavaFX requires the font resource to be in the same location where the CSS file is located.

  • Define the root styles.

  • Define two custom style classes, .tunderstorm and .rain. They set the weather type accordingly.

  • Define styles for both states: normal and dangerous. Dangerous state is shown with a reddish background.

Our test application is almost trivial. We create several WeatherIcon controls setting weather type using CSS style classes or on code. We then present them using vertical layout (VBox):
/* chapterX/cssapi/WeatherApp.java */
public class WeatherApp extends Application {
    @Override
    public void start(Stage primaryStage)  {
        WeatherIcon rain = new WeatherIcon();
        rain.getStyleClass().add("rain");
        WeatherIcon thunderstorm = new WeatherIcon();
        thunderstorm.getStyleClass().add("thunderstorm");
        WeatherIcon clouds = new WeatherIcon( WeatherType.CLOUDY);
        VBox root = new VBox(10, rain, thunderstorm, clouds);
        root.setAlignment(Pos.CENTER);
        Scene scene = new Scene( root);
        scene.getStylesheets().add( getClass().getResource("styles.css").toExternalForm());
        primaryStage.setTitle("WeatherType Application");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
As you can see in Figure 5-6, the “dangerous” Thunderstorm weather is highlighted with red background, while the rest of the weather icons are not.
../images/468104_1_En_5_Chapter/468104_1_En_5_Fig6_HTML.jpg
Figure 5-6

WeatherType application using pseudo-classes for dynamic style updates

In this example, we chose to change the pseudo-class automatically, as soon as the weather type changed. It is very much possible to expose the pseudo-class property as public, which will give us a way to change it independently from the weather type.

We have shown a very powerful way to extend the JavaFX CSS with additional style classes, styleable properties, and pseudo-classes to represent additional component states using advanced JavaFX CSS APIs.

CSS in JavaFX Applications: Summary

Overall, having the ability of styling the UI using CSS stylesheets is a huge leap forward. It brings the following benefits:
  • Division of responsibilities

The code and styling are clearly separated and can be updated independently.
  • Greater design consistency

CSS stylesheets can easily be reused, giving developers greater design consistency.
  • Lightweight code

Since the code is separate from the styling, it is not overloaded with parts that only do styling, which provides for much more lightweight code.
  • Ability to quickly change styling

Styling can be changed by simply switching a few definitions in the stylesheet without touching any code. It is also easily possible to provide entirely different styling based on hardware platform or operating system.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.236.255