Статьи

TableNode: создание пользовательской прокручиваемой таблицы в JavaFX

В июле 2008 года я основал категорию JFX Custom Nodes, которая содержит растущую серию публикаций, в которых графический дизайнер (Марк Дингман из Malden Labs) и я работаем над воображаемым приложением Sound Beans . Цели создания этого приложения состоят в том, чтобы продемонстрировать, как создавать пользовательские узлы JavaFX, и предоставить пример того, как графический дизайнер и разработчик приложений могут эффективно работать вместе при разработке приложений JavaFX. 

В первом посте из этой серии, « Прокручиваем свои собственные JavaFX« Пользовательские узлы »: пример графического меню» , показано, как создавать свои собственные элементы управления пользовательским интерфейсом в JavaFX. В этом посте мы определили пользовательские узлы MenuNode и ButtonNode, чтобы вы могли легко создавать меню, состоящие из кнопок, которые появляются и расширяются при наведении на них курсора мыши. Последующие посты в этой серии имеют:

  • определил DeckNode, который хранит набор экземпляров Node и отображает один из этих узлов одновременно.
  • определил элемент управления ProgressNode, который можно использовать для отображения хода выполнения операции. В этом посте также появился давно назревший класс модели в приложении Sound Beans.

В сегодняшнем посте мы собираемся создать собственное имя узла TableNode , целью которого является предоставление прокручиваемой таблицы, строки которой можно просматривать и выбирать. Каждая ячейка в таблице может содержать подкласс Node , так что это соответствует подходу, ориентированному на узлы, который будет использован JavaFX SDK 1.0. Кстати, я ожидаю, что JavaFX SDK 1.0 будет иметь какой-то элемент управления пользовательским интерфейсом таблицы. В любом случае, вот скриншот TableNode , используемого в нашей воображаемой программе Sound Beans :

Tablenodeexample


Это основано на компиляции плейлиста (макете), которую дал мне Марк Дингман, показанной в публикации Getting Decked: Another JavaFX Custom Node . Впоследствии я попросил у него композицию полосы прокрутки, которую я мог бы реализовать, рисуя фигуры (в отличие от использования изображений). Композиция Марка включала скругленный прямоугольник для пропорционального большого пальца полосы прокрутки, показанного выше, с дорожкой полосы прокрутки, имеющей небольшой горизонтальный градиент.

В этой итерации нашей воображаемой программы Sound Beans число в верхнем левом углу пользовательского интерфейса будет меняться при щелчке по различным строкам в таблице, демонстрируя, что вы можете привязаться к атрибуту selectedIndex TableNode . В будущих итерациях мы будем изменять графику альбома, название и т. Д., И мы будем указывать это число в верхнем левом углу. В любом случае, попробуйте, нажав на эту ссылку Java Web Start, имея в виду, что вам понадобится как минимум JRE 6. Кроме того, установка Java SE 6, обновление 10 , ускорит развертывание.

Webstartsmall2

Вот код для пользовательского узла TableNode в файле с именем TableNode.fx :

/*
* TableNode.fx -
* A custom node that contains rows and columns, each cell
* containing a node.
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to demonstrate how to create custom nodes in JavaFX
*/

package com.javafxpert.custom_node;

import javafx.input.*;
import javafx.scene.*;
import javafx.scene.geometry.*;
import javafx.scene.paint.*;
import javafx.scene.transform.*;
import java.lang.System;

/*
* A custom node that contains rows and columns, each cell
* containing a node. Column widths may be set individually,
* and the height of the rows can be set. In addition, several
* other attributes such as width and color of the scrollbar
* may be set. The scrollbar will show only when necessary,
* and overlays the right side of each row, so the rightmost
* column should be given plenty of room to display data and
* a scrollbar.
*/
public class TableNode extends CustomNode {

/*
* Contains the height of the table in pixels.
*/
public attribute height:Integer = 200;

/*
* Contains the height of each row in pixels.
*/
public attribute rowHeight:Integer;

/*
* A sequence containing the column widths in pixels. The
* number of elements in the sequence determines the number of
* columns in the table.
*/
public attribute columnWidths:Integer[];

/*
* A sequence containing the nodes in the cells. The nodes are
* placed from left to right, continuing to the next row when
* the current row is filled.
*/
public attribute content:Node[];

/*
* The selected row number (zero-based)
*/
public attribute selectedIndex:Integer;

/*
* The height (in pixels) of the space between rows of the table.
* This space will be filled with the tableFill color.
*/
public attribute rowSpacing:Integer = 1;

/*
* The background color of the table
*/
public attribute tableFill:Paint;

/*
* The background color of an unselected row
*/
public attribute rowFill:Paint;

/*
* The background color of a selected row
*/
public attribute selectedRowFill:Paint;

/*
* The color or gradient of the vertical scrollbar.
*/
public attribute vertScrollbarFill:Paint = Color.BLACK;

/*
* The color or gradient of the vertical scrollbar thumb.
*/
public attribute vertScrollbarThumbFill:Paint = Color.WHITE;

/*
* The width (in pixels) of the vertical scrollbar.
*/
public attribute vertScrollbarWidth:Integer = 20;

/*
* The number of pixels from the left of a cell to place the node
*/
private attribute cellHorizMargin:Integer = 10;

/*
* Contains the width of the table in pixels. This is currently a
* calculated value based upon the specified column widths
*/
private attribute width:Integer = bind
computePosition(columnWidths, sizeof columnWidths);

private function computePosition(sizes:Integer[], element:Integer) {
var position = 0;
if (sizeof sizes > 1) {
for (i in [0..element - 1]) {
position += sizes[i];
}
}
return position;
}

/**
* The onSelectionChange function attribute that is executed when the
* a row is selected
*/
public attribute onSelectionChange:function(row:Integer):Void;

/**
* Create the Node
*/
public function create():Node {
var numRows = sizeof content / sizeof columnWidths;
var tableContentsNode:Group;
var needScrollbar:Boolean = bind (rowHeight + rowSpacing) * numRows > height;
Group {
var thumbStartY = 0.0;
var thumbEndY = 0.0;
var thumb:Rectangle;
var track:Rectangle;
var rowRef:Group;
content: [
for (row in [0..numRows - 1], colWidth in columnWidths) {
Group {
transform: bind
Translate.translate(computePosition(columnWidths, indexof colWidth) +
cellHorizMargin,
((rowHeight + rowSpacing) * row) + (-1.0 * thumbEndY *
((rowHeight + rowSpacing) * numRows) / height))
content: bind [
Rectangle {
width: colWidth
height: rowHeight
fill: if (indexof row == selectedIndex)
selectedRowFill
else
rowFill
},
Line {
startX: 0
startY: 0
endX: colWidth
endY: 0
strokeWidth: rowSpacing
stroke: tableFill
},
rowRef = Group {
var node =
content[indexof row * (sizeof columnWidths) + indexof colWidth];
transform: bind Translate.translate(0, rowHeight / 2 -
node.getHeight() / 2)
content: node
}
]
onMouseClicked:
function (me:MouseEvent) {
selectedIndex = row;
onSelectionChange(row);
}
}
},
// Scrollbar
if (needScrollbar)
Group {
transform: bind Translate.translate(width - vertScrollbarWidth, 0)
content: [
track = Rectangle {
x: 0
y: 0
width: vertScrollbarWidth
height: bind height
fill: vertScrollbarFill
},
//Scrollbar thumb
thumb = Rectangle {
x: 0
y: bind thumbEndY
width: vertScrollbarWidth
height: bind 1.0 * height / ((rowHeight + rowSpacing) * numRows) * height
fill: vertScrollbarThumbFill
arcHeight: 10
arcWidth: 10
onMousePressed: function(e:MouseEvent):Void {
thumbStartY = e.getDragY() - thumbEndY;
}
onMouseDragged: function(e:MouseEvent):Void {
var tempY = e.getDragY() - thumbStartY;
// Keep the scroll thumb within the bounds of the scrollbar
if (tempY >=0 and tempY + thumb.getHeight() <= track.getHeight()) {
thumbEndY = tempY;
}
else if (tempY < 0) {
thumbEndY = 0;
}
else {
thumbEndY = track.getHeight() - thumb.getHeight();
}
}
onMouseDragged: function(e:MouseEvent):Void {
var tempY = e.getDragY() - thumbStartY;
// Keep the scroll thumb within the bounds of the scrollbar
if (tempY >=0 and tempY + thumb.getHeight() <= track.getHeight()) {
thumbEndY = tempY;
}
else if (tempY < 0) {
thumbEndY = 0;
}
else {
thumbEndY = track.getHeight() - thumb.getHeight();
}
}
}
]
}
else
null
]
clip:
Rectangle {
width: bind width
height: bind height
}
onMouseWheelMoved: function(e:MouseEvent):Void {
var tempY = thumbEndY + e.getWheelRotation() * 4;
// Keep the scroll thumb within the bounds of the scrollbar
if (tempY >=0 and tempY + thumb.getHeight() <= track.getHeight()) {
thumbEndY = tempY;
}
else if (tempY < 0) {
thumbEndY = 0;
}
else {
thumbEndY = track.getHeight() - thumb.getHeight();
}
}
}
}
}

 

Как видно из общедоступных атрибутов, разработчик может настроить несколько атрибутов TableNode , включая высоту таблицы, высоту строк, ширину каждого отдельного столбца, а также цвета или градиенты различных пользовательских интерфейсов. элементы. Обратите внимание, что код в конце списка обеспечивает поддержку колесика мыши. Теперь взглянем на основную программу, особенно на раздел, обозначенный The "Play" pageкомментарием, где создается экземпляр TableNode , в файле с именем TableNodeExampleMain.fx :

/*
* TableNodeExampleMain.fx -
* An example of using the TableNode custom node. It also demonstrates
* the ProgressNode, DeckNode, MenuNode and ButtonNode custom nodes
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to demonstrate how to create custom nodes in JavaFX
*/
package com.javafxpert.table_node_example.ui;

import javafx.application.*;
import javafx.ext.swing.*;
import javafx.scene.*;
import javafx.scene.geometry.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.transform.*;
import java.lang.Object;
import java.lang.System;
import com.javafxpert.custom_node.*;
import com.javafxpert.table_node_example.model.*;

var deckRef:DeckNode;

Frame {
var model = TableNodeExampleModel.getInstance();
var stageRef:Stage;
var menuRef:MenuNode;
title: "TableNode Example"
width: 500
height: 400
visible: true
stage:
stageRef = Stage {
fill: Color.BLACK
content: [
deckRef = DeckNode {
fadeInDur: 700ms
content: [
// The "Splash" page
Group {
var vboxRef:VBox;
var splashFont =
Font {
name: "Sans serif"
style: FontStyle.BOLD
size: 12
};
id: "Splash"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/splashpage.png"
}
},
vboxRef = VBox {
translateX: bind stageRef.width - vboxRef.getWidth() - 10
translateY: 215
spacing: 1
content: [
Text {
content: "A Fictitious Audio Application that Demonstrates"
fill: Color.WHITE
font: splashFont
},
Text {
content: "Creating JavaFX Custom Nodes"
fill: Color.WHITE
font: splashFont
},
Text {
content: "Application Developer: Jim Weaver"
fill: Color.WHITE
font: splashFont
},
Text {
content: "Graphics Designer: Mark Dingman"
fill: Color.WHITE
font: splashFont
},
]
}
]
},
// The "Play" page
VBox {
var tableNode:TableNode
id: "Play"
spacing: 4
content: [
Group {
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/playing_currently.png"
}
},
Text {
textOrigin: TextOrigin.TOP
content: bind "{tableNode.selectedIndex}"
font: Font {
size: 24
}
}
]
},
tableNode = TableNode {
height: 135
rowHeight: 25
rowSpacing: 2
columnWidths: [150, 247, 25, 70]
tableFill: Color.BLACK
rowFill: Color.rgb(28, 28, 28)
selectedRowFill: Color.rgb(45, 45, 45)
selectedIndex: -1
vertScrollbarWidth: 20
vertScrollbarFill: LinearGradient {
startX: 0.0
startY: 0.0
endX: 1.0
endY: 0.0
stops: [
Stop {
offset: 0.0
color: Color.rgb(11, 11, 11)
},
Stop {
offset: 1.0
color: Color.rgb(52, 52, 52)
}
]
}
vertScrollbarThumbFill: Color.rgb(239, 239, 239)
content: bind
for (obj in model.playlistObjects) {
if (obj instanceof String)
Text {
textOrigin: TextOrigin.TOP
fill: Color.rgb(183, 183, 183)
content: obj as String
font:
Font {
size: 11
}
}
else if (obj instanceof Image)
ImageView {
image: obj as Image
}
else
null
}
onSelectionChange:
function(row:Integer):Void {
System.out.println("Table row #{row} selected");
}
}
]
},
// The "Burn" page
Group {
var vboxRef:VBox;
id: "Burn"
content: [
vboxRef = VBox {
translateX: bind stageRef.width / 2 - vboxRef.getWidth() / 2
translateY: bind stageRef.height / 2 - vboxRef.getHeight() / 2
spacing: 15
content: [
Text {
textOrigin: TextOrigin.TOP
content: "Burning custom playlist to CD..."
font:
Font {
name: "Sans serif"
style: FontStyle.PLAIN
size: 22
}
fill: Color.rgb(211, 211, 211)
},
ProgressNode {
width: 430
height: 15
progressPercentColor: Color.rgb(191, 223, 239)
progressTextColor: Color.rgb(12, 21, 21)
progressText: bind "{model.remainingBurnTime} Remaining"
progressFill:
LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
stops: [
Stop {
offset: 0.0
color: Color.rgb(0, 192, 255)
},
Stop {
offset: 0.20
color: Color.rgb(0, 172, 234)
},
Stop {
offset: 1.0
color: Color.rgb(0, 112, 174)
},
]
}
barFill:
LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
stops: [
Stop {
offset: 0.0
color: Color.rgb(112, 112, 112)
},
Stop {
offset: 1.0
color: Color.rgb(88, 88, 88)
},
]
}
progress: bind model.burnProgressPercent / 100.0
},
ComponentView {
component:
FlowPanel {
background: Color.BLACK
content: [
Label {
text: "Slide to simulate burn progress:"
foreground: Color.rgb(211, 211, 211)
},
Slider {
orientation: Orientation.HORIZONTAL
minimum: 0
maximum: 100
value: bind model.burnProgressPercent with inverse
preferredSize: [200, 20]
}
]
}
}
]
}
]
},
// The "Config" page
Group {
id: "Config"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/config.png"
}
}
]
},
// The "Help" page
Group {
id: "Help"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/help.png"
}
}
]
}
]
},
menuRef = MenuNode {
translateX: bind stageRef.width / 2 - menuRef.getWidth() / 2
translateY: bind stageRef.height - menuRef.getHeight()
buttons: [
ButtonNode {
title: "Play"
imageURL: "{__DIR__}icons/play.png"
action:
function():Void {
deckRef.visibleNodeId = "Play";
}
},
ButtonNode {
title: "Burn"
imageURL: "{__DIR__}icons/burn.png"
action:
function():Void {
deckRef.visibleNodeId = "Burn";
}
},
ButtonNode {
title: "Config"
imageURL: "{__DIR__}icons/config.png"
action:
function():Void {
deckRef.visibleNodeId = "Config";
}
},
ButtonNode {
title: "Help"
imageURL: "{__DIR__}icons/help.png"
action:
function():Void {
deckRef.visibleNodeId = "Help";
}
},
]
}
]
}
}

deckRef.visibleNodeId = "Splash";

Модель за пользовательским интерфейсом

Поскольку «способ JavaFX» заключается в связывании атрибутов пользовательского интерфейса с моделью, атрибут содержимого TableNode привязывается к модели, как показано выше. Ниже показана модель для программы Sound Beans в файле с именем TableNodeExampleModel.fx . Обратите внимание, что последовательность playlistObjects может содержать объекты любого типа, и что мы специально избегаем помещать экземпляры Node в модель (так как они принадлежат пользовательскому интерфейсу). Поэтому для заполнения TableNode я использую подход, в котором модель содержит строки, такие как названия альбомов и URL-адрес изображения. Во время привязки к атрибуту содержимого TableModel, показанного выше, подклассы узла (например, тексти ImageView ) созданы.

/*
* TableNodeExampleModel.fx -
* The model behind the TableNode example
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
*/
package com.javafxpert.table_node_example.model;

import java.lang.Object;
import javafx.scene.*;
import javafx.scene.image.*;
import javafx.scene.text.*;

/**
* The model behind the TableNode example
*/
public class TableNodeExampleModel {

/**
* The total estimated number of seconds for the burn.
* For this example program, we'll set it to 10 minutes
*/
public attribute estimatedBurnTime:Integer = 600;

/**
* The percent progress of the CD burn, represented by a number
* between 0 and 100 inclusive.
*/
public attribute burnProgressPercent:Integer on replace {
var remainingSeconds = estimatedBurnTime * (burnProgressPercent / 100.0) as Integer;
remainingBurnTime = "{remainingSeconds / 60}:{%02d (remainingSeconds mod 60)}";
};

/**
* The time remaining on the CD burn, expressed as a String in mm:ss
*/
public attribute remainingBurnTime:String;

/**
* An image of a play button to be displayed in each row of the table
*/
private attribute playBtnImage = Image {url: "{__DIR__}images/play-btn.png"};

/**
* The song information in the playlist
*/
public attribute playlistObjects:Object[] =
["Who'll Stop the Rain", "Three Sides Now", playBtnImage, "2:43",
"Jackie Blue", "Ozark Mountain Devils", playBtnImage, "2:15",
"Come and Get Your Love", "Redbone", playBtnImage, "3:22",
"Love Machine", "Miracles", playBtnImage, "2:56",
"25 or 6 to 4", "Chicago", playBtnImage, "3:02",
"Free Bird", "Lynard Skynard", playBtnImage, "5:00",
"Riding the Storm Out", "REO Speedwagon", playBtnImage, "3:00",
"Lay it on the Line", "Triumph", playBtnImage, "2:00",
"Secret World", "Peter Gabriel", playBtnImage, "4:00"];



//-----------------Use Singleton pattern to get model instance -----------------------
private static attribute instance:TableNodeExampleModel;

public static function getInstance():TableNodeExampleModel {
if (instance == null) {
instance = TableNodeExampleModel {};
}
else {
instance;
}
}
}

Как всегда, если у вас есть какие-либо вопросы или предложения, пожалуйста, оставьте комментарий. Кстати, изображения для этой статьи можно загрузить, чтобы вы могли построить и запустить этот пример с графикой. Это zip-файл, который можно развернуть в пути к классам проекта. Вам понадобится код ButtonNode , MenuNode , DeckNode и ProgressNode из предыдущих постов этой серии JFX Custom Nodes .


Есть вопросы по JavaFX?

Ну, тебе повезло! С 18 по 22 августа 2008 г. будет проходить « Спросите экспертов: предварительный просмотр JavaFX» .
Вы можете оставить свои вопросы во время этого сеанса и получить ответы от ключевых членов команды разработчиков JavaFX от Sun.

С уважением,
Джим Уивер
JavaFXpert.com