用 JavaFx 中的选项卡填充所有 TabPane 宽度

Fill all the TabPane width with tabs in JavaFx

在JavaFx中,我做了一个TabPane这个样子:

如你所见,space右边有一个空白,留给箭头按钮,以防TabPane太短。我想完全填充 TabPane 宽度,没有空白。

这是我的代码: 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));
    }
}

你好-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;
}

你好-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>

在控制器中,我使用绑定来设置 tabPane 宽度,在 CSS 中,我尝试使箭头 space 消失。我也想获得 GAP 常量的 RID。你有解决办法吗?

潜在方法

三种可能的解决方案:

  1. 使用 CSS.

    以您想要的方式设置现有选项卡的样式
    • 如您所见,这很难。
    • 如果默认选项卡样式和皮肤实现在未来版本中发生变化,它也会很脆弱。
  2. 创建一个新的 TabPaneSkin 并关联 CSS。

    • 这可能不那么脆弱,因为现在您有了自己的皮肤实现。
    • 然而,现有的 TabPaneSkin 实现非常复杂,即使是微不足道的定制也非常非常困难。
  3. 实施您自己的自定义布局、控件和 CSS 以管理切换窗格。

    • 这非常稳定,因为您只依赖基本的标准 public 控件,例如按钮和布局窗格。
    • 这是非常可定制的,因为您从空白开始,然后添加您想要的功能。
    • TabPane 控件在菜单、拖动选项卡、添加选项卡、动画选项卡、键盘输入支持等方面具有 很多 的 in-built 功能。
      • 在自定义实现中,您将失去所有这些附加功能。
      • 但是,对于许多应用程序,您可能不需要或不需要这些附加功能。
      • 如果您确实需要额外的功能,请使用前两种方法中的任何一种,否则我建议您使用这种方法。

自定义窗格开关实现

结构

  • VBox(contentPane, controlPane)

    • contentPane 包含您的可切换窗格并设置为:

      VBox.setVgrow(contentPane, Priority.ALWAYS);
      
    • controlPane提供标签切换按钮:

      HBox(new RadioButton("Tracks"), new RadioButton("Volumes"));
      

操作单选按钮时,contentPane 将替换为该按钮的相应窗格。

使用单选按钮而不是切换按钮,因此,当将切换组分配给按钮时,一次只能选择一个按钮。

单选按钮的 radio-button 样式已被删除,样式类似于切换按钮(通过 CSS),因此它们看起来更像标准按钮。

示例代码

此示例内联 CSS 而不是提供单独的文件,它还使用了 fx:root 结构。如果您愿意,您可以有一个单独的 CSS 文件而不使用 fx:root 结构。

fx:root 和内联 CSS 构造缺少一些有用的工具支持。如果不使用这些功能,您将在场景生成器中获得更好的所见即所得视图,并在 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>