14. Dezember 2015
Rico Hentschel
0

JavaFX Styleable Properties

In JavaFX können Custom-Komponenten auf zwei Wegen erstellt werden – zum einen mit der Controls API und zum anderen über eine Ableitung von einer Container-Klasse (wie Pane). Im Artikel sprechen wir über Custom Controls und meinen damit beide Varianten.

Damit die implementierten Custom Controls flexibel gestaltet und konfiguriert werden können, lohnt sich der Einsatz von StyleableProperties. Diese ermöglichen ein Anpassen der Custom Controls mit CSS. So kann beispielsweise ein Designer ohne Kenntnis von der Implementierung, ein modernes Design für eine bestehende Oberfläche mit CSS kreieren, ohne das Custom Control mit Java Code anzupassen. Damit dies jedoch möglich ist, müssen bestimmte Regeln beim Erstellen von StyleableProperties beachtet werden.

Einfache Property

Beginnen wir zunächst mit einer beispielhaften Implementierung einer Komponente, welche sich von einer Pane ableitet und eine SimpleObjectProperty besitzt. Die Eigenschaft könnte die Aufgabe haben, das Custom Control visuell oder auch das Verhalten zu konfigurieren.

public class MyPane extends Pane
{
   /**
   * determines the MyField Property
   */
   private final ObjectProperty<MyClass> myField = new SimpleObjectProperty<MyClass>(this, "myField", new MyClass(0));

   /**
   * sets the value of the MyField
   *
   * @param value, value to be used
   */
   public final void setMyField(MyClass value) { myField.set(value); }

   /**
   * gets the value of the MyField
   *
   * @return the value of the MyField
   */
   public final MyClass getMyField() { return myField.get(); }
   /**
   * returns the MyField
   *
   * @return the MyField Property
   */
   public final ObjectProperty<MyClass> myFieldProperty() { return myField; }
}

Ein erster Schritt in Richtung einfacher Individualisierbarkeit der Komponente ist die Namenskonvention der Property-Zugriffsmethoden. Damit Properties eines Custom Elements später im Scene Builder verwendet werden können, müssen sie nach den folgenden Property-Namenskonventionen implementiert sein.

  • public final <type> getMyField()
  • public final void setMyField()
  • public final SimpleDoubleProperty myFieldProperty()

 

Der Scene Builder ist in der Lage einen Teil der Properties (nicht nur der StylableProperties) mittel Controls darzustellen. Zum Beispiel können Strings durch Textfelder eingestellt werden.

JavaFX-Styleable-Properties

Abbildung 1 – Eigenschaft eines Controls, die im Scene Builder mittels TextField angepasst werden kann.

Ein Guide zum Thema Properties von Oracle gibt es hier.

Styleable Property

Im Gegensatz zu normalen Properties, erlauben StyleableProperties das Festlegen des Wertes der Property mittels CSS. Dies kann über eine CSS-Datei, innerhalb von FXML oder im Java Code verwendet werden. In der CSS-Datei kann das Custom Control nun über Klassen und ID-Selektoren gestylt werden, wenn das Control die jeweilige Klasse/ID besitzt. Beim instantiieren eines Custom Controls, wird geprüft, ob im CSS eine CSS-Eigenschaft für ein entsprechendes Styleable Property des Custom Controls vorhanden ist – ist das der Fall, wird der Wert aus dem CSS mithilfe eines Mappers als Wert des StyleableProperty festgelegt.

Folgendes Beispiel setzt das StyleableProperty der Button Klasse:

.button
{
   -fx-text-fill: white;
}

Um eigene StyleableProperties zu erstellen, muss je nach Typ der Eigenschaft die richtige Property-Basisklasse gewählt werden:

  • Styleable<TYPE>Property (je nachdem ob für den TYPE eine StyleableProperty-Implementierung vorhanden ist)
  • StyleableObjectProperty<TYPE> (muss bei eigenen Typen verwendet werden – das Äquivalent zu ObjectProperty<TYPE>)

 

Des Weiteren werden zusätzlich folgende Komponenten benötigt, um das Styleable Property in Gang zu bekommen:

  • javafx.css.Styleable (Interface, das mit CSS stylebare Elemente markiert)
  • CssMetaData<? extends Styleable, ?> (dient der Zuordnung zwischen CSS-Elementen und der Styleable Property und wird weiter unten beschrieben)
  • getCssMetaData() (muss in der Klasse überschrieben werden, die Styleable implementiert, damit die StyleableProperties bekannt sind, wenn die CSS-Werte verarbeitet werden)
  • StyleConverterImpl<?, ?> (Implementation eines Converters, der den CSS-Wert der ausgelesen wurde in den nötigen Type konvertiert. Eine Erläuterung hierzu ist weiter unten im Beitrag zu finden.)

 

Im Folgenden wurde die oben beschriebene Klasse so erweitert, dass die eingangs einfache Property nun Styleable ist.

public class MyPane extends Pane
{
   /**
   * determines the MyField Property
   */
   private final ObjectProperty<MyClass> myField = new SimpleStyleableObjectProperty<MyClass>(MY_FIELD, this, "myField", new MyClass(0));

   /**
   * CssMetaData to make the MyField styleable via Css, ist static cause of the way the cssmetadata is lookup in parent classes, hence we need to follow those rules
   */
   private static final CssMetaData<MyPane, MyClass> MY_FIELD = new CssMetaData<MyPane, MyClass>("-saxsys-my-field", MyClassConverter.getInstance(), new MyClass(0))
   {
     /**
       * determines if the property can be set using Css
       * @param node, node which contains the property
       * @return true if he property can be set, otherwise false
       */
     @Override public boolean isSettable(MyPane node)
     {
         return node.myField == null || !node.myField.isBound();
     }

     /**
       * returns the property which is styleable
       * @param node, node which contains the property
       * @return the property which is styleable
       */
     @Override public StyleableProperty<MyClass> getStyleableProperty(MyPane node)
     {
         return (StyleableProperty<MyClass>) node.myFieldProperty();
     }
   };

   /**
   * sets the value of the MyField
   *
   * @param value, value to be used
   */
   public final void setMyField(MyClass value) { myField.set(value); }

   /**
   * gets the value of the MyField
   *
   * @return the value of the MyField
   */
   public final MyClass getMyField() { return myField.get(); }

   /**
   * returns the MyField
   *
   * @return the MyField Property
   */
   public final ObjectProperty<MyClass> myFieldProperty() { return myField; }

   /**
   * contains all styleable CssMetaData needed, this class is static to function as a container which will only be created by the class loader, the first time arround that an instance of this class is created
   */
   private static class StyleableProperties
   {

     /**
    * this will hold all the available styleables that are available for this class, it will also contain the base classes StyleableProperties
    */
     private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;

     /**
    * In this region we will initialize all the list of StyleableProperties
    */
     static
     {

       /**
        * Here we need to make sure to include all the StyleableProperties from the parent class, otherwise we will loose the ability to style properties of the parent class
        */
         final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Pane.getClassCssMetaData());

         // here all the cssmetadata of all StyleableProperties of this class will need to be added in order to make them styleable
         styleables.add(MY_FIELD);

         STYLEABLES = Collections.unmodifiableList(styleables);
     }
   }

   /**
   * @return The CssMetaData associated with this class, which may include the
   * CssMetaData of its super classes.
   */
   public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData()
   {
     return StyleableProperties.STYLEABLES;
   }

   /**
   * This method should delegate to {@link Node#getClassCssMetaData()} so that
   * a Node's CssMetaData can be accessed without the need for reflection.
   *
   * @return The CssMetaData associated with this node, which may include the
   * CssMetaData of its super classes.
   */
   @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData()
   {
     return getClassCssMetaData();
   }

  /**
  * place this class in ist own file
  **/
  public class MyClass
  {
    private int blub;

    public MyClass(int a)
    {
       blub = a;
    }
  }

  /**
  * this class will convert incoming strings from a css file to MyClass   elements
  * were are using an eager singleton implementation here, but this is by   no means necessary ofc, its just for convenience
  **/
  public class MyClassConverter extends StyleConverterImpl<String, MyClass>
  {
    static final MyClassConverter INSTANCE = new MyClassConverter();

    public static StyleConverter<String, MyClass> getInstance()
    {
       return INSTANCE;
    }

    private MyClassConverter()
    {
       super();
    }

    @Override public MyClass convert(ParsedValue<String, MyClass> value, Font font)
    {
       int valueMyClass = 0;

       if(value.getValue().equals("ten"))
         valueMyClass = 10;

       return new MyClass(valueMyClass);
    }
  }
}

Styleable Interface

Die Grundlage für den Einsatz von StyleableProperties ist das Interface javafx.css.Styleable. Alle Nodes welche dieses Interface implementieren, können über CSS anpassbare Eigenschaften besitzen. Innerhalb des Interface wird die Methode getCssMetaData() genutzt, um alle StyleableProperties der Klasse zurückzugeben, welche dann beim Auswerten der bereitgestellten CSS-Datei verglichen werden. Dies geschieht auf folgende Weise:

  • es werden alle Einträge der CSS-Datei ausgelesen und verarbeitet
  • es wird in der Liste der CssMetaData nach einem Eintrag gesucht, welcher der gegebenen CSS-Eigenschaft (z.B. saxsys-my-field) entspricht
    • es ist darauf zu achten hier keine existierenden Namen zu nutzen, da man sonst bestehende Styles überschreibt oder der eigene Style falsch ausgewertet wird
    • am besten nicht den -fx- Präfix nutzen, sondern einen Eigenen (z.B -saxsys-)
  • aus dem ermittelten CssMetaData-Eintrag wird nun die Styleable Property gelesen und unter Verwendung eines Converters der Wert in den nötigen Typ umgewandelt und auf die Styleable Property angewendet

 

CssMetaData

Wie zu sehen ist, benötigt jede Styleable Property ein CssMetaData<? extends Styleable, ?> Objekt. Diese dient der Zuordnung der Styleable Property zu dem entsprechenden CSS-Element, sowie der Zuordnung, welcher Converter genutzt wird. Innerhalb der Klasse müssen zwei Methoden überschrieben werden, damit beim Lookup der CSS-Datei die Werte den entsprechenden Properties zugeordnet werden können.

Die Methode public boolean isSettable(<MYCONTROLTYPE>) bestimmt, ob ein Style im CSS überhaupt auf die Styleable Property angewendet werden kann. Dies ist nur der Fall, wenn die Property bereits initialisiert wurde (falls eine Lazy Implementation genutzt wurde) und kein Binding für die Property existiert, da man sonst das Binding beeinflusst, was in einer Runtime Exception resultiert. Die Übergabe der Styleable Property – auf welche der CSS-Wert angewendet werden soll – erfolgt in der Methode public StyleableProperty<TYPE> getStyleablePoperty(MYCONTROLTYPE). Hier ist sicherzustellen, dass die korrekte Property übergeben wird.

Nutzung von Convertern

Wie bereits oben angemerkt wird ein Konverter benötigt, mit dem man den aus der CSS-Datei ausgelesenen Wert in den entsprechenden Typ konvertieren muss. Im Folgenden ein Codebeispiel bei welchem der CSS-Wert saxsys-my-field: ten; in MyClass konvertiert wird:

CSS-Datei

.myPane
{
   -saxsys-my-field: ten;
}

MyClass

public class MyClass
{
  private int blub;

  public MyClass(int a)
  {
     blub = a;
  }
}

MyClassConverter

In der Methode public <TDesiredType> convert(ParsedValue<<TSourceType>, <TDesiredType>> value, Font) der Converter-Klasse erfolgt die eigentliche Konvertierung des CSS-Wertes. Es besteht beim Verarbeiten der CSS-Datei jedoch Klärungsbedarf – wie etwa das Insets Properties mit Komma getrennt innerhalb der CSS-Datei angegeben werden können und wie die Verarbeitung dieser Werte beim Parsen der CSS-Datei ausgewertet werden – weshalb der Punkt der Konvertierung hier nur auf die einfachste Art beschrieben wird. Nach bisherigen Erkenntnissen wird nur der erste gefundene CSS-Wert genutzt. Sobald ein Leerzeichen auftritt, wird abgebrochen. Falls also mehrere Werte angegeben werden, müssen diese mit Anführungszeichen umfasst werden.

Beispiel:

  • saxsys-my-field: ten; -> wird zu ten
  • saxsys-my-field: ten eleven; -> wird zu ten da nach dem ersten Leerzeichen abgebrochen wird
  • saxsys-my-field: "ten eleven"; -> wird zu ten eleven da es mit Anführungszeichen umfasst ist

 

/**
* this class will convert incoming strings from a css file to MyClass elements
* were are using an eager singleton implementation here, but this is by no means necessary ofc, its just for convenience
**/
public class MyClassConverter extends StyleConverterImpl<String, MyClass>
{
  static final MyClassConverter INSTANCE = new MyClassConverter();

  public static StyleConverter<String, MyClass> getInstance()
  {
     return INSTANCE;
  }

  private MyClassConverter()
  {
     super();
  }

  @Override public MyClass convert(ParsedValue<String, MyClass> value, Font font)
  {
     int valueMyClass = 0;

     if(value.getValue().equals("ten"))
       valueMyClass = 10;

     return new MyClass(valueMyClass);
  }
}

Nutzung im Scene Builder

Innerhalb des Scene Builder sind die neuen Komponenten unter dem Reiter Properties -> Custom zu finden. Wird eine solche Komponente eingesetzt, sind die Properties im Inspector (rechts) zu sehen. Es ist jedoch aus Sicht der Softwarearchitektur ratsam, die Konfiguration über CSS vorzunehmen.

JavaFX-Styleable-Properties-02

Abbildung 2 – Scene Builder in dem myPane genutzt wird, rechts die verfügbaren Properties.

Fazit

Durch den Einsatz von StyleableProperties unter JavaFX können Custom Elements flexibel mit CSS gestaltet werden. Allerdings muss man den Aufwand/Nutzen betrachten, denn der Haupt-Anwendungsfall sind Komponenten mit einem hohen Grad der Wiederverwendung und Flexibilität. Wenn diese Vorteile nicht benötigt werden, ist der Mehraufwand nicht immer gerechtfertigt.

Rico Hentschel ist Software-Entwickler bei der Saxonia Systems AG und hat bisher hauptsächlich in der Sprache C# Anwendungen entwickelt. Seit einiger Zeit beschäftigt er sich mit der Implementierung von Oberflächen mithilfe von JavaFX.

LinkedIn Xing 

TeilenTweet about this on TwitterShare on Facebook0Share on Google+0Share on LinkedIn0