JavaFX – Tabbed Pane & Tab Panel


JavaFX - Tabbed Pane & Tab Panel

JavaFX - Tabbed Pane & Tab Panel

Introduction

With each release of JavaFX, more components and controls are becoming available for developers to use when building applications. However, what if you needed a component that wasn’t included in the current JavaFX release? Well, you have a few options: you could Wait, Find, Pay or Build it yourself (or suck your thumb and rock back and forth in a fetal position underneath your desk).  The current version of JavaFX 1.3.x did not include a Tabbed Pane class, so I decided to create my own. My intent was to see how rapidly I could create a Tabbed Pane and Tab Panel that looked pretty decent.

Disclaimer: This implementation does not follow best practices (API design). So use at your own risk. I encourage the reader to take the code apart and to improve it as they see fit.
To launch the Tab Demo click button (Java Webstart App).

Tab Demo

Screen Shots

Tab Demo - Tab 0

Tab Demo - Tab 0

Tab Demo - Tab 1

Tab Demo - Tab 1

Tab Demo - Tab 2

Tab Demo - Tab 2

Source Code

Main.fx

Below is the main application which assembles a TabbedPane to be displayed in a Scene on the Stage.

package tabdemo;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.effect.DropShadow;
import javafx.scene.paint.Color;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.paint.RadialGradient;
import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextBox;
import javafx.animation.Timeline;
import javafx.animation.Interpolator;
import javafx.scene.shape.Rectangle;
import javafx.stage.StageStyle;
import javafx.scene.input.MouseEvent;
import javafx.scene.Group;
import javafx.scene.text.Text;
import javafx.scene.text.Font;
import tabdemo.tabs.TabPane;
import tabdemo.tabs.TabPanel;
import javafx.scene.paint.LinearGradient;
import javafx.scene.control.Button;

/**
 * @author cdea
 */
var stageWidth:Float = 450;
var stageHeight:Float = 400;

var tabPane:TabPane = TabPane {
  translateY: 20
};

var tab0:TabPanel = TabPanel {
  index:0
  title: "Tab 0"
  parentTabPane:tabPane
  content:[Circle {
            centerX: 100, centerY: 100
            radius: 40
            fill: RadialGradient {
                    centerX: 5
                    centerY: 5
                    focusX: 0.1
                    focusY: 0.1
                    radius: 10
                    stops: [
                      Stop {
                        color : Color.BISQUE
                        offset: 0.0
                      },
                      Stop {
                        color : Color.BLUE
                        offset: 1.0
                      },
                    ] // stops
           } // radial gradient
           effect: DropShadow {
              offsetX: 10
              offsetY: 10
              color: Color.BLACK
              radius: 10
           } // effect
         } // Circle
  ] // content
}

var firstNameRow = HBox {
  spacing:7
	content: [Label {
    text: "First Name:"
  },
  TextBox {
    selectOnFocus: true
  }
  ]
}
var lastNameRow = HBox {
  spacing:7
	content: [Label {
    text: "Last Name:"
  },
  TextBox {
    selectOnFocus: true
  }
  ]
}

var formView = VBox {
  spacing:7
  translateX:20
  translateY:100
	content: [firstNameRow,lastNameRow
  ]
}

// all widgets or controls to be laid out.

var tab1:TabPanel = TabPanel {
  index:1
  title: "Tab 1"
  parentTabPane:tabPane
  content:[formView]
}

def spinRect = Rectangle {
	translateX: 100,
  translateY: 160
	width: 80, height: 80
	fill: Color.BLUE
}

var animateThing = Timeline {
	repeatCount: 4
  autoReverse:true
	keyFrames: [
		at (1s) { spinRect.rotate => 100.0 tween Interpolator.EASEBOTH }
	];
}
animateThing.play();

var activateSpin = Button {
  translateX: 30
  translateY: 60
	text: "Click to Spin"
	action: function() {
    animateThing.playFromStart();
	}
}

var tab2:TabPanel = TabPanel {
  index:3
  title: "Tab 2"
  parentTabPane:tabPane
  content: [spinRect, activateSpin]
}

tabPane.content = [tab0, tab1, tab2];

var currentX:Number;
var currentY:Number;

var closeButton:Circle;

var titleGrabClose = Group {
  content : [
      // the window background
      Rectangle {
      x: 0, y: 0
      arcHeight:15
      arcWidth:15
      width: stageWidth, height: stageWidth
      fill: Color.WHITE
      opacity: 0.85
    },
    // grabby area
    Rectangle {
      x: 0, y: 0
      arcHeight:15
      arcWidth:15
      width: stageWidth, height: 20
      fill: LinearGradient {
        startX : 0.0
        startY : 0.0
        endX : 1.0
        endY : 0.0
        stops: [
          Stop {
            color : Color.LIGHTBLUE
            offset: 0.0
          },
          Stop {
            color : Color.BLUE
            offset: 1.0
          },

        ]
      }

      opacity: 80
      onMousePressed: function(e: MouseEvent): Void {
        currentX = e.screenX - stage.x;
        currentY = e.screenY - stage.y;
      }

      onMouseDragged: function(e: MouseEvent): Void {
        stage.x = e.screenX - currentX;
        stage.y = e.screenY - currentY;
      }
    },

    // window title text
    Text {
      font : Font {
        size: 18
        embolden:true
      }
      x: 10, y: 15
      content: "Tabs Demo"
    }

    // mouse close area
    closeButton = Circle {
      centerX: stageWidth - 10 , centerY: 10
      radius: 8
      fill: Color.WHITE
      onMousePressed: function(e: MouseEvent): Void {
        javafx.lang.FX.exit();
      }
    },

    // X marks the spot
    Text {
      font : Font {
        size: 14
      }
      x: (2+ stageWidth - closeButton.radius * 2)
      y: 15
      content: "X"
   }
  ] // content
} // titleGrabClose

var stage: Stage;
stage = Stage {
    style: StageStyle.TRANSPARENT
    title: "Slide Tab Demo"
    width: stageWidth
    height: stageHeight
    scene: Scene {
        width: stage.width
        height: stage.height
        content: [titleGrabClose, tabPane]
        fill:null
    };
    resizable:false
    opacity: 0.85
}

Code walk through of Main.fx

  • Lines 30-31 : initialize Stage
  • Lines 33-35: Create a TabbedPane really a JavaFX Stack
  • Lines 37-69: Create Tab 0 which contains a Circle
  • Lines 71-98: Create a simple form with Labels and controls
  • Lines 102-107: Create Tab 1 which contains the simple form
  • Lines 109-114: Create a Blue Rectangle to be put in Tab 2
  • Lines 116-122: Create a animator to control Blue Rectangle
  • Line 123: Start the animation
  • Lines 125-132: Create a button to reanimate the Blue Rectangle when pressed
  • Lines 134-139: Create Tab 2 which will contain the button and Blue Rectangle
  • Line 141: Put all tab panels into the tabbed pane object.
  • Lines 143-225: Create a drag-able title area with a close button on the upper right.
  • Lines 227-241: Display Stage containing TabPane and drag-able title.

tabs.fx

Below is the library that contains a TabPanel and TabbedPane classes.

package tabdemo;

import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Stack;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.ShapeSubtract;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextOrigin;

/**
 * @author cdea
 */
public class TabPanel extends CustomNode {
  public-init var index:Integer;
  public-init var title:String;
  public var parentTabPane:Stack;
  public var content:Node[] = [];
  // The clickable tab area
  def tabRect = Rectangle {
        x: 10 + ((4 + 50) * index), y: 10
        arcHeight:7
        arcWidth:7
        width: 50, height: 25
        opacity:0
        onMousePressed: function( e: MouseEvent ):Void {
          var tabs:Node[] = parentTabPane.content;
          for (tab in tabs) {
            var tabPanel:TabPanel = tab as TabPanel;
            if (index == tabPanel.index) {
              delete tab from parentTabPane.content;
              insert tab into parentTabPane.content;
              break;
            } // found so make it the top of stack [add to end]
          } // loop through all tab panels
        }
        onMouseEntered: function(e: MouseEvent): Void {
          println("Entered {title}");
        }
        onMouseExited: function(e: MouseEvent): Void {
          println("Exited {title}");
        }

  }

  // tab text
  def tabText = Text {
    content: title
    textOrigin: TextOrigin.TOP
    font: Font.font("Verdana", FontWeight.BOLD, 10);
    x: 15 + ((4 + 50) * index)
    y: 15
  }

  // merge or blend the bottom of tab with the main content region
  def blendBottomOfTab = Rectangle {
    x: 10 + ((4 + 50) * index), y: 10 + 20
    width: 50, height: 25
  }

  // content area background area
  def tabPanelContentArea = Rectangle {
    x: 10, y: 10 + 25
    arcHeight:15
    arcWidth:15
    width: 350, height: 300
  }

  def tabArea = ShapeSubtract {
    a: [  tabRect,
          blendBottomOfTab,
          tabPanelContentArea
    ] // union tab and content area

    fill: LinearGradient {
            startX: 0.0, startY: 0.0, endX: 1.0, endY: 1.0
            proportional: true
            stops: [
                Stop { offset: 0.0 color: Color.WHITE }
                Stop { offset: 1.0 color: Color.GRAY }
            ]
    } // fill
    stroke: Color.BLACK
    effect: DropShadow {offsetX: 2 offsetY: 4}
  }

  public override function create(): Node {
    return Group {
      content: [tabArea, tabRect, content, tabText]
    }
  }
}

public class TabPane extends Stack {}

Code walk through of tabs.fx

  • Line 22: Create a custom node representing a single Tab Panel
  • Line 23: Index is the position and order of a tab from left to right on a Tabbed Pane
  • Line 24: Tabs title text
  • Line 25: The owning Tabbed Pane
  • Line 26: The contents that would be displayed on this Tab Panel
  • Lines 28-52: The tab’s rectangular click region surrounding the tab title text region
  • Lines 55-61: The Text containing the title text to be overlaid on the click-able tab area
  • Lines 63-67: This is a simple rectangle to make the bottom of the click-able tab area connect smoothly to the main Tab Panel content region
  • Lines 70-75: This is the main Tab Panel content region
  • Lines 77-93: Using the ShapeSubtract on attribute ‘a’ union the three rectangles (Lines 28-75) to create a smooth looking Tab Panel
  • Lines 95-100: Creating and assembling the Tab Panel
  • Line 102: Class representing a Tabbed Pane

Conclusion

With many types of application contexts having variations of tabbed panes (Look-n-Feel) and numerous features (functionality), one might be hard pressed to find the right one for their application.  I find that it is very important in a GUI toolkit to be able to quickly prototype a control or container component before investing heavily on designing an advanced component with a good API.  JavaFX allows me to rapidly create a TabbedPane while adequately providing simple functionality.

Any feedback is welcome.

References

6 thoughts on “JavaFX – Tabbed Pane & Tab Panel

  1. Pingback: JavaFX links of the week, July 26 // JavaFX News, Demos and Insight // FX Experience

  2. Pingback: Java desktop links of the week, July 26 | Jonathan Giles

  3. Pingback: JavaPins

Leave a comment