2008년 7월 저는 그래픽 디자이너(Mark Dingman of Malden Labs)와 공동 작업으로 상상 속의 Sound Beans 애플리케이션을 만드는 연속 게시물이 포함된 JFX 사용자 지정 노드 카테고리를 시작했습니다.  이 애플리케이션을 작성하는 목표는 JavaFX 사용자 지정 노드를 만드는 방법을 보여주고, 그래픽 디자이너와 애플리케이션 개발자가 공동으로 JavaFX 애플리케이션을 효과적으로 개발할 수 있는 방법에 관한 사례 연구를 제공하기 위한 것입니다. 

이 연속 게시물의 첫 게시물인 자신만의 JavaFX "사용자 지정 노드" 만들기: 그래픽 메뉴의 예는 JavaFX에서 자신만의 UI를 만드는 방법을 보여줍니다. 해당 게시물에서는 마우스를 갖다댈 경우 밝아지고 확장되는 버튼으로 구성된 메뉴를 쉽게 만들 수 있도록 MenuNodeButtonNode 사용자 지정 노드를 정의했습니다.  이어지는 다음 게시물에서는 다음 내용을 다룹니다.

오늘의 게시물에서는 테이블의 행을 보고 선택할 수 있는 확장 가능한 테이블을 제공할 수 있도록 TableNode라는 이름의 사용자 지정 노드를 작성해보겠습니다. 테이블의 각 셀에는 Node의 하위 클래스를 포함할 수 있으므로 JavaFX SDK 1.0에서 사용할 노드 중심 방식으로 줄에 놓여질 수 있습니다. 또한, JavaFX SDK 1.0에 일종의 테이블 UI 컨트롤을 포함하려고 생각하고 있습니다. 다음은 상상 속의 Sound Beans 프로그램에서 사용하는 TableNode의 스크린샷입니다.

테이블


이것은 Mark Dingman이 제공한 재생 목록 comp(웹 사이트에 대한 종합 이미지, 모형)를 기초로 만든 것입니다(Getting Decked: Another JavaFX Custom Node post 참조). 그 다음에는 Mark에게 모양을 그려(이미지를 사용하는 것과는 다름) 구현할 수 있는 스크롤바 comp를 부탁했습니다. Mark의 comp에는 위에서 보듯이 수평 스크롤바 트랙과 함께 둥근 사각형 모양의 진행률 스크롤바 썸이 있습니다.

상상 속의 Sound Beans 프로그램에 대한 이번 반복에서는 테이블에서 다른 행을 클릭하면 UI의 왼쪽 위 모서리의 숫자가 변경되어 TableNodeselectedIndex 속성을 바인딩할 수 있음을 나타냅니다. 다음의 반복에서는 앨범 그래픽, 제목 등이 바뀌게 하고 왼쪽 위 모서리에 해당 번호가 나타나도록 할 것입니다. 이 Java Web Start 링크를 클릭하여 이 코드를 사용해보십시오. JRE 6 이상이 필요합니다. 또한 자바 SE 6 업데이트 10을 설치하면 배포 시간이 단축됩니다.

Web

다음은 TableNode.fx 파일에 있는 TableNode 사용자 지정 노드의 코드입니다.

/*
*  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();
       
}
     
}
   
}    
 
}
}
 

public 속성에서 볼 수 있듯이, 테이블 높이, 행 높이, 각 열의 폭, 다양한 UI 요소의 색 또는 그라데이션 등 개발자가 구성할 수 있는 여러 가지 TableNode 속성이 있습니다. 목록 끝의 코드는 마우스 휠 지원을 제공합니다. 이제 The "Play" page로 주석 처리한 섹션을 중심으로 기본 프로그램을 살펴보겠습니다. 이 섹션이 TableNodeExampleMain.fx 파일에 TableNode 인스턴스가 만들어지는 부분입니다.

/*
 *  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";


The Model Behind the UI

"JavaFX 방식"은 UI 속성을 모델에 바인딩하는 것이므로 위에서 보는 바와 같이 TableNode의 콘텐츠 속성이 모델에 바인딩됩니다. 아래는 현재까지 TableNodeExampleModel.fx 파일에 만들어진 Sound Beans 프로그램의 모델입니다. playlistObjects 시퀀스에는 어떠한 종류의 개체도 포함할 수 있으며, Node 인스턴스를 모델에 포함하지만 않으면 됩니다(이러한 인스턴스는 UI에 속하므로). 그러므로 TableNode,를 채우려면 모델에 앨범 제목 및 이미지 URL 같은 문자열을 포함하는 방식을 사용합니다. 위에 표시된 TableModel의 콘텐츠 속성에 바인딩하는 동안 Node 하위 클래스(예: TextImageView)가 만들어집니다.

/*
 *  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 파일입니다. 이 JFX 사용자 지정 노드 연속 게시물 중 이전 게시물에 소개했던 ButtonNode, MenuNode, DeckNode, ProgressNode 코드가 필요합니다.

감사합니다.
Jim Weaver
JavaFXpert.com

이 글의 영문 원본은
Creating a Custom Scrollable Table with JavaFX
에서 보실 수 있습니다.

블로그 이미지

맨오브파워

한계를 뛰어 넘어서..........

,