juin
2014
Lorsque j’ai mentionné les graphes bitmap récemment, j’ai sous-entendu à la fin de mon post qu’il était possible d’utiliser des propriétés stylables pour configurer le rendu des séries dessinées à l’écran. En effet, désormais, ces séries n’étant plus liées à des nœuds graphiques, les CSS ne peuvent plus s’appliquer sur elles et donc on en est réduit à devoir coder leur couleur en dur dans le code. On pourrait créer des propriétés FX, ce qui permettrait de changer ces couleurs via l’API, mais alors ici aussi les CSS ne sauraient s’appliquer.
Cependant, en observant le guide de référence des CSS de JavaFX on peut voir que certaines classes de style disposent de sélecteurs CSS qui leur sont propres et ne proviennent pas de leurs parents. Par exemple, hbox et vbox disposent de -fx-spacing, grid-pane dispose de -fx-hgap et -fx-vgap. C’est donc qu’il doit être possible quelque part de définir de nouveaux sélecteurs !
Ceux qui ont utilisé JavaFX 1 se souviennent peut-être que dans la toutes première version, il était possible d’utiliser les CSS pour setter à peu près toutes les propriétés graphiques (couleur, taille des trait, type des traits, espacement, certains positionnement, etc.) de n’importe quel nœud custom qu’on était amené à créer. Si mes souvenirs ne me font pas défaut, on pouvait même initialiser des propriétés contenant des valeurs d’objet sans aucun rapport avec l’affichage. Au fil des évolutions cette fonctionnalité a été restreinte puis a disparu et aux alentours de JavaFX 1.2~1.3 on s’est retrouvé avec un set de sélecteurs fixe (ceux mentionnés actuellement dans le guide) pour les nœuds de l’API sans pouvoir en ajouter des nouveaux pour nos composants perso. Cela a été repris tel quel dans JavaFX 2.x : impossible de créer ses propres sélecteurs.
Cela pose quelques problèmes si on veut style des choses à notre convenance :
- Il faut extraire les fichier CSS de Caspian et Modena et les lire en détails pour comprendre comment sont agencés les skins des contrôles de base.
- Cela rend le CSS utilisé très dépendant du style utilisé (les skins Caspian et Modena ne sont pas forcement agencés à l’identique au niveau de leurs sous-nœuds et en plus peuvent changer à chaque nouvelle version de JavaFX).
- En plus, certaines de ces propriétés sont settables uniquement via le CSS et pas du tout par l’API ; dans d’autres cas c’est le problème inverse qui se pose.
- On reste limité aux sélecteurs de notre classe parente pour tout contrôle perso qu’on serait amené à créer.
- On est obligé d’exposer publiquement dans notre doc ou notre CSS les détails des sous-nœuds qu’on utilise.
Prenons un exemple tout simple ; nous allons créer un contrôle très basique : une zone rectangulaire dans laquelle on affiche deux lignes qui se croisent.
private final Line line1 = new Line();
private final Line line2 = new Line();
public MyControl() {
setId("MyControl"); // NOI18N.
getStyleClass().add("my-control"); // NOI18N.
getChildren().setAll(line1, line2);
final URL cssURL = getClass().getResource("MyControl.css"); // NOI18N.
getStylesheets().add(cssURL.toExternalForm());
}
@Override
protected void layoutChildren() {
super.layoutChildren();
final double width = getWidth();
final double height = getHeight();
final Insets insets = getInsets();
final double x1 = insets.getLeft();
final double y1 = insets.getTop();
final double x2 = width - (insets.getLeft() + insets.getRight());
final double y2 = height - (insets.getTop() + insets.getBottom());
line1.setStartX(x1);
line1.setStartY(y1);
line1.setEndX(x2);
line1.setEndY(y2);
line2.setStartX(x1);
line2.setStartY(y2);
line2.setEndX(x2);
line2.setEndY(y1);
}
}
Voici la feuille de style attachée à ce contrôle :
-fx-background-color: black, lightgray;
-fx-background-insets: 0px, 1px;
-fx-padding: 1px;
}
Et enfin affichons le tout à l’écran :
@Override
public void start(Stage primaryStage) {
final MyControl myControl = new MyControl();
myControl.setManaged(false);
myControl.resizeRelocate(100, 100, 300, 150);
final Pane root = new Pane();
root.getChildren().add(myControl);
final Scene scene = new Scene(root, 800, 800);
primaryStage.setTitle("Test"); // NOI18N.
primaryStage.setScene(scene);
primaryStage.show();
ScenicView.show(scene);
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
Ce qui nous donne le résultat suivant :
Nous souhaitons maintenant changer la couleur des lignes depuis la feuille de style. Une première approche consiste à définir une nouvelle classe de style pour chacune des lignes:
line2.getStyleClass().add("line"); // NOI18N.
Et ensuite de l’utiliser dans le fichier CSS :
-fx-stroke: red;
}
Les lignes croisées sont désormais rouge dans l’affichage.
Cette manière de procéder est simple à mettre en place et efficace mais elle a le désavantage du fait qu’il faille connaitre les détails de ce qui constitue le contenu de l’apparence de notre contrôle.
JavaFX 8 résout le problème en rendant désormais publique l’API CSS ; c’est à dire qu’on peut désormais faire à nouveau ce qu’on pouvait faire dans JavaFX 1.0 : définir nos propres sélecteurs et les lier à des propriétés accessibles depuis l’API. Par contre attention, il va falloir se retrousser les manches car c’est un travail de longue halène. Dans JavaFX 1, en langage JavaFX Script, tout était implicite, mais ici c’est loin d’être le cas…
Commençons par définir une nouvelle propriété au niveau de notre classe :
public final Paint getCrossLine() {
return crossLine.get();
}
public final void setCrossLine(final Paint value) {
crossLine.set(value);
}
public final ObjectProperty<Paint> crossLineProperty() {
return crossLine;
}
Et nous allons binder la couleur du trait de nos ligne sur cette propriété :
line2.strokeProperty().bind(crossLineProperty());
Désormais, nous pouvoir setter la couleur de nos ligne via l’API mais plus via les CSS ce qui s’avère ennuyeux. Il va donc nous falloir transformer cette propriété régulière en propriété stylable.
Commençons par définir une méta donnée CSS. Cette instance de la classe javafx.css.CssMetaData permet de faire le pont entre la feuille de style et la propriété : elle définit le sélecteur à utiliser et, si la propriété n’est pas bindée dans l’API, transfère tout changement de valeur du sélecteur dans le CSS vers cette propriété dans l’objet. La documentation de la classe conseille de créer des instances statiques finales compte tenu de la très grande fréquence d’utilisation de ces objets.
@Override
public boolean isSettable(final MyControl node) {
return !node.crossLineProperty().isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(final MyControl node) {
return (StyleableObjectProperty)node.crossLineProperty();
}
};
On notera qu’il faut fournir un convertisseur en paramètre qui permet, à partir du texte contenu dans le CSS, de parvenir à instancier le type d’objet stocké dans la propriété. La classe javafx.css.StyleConverter fourni des fabriques par défaut qui supportent les types actuellement mentionnés dans le guide de référence (nombre, booléen, police de caractères, chaine de caractères, énumération, couleur, peinture, bordure, taille, URL). Cela sous-entend également qu’il est probablement possible d’utiliser des convertisseurs customisés et de passer n’importe quel type de valeur via le CSS.
Il nous faut désormais modifier la définition de notre propriété comme suit :
Cependant, en l’état, cela ne fonctionne pas encore. Si on met dans le CSS :
-fx-background-color: black, lightgray;
-fx-background-insets: 0px, 1px;
-fx-padding: 1px;
-cross-line: green;
}
Nos ligne apparaissent toujours noires à l’écran…
Il manque un bout de code : la méta donnée de la propriété doit être insérée dans la liste des méta données CSS de notre classe. En effet, chaque classe de nœuds graphiques, à commencer par notre nœud parent, la classe Region, dispose d’une liste de propriétés stylables. Cette liste est accessible via la méthode statique getClassCssMetaData(). Chaque instance dispose également d’une telle liste accessible via la méthode getCssMetaData(). Évidement on s’arrangera pour retourner la même liste statique histoire de faire quelques économies. Nous allons commencer par récupérer la liste des méta données de la classe Region à laquelle nous allons ajouter la méta donnée de notre nouvelle propriété stylable. Il nous faut donc insérer le code suivant :
private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
static {
final List<CssMetaData<? extends Styleable, ?>> temp = new LinkedList(Region.getClassCssMetaData());
temp.add(CROSS_LINE_METADATA);
cssMetaDataList = Collections.unmodifiableList(temp);
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return cssMetaDataList;
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
Désormais notre controle se comporte comme prévu : au lancement les lignes sont enfin affichées en vert !
De plus, il nous est toujours possible de changer la couleur via l’API et même de binder cette propriété sur un autre contrôle (ex: la valeur d’un ColorPicker), ce qui prendra alors le pas sur la valeur établie via le CSS.
Voici le code complet de la classe:
private final Line line1 = new Line();
private final Line line2 = new Line();
public MyControl() {
setId("MyControl"); // NOI18N.
getStyleClass().add("my-control"); // NOI18N.
getChildren().setAll(line1, line2);
final URL cssURL = getClass().getResource("MyControl.css"); // NOI18N.
getStylesheets().add(cssURL.toExternalForm());
// line1.getStyleClass().add("line");
// line2.getStyleClass().add("line");
line1.strokeProperty().bind(crossLineProperty());
line2.strokeProperty().bind(crossLineProperty());
}
@Override
protected void layoutChildren() {
super.layoutChildren();
final double width = getWidth();
final double height = getHeight();
final Insets insets = getInsets();
final double x1 = insets.getLeft();
final double y1 = insets.getTop();
final double x2 = width - (insets.getLeft() + insets.getRight());
final double y2 = height - (insets.getTop() + insets.getBottom());
line1.setStartX(x1);
line1.setStartY(y1);
line1.setEndX(x2);
line1.setEndY(y2);
line2.setStartX(x1);
line2.setStartY(y2);
line2.setEndX(x2);
line2.setEndY(y1);
}
private static final CssMetaData<MyControl, Paint> CROSS_LINE_METADATA = new CssMetaData<MyControl, Paint>("-cross-line", StyleConverter.getPaintConverter(), Color.BLACK) { // NOI18N.
@Override
public boolean isSettable(final MyControl node) {
return !node.crossLineProperty().isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(final MyControl node) {
return (StyleableObjectProperty) node.crossLineProperty();
}
};
private final StyleableObjectProperty<Paint> crossLine = new SimpleStyleableObjectProperty<>(CROSS_LINE_METADATA, this, "crossLine", Color.BLACK); // NOI18N.
public final Paint getCrossLine() {
return crossLine.get();
}
public final void setCrossLine(final Paint value) {
crossLine.set(value);
}
public final ObjectProperty<Paint> crossLineProperty() {
return crossLine;
}
// List of CSS metadata for this class and instance.
private static final List<CssMetaData<? extends Styleable, ?>>
static {
final List<CssMetaData<? extends Styleable, ?>> temp = new LinkedList(Region.getClassCssMetaData());
temp.add(CROSS_LINE_METADATA);
cssMetaDataList = Collections.unmodifiableList(temp);
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return cssMetaDataList;
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
}
Ouf ! nous avons enfin pu définir une nouvelle propriété stylable. Par contre bonjour la quantité de code à écrire pour juste une seule propriété (je vous laisse imaginer quand on en a 10 ou plus). La bonne nouvelle c’est que David Grieve de l’équipe JavaFX bosse sur un système de factory qui devrait considérablement réduire la quantité de code nécessaire pour l’écriture d’une telle propriété. Ainsi sur un tweet récent, il indique :
Well on my way to reducing #JavaFX CSS boilerplate to StyleableProperty myFill = createStyleablePaintProperty(« -my-fill »);
On ne peut qu’espérer que ceci fasse rapidement son apparition dans JavaFX 8_u20 ou dans une mise à jour ultérieure.
Commentaires récents
- Back from the future… dans
- Back from the future… dans
- Static linking = does not Compute dans
- Paquetage x 2 dans
- Why you little… dans