javacssjavafxfxmltabpanel

Fill all the TabPane width with tabs in JavaFx


In JavaFx, I have done a TabPane with this appearance:

enter image description here

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 ?


Solution

  • Potential approaches

    Three possible solutions:

    1. Style the existing tabs the way you want using CSS.

      • As you have seen this is difficult.
      • It is also brittle if the default tab styles and skin implementations change in future versions.
    2. Create a new TabPaneSkin and associated CSS.

      • This is potentially less brittle as now you have your own skin implementation.
      • However the existing TabPaneSkin implementation is really complex and even trivial customization of it is very, very difficult.
    3. Implement your own custom layout, controls, and CSS for managing switching panes.

      • This is very stable as you are just relying on the basic standard public controls like buttons and layout panes.
      • This is extremely customizable as you are starting with a blank slate and then adding the functionality you desire.
      • The TabPane control has lots of in-built functionality around menus, dragging tabs, adding tabs, animating tabs, keyboard input support, etc.
        • In a custom implementation, you will lose all of this additional functionality.
        • But, you probably don't need or want that additional functionality anyway for many applications.
        • If you actually do need the additional functionality, then use either of the first two approaches, otherwise I suggest you use this approach.

    Custom pane switch implementation

    Structure

    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.

    tracks

    volumes

    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>