Статьи

Викинги и волшебники в JavaFX

У меня есть две цели для сегодняшней статьи:

  1. Продолжайте учить вас, как создавать пользовательские элементы управления в JavaFX. Этот урок является новым дополнением к категории пользовательских узлов JFX и предоставляет инфраструктуру, на которой вы можете легко создавать «волшебников».
  2. Выразите свою благодарность комитету JavaZone за приглашение выступить по JavaFX на JavaZone 2008 в Осло, Норвегия, который состоится 17 и 18 сентября.

[Img_assist | NID = 5028 | название = | убывание = | ссылка = нет | ALIGN = слева | ширина = 300 | высота = 70]

 

 

 

Для достижения обеих целей я создал «волшебника» (в духе веселья), в котором вы можете узнать, являетесь ли вы викингом или нет. Посетив Норвегию в прошлом, я обнаружил, что норвежцы, как правило, очень гордятся своим наследием викингов. Программа в этом посте была создана из уважения к этому настроению, и я проконсультировался с одним из норвежских организаторов JavaZone для забавных идей для включения в этот мастер. Как и в остальной части серии, Марк Дингман из Malden Labs создал графические макеты и активы. Вот несколько скриншотов мастера, который я называю «Ты викинг?»

[Img_assist | NID = 5073 | название = | убывание = | ссылка = нет | Align = нет | ширина = 505 | Высота = 400]

 

Как показано на предыдущем снимке экрана, этот мастер задает вопросы пользователю. Ответ пользователя на заданный вопрос определяет, какая страница мастера будет показана далее. Если вы ответите на вопросы, как настоящий викинг, то мастер поздравит вас со страницей, показанной на скриншоте ниже:

[Img_assist | NID = 5074 | название = | убывание = | ссылка = нет | ALIGN = слева | ширина = 505 | Высота = 400]

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Идите и попробуйте программу . Вам понадобится JRE 6, и, пожалуйста, обратите внимание, что Java SE 6 Update 10 обеспечит вам более быстрое развертывание.

Создание мастера

В дополнение к использованию некоторых классов из категории пользовательских узлов JFX блога (а именно классов MenuNode , ButtonNode и DeckNode ) в этом примере представлены еще три класса: WizardNode, WizardPoint и OptionsNode. Прежде чем показывать вам код для этих новых классов, я бы хотел, чтобы вы увидели основную программу в этом «Вы викинг?» Пример, который находится в файле с именем WizardNodeExampleMain.fx :

/*
* WizardNodeExampleMain.fx -
* An example of using the WizardNode custom node. It also demonstrates
* the OptionsNode, MenuNode and ButtonNode custom nodes, as well as the
* WizardPoint class.
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to demonstrate how to create custom nodes in JavaFX
*/
package com.javafxpert.wizard_node_example.ui;

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

var wizardRef:WizardNode;

Frame {
var stageRef:Stage;
var menuRef:MenuNode;
var pageFont =
Font {
name: "Sans serif"
size: 28
};
title: "WizardNode Example"
width: 505
height: 400
visible: true
stage:
stageRef = Stage {
fill: Color.BLACK
content: [
wizardRef = WizardNode {
flowPath:
WizardPoint {
nodeID: "Shaving" // The id attribute of the Node to be displayed
nextPoints: [
WizardPoint {
advanceState: "ElectricRazor"
nodeID: "NotViking"
},
WizardPoint {
advanceState: "SafetyRazor"
nodeID: "NotViking"
},
WizardPoint {
advanceState: "StraightRazor"
nodeID: "NotViking"
},
WizardPoint {
advanceState: "ShaveWithSword"
nodeID: "DiscoveredAmerica"
nextPoints: [
WizardPoint {
advanceState: "ChristopherColumbus"
nodeID: "NotViking"
},
WizardPoint {
advanceState: "LeifEricson"
nodeID: "WearHelmetHowOften"
nextPoints: [
WizardPoint {
advanceState: "AlwaysWearsHelmet"
nodeID: "OwnWoodenBoat"
nextPoints: [
WizardPoint {
advanceState: "FiberglassBoat"
nodeID: "NotViking"
},
WizardPoint {
advanceState: "WoodenBoat"
nodeID: "IsViking"
},
WizardPoint {
advanceState: "NoBoat"
nodeID: "NotViking"
},
]
},
WizardPoint {
advanceState: "WearsHelmetOnOccasions"
nodeID: "NotViking"
},
WizardPoint {
advanceState: "WearsHelmetOncePerWeek"
nodeID: "NotViking"
},
WizardPoint {
advanceState: "NeverWearsHelmet"
nodeID: "NotViking"
},
]
},
WizardPoint {
advanceState: "TheBeatles"
nodeID: "NotViking"
},
]
},
]
}
fadeInDur: 700ms
canCancel: true
canFinish: false
backgroundNode:
Group {
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/viking_jprep_background.png"
}
},
ImageView {
image:
Image {
url: "{__DIR__}images/viking_jprep_helmet.png"
}
},
]
}
content: [
Group {
var optionsNode:OptionsNode;
transform: Translate.translate(50, 130);
id: "Shaving"
content: [
optionsNode = OptionsNode {
heading: "How Do You Shave?"
options: [
"I shave with an electric razor",
"I shave with a safety razor",
"I shave with a straight razor",
"I shave with my sword",
]
optionAdvanceStates: [
"ElectricRazor",
"SafetyRazor",
"StraightRazor",
"ShaveWithSword"
]
action:
function():Void {
wizardRef.candidateAdvanceState = optionsNode.optionAdvanceState;
}
}
]
},
Group {
var optionsNode:OptionsNode;
transform: Translate.translate(50, 130);
id: "DiscoveredAmerica"
content: [
optionsNode = OptionsNode {
heading: "Who Discovered America?"
options: [
"Christopher Columbus",
"Leif Ericson",
"The Beatles",
]
optionAdvanceStates: [
"ChristopherColumbus",
"LeifEricson",
"TheBeatles",
]
action:
function():Void {
wizardRef.candidateAdvanceState = optionsNode.optionAdvanceState;
}
}
]
},
Group {
var optionsNode:OptionsNode;
transform: Translate.translate(50, 130);
id: "WearHelmetHowOften"
content: [
optionsNode = OptionsNode {
heading: "How Often Do You Wear Your Helmet?"
options: [
"I always wear it, even in bed",
"Only on special occasions",
"Once a week",
"Never - it gives me Helmet Hair"
]
optionAdvanceStates: [
"AlwaysWearsHelmet",
"WearsHelmetOnOccasions",
"WearsHelmetOncePerWeek",
"NeverWearsHelmet",
]
action:
function():Void {
wizardRef.candidateAdvanceState = optionsNode.optionAdvanceState;
}
}
]
},
Group {
var optionsNode:OptionsNode;
transform: Translate.translate(50, 130);
id: "OwnWoodenBoat"
content: [
optionsNode = OptionsNode {
heading: "Do You Own a Wooden Boat?"
options: [
"No, mine is fiberglass",
"Of course it's made out of wood!",
"I don't own a boat",
]
optionAdvanceStates: [
"FiberglassBoat",
"WoodenBoat",
"NoBoat",
]
action:
function():Void {
wizardRef.candidateAdvanceState = optionsNode.optionAdvanceState;
}
}
]
},
HBox {
id: "IsViking"
transform: Translate.translate(50, 110);
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/viking_yes.png"
}
},
VBox {
transform: Translate.translate(0, 20);
spacing: 40
content: [
Text {
textOrigin: TextOrigin.TOP
content: "Congratulations!"
fill: Color.WHITE
font: pageFont
},
Text {
textOrigin: TextOrigin.TOP
content: "You are a Viking!"
fill: Color.WHITE
font: pageFont
},
]
}
]
},
HBox {
id: "NotViking"
transform: Translate.translate(50, 110);
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/viking_no.png"
}
},
VBox {
transform: Translate.translate(0, 20);
spacing: 40
content: [
Text {
textOrigin: TextOrigin.TOP
content: "Sorry! You are most"
fill: Color.WHITE
font: pageFont
},
Text {
textOrigin: TextOrigin.TOP
content: "likely not a Viking!"
fill: Color.WHITE
font: pageFont
},
]
}
]
},
]
},
menuRef = MenuNode {
translateX: bind stageRef.width / 2 - menuRef.getWidth() / 2
translateY: bind stageRef.height - menuRef.getHeight() * 0.75
buttons: [
ButtonNode {
imageURL: "{__DIR__}images/viking_jprep_btn_previous.png"
enabled: bind wizardRef.canGoBack
scale: 1.0
action:
function():Void {
wizardRef.goBack();
}
},
ButtonNode {
imageURL: "{__DIR__}images/viking_jprep_btn_next.png"
enabled: bind wizardRef.canAdvance
scale: 1.0
action:
function():Void {
wizardRef.advance();
}
},
ButtonNode {
imageURL: "{__DIR__}images/viking_jprep_btn_cancel.png"
enabled: bind wizardRef.canCancel
scale: 1.0
action:
function():Void {
wizardRef.reset();
}
},
ButtonNode {
imageURL: "{__DIR__}images/viking_jprep_btn_finish.png"
enabled: bind wizardRef.atEndpoint
scale: 1.0
action:
function():Void {
wizardRef.reset();
}
},
]
}
]
}
closeAction:
function():Void {
System.exit(0);
}
}

 

Как показано выше, одной из важных особенностей мастера является то, что я называю его траекторией потока . Flowpath атрибут WizardNode класса позволяет сформулировать все точки в потоке мастера, а также сформулировать пути навигации между точками. Каждая точка в пути потока мастера представлена инструментарием WizardPoint, а каждый путь навигации между точками представлен элементом в атрибуте последовательности nextPoints . NextPoints атрибут содержит последовательность WizardPoint экземпляров, каждый из которых держит в advanceStateстроковый атрибут, который позволяет перейти к этой точке. Как видно из приведенного выше списка, каждый экземпляр WizardPoint также содержит атрибут nodeID, который соответствует атрибуту id узла (содержится в атрибуте последовательности содержимого узла WizardNode ). Посмотрев на этот атрибут в приведенном выше листинге, вы заметите, что все страницы мастера ( экземпляры узлов ) хранятся в этой последовательности. Как вы вскоре увидите, именно класс WizardNode отвечает за отображение ассоциированного экземпляра Node после перехода к определенной точке мастера.

Еще один важный атрибут вКласс WizardNode является backgroundNode . Этот атрибут позволяет вам создать фон, который будет отображаться за каждой страницей мастера. Взгляните на листинг WizardNode.fx ниже и обратите внимание, что он расширяет класс DeckNode , который содержит функциональные возможности переключения между экземплярами Node по их атрибутам id.

/*
* WizardNode.fx -
* A custom node that functions as a "wizard", progressively displaying
* UI nodes according to a supplied "flow". Each node, typically as a result
* of user interaction, may set a state that determines which node of the flow
* will be displayed.
*
* 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.lang.*;
import java.lang.System;

/*
* A custom node that functions as a "wizard", progressively displaying
* UI nodes according to a supplied "flow". Each node, typically as a result
* of user interaction, may set a state that determines which node of the flow
* will be displayed.
*/
public class WizardNode extends DeckNode {

/**
* The flow of the wizard, expressed as a series of related
* WizardPoint instances
*/
public attribute flowPath:WizardPoint;

/**
* The current path that the user is taking through the wizard, expressed as
* a sequence of WizardPoint instances
*/
public attribute currentPath:WizardPoint[];

/**
* Determines whether the user can advance to the next wizard page
*/
public attribute canAdvance:Boolean = false;

/**
* Determines whether the user can go back to the previous wizard page
*/
public attribute canGoBack:Boolean = bind sizeof currentPath > 1;

/**
* Determines whether the user can cancel out of this wizard
*/
public attribute canCancel:Boolean;

/**
* Determines whether the user can finish this wizard
*/
public attribute canFinish:Boolean;

/**
* Determines whether the user is at and end point in the wizard,
* which is defined by not having any nextPoints
*/
public attribute atEndpoint:Boolean = bind
(sizeof currentPoint.nextPoints) == 0;

/**
* A reference to the WizardPoint instance that the user is currently on
*/
private attribute currentPoint:WizardPoint on replace {
if (currentPoint != null) {
visibleNodeId = currentPoint.nodeID;
canAdvance = false;
}
};

postinit {
reset();
}

/**
* Resets the wizard to its initial state
*/
public function reset():Void {
currentPoint = flowPath;
currentPath = [];
insert currentPoint into currentPath;
}

/**
* The requested advanceState to be navigated to from this point. If this is
* a valid advanceState, the canAdvance attribute is set to true, otherwise
* it is set to false
*/
public attribute candidateAdvanceState:String on replace {
if (candidateAdvanceState != "") {
var newPoint = currentPoint.getPointByAdvanceState(candidateAdvanceState);
canAdvance = newPoint != null;
}
}

/**
* Request that the current WizardPoint be changed to the candidateAdvanceState
* TODO: Consider throwing an exception if unsuccessful
*/
public function advance():Void {
var newPoint = currentPoint.getPointByAdvanceState(candidateAdvanceState);
if (newPoint != null) {
currentPoint = newPoint;

// Put this WizardPoint on the current path through the flow path
insert currentPoint into currentPath;
}
}

/**
* Request that the current WizardPoint be changed to the one that precedes
* it in the current path that the user has taken through the flow. In other
* words, go back.
* TODO: Consider throwing an exception if unsuccessful
*/
public function goBack():Void {
var idx = sizeof currentPath - 1;
if (idx > 0) {
delete currentPath[idx];
// Put this WizardPoint on the current path through the flow path
currentPoint = currentPath[idx - 1];
}
}
}

Вот список упомянутых ранее классов WizardPoint :

/*
* WizardPoint.fx -
* A point in the flow of a "wizard", that is related to a given wizard page
* (graphical Node). Wizard point instances are related to each other in
* a network of points that determines the potential flows within the wizard.
*
* 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 java.lang.System;

/**
* A point in the flow of a "wizard", that is related to a given wizard page
* (graphical Node). Wizard point instances are related to each other in
* a network of points that determines the potential flows within the wizard.
*/
public class WizardPoint {

/**
* The id attribute of the Node (wizard page) to be displayed
*/
public attribute nodeID:String;

/**
* A String that controls the condition by which this WizardPoint may be
* landed upon. If advanceState is an empty string, then this point may be
* landed upon by default
*/
public attribute advanceState:String;

/**
* The WizardPoint instances that may be navigated to from this point.
*/
public attribute nextPoints:WizardPoint[]; //TODO make sure that this creates an empty sequence, not a null reference

/**
* Returns the WizardPoint instance that may be navigated to from this point
* for a given advanceState
* TODO: Decide whether this is needed
*/
public function getPointByAdvanceState(advanceState:String):WizardPoint {
var points =
for (point in nextPoints where point.advanceState == advanceState) point;
if (sizeof points > 0) points[0]
else null;
}
}

Обратите также внимание на WizardNodeExampleMain.fx листинг было показано ранее , что я использую OptionsNode пользовательский узел в некоторых из страниц мастера. Этот класс позволяет быстро и легко сформулировать набор взаимоисключающих опций, а также заголовок списка опций. Он был разработан для использования с этой функциональностью мастера, так как он способен удерживать advanceState для каждого из параметров и сохранять advanceState для параметра, выбранного пользователем. Посмотрите на список для класса OptionsNode.fx ниже:

/*
* OptionsNode.fx -
* A custom node that enables the user to choose
* from a list of options.
*
* 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.ext.swing.*;
import javafx.scene.*;
import javafx.scene.paint.*;
import java.lang.System;

public class OptionsNode extends CustomNode {

/**
* A header for the options
*/
public attribute heading:String;

/**
* A sequence containing the choice of options
*/
public attribute options:String[];

/**
* A sequence containing the WizardNode advanceState associated with the
* options.
*/
public attribute optionAdvanceStates:String[];

/**
* The font to use for the heading
*/
public attribute headingFont =
Font {
name: "Sans serif"
size: 24
};

/**
* The font to use for the options
*/
public attribute optionsFont =
Font {
name: "Sans serif"
size: 22
};

/**
* The color to use for the text
*/
public attribute textColor = Color.rgb(253, 253, 253);

/**
* The advanceState associated with a selected option
*/
public attribute optionAdvanceState:String;

/**
* The action function attribute that is executed when the
* a selection is made
*/
public attribute action:function():Void;

/**
* Reset the selected radio buttons
*/
public function reset():Void {
toggleGroup.clearSelection();
}

/**
* Called when this page is shown in the wizard. Reset the radio button
* selections
*/
public function onShow():Void {
reset();
}

/**
* A reference to the ToggleGroup associated with the RadioButton instances
*/
private attribute toggleGroup:ToggleGroup = ToggleGroup{};

/**
* Create the Node
*/
public function create():Node {
ComponentView {
component:
GridPanel {
background:
Color.rgb(255, 255, 255, 0.0) // Transparent
rows: bind sizeof options + 1
columns: 1
content: bind [
Label {
text: heading
font: headingFont
foreground: textColor
},
for (option in options)
RadioButton {
var idx:Integer = indexof option;
toggleGroup: toggleGroup
text: option
font: optionsFont
foreground: textColor
action:
function():Void {
optionAdvanceState = optionAdvanceStates[idx];
action();
}
}
]
}
}
}
}

 

Всегда есть возможности для улучшения

Существует по крайней мере одна область функциональности, в которой я хотел бы внести улучшения: в настоящее время страница мастера является подклассом Node , но может быть полезно определить пользовательский узел (возможно, с именем WizardPageNode ) у него есть функции жизненного цикла и некоторые вспомогательные функции. Я приветствую ваши мысли по этому поводу, а также любые другие комментарии или вопросы, которые у вас есть. В любом случае, пожалуйста, оставьте комментарий.

Кроме того, если вы находитесь на JavaZone 2008, представьтесь, как я хотел бы с вами познакомиться!

Спасибо,
Джим Уивер
JavaFXpert.com