In JavaFx, I have done a TabPane with this appearance:
As you see, there is a blank on the right, at the space reserved to the arrow button in case of too short TabPane. I would like to fill the TabPane width entirely, without the blank.
Here is my code: HelloApplication.java:
package com.example.demo;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
HelloController.java:
package com.example.demo;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TabPane;
import javafx.scene.layout.BorderPane;
import java.net.URL;
import java.util.ResourceBundle;
public class HelloController implements Initializable {
private static final int GAP = 19;
@FXML
private TabPane tabPane = null;
@FXML
private BorderPane borderPane = null;
@Override
public void initialize(URL location, ResourceBundle resources) {
// Permits to change the width of the tabs to fit all the space.
tabPane.tabMinWidthProperty()
.bind(tabPane.widthProperty()
.divide(tabPane.getTabs()
.size())
.subtract(GAP));
}
}
hello-view.css:
.tab-header-area {
-fx-padding: 0 0 0 0;
}
.tab-header-background {
-fx-background-color: transparent;
}
.tab-down-button {
-fx-padding: -7;
}
.tab-down-button .arrow {
-fx-padding: -7;
}
.tab {
-fx-background-color: transparent;
-fx-border-width: 0 0 0 0;
-fx-background-radius: 0;
-fx-background-insets: 0;
-fx-focus-color: transparent;
-fx-faint-focus-color: transparent;
}
.tab-label {
-fx-font-size: 13px;
-fx-font-weight: bold;
}
.tab:hover {
-fx-background-color: cyan;
-fx-border-color: black;
-fx-border-width: 0 0 2 0;
}
.tab:pressed {
-fx-background-color: gray;
-fx-border-color: black;
-fx-border-width: 0 0 2 0;
}
.tab:selected {
-fx-background-color: blue;
-fx-border-color: black;
-fx-border-width: 0 0 2 0;
}
hello-view.fxml:
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import java.net.URL?>
<TabPane fx:id="tabPane" side="BOTTOM" tabClosingPolicy="UNAVAILABLE" xmlns="http://javafx.com/javafx/null" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.demo.HelloController">
<tabs>
<Tab fx:id="trackTab" text="Tracks">
<content>
<BorderPane fx:id="borderPane"/>
</content>
</Tab>
<Tab text="Volumes">
<content>
<AnchorPane minHeight="0.0" minWidth="0.0" />
</content>
</Tab>
</tabs>
<stylesheets>
<URL value="@hello-view.css" />
</stylesheets>
</TabPane>
In the controller, I use a binding to have the tabPane width and in the CSS, I try to make the arrow space disappear. Also I would like to get RID of the GAP constant. Have you got a solution ?
Potential approaches
Three possible solutions:
Style the existing tabs the way you want using CSS.
Create a new TabPaneSkin and associated CSS.
Implement your own custom layout, controls, and CSS for managing switching panes.
Custom pane switch implementation
Structure
VBox(contentPane, controlPane)
contentPane contains your switchable panes and is set to:
VBox.setVgrow(contentPane, Priority.ALWAYS);
controlPane provides the tab switching buttons:
HBox(new RadioButton("Tracks"), new RadioButton("Volumes"));
When a radio button is actioned, the contentPane is replaced by the appropriate pane for the button.
RadioButtons are used rather than ToggleButtons, so that, when a toggle group is assigned to the buttons, only one is selectable at a time.
The radio buttons have their radio-button
style removed and are styled like toggle buttons (via CSS) so they appear a bit more like a standard button.
Example Code
This example inlines the CSS rather than supplying a separate file, it also uses the fx:root
construct. You could have a separate CSS file and not use the fx:root
construct if you wish.
The fx:root and inline CSS constructs lack some useful tool support. If these features are not used, you get nicer WYSIWYG viewing in scene builder and improved intelligent editing in your IDE.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mixer</artifactId>
<version>1.0-SNAPSHOT</version>
<name>mixer</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>17.0.2</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>17.0.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
module-info.java
module com.example.mixer {
requires javafx.controls;
requires javafx.fxml;
opens com.example.mixer to javafx.fxml;
exports com.example.mixer;
}
MixerApp.java
package com.example.mixer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class MixerApp extends Application {
@Override
public void start(Stage stage) {
stage.setScene(new Scene(new Mixer()));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Mixer.java
package com.example.mixer;
import javafx.fxml.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import java.io.IOException;
import java.util.Map;
public class Mixer extends VBox {
private final Map<Toggle, Pane> paneMap;
private static final String CSS = """
data:text/css,
.mixer {
tracks-color: honeydew;
volumes-color: lemonchiffon;
}
.tracks-pane {
-fx-background-color: tracks-color;
-fx-font-size: 20px;
}
.volumes-pane {
-fx-background-color: volumes-color;
-fx-font-size: 20px;
}
.tracks-pane-selector {
-fx-base: tracks-color;
-fx-font-size: 16px;
}
.volumes-pane-selector {
-fx-base: volumes-color;
-fx-font-size: 16px;
}
""";
public Mixer() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("mixer.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
getStylesheets().add(CSS);
// we want the pane selectors styled as toggle-buttons rather than radio-buttons,
// so we remove their radio styles.
tracksPaneSelector.getStyleClass().remove("radio-button");
volumesPaneSelector.getStyleClass().remove("radio-button");
StackPane tracksPane = new StackPane(new Label("Tracks"));
tracksPane.getStyleClass().add("tracks-pane");
StackPane volumesPane = new StackPane(new Label("Volumes"));
volumesPane.getStyleClass().add("volumes-pane");
paneMap = Map.of(
tracksPaneSelector, tracksPane,
volumesPaneSelector, volumesPane
);
displaySelectedPane();
paneToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) ->
displaySelectedPane()
);
}
private void displaySelectedPane() {
contentPane.getChildren().setAll(
paneMap.get(paneToggleGroup.getSelectedToggle())
);
}
// FXML fields generated from skeleton.
@FXML
private StackPane contentPane;
@FXML
private HBox paneControls;
@FXML
private ToggleGroup paneToggleGroup;
@FXML
private RadioButton tracksPaneSelector;
@FXML
private RadioButton volumesPaneSelector;
}
mixer.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<fx:root fx:id="mixerLayout" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="150.0" prefWidth="300.0" styleClass="mixer" type="VBox" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1">
<children>
<StackPane fx:id="contentPane" VBox.vgrow="ALWAYS" />
<HBox fx:id="paneControls">
<children>
<RadioButton fx:id="tracksPaneSelector" maxWidth="1.7976931348623157E308" mnemonicParsing="false" selected="true" text="Tracks" HBox.hgrow="SOMETIMES">
<toggleGroup>
<ToggleGroup fx:id="paneToggleGroup" />
</toggleGroup>
<styleClass>
<String fx:value="toggle-button" />
<String fx:value="tracks-pane-selector" />
</styleClass>
</RadioButton>
<RadioButton fx:id="volumesPaneSelector" maxWidth="1.7976931348623157E308" mnemonicParsing="false" text="Volumes" toggleGroup="$paneToggleGroup" HBox.hgrow="SOMETIMES">
<styleClass>
<String fx:value="toggle-button" />
<String fx:value="volumes-pane-selector" />
</styleClass>
</RadioButton>
</children>
</HBox>
</children>
</fx:root>