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
에서 보실 수 있습니다.

블로그 이미지

맨오브파워

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

,

이제 JavaFX SDK 기술 Preview가 릴리스되었으므로 자신만의 "사용자 지정 노드"를 빠르게 만드는 방법을 설명하겠습니다. JavaFX에서 사용자 지정 노드는 위젯, 가젯, UI 구성 요소 등 어느 것이나 모두 의미할 수 있으나 목적은 동일합니다. 다시 사용 가능한 JavaFX 프로그램용 UI를 만들 수 있도록 하는 것입니다. 오늘의 예는 사용자 지정 노드(2개)를 만드는 방법을 보여줍니다. 다음 스크린샷을 참조하십시오.

 

그런데 현재 이 예에 구현된 코드가 단순해진 것은 Edgar Merino 덕분입니다. 이 코드를 사용해보려면 Java Web Start 링크를 클릭하십시오. JRE 6 이상이 필요합니다. 또한 자바 SE 6 업데이트 10을 설치하면 배포 시간이 단축됩니다.



JavaFX SDK Packages are Taking Shape 게시물에서 언급한 것처럼 JavaFX는 UI를 개발할 때 그래픽 "노드 중심" 방식을 채택하고 있으므로 JavaFX 사용자 인터페이스의 거의 대부분은 노드(Node)입니다.  사용자 지정 노드를 만들려는 경우 CustomNode 클래스를 확장하여 원하는 특성과 동작을 지정합니다.  아래 코드는 이 예에서 이미지를 표시하고 마우스 이벤트에 응답(예: 마우스를 위에 갖다대면 좀 더 투명해지고 텍스트가 표시되는 이벤트)하는 사용자 지정 노드를 만드는 코드입니다. 

주: javafx.ext.swing 패키지에 있는 Button 클래스를 사용하지 않는 이유가 궁금하실 것입니다. 이유는 Button 클래스는 Node가 아니라 Component이기 때문이며, 위에 언급한 대로 노드 중심 방식으로 변화되는 방향을 따르는 것이 가장 좋다고 생각합니다. 어떤 부분에서는 노드를 하위 클래스로 만드는 버튼이 나타납니다. 이 경우에는 ButtonNode 클래스가 더 이상 필요하지 않을 수 있습니다.

ButtonNode.fx


/*
*  ButtonNode.fx -
*  A node that functions as an image button
*
*  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
*  and Edgar Merino (http://devpower.blogsite.org/) to demonstrate how
*  to create custom nodes in JavaFX
*/


package com.javafxpert.custom_node;

import javafx.animation.*;
import javafx.input.*;
import javafx.scene.*;
import javafx.scene.effect.*;
import javafx.scene.geometry.*;
import javafx.scene.image.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.transform.*;

public class ButtonNode extends CustomNode {
 
/**
   * The title for this button
   */

 
public attribute title:String;

 
/**
   * The Image for this button
   */

 
private attribute btnImage:Image;

 
/**
   * The URL of the image on the button
   */

 
public attribute imageURL:String on replace {
    btnImage
=
     
Image {
        url
: imageURL
     
};
 
}
   
 
/**
   * The percent of the original image size to show when mouse isn't
   * rolling over it.  
   * Note: The image will be its original size when it's being
   * rolled over.
   */

 
public attribute scale:Number = 0.9;

 
/**
   * The opacity of the button when not in a rollover state
   */

 
public attribute opacityValue:Number = 0.8;

 
/**
   * The opacity of the text when not in a rollover state
   */

 
public attribute textOpacityValue:Number = 0.0;

 
/**
   * A Timeline to control fading behavior when mouse enters or exits a button
   */

 
private attribute fadeTimeline =
   
Timeline {
      toggle
: true
      keyFrames
: [
       
KeyFrame {
          time
: 600ms
          values
: [
            scale
=> 1.0 tween Interpolator.LINEAR,
            opacityValue
=> 1.0 tween Interpolator.LINEAR,
            textOpacityValue
=> 1.0 tween Interpolator.LINEAR
         
]
       
}
     
]
   
};

 
/**
   * This attribute is interpolated by a Timeline, and various
   * attributes are bound to it for fade-in behaviors
   */

 
private attribute fade:Number = 1.0;
 
 
/**
   * This attribute represents the state of whether the mouse is inside
   * or outside the button, and is used to help compute opacity values
   * for fade-in and fade-out behavior.
   */

 
private attribute mouseInside:Boolean;

 
/**
   * The action function attribute that is executed when the
   * the button is pressed
   */

 
public attribute action:function():Void;
   
 
/**
   * Create the Node
   */

 
public function create():Node {
   
Group {
     
var textRef:Text;
      content
: [
       
Rectangle {
          width
: bind btnImage.width
          height
: bind btnImage.height
          opacity
: 0.0
       
},
       
ImageView {
          image
: btnImage
          opacity
: bind opacityValue;
          scaleX
: bind scale;
          scaleY
: bind scale;
          translateX
: bind btnImage.width / 2 - btnImage.width * scale / 2
          translateY
: bind btnImage.height - btnImage.height * scale
          onMouseEntered
:
           
function(me:MouseEvent):Void {
              mouseInside
= true;
              fadeTimeline
.start();
           
}
          onMouseExited
:
           
function(me:MouseEvent):Void {
              mouseInside
= false;
              fadeTimeline
.start();
              me
.node.effect = null
           
}
          onMousePressed
:
           
function(me:MouseEvent):Void {
              me
.node.effect = Glow {
                level
: 0.9
             
};
           
}
          onMouseReleased
:
           
function(me:MouseEvent):Void {
              me
.node.effect = null;
           
}
          onMouseClicked
:
           
function(me:MouseEvent):Void {
              action
();
           
}
       
},
        textRef
= Text {
          translateX
: bind btnImage.width / 2 - textRef.getWidth() / 2
          translateY
: bind btnImage.height - textRef.getHeight()
          textOrigin
: TextOrigin.TOP
          content
: title
          fill
: Color.WHITE
          opacity
: bind textOpacityValue
          font
:
           
Font {
              name
: "Sans serif"
              size
: 16
              style
: FontStyle.BOLD
           
}
       
},
     
]
   
};
 
}
}  


위의 ButtonNode.fx 코드 목록에서는 다음 내용을 짚고 넘어가겠습니다.
  • ButtonNode 클래스는 CustomNode를 확장합니다.
  • 이 새로운 클래스는 사용자 지정 노드에 표시될 이미지와 텍스트를 저장하는 속성을 채용합니다.
  • create() 함수는 사용자 지정 노드의 UI 모양과 동작에 관한 선언 표현식을 반환합니다.
  • javafx.scene.effect 패키지의 Glow 효과는 이미지를 클릭할 경우 이미지에 빛나는 효과를 줄 때 사용됩니다.
  • 이미지의 투명도, 이미지의 크기, 사용자 지정 노드의 제목은 마우스를 누르거나 버튼을 놓을 때 전환됩니다. Timeline은 이러한 전환이 점진적으로 이루어지도록 하는 데 사용됩니다.
  • 투명도를 조정하고 Glow 효과를 적용한 후에 onMouseClicked 함수가 목록의 앞 부분에 정의된 action() 함수 속성을 호출합니다. 그러면 사용자 지정 노드가 Button과 유사하게 동작합니다.

"메뉴"에 ButtonNode 인스턴스 배열

Setting the "Stage" for the JavaFX SDK 게시물에서 설명한 것처럼 HBox 클래스는 javafx.scene.layout 패키지에 있으며, 이 패키지 안의 다른 노드를 배열하는 노드입니다. 아래와 같은 MenuNode 사용자 지정 노드는 ButtonNode 인스턴스를 수평으로 배열하고, javafx.scene.effects 패키지의 Reflection 클래스는 해당 버튼 아래에 멋진 반사 효과를 추가합니다. 코드는 다음과 같습니다.

MenuNode.fx

/*
 *  MenuNode.fx -
 *  A custom node that functions as a menu
 *
 *  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.scene.*;
import javafx.scene.effect.*;
import javafx.scene.layout.*;

public class MenuNode extends CustomNode {

 
/*
   * A sequence containing the ButtonNode instances
   */

 
public attribute buttons:ButtonNode[];
   
 
/**
   * Create the Node
   */

 
public function create():Node {
   
HBox {
      spacing
: 10
      content
: buttons
      effect
:
       
Reflection {
          fraction
: 0.50
          topOpacity
: 0.8
       
}
   
}    
 
}
}  


사용자 지정 노드 사용

이제 사용자 지정 노드를 정의했으므로 간단한 프로그램에서 이 노드를 사용하는 방법을 보여드리겠습니다. 이 블로그를 따라해보신 분은 "JavaFX가 UI와 모델을 바인딩하는 방식"을 알게 되었을 것입니다. 이 간단한 예에서는 사용자 지정 노드를 만드는 방법을 알리는 데 중점을 두기 때문에 모델을 만들어서 이 모델에 UI를 바인딩하는 복잡한 작업까지 보여드리지는 않겠습니다. 대신, ButtonNode 인스턴스를 클릭할 때마다 문자열을 콘솔에 인쇄하는 간단한 작업을 보여드리겠습니다. 이번 예의 기본 프로그램의 코드는 다음과 같습니다.

MenuNodeExampleMain.fx

/*
 *  MenuNodeExampleMain.fx -
 *  An example of using the MenuNode custom 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.menu_node_example.ui;

import javafx.application.*;
import javafx.scene.paint.*;
import javafx.scene.transform.*;
import java.lang.System;
import com.javafxpert.custom_node.*;

Frame {
 
var stageRef:Stage;
 
var menuRef:MenuNode;
  title
: "MenuNode Example"
  width
: 500
  height
: 400
  visible
: true
  stage
:
    stageRef
= Stage {
      fill
: Color.BLACK
      content
: [
        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 {
                 
System.out.println("Play button clicked");
               
}
           
},
           
ButtonNode {
              title
: "Burn"
              imageURL
: "{__DIR__}icons/burn.png"
              action
:
               
function():Void {
                 
System.out.println("Burn button clicked");
               
}
           
},
           
ButtonNode {
              title
: "Config"
              imageURL
: "{__DIR__}icons/config.png"
              action
:
               
function():Void {
                 
System.out.println("Config button clicked");
               
}
           
},
           
ButtonNode {
              title
: "Help"
              imageURL
: "{__DIR__}icons/help.png"
              action
:
               
function():Void {
                 
System.out.println("Help button clicked");
               
}
           
},
         
]
       
}
     
]
   
}
}

앞서 언급한 대로 사용자가 해당 ButtonNode를 마우스로 클릭할 때마다 호출된 함수에 action 속성이 할당됩니다. 그리고 __DIR__ 표현식은 CLASS 파일이 있는 디렉토리로 평가됩니다. 이 경우 그래픽 이미지는 com/javafxpert/menu_node_example/ui/icons 디렉토리에 있습니다.

이 기사의 이미지를 다운로드한 후 이 그래픽으로 이 예에서 소개한 대로 작성하고 실행할 수 있습니다. 이 이미지는 프로젝트의 클래스 경로에서 확장할 수 있는 zip 파일입니다.

이 파일은 JavaFX SDK Technology Preview에 유용한 사용자 지정 노드 라이브러리를 작성하여 이 블로그의 JFX Custom Nodes 카테고리에 게시하기 위해 만든 것입니다. 사용자 지정 노드와 관련하여 아이디어가 있거나 자신이 개발한 사용자 지정 노드를 공유하려면 lat-inc.com의 jim.weaver로 연락해주십시오.

이 게시물을 실행한 후에 Weiqi Gao가 Java WebStart Works On Debian GNU/Linux 4.0 AMD64 게시물에 몇 가지 좋은 소식을 올렸습니다. JavaFX 스크립트 설명서의 기술 검토를 훌륭하게 해주셔서 저는 Weiqi(발음: 웨이치) 씨가 매우 좋습니다. ;-)

감사합니다.
Jim Weaver
JavaFX Script: Dynamic Java Scripting for Rich Internet/Client-side Applications

지금 바로 the book's Apress site에서 eBook(PDF)을 다운로드할 수 있습니다.

이 글의 영문 원본은
Rolling Your Own JavaFX "Custom Nodes": A Graphical Menu Example
에서 보실 수 있습니다.

블로그 이미지

맨오브파워

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

,
2007년 8월과 9월에 썬 개발자 네트워크의 John O'Conner는 JavaFX 스크립트 프로그래밍 언어(이 기사에서는 JavaFX 스크립트라고 줄여서 부름)를 시작하는 사용자에게 도움을 주고자 "학습 곡선 일지(Learning Curve Journal)"라는 제목의 시리즈를 기고했습니다.

 그 이후로 이 언어의 많은 중요한 부분이 개선되었습니다. 아마도 가장 중요한 변화는 JavaFX 스크립트의 초기 인터프리터 기반 버전을 대신하여 컴파일러 기반 버전을 사용할 수 있게 되었다는 점입니다. 학습 곡선 일지 시리즈의 1편, 2편 및 3편은 컴파일러 기반 버전의 언어 사용 방법을 보여주기 위해 업데이트 되었습니다. 최신 내용을 반영하여 다른 변경 사항도 적용되었습니다. 4편은 시리즈의 2편에서 시작된 JavaFX 이미지 검색 애플리케이션을 완결하는 새로운 부분입니다.

학습 곡선 일지의 앞 부분에서는 JavaFX 스크립트를 사용하여 Flickr에서 이미지를 검색하는 기존 이미지 검색 애플리케이션의 사용자 인터페이스(UI)를 재현했습니다. 결과 JavaFX 스크립트는 원래 UI와 완전히 똑같진 않지만 상당히 근접했습니다. 그림 1은 JavaFX 스크립트 구현으로 생성된 기본 UI를 보여줍니다.


그림 1. JavaFX 이미지 검색 애플리케이션 UI

 UI 구축에는 계층 구조적인 Swing 기반 접근 방법을 따랐습니다. 이 시리즈의 1편에서 언급한 것처럼 JavaFX 개발자는 앞으로 노드 기반 접근 방법을 사용할 것입니다.

이제 애플리케이션의 JavaFX 스크립트 버전을 완료하고 사용자가 Flickr 웹 사이트에서 이미지를 검색, 나열 및 표시할 수 있도록 하겠습니다. 완성된 JavaFX 스크립트 애플리케이션을 위해 NetBeans 프로젝트 다운로드를 받을 수 있습니다.


이미지 검색

구 현할 첫 번째 동작은 이미지 검색입니다. 사용자가 UI의 검색 필드에 검색어를 입력하면 애플리케이션은 Flickr 웹 사이트에서 이미지 검색을 시작합니다. 검색어와 매칭되는 이미지가 있으면 애플리케이션은 최대 100개의 매칭되는 축소판 이미지의 목록을 로드하여 UI의 Matched Images 영역에 선택 가능한 목록으로 표시합니다. 또한 진행 표시줄이 이미지 검색을 추적합니다.

기 존 이미지 검색 애플리케이션은 Matched Images 영역에 반환된 축소판 이미지의 목록을 제목과 함께 표시합니다. 우리는 JavaFX 스크립트 애플리케이션 목록에서 매칭되는 이미지의 반환된 제목만 보여주도록 단순화하겠습니다. JavaFX 스크립트의 ListItem 구성요소는 현재 icon 속성을 갖지 않습니다. 따라서 현재 ListListItem 구성요소가 생성하는 목록은 이미지를 포함할 수 없습니다.

이미지 검색을 위해 ImageSearcherPhoto의 두 가지 JavaFX 스크립트 클래스를 생성했습니다. 또한 진행 표시줄을 위한 별도의 클래스도 생성했습니다. JavaFX 스크립트 패키지에 ProgressBar 구성요소는 아직 없습니다. 임시 해결책으로 학습 곡선 일지 시리즈에서 구축한 이미지 검색 UI에 Swing JProgressBar 구성요소로부터 JavaFX 스크립트 진행 표시줄을 생성하는 createProgressBar 함수를 포함했습니다. 우리는 이 함수를 자체 파일의 자체 클래스로 이동하기로 결정했습니다. 이는 애플리케이션의 주요 부분을 깔끔하게 만드는 이점이 있습니다. 따라서 애플리케이션에는 이제 4개의 파일이 있습니다.

  • Main.fx: 애플리케이션의 주요 부분을 제공합니다. UI를 표시하고 이미지 검색 및 가져오기를 호출합니다.
  • ImageSearcher.fx: 이미지 검색을 수행합니다.
  • Photo.fx: Flickr 사진을 나타냅니다.
  • TempProgressBar.fx: 진행 표시줄을 생성합니다.

다음은 이미지 검색을 호출하는 Main.fx의 코드입니다.


  var searcher = ImageSearcher {
      callback: function(photos:Photo[]):Void {
          thumbnailList.items = for(photo in photos) {
              ListItem {
                  text: photo.title
                  value: photo
              }
          };

          matchedImagePB.indeterminate= false;
      }
  };

  var search = function():Void {
      System.out.println("searching... {searchTextField.text}");
      matchedImagePB.indeterminate = true;
      searcher.search(searchTextField.text);
  };

  searchTextField.action=search;

사용자가 UI의 검색 필드에 검색어를 입력하면 ImageSearcher 클래스 내의 search 함수를 호출하는 작업을 트리거하고 검색어를 search 함수로 전달합니다.

변수에 함수가 지정된 것에 유의하십시오.


  var search = function():Void {...}
 
 JavaFX 스크립트에서 함수는 변수에 지정되거나 매개 변수로써 다른 함수에 전달될 수 있는 1급 개체(first-class object)입니다.

또한 함수는 matchedImagePB의 indeterminate 등록 정보를 true로 설정합니다.


  var matchedImagePB = TempProgressBar { };

        matchedImagePB.indeterminate = true;
 


TempProgressBar 클래스는 matchedImagePB 변수에 지정됩니다. 따라서 matchedImagePB의 indeterminate 등록 정보를 설정하는 것은 실질적으로 TempProgressBar 개체의 indeterminate 등록 정보를 설정하는 것입니다. 그 영향을 이해하기 위해 TempProgressBar 클래스를 살펴보겠습니다.


TempProgressBar 클래스

다음은 TempProgressBar 클래스입니다.


 
package javafxscriptimgsearch2;

  import javafx.ext.swing.*;

  public class TempProgressBar extends Component {
      protected function createJComponent():javax.swing.JComponent {
          return new javax.swing.JProgressBar();
      }
      public attribute indeterminate:Boolean = false on replace {
          var prog = this.getJComponent() as javax.swing.JProgressBar;
          prog.setIndeterminate(indeterminate);
      }
}
 

클래스에는 createJComponent 함수와 indeterminate 속성이 있습니다.
replace
트리거가 indeterminate 속성에 연결되어 있습니다.


      public attribute indeterminate:Boolean = false on replace {
          var prog = this.getJComponent() as javax.swing.JProgressBar;
          prog.setIndeterminate(indeterminate);
      }
 

애플리케이션의 주요 부분이 속성을 true로 설정하는 것과 같이 속성 값이 변경되면 트리거는 진행 표시줄을 생성합니다. 특히 트리거는 getJComponent() 함수를 사용하여 JavaFX 스크립트 구성요소로 캡슐화된 Swing JProgressBar 구성요소를 생성합니다. 트리거는 또한 JProgressBar 구성요소의 indeterminate 등록 정보를 true로 설정하여 검색 진행 중에 진행 표시줄이 계속 움직이도록 합니다.

트 리거는 JavaFX 스크립트의 강력한 기능 중 하나입니다. 트리거는 특정 조건 충족 시 코드 블록을 실행하도록 합니다. 또한 기존 Swing 구성요소의 재사용이 얼마나 쉬운지에도 유의하십시오. Swing 구성요소를 사용하려면 JavaFX 스크립트로 간단한 래퍼(wrapper)를 작성하기만 하면 됩니다.


ImageSearcher 클래스

다음은 ImageSearcher 클래스입니다.


    package javafxscriptimgsearch2;

import javax.xml.parsers.*;
import org.xml.sax.helpers.DefaultHandler;
import java.lang.System;
import java.lang.Thread;
import java.lang.Runnable;
import javax.swing.SwingUtilities;

public class ImageSearcher {
public attribute callback: function(photos:Photo[]):Void;

public function search(search:String) {
var thread = new Thread(Runnable {
public function run():Void {
var photos:Photo[];

var handler = DefaultHandler {
public function startDocument() { }
public function startElement(uri:String, localName:String, qName:String , attributes:org.xml.sax.Attributes ) {
if(qName == "photo") {
var photo = Photo {
id: attributes.getValue("id")
server: attributes.getValue("server")
farm: attributes.getValue("farm")
title: attributes.getValue("title")
secret: attributes.getValue("secret")
};
insert photo into photos;
}
}
public function endElement(uri:String , localName:String , qName:String ) { }
public function endDocument() { }

};

var SEARCH_URL = "http://api.flickr.com/services/rest/?" +
"method=flickr.photos.search";
var key = "339db1433e5f6f11f3ad54135e6c07a9";
var MAX_IMAGES = 100;
var searchUrl = "{SEARCH_URL}&api_key={key}&per_page={MAX_IMAGES}&text={search}";
var url = new java.net.URL(searchUrl);
var is = url.openStream();
var factory = SAXParserFactory.newInstance();
var saxParser = factory.newSAXParser();
saxParser.parse(is, handler);

SwingUtilities.invokeLater(Runnable {
public function run():Void {

if(callback != null) {
callback(photos);
}
}
});
}
});
thread.start();
}
}
 

ImageSearcher 클래스는 애플리케이션에서 이미지 검색을 수행하는 search 함수의 래퍼(wrapper)입니다.

새 스레드를 시작하여 search 함수가 시작됩니다. 스레드 내에서 함수는 검색어와 매칭되는 이미지(Flickr에서는 사진)를 가져오기 위해 Flickr 사진 검색 웹 서비스를 호출합니다. 다음은 Flickr 서비스를 호출하는 코드입니다.


   var SEARCH_URL = "http://api.flickr.com/services/rest/?" +
          "method=flickr.photos.search";
  var key = "339db1433e5f6f11f3ad54135e6c07a9";
  var MAX_IMAGES = 100;
  var searchUrl = "{SEARCH_URL}&api_key={key}&per_page={MAX_IMAGES}&text={search}";
  var url = new java.net.URL(searchUrl);
  var is = url.openStream();
 


호출은 Flickr API의 REST(Representational State Transfer) 버전을 사용합니다(Flickr는 XML-RPC 및 SOAP 버전도 제공합니다). API에 전달되는 인수에는 API 키, 반환되는 이미지의 최대 갯수, 사용자가 지정한 검색어가 포함됩니다. 애플리케이션에서 최대 100개의 매칭되는 이미지 목록을 다운로드하고 싶으므로 이미지 최대 갯수를 100으로 설정했습니다.

사진 검색 서비스는 사진을 XML 문서로 반환하므로 문서를 분석하는 기법이 필요합니다. 이 애플리케이션에서는 분석 수행을 위해 SAX DefaultHandler를 사용했습니다.

   var handler = DefaultHandler {
public function startDocument() { }
public function startElement(uri:String, localName:String, qName:String , attributes:org.xml.sax.Attributes ) {
if(qName == "photo") {
var photo = Photo {
id: attributes.getValue("id")
server: attributes.getValue("server")
farm: attributes.getValue("farm")
title: attributes.getValue("title")
secret: attributes.getValue("secret")
};
insert photo into photos;
}
}
public function endElement(uri:String , localName:String , qName:String ) { }
public function endDocument() { }

};

 

각 Flickr 사진에 대해 DefaultHandler사진 개체를 인스턴스화하고 속성 값을 Flicker 사진의 해당 속성 값으로 설정합니다. DefaultHandler는 그 다음 각 Photo 개체를 photos라는 시퀀스에 추가합니다. 학습 곡선 일지 3편: JavaFX 스크립트 함수에서 시퀀스는 동일한 유형을 갖는 개체의 순서별 목록을 나타냈습니다.

search 함수는 그 다음 callback을 호출하는 다른 스레드를 시작합니다.


 
 SwingUtilities.invokeLater(Runnable {
          public function run():Void {

              if(callback != null) {
                  callback(photos);
              }
          }
  }
 

SwingUtilities.invokeLater 메소드는 스레드의 Runnable 작업을 이벤트 디스패치 스레드에 놓습니다.


Callback 함수

ImageSearcher 클래스는 속성 유형이 함수인 callback이라는 속성을 갖습니다. 함수는 photos 시퀀스를 매개 변수로 받고 아무 것도 반환하지 않습니다.


   
public attribute callback: function(photos:Photo[]):Void;
 
ImageSearcher 클래스가 인스턴스화되면(애플리케이션의 주요 부분에 발생) callback 함수는 UI의 Matched Images 영역에 표시하기 위해 제목의 목록을 구축합니다. 목록의 각 항목은 속성 값이 반환된 사진의 제목인 text 속성과 속성 값이 Photo 개체인 value 속성을 갖습니다. 다음 코드는 애플리케이션의 주요 부분 내에서 ImageSearcher 클래스를 인스턴스화하고 callback 함수를 제공합니다.

  var searcher = ImageSearcher {
       callback: function(photos:Photo[]):Void {
          thumbnailList.items = for(photo in photos) {
              ListItem {
                  text: photo.title
                  value: photo
              }
          };

          matchedImagePB.Indeterminate= false;
      }
  };
 


매칭되는 사진 목록을 구축한 후 callback 함수는 Matched Images 진행 표시줄의 indeterminate 등록 정보를 기본값인 false로 다시 설정합니다.


이미지 검색 수행

이미지 검색을 다루는 코드를 검토했으니 이제 작업을 살펴봅시다. 그림 2는 사용자가 검색어를 입력한 후 Search 필드와 Matched Images 진행 표시줄의 상태를 보여줍니다.



그림 2.
검색어 입력

애플리케이션이 매칭되는 이미지를 검색하는 동안 검색어를 기반으로 검색 중임을 나타내는 메시지가 나타납니다. 이 예제에서는 "searching... polar bear" 메시지가 나타납니다.

그림 3은 매칭되는 이미지의 반환된 제목 목록 일부를 보여줍니다.


그림 3. 이미지 검색 결과

검색 함수가 올바르게 작동합니다!


이미지 표시

구현할 다음 동작은 이미지 표시입니다. 사용자가 반환된 이미지 제목 목록에서 제목을 선택하면 애플리케이션은 해당 이미지를 Flickr 사이트에서 가져와 UI의 Selected Image 영역에 표시해야 합니다.

표시를 위해 이미지를 가져오도록 onChange 위임을 추가하는 List 클래스의 사용자 정의 하위 클래스인 PhotoList라는 클래스를 추가했습니다. 다음은 Main.fx 파일에서 PhotoList 가 인스턴스화되는 방법을 보여줍니다.


   class PhotoList extends List {
       
public attribute onChange:function(photo:Photo);
       
public attribute selectedPhoto:ListItem = bind selectedItem on replace {
           
var photo = selectedPhoto.value as Photo;
           
if(onChange != null) {
               onChange
(photo);
           
}
       
}
   
}
 
   
var thumbnailList = PhotoList {
       preferredSize
:[300, 230]
       hmax
: Layout.UNLIMITED_SIZE
       vmax
: Layout.UNLIMITED_SIZE
   
};


사용자가 목록에서 항목을 선택하면 애플리케이션은 PhotoList 개체의 selectedPhoto 속성을 선택된 항목으로 설정합니다. 이는 PhotoList 개체의 selectedPhoto 속성이 다음 표현식의 결과에 바인딩되었기 때문입니다.

   
      selectedItem on replace {
      var photo = selectedPhoto.value as Photo;
  }
 

바인딩은 표현식의 결과를 변수와 연관시키는 것을 의미합니다. 표현식이 변경되면(이 경우 selectedItem가 변경됨) 변수 값은 변경됩니다(이 경우 PhotoListselectedPhoto 속성이 자동으로 업데이트됨). 바인딩은 JavaFX 스크립트의 또 다른 강력한 기능입니다. 바인딩을 사용하여 애플리케이션의 부분을 직접적이고 우아하게 동기화할 수 있습니다.

또한 사용자가 목록에서 항목을 선택하면 애플리케이션은 PhotoList 개체의 onChange 속성과 연관된 이미지 로더 함수를 호출합니다. 이 함수는 선택된 진행 표시줄의 indeterminate 등록 정보를 true로 설정하여 이미지를 가져오는 동안 계속 진행되도록 합니다. 그 다음 선택된 Photo 개체에서 해당 이미지를 가져오기 위해 loadFullImage 함수를 호출합니다.


 class PhotoList extends List {
      public attribute onChange:function(photo:Photo);
      ...
          if(onChange != null) {
              onChange(photo);
       }

  // configure the image loader
  var imageLoader = function(photo:Photo):Void {
      if(photo != null) {
          selectedImagePB.indeterminate = true;
          photo.loadFullImage(function():Void{
              selectedImageDisplay.icon = Icon { image: photo.fullImage };
              selectedImagePB.indeterminate = false;
          });
      }
  };

thumbnailList.onChange = imageLoader;
 

이미지 로더는 Selected Image 영역에 표시하기 위해 이미지를 설정하고 이미지를 가져온 후 선택된 진행 표시줄의 indeterminate 등록 정보를 다시 false로 설정합니다.


  selectedImageDisplay.icon = Icon { image: photo.fullImage };
  selectedImagePB.indeterminate = false;
 

Photo 클래스

Photo 클래스를 살펴봅시다.

    package javafxscriptimgsearch2;

import javafx.scene.image.*;
import java.lang.*;
import javax.swing.SwingUtilities;
import javax.imageio.ImageIO;

public class Photo {
public attribute id:String;
public attribute server:String;
public attribute farm:String;
public attribute title:String;
public attribute secret:String;

private attribute image:Image = null;

public attribute fullImage:Image = null;

public attribute fullImageURL = bind "http://static.flickr.com/{server}/{id}_{secret}.jpg";

public function loadFullImage(
callback:function():Void
):Void {

if(image == null) {
var thread = new Thread(Runnable {
public function run():Void {
var strImageUrl = "http://static.flickr.com/{server}/{id}_{secret}.jpg";
System.out.println("loading: {strImageUrl}");
var buffImg = ImageIO.read(new java.net.URL(strImageUrl));

SwingUtilities.invokeLater(Runnable {
public function run():Void {
image = Image.fromBufferedImage(buffImg);
fullImage = image;
if(callback != null) {
callback();
}
}
});

}});
thread.start();
} else {
callback();
}

}
}
 

Photo 클래스는 Flickr에 저장된 사진과 연관시키는 XML 속성에 해당하는 몇 가지 속성을 갖습니다. 예를 들어 Photo 클래스의 id 속성은 Flickr에 저장된 사진의 id 속성에 해당합니다. 또한 클래스는 Flickr에서 사진을 가져오는 loadFullImage 함수도 제공합니다.

새 스레드를 시작하여 loadFullImage 함수가 시작됩니다. 스레드 내에서 함수는 특정 사진에 대해 사진 소스 URL을 구성한 다음 사진을 가져옵니다. 다음은 사진 소스 URL을 구성하고 사진을 가져오는 코드입니다


   var strImageUrl = "http://static.flickr.com/{server}/{id}_{secret}.jpg"

   var buffImg = ImageIO.read(new java.net.URL(strImageUrl));
 


loadFullImage 함수가 URL 구성을 위해 Photo 개체의 server, idsecret 속성을 사용하는 것에 유의합니다. 또한 이미지 로드를 위해 자바 imagio 패키지의 ImageIO.read 메소드를 사용했습니다.

함수는 JavaFX 스크립트 Image 클래스의 fromBufferedImage 메소드를 사용하여 가져온 사진을 Photo 개체의 image 속성에 지정합니다. 그런 다음 호출자(이 경우 애플리케이션의 주요 부분에 있는 이미지 로더)에게의 callback을 호출하는 다른 스레드를 시작합니다.


   SwingUtilities.invokeLater(Runnable {
       public function run():Void {
           image = Image.fromBufferedImage(buffImg);
           fullImage = image;
           if(callback != null) {
               callback();
           }
       }
   });
 

SwingUtilities.invokeLater 메소드는 스레드의 Runnable 작업을 이벤트 디스패치 스레드에 놓습니다.


목록에 이미지 표시

애플리케이션의 이미지 표시 부분을 테스트해봅시다. 그림 4는 Matched Images 영역에서 이미지를 선택한 후 Selected Image 진행 표시줄의 상태를 보여줍니다.


그림 4. 표시할 이미지 선택

애플리케이션이 이미지를 가져오면 이미지의 사진 소스 URL을 식별하는 메시지를 표시합니다. 이 예제에서는 "loading: http://static.flickr.com/3234/2595583764_a2e6661d3a.jpg" 메시지가 나타납니다. 이미지 가져오기가 끝나면 애플리케이션은 "loaded" 메시지를 표시합니다.

그림 5는 UI의 Selected Image에 선택한 이미지가 표시된 완전한 UI를 보여줍니다.


그림 5. 표시된 이미지

애플리케이션의 이미지 가져오기 부분이 작동합니다. 애플리케이션도 의도대로 작동합니다.

완성된 JavaFX 스크립트 애플리케이션을 위해 NetBeans 프로젝트 다운로드를 받을 수 있습니다.

요약

이 학습 곡선 일지 시리즈에서는 JavaFX 스크립트 프로그래밍 언어(이 시리즈에서는 JavaFX 스크립트라고 줄여서 부름)의 사용을 시작하는데 도움이 되는 몇 가지 기본 개념 및 기법을 소개했습니다.

시리즈의 1편에서는 간단한 JavaFX 애플리케이션을 만드는 방법을 소개했습니다.

2편에서는 풍부한 UI 생성을 위해 사용 가능한 JavaFX 라이브러리 내의 일부 클래스와 언어의 선택적 구문을 설명했습니다. UI 구축에는 계층 구조적인 Swing 기반 접근 방법을 따랐습니다. 이 시리즈의 1편에서 언급한 것처럼 앞으로 JavaFX 개발자는 노드 기반 접근 방법을 사용할 것입니다. 노드 기반 접근 방법에서는 UI 구축을 위해 다른 JavaFX 라이브러리를 사용할 것입니다.

3편에서는 뷰와 데이터 모델 등의 애플리케이션 부분을 동기화하기 위해 사용 가능한 JavaFX 스크립트의 바인딩 기능과 JavaFX 스크립트 함수를 다뤘습니다.

4편(본 기사)에서는 웹 서비스 액세스를 위한 JavaFX 스크립트 사용 방법을 보여주었습니다. 그 과정 중에 FX 스크립트에서 Swing 클래스와 같은 자바 기술 슬래스의 액세스가 얼마나 쉬운지도 보여주었습니다.

또한 이 시리즈를 통해 코드 완성과 같은 기능이 어떻게 NetBeans IDE 6.1에서 JavaFX 애플리케이션의 빌드 및 실행을 단순화하는지도 볼 수 있었습니다. JavaFX Preview SDK를 사용하여 명령줄에서 JavaFX 애플리케이션을 빌드 및 실행할 수도 있습니다.

JavaFX 포함 NetBeans IDE 6.1을 다운로드하여 설치하거나 JavaFX Preview SDK를 다운로드하여 JavaFX 스크립트를 시작하십시오.


자세한 정보
이 글의 영문 원본은
Learning Curve Journal, Part 4: Accessing a Web Service
에서 보실 수 있습니다.
블로그 이미지

맨오브파워

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

,
2007년 8월과 9월에 썬 개발자 네트워크의 John O'Conner는 JavaFX 스크립트 프로그래밍 언어(이 기사에서는 JavaFX 스크립트라고 줄여서 부름)를 시작하는 사용자에게 도움을 주고자 "학습 곡선 일지(Learning Curve Journal)"라는 제목의 시리즈를 기고했습니다.

그 이후로 이 언어의 많은 중요한 부분이 개선되었습니다. 아마도 가장 중요한 변화는 JavaFX 스크립트의 초기 인터프리터 기반 버전을 대신하여 컴파일러 기반 버전을 사용할 수 있게 되었다는 점입니다. 이전의 학습 곡선 일지에서는 인터프리터 기반 버전 사용에 대해 설명했습니다.

업데이트된 학습 곡선 일지에서는 컴파일러 기반 버전의 언어 사용법을 보여줍니다. 최신 내용을 반영하여 다른 변경 사항도 적용되었습니다.

지난 학습 곡선 기사에서는 간단한 사용자 인터페이스(UI)를 구현하고 UI 시험이 성공적이었음을 확인했습니다. 결과 JavaFX 스크립트는 Flickr에서 이미지를 검색하는 이미지 검색 애플리케이션 구축을 위한 원래 자바 프로그래밍 언어 UI와 비슷하게 보였습니다. 그림 1은 결과적인 기본 프레임을 보여줍니다.


그림 1. 원래 UI를 복제한 JavaFX 이미지 검색 애플리케이션

선언적 JavaFX 스크립트 구문을 사용한 결과 코드는 자바 언어 UI의 이식을 위한 괜찮은 시작이었습니다. 그러나 유휴 상태의 프레임, 응답하지 않는 검색 필드, 비활성 진행 표시줄, 빈 목록 상자 및 빈 이미지 레이블에 대한 추가 작업이 필요합니다. 여기에서는 아무 일도 발생하지 않습니다. 현재까지는 활성 데이터모델에 아무 UI 요소도 연결되지 않았으며 사용자 상호작용에도 응답하지 않습니다. 예를 들어 Search 텍스트 필드는 입력한 문자를 받아서 보여주지만 아직 아무 것도 하지 않습니다.

이 골격 뿐인 UI에는 추가 작업이 필요합니다. 이를 위해 비활성 애플리케이션에서 필요한 작업을 수행하도록 하는 함수가 필요합니다.


함수

JavaFX 스크립트 함수는 자바 프로그래밍 언어 메소드와 비슷합니다. 이들 메소드와 마찬가지로 함수는 매개 변수와 반환 값을 갖습니다. 또한 속성 및 변수와 if-then, while 루프, for 루프 및 기타 조건문도 가질 수 있습니다. 다음은 몇 가지 유효한 함수의 예입니다.


 
function z(a,b) {
      var x = a + b;
      var y = a - b;
      return sq(x) / sq (y);
  }

  function sq(n) {return n * n; }

  function main() {
      return z(5, 10);
  }

  function min(x1 : Number, x2 : Number ): Number {
      if (x1 < x2) {
        return x1;
      }
      else {
        return x2;
  }
 

반환 값 유형이나 매개 변수 유형을 선언할 필요가 없습니다. 그러나 자바 언어 프로그래머인 저에게는 min 함수에서와 같이 유형을 사용하는 것이 익숙하므로 가능한 경우에는 항상 사용하곤 합니다. 매개 변수 유형을 선언하는 것은 명확성에 도움이 됩니다. 따라서 z 함수를 다음과 같이 다시 작성하겠습니다.


function z(a: Number, b: Number): Number {
   var x: Number = a + b;
   var y: Number = a - b;
   return sq(x) / sq (y);
}
 


return 키워드는 선택 사항입니다.

   function sq(n) {n*n;}

위 함수는 다음과 같습니다.

   function sq(n) {return n*n;}

함수는 function 키워드로 시작합니다. 그 뒤에는 함수 이름과 매개 변수 목록이 나옵니다. JavaFX 스크립트에서는 유형이 변수나 함수 이름 다음에 나옵니다. 예를 들면 b: Number 매개변수는 b 라는 인수의 유형이 Number라는 의미입니다. 마지막으로 함수는 Number를 반환하므로 이를 매개 변수 목록 뒤에 선언할 수 있습니다. 함수의 본문 앞뒤에는 자바 언어에서 메소드 본문을 둘러싸는 것과 같이 괄호가 있습니다.

함수는 매개 변수나 기타 참조 변수가 변경될 때마다 반환 값을 재평가합니다. 이 기능은 개체를 자주 변경될 수 있는 특정 값에 바인드하려는 경우 유용합니다. 바인딩에 대해서는 나중에 자세히 설명하겠습니다.

클래스에 대해 함수가 정의될 수도 있습니다. 다음은 Friends 클래스에 대해 정의된 함수의 예입니다.


   import java.lang.System;
  import javafx.lang.Sequences;

  class Friends {
      attribute knownNames: String[];
      function sayHello(name: String): String {
          var index = Sequences.indexOf(knownNames,name);
          if (index >= 0) {
              return "Hello, {name}!";
              } else {
              return "Sorry, I can't talk to strangers.";
              }
      }
  }

  var buddies = Friends {
      knownNames: ["John", "Robyn", "Jack", "Nick", "Matthew",
      "Tressa", "Ruby"]
  };

  var greeting = buddies.sayHello("John");
  System.out.println(greeting);
 


이 작은 프로그램은 sayHello 메소드에 아는 이름을 입력하면 "Hello"라고 응답합니다. 그렇지 않은 경우엔 "can't talk to strangers"라고 합니다. Friends 클래스에는 한 가지 속성과 속성을 사용하여 메시지를 반환하는 함수가 포함되어 있습니다. Sequences 클래스를 사용하는 것에 유의합니다. 이 클래스는 시퀀스 조작을 위한 다양한 함수를 포함합니다. 시퀀스는 이 예제에서 아는 이름의 목록과 같이 개체의 순서별 목록을 나타냅니다. SequencesindexOf 함수는 지정된 시퀀스에서 같은 값을 갖는 개체를 검색합니다. 여기에서 indexOf는 아는 이름 시퀀스에서 지정된 이름(이 예제에서는 "John")과 매칭되는 개체를 검색합니다.


반응적 UI 요소

UI 요소(JavaFX 스크립트 라이브러리에서는 노드)는 키 입력이나 마우스 클릭과 같은 사용자 상호작용에 응답할 수 있습니다. 위젯은 action, onMouseClicked, onKeyTyped 및 기타 이벤트 기반 속성을 갖습니다. 이들 속성과 함수를 연관시킬 수 있습니다. 예를 들어 함수를 TextFieldaction 속성과 연관시키면 해당 함수는 필드 내에서 Enter를 누를 때 실행됩니다. Button 위젯의 동일한 action 속성은 사용자가 클릭할 때마다 활성화됩니다.

JavaFX 이미지 검색 애플리케이션과 관련된 함수가 필요하므로 UI 요소에 대한 이벤트 핸들러를 작성하기로 했습니다. 다음 애플리케이션은 두 개의 버튼과 하나의 레이블을 생성합니다. Bigger 버튼을 누르면 레이블의 글꼴 크기가 증가하고 텍스트가 변경됩니다. Smaller 버튼을 누르면 레이블의 글꼴 크기가 감소하고 텍스트가 변경됩니다.

이 애플리케이션에서는 계층 구조적인 Swing 기반 접근 방법을 따릅니다. 이 시리즈의 1편에서 언급한 것처럼 앞으로는 JavaFX 스크립트 개발자가 노드 기반 접근 방법을 사용하도록 할 것입니다.


   import javafx.ext.swing.SwingFrame;
  import javafx.ext.swing.BorderPanel;
  import javafx.ext.swing.FlowPanel;
  import javafx.ext.swing.Button;
  import javafx.scene.Font;
  import javafx.scene.HorizontalAlignment;
  import javafx.ext.swing.Label;

  var font = Font { size: 18 };

  class FontDataModel {
      attribute text: String;

      function increaseFontSize() {
         font = Font { size: font.size + 1 };
         text= "Font Test ({font.size})";
         }
      function decreaseFontSize() {
         font = Font { size: font.size - 1 };
         text= "Font Test ({font.size})";
      }
  }

  SwingFrame {
       var myFont = FontDataModel {
           text: "Font Test (18)"

       }

      content:
      BorderPanel {
          top: FlowPanel {
              alignment: HorizontalAlignment.LEADING
              content: [
                  Button { text:"Bigger"
                      action :
                      function() {
                           myFont.increaseFontSize();
                      }
              },

              Button { text:"Smaller"
                  action : function() {
                      myFont.decreaseFontSize();
                  }
              }

              ]
          }
          center:
              Label {
                  width: 200
                  font: bind font
                  text: bind myFont.text

          }

       }
       visible: true
  }
 

이 코드를 잘라내어 JavaFX 애플리케이션에 바로 붙여넣을 수 있습니다. Preview 버튼을 사용하면 그림 2의 결과가 보입니다.


그림 2. 미리보기 기능을 사용하여 JavaFX 스크립트 언어를 대화형으로 시험

이 Bigger-Smaller 글꼴 애플리케이션은 텍스트 문자열 속성을 갖는 FontDataModel을 생성합니다. 애플리케이션은 FontDataModel 인스턴스를 생성하고 text 속성을 초기화합니다. FontDataModel에도 increaseFontSizedecreaseFontSize라는 두 가지 함수가 있습니다. 이들 함수는 텍스트 속성을 변경하고 Font 클래스의 인스턴스를 생성하며 인스턴스의 글꼴 크기를 업데이트합니다.

왜 글꼴을 FontDataModel의 속성으로 지정하고 텍스트 속성과 같은 방법으로 함수에서 업데이트하지 않는지 궁금할 지도 모릅니다. 이러한 접근 방법은 Font가 변경할 수 없는 개체, 즉 원래 개체의 속성을 변경할 수 없기 때문에 사용할 수 없습니다. 개체의 인스턴스에서만 속성을 변경할 수 있습니다.

각 버튼은 연관된 함수를 포함하는 action 속성을 갖습니다. 예를 들어 Bigger 레이블이 있는 버튼은 myFont 변수의 increaseFontSize 함수를 호출합니다.


           
Button { text:"Bigger"
       action :
      function() {
           myFont.increaseFontSize();
      }
   
}
 

버튼을 누를 때마다 글꼴 크기와 텍스트가 변경되는 것을 볼 수 있습니다. 그림 3은 Bigger 버튼을 두 번 눌렀을 때의 결과를 보여줍니다.


그림 3. 버튼을 클릭하면 텍스트와 글꼴 크기 변경

분명히 increaseFontSize 메소드는 글꼴과 텍스트를 변경합니다. 이를 위해 바인딩이라는 JavaFX 스크립트 기능을 사용했습니다.


뷰와 모델 바인딩

JavaFX 스크립트에는 하나의 속성이 다른 속성의 변경을 추적하도록 하는 bind 연산자가 있습니다. 하나의 속성을 다른 속성에 바인딩한다는 것은 바인딩된 속성이 대상 속성의 변경 사항을 항상 인지한다는 것을 의미합니다. 이 Bigger-Smaller 글꼴 애플리케이션에서는 Label이 글꼴과 텍스트의 변경 사항을 추적하도록 하려고 합니다. 이를 위해 Labelfont 속성을 Font 변수에 바인드하도록 bind 연산자를 사용했습니다. 또한 Labeltext 속성을 FontDataModeltext 속성에 바인드하도록 bind 연산자를 사용했습니다.

다음은 Label 선언입니다.


 
Label {
      width: 200
      font: bind font
      text: bind myFont.text

  }
 

bind 연산자는 함수에도 사용할 수 있습니다. 함수는 인수나 참조 변수가 변경될 때마다 결과를 업데이트하므로 함수에의 바인딩은 단일 속성에의 바인딩과 마찬가지로 작용합니다. 사실 함수는 실제로 바인딩과 함께 사용하도록 설계되었습니다. 본문 내의 매개 변수 및 참조 변수를 모두 포함하는 모든 종속성을 자동으로 추적하는 재사용 가능한 하위 루틴으로의 바인딩을 리팩터링하기 위해 이들을 사용할 수 있습니다.

표 1에서 코드의 일부를 참조하십시오.

표1. 함수와 함께/함수 없이 바인드 사용
함수 없이 바인드
함수와 함께 바인드
   import java.lang.System;

class Data {
attribute foo: Number;
attribute baz: Number;
}

var data = Data {
foo: 4
baz: 7
};

var zoo = bind data.foo +
data.baz + 10;
System.out.println(zoo);
data.baz = 12;
System.out.println(zoo);
   import java.lang.System;

class Data {
attribute foo: Number;
attribute baz: Number;
function add(x, y, z): Number {
return x+y+z;
}
}

var data = Data {
foo: 4
baz: 7
};

var zoo = bind data.add(data.foo, data.baz, 10);
System.out.println(zoo);
data.baz = 12;
System.out.println(zoo);
출력:
21.0
26.0
출력:
21.0
26.0

요약

JavaFX 애플리케이션에 동작을 추가하기 위해 함수를 사용합니다. UI 구성요소의 속성은 함수에 매핑됩니다. action, onMouseClickedonKeyTyped와 같은 UI 이벤트 처리를 위해 이들 함수를 정의할 수 있습니다. 함수에는 함수 본문 내에서 매개 변수 및 참조 변수를 재평가하는 추가적인 등록 정보가 있습니다.

bind 연산자를 사용하여 하나의 속성을 다른 속성에 연결할 수 있습니다. 이는 UI 위젯이 모델 속성을 추적하도록 할 때 특히 유용합니다. 뷰 속성을 모델 속성에 바인딩한다는 것은 모델과 뷰가 동일한 데이터로 항상 동기화됨을 의미합니다.

JavaFX 이미지 검색 애플리케이션의 UI에 아직 기능을 추가하지 않았지만 함수가 필요하다는 것을 알게 되었습니다. 검색 텍스트를 가져오기 위해서는 거의 확실히 함수를 사용해야 합니다. 같은 함수가 이미지를 가져오기 위해 Flickr 사이트에도 액세스할 것입니다. 또한 뷰를 기본 모델에 연결하기 위해 bind 연산자도 사용해야 할 것입니다.

이제 UI를 일부 기본 작업 및 함수에 연결하는 방법을 알게 되었습니다. 또한 UI를 기본 모델에 연결하기 위해 bind 연산자도 사용할 수 있습니다.

자세한 정보
이 글의 영문 원본은
Learning Curve Journal, Part 3: JavaFX Script Functions
에서 보실 수 있습니다.
블로그 이미지

맨오브파워

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

,
2007 년 8월과 9월에 썬 개발자 네트워크의 John O'Conner는 JavaFX 스크립트 프로그래밍 언어(이 기사에서는 JavaFX 스크립트라고 줄여서 부름)를 시작하는 사용자에게 도움을 주고자 "학습 곡선 일지(Learning Curve Journal)"라는 제목의 시리즈를 기고했습니다.

그 이후로 이 언어의 많은 중요한 부분이 개선되었습니다. 아마도 가장 중요한 변화는 JavaFX 스크립트의 초기 인터프리터 기반 버전을 대신하여 컴파일러 기반 버전을 사용할 수 있게 되었다는 점입니다. 이전의 학습 곡선 일지에서는 인터프리터 기반 버전 사용에 대해 설명했습니다.

업데이트된 학습 곡선 일지에서는 컴파일러 기반 버전의 언어 사용법을 보여줍니다. 최신 내용을 반영하여 다른 변경 사항도 적용되었습니다.

사용자 인터페이스를 정의하기 위해 10년 가까이 자바 프로그래밍 언어를 사용해온 저는 JavaFX 스크립트를 처음 사용해보고 두 가지 환경 사이의 큰 차이점을 금방 느낄 수 있었습니다. 프로그래머는 자바 언어에서 사용자 인터페이스(UI) 정의를 위해 절차적 언어를 사용하지만 JavaFX Script에서는 UI 정의에 선언적 문을 사용할 수 있습니다. 이는 커다란 차이이며 여기에 적응하는 데는 약간의 시간과 노력이 필요할 수 있습니다.

UI 생성을 위한 새로운 선언적 스타일에 대해 알아보고자 기존 애플리케이션 UI를 자바 언어 구현에서 JavaFX 스크립트로 이식하기로 했습니다. 썬 개발자 네트워크자바 언어 허브에 있는 Swingworker 기사에서 만든 이미지 뷰어 애플리케이션을 선택했습니다. 원래 애플리케이션은 Java SE 6에서 SwingWorker 클래스의 사용 방법을 보여주기 위한 것이었지만 UI 자체는 JavaFX 스크립트로의 간편한 이전을 제공할 만큼 충분히 단순해 보입니다.


기존 사용자 인터페이스

기 존 애플리케이션에서는 사용자가 유명한 Flickr 웹 사이트에서 이미지를 검색, 나열 및 표시할 수 있도록 했습니다. 사용자는 검색어를 입력할 수 있으며 애플리케이션은 REST API를 사용하여 매칭되는 축소판 이미지 목록을 Flickr에 쿼리합니다. 사용자는 축소판 이미지를 하나 선택하여 크고 상세한 이미지를 가져올 수 있습니다. 그림 1은 검색 결과가 나온 기존 애플리케이션을 보여줍니다.

그림 1. 검색 결과가 나온 애플리케이션 UI

이 UI는 위에서부터 아래로 다음 구성요소로 이루어져 있습니다.

  • 기본 프레임 창 컨테이너
     
  • 검색 레이블 및 검색 텍스트 필드
     
  • 검색 매칭 레이블 및 진행 표시줄
     
  • 매칭되는 축소판 이미지 목록과 짧은 설명
     
  • 선택 레이블 및 진행 표시줄
     
  • 선택한 이미지를 보여주는 레이블
     

UI는 JFrame, JLabel, JProgressBar, JScrollPaneJList.와 같은 일반 Swing 구성요소로 이루어집니다. JList 구성요소는 축소판 이미지와 제공되는 짧은 설명을 표시하기 위한 사용자 정의 렌더러를 갖지만 여전히 상대적으로 간단한 UI로서 JavaFX 스크립트의 선언적 UI 측면을 조사하는 데 도움이 될 것입니다. 전체 애플리케이션의 구현을 시도해 보겠지만 현재로는 기존 UI와 적당히 비슷한 정도로도 충분합니다. 작동하는 데모처럼 극적이진 않겠지만 그림 2에 나온 비활성 UI는 JavaFX 스크립트의 선언적 UI에 대한 초기 목표를 보여줍니다.

그림 2. 애플리케이션 UI

원래 UI는 NetBeans IDE 6.1과 Matisse GUI 편집기를 사용하여 구현했습니다. Swingworker 기사에서 모든 원래 코드 및 생성된 UI 주변의 코드를 다운로드 받을 수 있습니다. 코드에서 이 UI를 생성하기 위해 NetBeans IDE가 GroupLayout를 어떻게 사용했는지 볼 수 있습니다.

    private void initComponents() {
lblSearch = new javax.swing.JLabel();
txtSearch = new javax.swing.JTextField();
lblImageList = new javax.swing.JLabel();
scrollImageList = new javax.swing.JScrollPane();
listImages = new JList(listModel);
lblSelectedImage = new javax.swing.JLabel();
lblImage = new javax.swing.JLabel();
progressMatchedImages = new javax.swing.JProgressBar();
progressSelectedImage = new javax.swing.JProgressBar();

setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
setTitle("Image Search");
lblSearch.setText("Search");
lblImageList.setText("Matched Images");

// ...
// event listeners, models, and cell renderers removed for this example
//

lblSelectedImage.setText("Selected Image");

lblImage.setBorder(javax.swing.BorderFactory.createLineBorder(
new java.awt.Color(204, 204, 204)));
lblImage.setFocusable(false);
lblImage.setMaximumSize(new java.awt.Dimension(500, 500));
lblImage.setMinimumSize(new java.awt.Dimension(250, 250));
lblImage.setOpaque(true);
lblImage.setPreferredSize(new java.awt.Dimension(500, 250));

javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
.addComponent(lblImage, javax.swing.GroupLayout.Alignment.LEADING,
javax.swing.GroupLayout.DEFAULT_SIZE, 462, Short.MAX_VALUE)
.addComponent(scrollImageList, javax.swing.GroupLayout.DEFAULT_SIZE,
462, Short.MAX_VALUE)
.addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(lblImageList)
.addComponent(lblSelectedImage))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(progressMatchedImages, javax.swing.GroupLayout.DEFAULT_SIZE,
350, Short.MAX_VALUE)
.addComponent(progressSelectedImage, javax.swing.GroupLayout.DEFAULT_SIZE,
350, Short.MAX_VALUE)))
.addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
.addComponent(lblSearch)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(txtSearch, javax.swing.GroupLayout.DEFAULT_SIZE,
411, Short.MAX_VALUE)))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(lblSearch)
.addComponent(txtSearch, javax.swing.GroupLayout.PREFERRED_SIZE,
javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(lblImageList)
.addComponent(progressMatchedImages, javax.swing.GroupLayout.PREFERRED_SIZE,
javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(scrollImageList, javax.swing.GroupLayout.PREFERRED_SIZE, 235,
javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(lblSelectedImage)
.addComponent(progressSelectedImage, javax.swing.GroupLayout.PREFERRED_SIZE,
javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(lblImage, javax.swing.GroupLayout.DEFAULT_SIZE, 305, Short.MAX_VALUE)
.addContainerGap())
);
pack();
}
 
NetBeans IDE를 사용한 UI 레이아웃은 쉽습니다. 드래그 앤 드롭이면 충분합니다. 이 예제에서 NetBeans IDE 6.1은 여러 가지 호스트 플랫폼에서 정확한 크기, 위치 및 구성요소의 간격을 제공하는 javax.swing.GroupLayout을 사용하여 UI 코드를 생성했습니다. 결과 코드는 그다지 읽기 쉽진 않지만 도구 지원이 뛰어나 레이아웃 코드를 직접 수동으로 작업할 필요가 없습니다.


선언적 JavaFX 스크립트 인터페이스

JavaFX 스크립트용의 설계 도구가 개발 중이지만 아직은 사용할 수 없습니다. 그러나 JavaFX 스크립트 플러그인이 포함된 NetBeans IDE 6.1의 미리보기 기능을 사용하면 수동으로 UI 코드를 입력하고 결과를 즉시 볼 수 있습니다.

이 인터페이스에 필요한 UI 구성요소는 javafx.ext.swing 패키지 및 javafx.scene으로 시작하는 다양한 패키지에 들어 있습니다. UI 구축은 계층 구조적인 Swing 기반 접근 방법을 따릅니다. 이 시리즈의 1편에서 언급한 것처럼 앞으로 JavaFX 개발자는 노드 기반 접근 방법을 사용할 것입니다. 노드 기반 접근 방법으로 UI를 구축하면 javafx.application 클래스의 일부 클래스가 필요할 것입니다.

JavaFX 스크립트는 패키지 구조 및 가져오기 문을 지원한다는 점에서 자바 언어와 유사합니다. 구성요소에 대해 공부하는 동안은 전체 패키지를 가져오는 대신 한 번에 하나씩 특정 구성요소를 가져오겠습니다. 이는 매우 지루하지만 하나씩 사용하게 함으로써 패키지에 어떤 구성요소가 있는지 보여줍니다.

JavaFX 스크립트 구성요소는 height, width, textcontent와 같은 속성을 갖습니다. content 속성은 자녀 구성요소를 갖습니다. 구성요소에 따라 content 속성은 array로 선언되는 여러 개의 구성요소를 포함할 수 있습니다. 이미지 검색 애플리케이션을 위해 사용자 인터페이스를 선언할 때는 JavaFX 스크립트 구성요소의 선택 및 사용 후 속성을 설정합니다.

예를 들어 다음의 짧은 스크립트는 빈 프레임을 정의합니다. 이 코드는 컨텐츠가 없는 프레임 컨테이너를 생성합니다.


  import javafx.ext.swing.SwingFrame;

  SwingFrame {
      title: "JFX Image Search"
      height: 500
      width: 500
      visible: true
  }
 

Swing의 JFrame에 해당하는 JavaFX 스크립트는 javafx.ext.swing.SwingFrame입니다. title 속성은 프레임에 나타나는 텍스트인 창 프레임 제목을 선언합니다. heightwidth 속성은 픽셀 단위로 크기 규격을 정의합니다. 마지막으로 visible 속성은 Swing의 JFrame 클래스 setVisible 메소드와 유사하게 프레임의 가시성 여부를 선언합니다.


Border Panel 및 Flow Panel

javafx.ext.swing 패키지에는 SwingFrame, Label, BorderPanel, FlowPanelList와 같은 구성요소가 포함됩니다. 이러한 이름은 일반적인 Swing 구성요소 같으므로 대상 UI 구현에 이들을 먼저 사용해보겠습니다. javafx.scene 패키지의 HorizontalAlignment 구성요소와 javafx.scene.paint 패키지의 Color 구성요소도 사용했습니다. 그러나 JavaFX 스크립트 패키지에 ProgressBar 구성요소는 아직 없습니다. 따라서 진행 표시줄을 생성하는 함수를 코딩했습니다. 이 시리즈의 3편에서는 JavaFX 스크립트 함수를 검토합니다. BorderPanelFlowPanel 등의 구성요소와 진행 표시줄의 함수를 사용하기로 결정하고 다음 JavaFXImageSearchUI1.fx 코드를 생성했습니다.


 
package com.sun.demo.jfx;

  import javafx.ext.swing.SwingFrame;
  import javafx.ext.swing.BorderPanel;
  import javafx.ext.swing.FlowPanel;
  import javafx.ext.swing.Label;
  import javafx.ext.swing.TextField;
  import javafx.ext.swing.List;
  import javafx.ext.swing.Component;
  import javafx.scene.paint.Color;
  import javafx.scene.HorizontalAlignment;
  import java.awt.Dimension;

  function createProgressBar(preferredSize:Integer[]) :Component {
      var jprogressbar = new javax.swing.JProgressBar();
      var comp = Component.fromJComponent(jprogressbar);
      comp.preferredSize = preferredSize;
      comp;
  }

  SwingFrame {
      title: "JFX Image Search"
      content: BorderPanel  {

          top: FlowPanel {
              alignment: HorizontalAlignment.LEFT
              content: [
              Label {
                  text: "Search"
                  },
              TextField {
                  columns: 50
              }
              ]
          }
          center: BorderPanel {
              top: FlowPanel {
                  alignment: HorizontalAlignment.LEFT
                  content: [
                  Label {
                      text: "Matched Images"

                      },
                  createProgressBar([360, 20])
                  ]
              }
              center: List {
                  preferredSize: [100, 200]
              }
              bottom: FlowPanel {
                  alignment: HorizontalAlignment.LEFT
                  content: [
                  Label {
                      text: "Selected Image"
                      },
                  createProgressBar([365, 20])
                  ]
              }
          }
          bottom: Label {
                  preferredSize: [400, 300]

          }

      }
      visible: true
  }
 

NetBeans IDE 6.1에 프로젝트를 생성한 후에 JFXImageSearchUI1.fx 파일에 위의 코드를 입력했습니다. 미리보기 버튼을 클릭하면 그림 3과 같은 결과가 나타납니다.

그림 3. JavaFX 이미지 검색 UI -- 첫 번째 시도


프레임의 컨텐츠는 BorderPanel 구성요소로, top 속성은 FlowPanel 구성요소에 지정되고 center 속성은 다른 BorderPanel 구성요소에 지정되며 bottom 속성은 Label 구성요소에 지정되었습니다. FlowPanel 구성요소는 content 등록 정보를 갖습니다. 이 등록 정보에 하나 이상의 구성요소를 넣을 수 있습니다. 여러 개의 구성요소를 삽입할 때는 array를 정의하는 괄호 속에 구성요소를 넣어야 합니다. 다음과 같이 쉼표를 사용하여 컨텐츠 array에서 개별 구성요소를 구분합니다.

  content: [
  Label {
     text: "Search"
     },
  TextField {
     columns: 50
  }
  ]
 

상단 FlowPanel에는 Label 구성요소와 TextField 구성요소가 중첩되어 있습니다. FlowPanel 의 방향 속성은 HorizontalAlignment.LEFT입니다. 이는 FlowPanel 내의 구성요소를 가로와 왼쪽으로 정렬합니다. 가운데의 BorderPanel은 이 영역의 상단에 가로로 정렬된 레이블과 진행 표시줄을, 중앙에 목록을, 하단에 다른 레이블과 진행 표시줄을 레이아웃합니다. 프레임의 하단에는 레이블이 들어갑니다.

FlowPanel 구성요소는 레이블-구성요소 쌍을 만들기에 좋습니다. 여기에서는 여러 개의 FlowPanel을 사용하여 레이블과 TextField 등의 구성요소를 묶었습니다.

createProgressBar 함수는 JavaFX 스크립트를 사용하여 Swing 구성요소로부터 javafx.gui 구성요소를 생성하는 방법을 보여줍니다. 여기에서는 Swing JProgressBar 구성요소로부터 JavaFX 스트립트 진행 표시줄을 생성했습니다.


 
 function createProgressBar(preferredSize:Integer[]) :Component {
          var jprogressbar = new javax.swing.JProgressBar();
          var comp = Component.fromJComponent(jprogressbar);
          comp.preferredSize = preferredSize;
          comp;
  }
 

첫 번째 시도에서 UI는 상당히 잘 작동하지만 원래 UI를 제대로 재현하지는 못합니다. 그래서 다른 접근 방법을 시도해 보겠습니다.


클러스터

원래 UI를 복제하려는 두 번째 시도에서는 JavaFX 스크립트 ClusterPanel 구성요소를 활용하겠습니다. 이 구성요소를 사용하여 프레임 내에서 구성요소를 클러스터할 수 있습니다. JavaFX 스크립트에는 ClusterPanel 내에서 구성요소를 순서대로나 병렬로 정리하기 위해 사용할 수 있는 SequentialClusterParallelCluster도 포함됩니다.

진행 표시줄, 검색 텍스트 필드, 축소판 이미지 목록 및 선택된 이미지 표시 영역의 최대 너비 및 높이를 설정하기 위해 JavaFX 스크립트 Layout 구성요소도 활용하기로 결정했습니다. Layout 구성요소는 UNLIMITED_SIZE 등의 레이아웃 관련 상수를 제공합니다.

UI의 두 번째 버전은 다음 JFXImageSearchUI.fx 파일에 코딩되었습니다.


   package com.sun.demo.jfx;

import javafx.ext.swing.Component;
import javafx.ext.swing.SwingFrame;
import javafx.ext.swing.Label;
import javafx.ext.swing.TextField;
import javafx.ext.swing.List;
import javafx.ext.swing.Cluster;
import javafx.ext.swing.Layout;
import javafx.ext.swing.ClusterPanel;
import javafx.ext.swing.SequentialCluster;
import javafx.ext.swing.ParallelCluster;
import javafx.scene.paint.Color;
import java.awt.Dimension;
import java.lang.System;

import javax.swing.border.LineBorder;

function createProgressBar() :Component {
var jprogressbar = new javax.swing.JProgressBar();
var comp = Component.fromJComponent(jprogressbar);
comp.hmax = Layout.UNLIMITED_SIZE;
comp;
}

var searchLabel = Label {
text: "Search:"

}
var searchTextField = TextField {
hmax: Layout.UNLIMITED_SIZE
columns: 50
}

var matchedImageLabel = Label {
text: "Matched Images"
}
var matchedImagePB = createProgressBar();


var thumbnailList = List {
preferredSize:[300, 230]
hmax: Layout.UNLIMITED_SIZE
vmax: Layout.UNLIMITED_SIZE
}

var selectedImageLabel = Label {
text: "Selected Image"
}
var selectedImagePB = createProgressBar();

var selectedImageDisplay = Label {
preferredSize: [400,300]
hmax: Layout.UNLIMITED_SIZE
vmax: Layout.UNLIMITED_SIZE
}

selectedImageDisplay.getJComponent().setOpaque(true);
selectedImageDisplay.getJComponent().setBackground(Color.WHITE.getAWTColor());
selectedImageDisplay.getJComponent().setBorder(new LineBorder(Color.BLACK.getAWTColor(), 1, true));

SwingFrame {
title: "JavaFX Image Search"
content:
// main panel within the frame
ClusterPanel {

hcluster: SequentialCluster {
content: [
ParallelCluster{ // mainCol
resizable: true
content: [
SequentialCluster{
content: [
ParallelCluster { //searchLabelCol
content: searchLabel
},
ParallelCluster { //searchTextFieldCol
resizable: true
content: searchTextField
}
]},
SequentialCluster {
content: [
ParallelCluster { //lblCol
content: matchedImageLabel
},
ParallelCluster { //progressBarCol
resizable: true
content: matchedImagePB
}
]},
thumbnailList,
SequentialCluster {
content: [
ParallelCluster { //lblCol
content: selectedImageLabel
},
ParallelCluster { //progressBarCol
resizable: true
content: selectedImagePB
}
]},
selectedImageDisplay
]
}
]

}

vcluster: SequentialCluster {
content: [
ParallelCluster{ //searchRow
content: SequentialCluster {
content: [
ParallelCluster { // row
content: [searchLabel, searchTextField]
}
]
}
},
ParallelCluster{ // matchedProgressRow
content: SequentialCluster {
content: [
ParallelCluster { // row
content: [matchedImageLabel, matchedImagePB]
}
]
}
},
ParallelCluster{ // thumbNailRow
resizable:true
content: thumbnailList
},
ParallelCluster{ // selectedProgressRow
content: SequentialCluster {
content: [
ParallelCluster { // row
content: [selectedImageLabel, selectedImagePB]
}
]
}
},
ParallelCluster{ // imageRow
content: selectedImageDisplay
}

]
}

}
visible: true
}


 
이 코드의 결과는 훨씬 보기 좋습니다.
그림 4에 나온 것처럼 구성요소는 이제 프레임의 왼쪽과 오른쪽으로 완벽하게 정렬되었습니다.

그림 4. JavaFX 이미지 검색 UI -- 두 번째 시도

프레임의 주 컨텐츠는 ClusterPanel 구성요소입니다. ClusterPanel 내에서 SequentialCluster 구성요소 그룹의 구성요소는 모두 순서대로 있습니다. ParallelCluster 구성요소 그룹의 구성요소는 병렬로 있습니다. 예를 들어 다음은 구성요소를 프레임의 상단 영역에 정리합니다. 검색 레이블과 검색 텍스트 필드를 병렬 열로 클러스터합니다.

 
 hcluster: SequentialCluster {
      content: [
          ParallelCluster{ // mainCol
              resizable: true
              content: [
                  SequentialCluster{
                      content: [
                          ParallelCluster {  //searchLabelCol
                              content: searchLabel
                          },
                          ParallelCluster { //searchTextFieldCol
                              resizable: true
                              content: searchTextField
                          }
              ]},
 

SequentialClusterSequentialClusterParallelCluster와 같은 ClusterElement 구성요소를 지정할 수 있는 content 속성을 갖습니다. ParallelCluster 구성요소는 크기 조정이 가능하므로 필요에 따라 프레임 너비를 채웁니다.

인터페이스의 나머지도 같은 패턴을 따릅니다. 인터페이스의 각 5개 영역 내에서 구성요소는 SequentialCluster 구성요소 내에서 ParallelCluster 구성요소를 사용하여 그룹화 및 정리됩니다. 예를 들어 다음 코드는 매칭되는 이미지 레이블과 매칭되는 이미지 진행 표시줄을 병렬 열로 클러스터합니다. 클러스터 다음에는 검색과 매칭되는 이미지의 축소판 목록이 나옵니다.


 
SequentialCluster {
      content: [
          ParallelCluster {  //lblCol
              content: matchedImageLabel
          },
          ParallelCluster { //progressBarCol
              resizable: true
              content: matchedImagePB
          }
      ]},
  thumbnailList,
 

한 가지 주목할 점은 javafx.ext.swing 패키지의 Label 구성요소가 테두리 특성, 불투명도 또는 배경 색상의 설정을 위한 속성을 현재 지원하지 않는다는 것입니다. 따라서 이들 설정을 위해 getJComponent 함수에 대한 Label 구성요소의 지원을 활용했습니다. 함수는 JavaFX 스크립트 구성요소로 캡슐화된 Swing jComponent를 반환합니다. 이 경우에는 Swing Label 구성요소를 반환합니다. 다음 코드를 사용하여 필요한 레이블 속성을 정의할 수 있습니다.


var selectedImageDisplay = Label {
         ...
  }

  selectedImageDisplay.getJComponent().setOpaque(true);
  selectedImageDisplay.getJComponent().setBackground(Color.WHITE.getAWTColor());
  selectedImageDisplay.getJComponent().setBorder(new LineBorder(Color.BLACK.getAWTColor(), 1, true));
 

설계 도구 없이 이미지 검색 사용자 인터페이스를 복제하는 것에 대해 처음에 걱정했던 것과 달리 가장 유용하고 필요한 기능은 빠르고 쉽게 액세스가 가능합니다. 또한 NetBeans IDE용 JavaFX 플러그인은 구성요소를 사용하여 작업할 때 사용 가능한 속성을 띄우는 문맥 인식 코드 완성을 제공합니다. 코드 완성을 사용하면 UI의 구현이 처음 생각처럼 어렵지만은 않습니다. 그림 5는 IDE에서 Ctrl+spacebar를 입력할 때 활성화되는 일부 팝업 옵션을 보여줍니다.


그림 5. 옵션


요약

UI 생성을 위한 선언적 구문을 살펴보기 위해 기존 애플리케이션의 UI를 이식했습니다. 원래 애플리케이션에서는 구성요소의 배치 및 정렬에 Swing의 GroupLayout을 사용했습니다. NetBeans IDE 6.1은 아직 JavaFX 스크립트를 위한 그래픽 설계 도구를 지원하지 않지만 수동으로 인터페이스를 레이아웃하는 것은 처음 걱정했던것 만큼 어렵지 않았습니다. ClusterPanel, SequentialClusterParallelCluster 구성요소의 조합을 통해 JavaFX 스크립트 구현은 원래 UI와 사실상 똑같아 보입니다. 이들 조합에 NetBeans IDE용 JavaFX 플러그인 및 문맥 인식 코드 완성이 더해져 작업은 더욱 쉬워졌습니다.


자세한 정보


이 글의 영문 원본은
# Learning Curve Journal, Part 2: Declarative User Interfaces
에서 보실 수 있습니다.
블로그 이미지

맨오브파워

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

,
2007년 8월과 9월에 썬 개발자 네트워크의 John O'Conner는 JavaFX 스크립트 프로그래밍 언어(이 기사에서는 JavaFX 스크립트라고 줄여서 부름)를 시작하는 사용자에게 도움을 주고자 "학습 곡선 일지(Learning Curve Journal)"라는 제목의 시리즈를 기고했습니다.

그 이후로 이 언어의 많은 중요한 부분이 개선되었습니다. 아마도 가장 중요한 변화는 JavaFX 스크립트의 초기 인터프리터 기반 버전을 대신하여 컴파일러 기반 버전을 사용할 수 있게 되었다는 점입니다. 이전의 학습 곡선 일지에서는 인터프리터 기반 버전 사용에 대해 설명했습니다. 업데이트된 학습 곡선 일지에서는 컴파일러 기반 버전의 언어 사용법을 보여줍니다. 최신 내용을 반영하여 다른 변경 사항도 적용되었습니다.

이 전과 마찬가지로 시리즈의 1편은 JavaFX 프로그램, 즉 JavaFX 스크립트 언어로 쓰여진 간단한 프로그램으로 시작합니다. JavaFX 스크립트에서의 프로그래밍을 위한 환경 설정 방법과 JavaFX 프로그램 빌드 및 실행 방법을 배우게 됩니다. 2편은 JavaFX 스크립트에서 사용 가능한 선언적 코딩 스타일에 중점을 둡니다. 이러한 스타일이 어떻게 그래픽 애플리케이션을 더욱 단순하고 직관적으로 만들 수 있는지 볼 수 있을 것입니다. 3편에서는 JavaFX 프로그램에서 작업 구현을 위한 JavaFX 스크립트 함수의 사용 방법을 보여줍니다. 4편에서는 웹 서비스 액세스를 위한 JavaFX 스크립트 사용 방법을 보여줍니다. 그 과정 중에 FX 스크립트에서 Swing 클래스와 같은 자바 기술 슬래스의 액세스가 얼마나 쉬운지도 보여줍니다.

JavaFX 스크립트는 개발자가 동적인 그래픽 컨텐츠 생성에 사용할 수 있는 새로운 스트립팅 언어입니다. 데스크탑에서 언어는 Swing 사용자 인터페이스(UI) 툴킷과 자바 2D API를 편리하게 사용할 수 있는 라이브러리를 제공합니다. Swing 또는 자바 2D를 대체하는 것이 아니고 풍부한 컨텐츠 개발자가 이들 API에 더욱 쉽게 액세스할 수 있도록 하는 것이 목적입니다. 모바일 시스템과 같은 다른 환경에서 JavaFX 스크립트는 Swing 이외의 사용자 인터페이스 기술을 사용합니다. JavaFX 스크립트를 사용하여 여러 가지 플랫폼과 운영 환경에서 실행되는 시각적으로 풍부한 애플리케이션을 만들 수 있습니다.

언어는 선언적 및 절차적 구문을 모두 제공합니다. 선언적으로 풍부한 사용자 인터페이스를 만든 다음 이벤트 처리 루틴과 작업을 추가할 수 있습니다.

그러나 대부분의 사용자는 좀 더 소박하게 시작해야 하며 이 기사의 목적도 그러합니다. 목표는 JavaFX 스크립트를 시작하는 방법을 보여주는 것입니다. 먼저 다음이 필요합니다.



자바 플랫폼 설정

개발자라면 물론 시스템에 JDK가 설치되어 있을 것입니다. 그러나 시스템을 한동안 업데이트하지 않은 경우에는 자바 SE 6이 설치되어 있는지 확인하십시오. 학습 곡선 일지는 JavaFX 스크립트의 컴파일러 기반 버전과 NetBeans IDE 6.1에서의 지원에 중점을 두고 있습니다. JavaFX 기술이 적용된 NetBeans IDE 6.1을 설치 및 사용하려면 시스템에 자바 SE 6의 최신 수준(현재는 자바 SE 6 업데이트 10 베타)을 설치하는 것이 좋습니다. 썬 개발자 네트워크의 자바 SE 다운로드 페이지에서 최신 JDK를 다운로드합니다. Mac OS X를 사용하는 경우에는 Apple Developer Connection의 자바 섹션에서 직접 Apple의 최신 자바 플랫폼 개발 키트(현재는 Mac OS X 10.5, 업데이트 1용 자바)를 다운로드 받을 수 있습니다.


자료 참조

새로운 환경이나 언어를 경험할 때는 교착 상태에 빠지거나 난관에 봉착하게 됩니다. 이는 최첨단 기술을 사용할 때 모두가 겪게 되는 과정입니다. 학습 곡선을 원만하게 하기 위해서는 훌륭한 문서와 예제가 매우 중요합니다. 썬 개발자 네트워크의 JavaFX 기술 허브와 함께 javafx.comProject OpenJFX 웹 사이트는 정확한 정보를 얻을 수 있는 최신 문서와 데모 자료를 제공합니다.

일 부 사용자들은 언어 참조 자료를 읽지도 않고 프로그래밍을 바로 시작하고자 할 수 있습니다. 또다른 사용자들은 JavaFX 스크립트를 실제로 사용하기 전에 모든 자료를 읽을 것입니다. 바로 시작하는 유형의 사용자더라도 일종의 언어 사양이나 자습서부터 시작해야 합니다. 전형적인 "Hello, world" 예제를 쓰기 전에 기본 언어 구문을 알아야 합니다. JavaFX 참조 페이지의 문서부터 시작하는 것이 좋습니다. 여기에서 JavaFX 스크립트 프로그래밍 언어 참조 자료 등의 참조 문서와 JavaFX 기술 시작하기NetBeans IDE를 사용하여 간단한 JavaFX 애플리케이션 생성 등의 많은 기사와 자습서로의 링크를 찾을 수 있습니다.


JavaFX 애플리케이션 생성

일부 언어 참조 문서를 읽었다면 이제는 간단한 JavaFX 애플리케이션을 만들어 볼 차례입니다. 명령줄에서 수동으로 JavaFX 애플리케이션 빌드 및 실행이 가능하긴 하지만 애플리케이션 개발을 단순화하는 많은 기능을 가진 NetBeans IDE 6.1를 사용해 봅시다. NetBeans용 JavaFX 플러그인을 설치해야 합니다.

NetBeans IDE 6.1을 설치하지 않은 경우에는 NetBeans IDE 6.1과 NetBeans용 JavaFX을 포함하는 하나의 패키지인 JavaFX 포함 NetBeans IDE 6.1 다운로드가 가능합니다. NetBeans IDE 6.1을 이미 설치한 경우에는 NetBeans 업데이트 센터에서 JavaFX 플러그인을 설치하여 JavaFX 기술 지원을 추가할 수 있습니다. NetBeans용 JavaFX는 현재 Windows 및 Mac OS/X 환경에서 사용 가능합니다. JavaFX 플러그인을 설치하면 NetBeans IDE 6.1을 사용하여 JavaFX 스크립트의 컴파일러 기반 버전으로 작성된 애플리케이션을 생성, 테스트, 디버그 및 배포할 수 있습니다. 플러그인은 JavaFX 스크립트 파일 포함을 위한 프로젝트 및 편집기 지원을 향상시킵니다. 또한 스크립트 엔진 및 라이브러리를 위한 코어 라이브러리도 제공합니다.

JavaFX 포함 NetBeans IDE 6.1이나 NetBeans용 JavaFX 플러그인을 설치했으면 첫 번째 JavaFX 애플리케이션을 빌드할 준비가 된 것입니다. 물론 "Hello, world!"부터 시작해야겠지요.

다음과 같이 프로젝트 생성을 시작합니다.

  1. 주 메뉴에서 File -> New Project를 선택합니다.
  2. New Project 마법사에서 JavaFX 범주와 JavaFX Script Application 프로젝트 유형을 선택합니다.
  3. Next 버튼을 클릭합니다.
  4. HelloWorldJFX와 같이 프로젝트의 이름을 지정합니다.
  5. 기본 프로젝트 위치를 수락하거나 다른 위치를 선택하여 이동합니다.
  6. Create Main Class 확인란을 선택된 상태로 두고 다른 기본 설정도 변경하지 않습니다.
  7. Finish 버튼을 클릭합니다.

그림 1과 같이 IDE는 지정된 프로젝트 폴더에 프로젝트 디렉토리를 생성하고 프로젝트 이름과 같은 HelloWorldJFX라는 이름을 부여합니다. HelloWorldJFX 프로젝트를 확장합니다. Source Packages 노드의 helloworldjfx 패키지 아래에 Main.fx 클래스 파일이 있습니다. IDE는 프로젝트 생성 시 Create Main Class 확인란이 선택되어 있었으므로 Main.fx 파일을 생성합니다. 이 파일에 애플리케이션의 소스 코드가 들어갑니다.


그림 1.
HelloWorldJFX 프로젝트 파일


  /*
   * Main.fx
   *
   * Created on ...
   */

  package helloworldjfx;

  /**
   * @author ...
   */

  // place your code here
 

// place your code here 라인을 다음 코드로 바꿉니다.

  import javafx.ext.swing.Label;

  Label {
      text: "Hello, world!"
  }
 


JavaFXScript 편집기는 기본 서식 설정과 코드 완성을 제공합니다. 우리와 같이 JavaFX 스크립트에 익숙하지 않은 프로그래머는 언어 구문에 확신이 들지 않을 때도 있으므로 코드 완성이 도움이 됩니다. Ctrl + Space 키를 누르면 편집기에서 코드 완성이 활성화됩니다.

또 한 JavaFX 스크립트 플러그인은 컴파일과 실행을 해 볼 필요 없이 애플리케이션의 결과를 볼 수 있는 미리보기 기능을 제공합니다. 소스 파일에 변경한 내용은 즉시 미리보기 창에 반영됩니다. 미리보기 기능은 현재 Mac OS X 플랫폼에서는 사용할 수 없습니다.

Enable Preview 버튼 을 클릭하여 미리보기 기능을 작동시킵니다. 그림 2와 같이 편집기 바로 위에서 출력을 볼 수 있습니다.



그림 2. 기본적인 "Hello, world!"

별로 놀라지 않으셨나요? 좋습니다. 설정 방법을 보여주려는 것 뿐이었지만 좀 더 재미있는 것을 해보도록 하겠습니다. JavaFX 환경은 모든 Swing UI 구성요소를 구현하므로 레이블에만 한정될 필요는 없습니다. 버튼이나 대화 상자 같은 다른 위젯을 사용할 수도 있습니다.

다음은 버튼의 이벤트 핸들러를 소개하는 예제입니다. 언어 참조 자료에서 actionfunction 구문을 읽었으면 버튼을 누를 때 메시지 상자가 표시되도록 해봅시다.


  import javafx.ext.swing.SwingFrame;
   
import javafx.ext.swing.Button;
   
import javafx.ext.swing.SwingDialog;
   
import javafx.ext.swing.Label;
 
   
SwingFrame {
       content
: Button {
           text
: "Press me!"
           action
: function() {
               
SwingDialog {
                   title
: "You pressed me"
                   content
: Label{ text: "Hey, don't do that!"}
                   visible
: true
               
}
       
}
   
}
 
   visible
: true
   
}



Main.fx 파일에 이를 입력한 후에 Press me! 버튼을 누르면 그림 3과 같은 결과가 나타납니다.

그림 3. "Press me!" 버튼 메시지


앞에서 말한 것처럼 미리보기 기능을 사용하여 코드를 컴파일 및 실행하지 않고도 애플리케이션의 결과를 볼 수 있습니다. 코드를 컴파일하려는 경우에는 Project 창에서 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 Build Project를 선택합니다. 애플리케이션을 실행하려면 Project 창에서 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 Run Project를 선택합니다.


구성요소 기반에서 노드 기반 UI로의 이동

이전 예제에서 Frame과 Dialog의 구성요소가 간단하게 FrameDialog가 아니라 SwingFrameSwingDialog라 는 클래스 이름을 갖는지 궁금하셨을지도 모릅니다. 그 답은 JavaFX 스크립트 개발자가 Swing 기반 구성요소의 계층 구조를 사용하는 기존 접근 방법 대신 노드 기반 접근 방법을 사용하여 UI를 구축하는 향후의 접근 방법에 관련되어 있습니다. 사실 이러한 향후의 접근 방법에 대한 초기 지원은 이미 JavaFX 라이브러리에 포함되어 있습니다. 예를 들면 javafx.application 패키지에는 노드 기반 접근 방법을 지원하는 Frame, DialogWindow 등의 클래스가 포함됩니다. 다른 접근 방법을 지원하는 클래스와의 혼동을 피하기 위해 javafx.application 패키지에 해당 항목이 있는 javafx.ext.swing 패키지는 클래스 이름에 접두어 "Swing"을 추가했습니다.

학습 곡선 시리즈에서는 계층 구조적인 Swing 기반 접근 방법을 사용했지만 javafx.ext.swing 패키지의 SwingFrame, SwingDialogSwingWindow와 같은 클래스는 임시적입니다. JavaFX 팀은 Swing 구성요소와 유사하지만 더욱 유연하고 강력한 새로운 노드 기반 구성요소 집합을 설계 중입니다.

노드 기반 접근 방법을 사용한 UI 구축에 대한 자세한 내용은 장면 그래프를 사용하여 JavaFX 스크립트에서 비주얼 개체 표시 기사를 참조하십시오. NetBeans IDE를 사용하여 간단한 JavaFX 애플리케이션 생성 자습서도 노드 기반 접근 방법을 사용하는 JavaFX 애플리케이션을 설명합니다.


프로파일

JavaFX는 특정 플랫폼이나 장치에서만 사용 가능한 클래스 그룹을 의미하는 프로파일을 지원합니다. javafx.ext.swing 패키지의 클래스는 데스크탑 프로파일에 들어 있으며 데스크탑 환경에서만 가용성이 보장됩니다. 예를 들어 많은 휴대 전화는 Swing 클래스를 갖지 않습니다. 휴대 전화와 TV를 포함한 모든 플랫폼에서 보장되는 클래스를 정의한 일반 프로파일도 있습니다. 새로운 노드 기반 구성요소는 일반 프로파일에 있으므로 모든 화면 및 장치에서 작동합니다.

JavaFX API 문서는 정확한 프로파일 사용을 보장하기 위해 하나의 프로파일에서 다른 프로파일로 전환할 수 있도록 하는 버튼을 제공합니다. 이 기사 시리즈에서는 데스크탑 환경을 위한 애플리케이션을 구축하므로 데스크탑 프로파일의 클래스와 일반 프로파일의 일부 클래스를 사용할 것입니다.


명령줄에서 JavaFX 애플리케이션 빌드 및 실행

다음과 같이 명령줄에서 JavaFX 애플리케이션을 빌드 및 실행할 수 있습니다.

  1. JavaFX Preview SDK 다운로드를 받습니다. SDK에는 JavaFX 스크립트 컴파일러, 문서, 런타임, 라이브러리 및 코드 샘플이 포함됩니다.
  2. 다운로드한 패키지를 확장합니다. 여러 디렉토리 중에 javafxcjavafx 명령을 위한 실행 가능 파일을 포함하는 bin 디렉토리가 보여야 합니다.
  3. fx 확장자를 갖는 파일(예: MyApp.fx)에 애플리케이션의 소스 코드를 저장합니다
  4. 다음과 같이 javafxc 명령을 수행하고 소스 파일을 지정하여 애플리케이션을 컴파일합니다.
       javafxc MyApp.fx

    애플리케이션을 위한 클래스 파일이 생성됩니다.

  5. 다음과 같이 javafx 명령을 수행하고 클래스 파일을 지정하여 애플리케이션을 위한 클래스 파일을 실행합니다.
       javafx MyApp

요약

새로운 기술을 조사할 때는 올바르게 시작하는 것이 중요합니다. 정확한 정보와 도구를 사용하여 시작하도록 하십시오. JavaFX 스크립트에 대한 최선의 방법은 4단계 절차를 따르는 것입니다.

  1. 최신의 Java SE Development Kit를 받습니다.
  2. 정보의 출처로 JavaFX 기술 허브, javafx.com 사이트Project OpenJFX 사이트를 사용합니다.
  3. IDE용 개발 플러그인을 받습니다. JavaFX 포함 NetBeans IDE 6.1은 새로운 JavaFX 스크립트의 컴파일러 기반 버전을 지원합니다. NetBeans IDE 6.1을 이미 설치한 경우에는 NetBeans 업데이트 센터에서 JavaFX 플러그인을 설치하여 JavaFX 기술 지원을 추가할 수 있습니다.
  4. 첫 번째 스크립트 시험에 미리보기 기능을 사용합니다.


자세한 정보

이 시리즈의 2편인 선언적 사용자 인터페이스를 참조하십시오.



이 글의 영문 원본은
Learning Curve Journal, Part 1: Exploring JavaFX Script
에서 보실 수 있습니다.

블로그 이미지

맨오브파워

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

,

썬의 흥미로운 새 제품군 JavaFX를 통해 개발자들은 광범위한 장비에서 Java를 활용하는 훌륭하고 풍부한 사용자 인터페이스를 구축할 수 있으며, 이는 "한번 작성하여 어디서든 실행할 수 있도록 한다"는 목표를 향해 한 단계 더 나아간 것입니다.

썬의 CTO인 Bob Brewin은 Global Systems Engineering 부문 부사장 Hal Stern과 본 Innovating@Sun 최신판에서 통합형 리치 클라이언트의 진화 즉, 과도한 TV 스크린 형식의 클라이언트에서 인대시(In-Dash) 타입의 자동차 시스템으로 또는 인터넷 상에서 이용할 수 있는 서비스 및 컨텐츠를 모두 활용할 수 있는 webtop으로의 진화를 이끄는 소프트웨어 기술에 대해 대담을 나누었습니다.

Java FX 제품군에서는 JavaFX 스크립트를 사용하여 개발자들이 전개하고자 하는 플랫폼에 관계 없이 장치들을 동일한 방식으로 대상화하여, 일관된 표준 선언문 형식의 스크립트 언어로 작성할 수 있도록 합니다. 주목할 만한 점들은 다음과 같습니다.

  • JavaFX Script 및 JavaFX Mobile의 기능
  • 사용자 생성 컨텐츠가 애플리케이션을 구동시키는 급증하는 컨텐츠 지향 환경에서 JavaFX의 위치
  • 보다 흥미롭고 유용한 애플리케이션을 위해 클라이언트에 기능 추가
  • 선언문 형식의 스크립트 언어, 특히 Java UI를 생성하는 것을 목적으로 하는 언어가 컨텐츠가 풍부한 애플리케이션 구축을 용이하게 하는 방법
  • 코드 재사용 경향을 지속시키며 그것을 UI에 적용하기
  • 개발자들이 UI 레벨에서 동일한 집합의 API로 작성하면서 “한 번 작성하여 어디서든 실행할 수 있도록 한다"는 목표를 달성하는 방법

Links :


Podcast의 스크립트 다운로드

  1. 영문판 보기
  2. 한글판 보기
  3. 영문 & 한글판 함께 보기

* 출처 : 이노베이션 블로그 - 2007년 6월 20일 수요일 작성

이 글의 원문은 http://blogs.sun.com/innovation/entry/t ··· x_effect 에서 보실 수 있습니다.

블로그 이미지

맨오브파워

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

,
기고인: JCO 양사열 부회장

필자는 작년부터 Web 2.0과 함께 부상한 RIA(Rich Internet Application)에 대해 귀가 따갑게 들어왔다. 필자가 현재 몸담고 있는 JCO내에서도 JCO가 직간접적으로 관여하는 각종 세미나 및 행사에서도 RIA는 큰 화두였으며 지금까지도 관심을 끄는 이슈로 부각되고 있다. 이런 이슈에 호응(?)하기 위해 Sun, Adobe, MS에서 JavaFX, Flex, Silverlight 라는 삼총사를 내놓았다. 세가지를 비교하는 것도 재미있는 일이겠지만 본인은 자바 프로그래머로만 10년 가까이 먹고 살았다. Silverlight는 어떤 것 이라는 것만 아는 편이고 Flex는 약간 끄적거려 보았다. 결국 가장 자신있는 Java 진영에서 나온 JavaFX 를 다루어 보기로 했다.

JavaFX를 처음 접한 것은 2007 JavaOne 이 끝난 후였다. 국내 유일의 자바 챔피언인 양수열 고문이(참고로 본인의 친형이다) JavaOne이 끝나고 귀국하고 만난 자리에서 JavaFX가 앞으로 엄청난 발전을 할 것이라고 흥분해서 말했던 기억이 있다. 그 후 몇 가지 관련된 글을 보았었고 2008년 JavaOne에 운이 좋게 참석하게 되어 다시 보게 된 JavaFX는 정말 놀라울 정도로 발전해 있었다. 예전에 간단한 Flash ActionScript 같았던 모습에서 Multimedia 쪽이 대폭 강화되어 수많은 동영상이 화면에 Display되면서 자유롭게 떠도는 Demo를 보고 그 놀라운 Performance에 놀라움을 금치 못했다. 그러면서도 ‘저 Demo가 일반적인 PC에서도 저만큼 가능할까?’ 라는 의문이 들었다. 그래서 결국 만들어 보게 되었으니…

시작하기
JavaFX를 처음 접하면서 느낀점은 JavaFX 문법이 JavaScript와 상당히 닮아 있다는 점이었다. JavaScript와 Java의 절묘한 짬뽕(?)이랄까? JavaFX Application을 물론 메모장으로도 개발할 수 있지만 IDE에 길들여진 개발자들이 메모장으로 개발할리가 없다. 그리고 JavaFX를 전면에 내세운 Sun에서도 개발자들이 편하고 능률적으로 개발할 수 있도록 NetBeans 최신버전에 JavaFX plugin을 추가해 놓았다. 이전에는 JavaFX Pad라는 응용 프로그램을 활용했었으나 좀 더 융통성있고 편하게 개발할 수 있도록 NetBeans에 통합해 놓은 것이다. JavaFX Pad에서와 같이 Source Edit 창에서 코드를 변경하면 위쪽 Preview 영역에서 바로 변경된 결과물을 볼 수 있고 오른쪽 팔레트를 이용해 각종 콤포넌트나 노드 이벤트 등을 원클릭으로 소스에 적용할 수 있다.
NetBeans JavaFX plugin 설치는 신상철 박사님이 운영하시는 JavaPassion을 참고하면 편하게 설정할 수 있다. (http://www.javapassion.com/handsonlabs/javafx_basics1/)

사용자 삽입 이미지
그림1: JavaFX plugin을 설치하고 실행중인 모습


본론으로
JavaFX를 이용해 Multimedia 파일을 컨트롤 하기 위해 필요한 정보와 샘플들을 우선 수집하였다. 일차적으로 Sample Code를 몇 개 받아 mp3 파일을 재생하는 테스트 코드를 만들었을 때 mp3 파일은 아주 잘 재생되었다. 하지만 동영상 파일로 바꾸었을 때는 아래와 같은 에러를 발생시켰다.

FX Media Object caught Exception com.sun.media.jmc.MediaUnsupportedException: Unsupported media: file:/D:/Projects/NetBeans/SimpleMovie/build/classes/simplemovie/SoHot.avi
source ='file:/D:/Projects/NetBeans/SimpleMovie/build/classes/simplemovie/SoHot.avi'


사용자 삽입 이미지
그림2 동영상 테스트시 발생한 에러메시지

Exception의 이름으로 인해 해당 동영상 파일이 FX 에서 지원하지 않는 Media 형식인걸 확인하였고 빠르게 손가락은 구글링을 하여 JavaFX가 지원하는 미디어 포멧을 찾아내었다.

JavaFX Media 지원타입
Container Types:
•  ASF (Advanced Systems Format),MPEG-1, AVI (Audio-Video Interleaved),WAVE, MIDI (Standard MIDI)

Encoding Types:
•  WMAUDIO8 (WindowsMedia Audio 2/7/8),WMSCREEN (Windows Media Screen), WMVIDEO9 (Windows Media Video 9), Voxware, MS MPEG-4, WMVIDEO7 (Windows Media Video 7), MPEG11 Video, MPEG-1 Audio, Uncompressed video (RGB/YUV/RLE), PCM, ADPCM, MP3, MIDI

Protocol Types:
•  HTTP, FILE ( Known not to work at present are media with DRM (Digital Rights Management), and media played directly from DVD or CD.)

Multimedia 파일을 컨트롤 하기 위해서는 항상 Codec이 문제인데 JavaFX에서는 생각보다 많은 종류의 Codec을 지원해주어 특별히 Codec에 신경 쓰지 않아도 되었다. (물론 가지고 있는 많은 수의 동영상은 실행되지 않았다.)

몇 가지 문제를 잡아나가면서 만들어 낸 결과물은 CustomNode를 사용해 재사용할 수 있는MediaViewNode를 만들었고 Timeline을 이용해 이 Node에 Animation 효과를 보여주는 프로그램을 만들어 보았다. 아래 소스를 실행하면 0.2 배율 스케일의 동영상 화면이 정지된 채로 실행되고 클릭시 회전하며 원래 비율로 커지고 실행되는 프로그램이다.

Main.fx

/*
* Main.fx
*
* Created on 2008. 11. 8, 오후 1:29:49
*/

package simplemovie;

import javafx.ext.swing.*;
import javafx.scene.*;
import javafx.scene.media.*;
import javafx.scene.effect.*;
import javafx.scene.paint.Color;

/**
* @author eclips
*/
SwingFrame {
title: "Movie Player"
width: 800
height: 600
closeAction: function() {
java.lang.System.exit( 0 );
}
visible: true

menus: [ ]

content: Canvas {
width:800
height:600
background: Color.WHITE
content: [
MediaViewNode {
// {__DIR__}은 클래스 파일이 있는 디렉토리이다
mediaURL: "{__DIR__}Nobody.avi"
viewX: 10
viewY: 10
}
]
}
}


MediaViewNode.fx

/*
* MediaViewNode.fx
*
* Created on 2008. 11. 8, 오후 6:48:47
*/

package simplemovie;

/**
* @author eclips
*/

import java.lang.System;

import javafx.scene.CustomNode;
import javafx.scene.*;
import javafx.input.*;
import javafx.scene.media.*;
import javafx.scene.effect.*;
import javafx.scene.paint.Color;
import javafx.scene.geometry.*;
import javafx.scene.transform.*;
import javafx.animation.Timeline;
import javafx.animation.KeyFrame;
import javafx.animation.Interpolator;

public class MediaViewNode extends CustomNode {
/**
* 동영상 화면의 X Sacle
*/
public attribute viewScaleX:Number = 0.2;

/**
* 동영상 화면의 Y Scale
*/
public attribute viewScaleY:Number = 0.2;

/**
* MediaViewNode의 회전 반경
*/
public attribute rotation:Number = 0.0;

public attribute viewX:Number = 50;
public attribute viewY:Number = 40;

/**
* 현재 활성화 여부
*/
private attribute actived:Boolean = false;
/**
* 현재 Animation 중인지 여부
*/
private attribute moving:Boolean = false;

private attribute media:Media;
private attribute mediaView:MediaView;

private attribute strokeColor:Color = Color.DARKGRAY;

// 동영상 미디어 객체 URL
public attribute mediaURL:String on replace {
media = Media {
source: mediaURL
};
}

// Media Player 객체
private attribute player =
MediaPlayer {
media: media,
autoPlay: false
}


private attribute choiceTimeLine =
Timeline {
keyFrames : [
KeyFrame {
time: 0ms
values: [
viewScaleX => 0.2,
viewScaleY => 0.2,
viewX => 10,
viewY => 10,
rotation => 0.0,
moving => true
]
},
KeyFrame {
time : 500ms
values: [
viewScaleX => 1.0 tween Interpolator.LINEAR,
viewScaleY => 1.0 tween Interpolator.LINEAR,
viewX => 40 tween Interpolator.LINEAR,
viewY => 40 tween Interpolator.LINEAR,
rotation => 360 tween Interpolator.LINEAR,
moving => false
]
action: function():Void {
player.play();
mediaView.toFront();
actived = true;
}
}
]
};

private attribute unchoiceTimeLine =
Timeline {
keyFrames : [
KeyFrame {
time: 0ms
values: [
viewScaleX => 1.0,
viewScaleY => 1.0,
viewX => 40,
viewY => 40,
rotation => 360,
moving => true
]
},
KeyFrame {
time : 500ms
values: [
viewScaleX => 0.2 tween Interpolator.LINEAR,
viewScaleY => 0.2 tween Interpolator.LINEAR,
viewX => 10 tween Interpolator.LINEAR,
viewY => 10 tween Interpolator.LINEAR,
rotation => 0.0 tween Interpolator.LINEAR,
moving => false
]
action: function():Void {
player.pause();
mediaView.toBack();
actived = false;
}
}
]
};

public function create():Node {
Group {
content: [
this.mediaView = MediaView {
mediaPlayer: player
scaleX: bind viewScaleX
scaleY: bind viewScaleY
translateX: bind viewX
translateY: bind viewY
transform: bind [Transform.rotate(rotation,200,150)]

onMouseEntered:
function(me:MouseEvent):Void {
strokeColor = Color.BLACK
}
onMouseExited:
function(me:MouseEvent):Void {
strokeColor = Color.DARKGRAY
}
onMouseClicked:
function(me:MouseEvent):Void {
System.out.println("clicked => " + actived);
if(moving == false) {
if(actived) {
unchoiceTimeLine.start();
System.out.println("inactive - " + viewScaleX);
}
else {
choiceTimeLine.start();
System.out.println("active - " + viewScaleX);
}
}
}
},
Rectangle {
width: 708
height: 472
scaleX: bind viewScaleX
scaleY: bind viewScaleY
translateX: bind viewX
translateY: bind viewY
stroke: strokeColor
strokeWidth: 10
transform: bind [Transform.rotate(rotation,200,150)]

onMouseEntered:
function(me:MouseEvent):Void {
strokeColor = Color.BLACK
}
onMouseExited:
function(me:MouseEvent):Void {
strokeColor = Color.DARKGRAY
}
}
]
}
}
}


실행 결과
사용자 삽입 이미지
그림3. 실행 결과
 

Clip을 클릭하면 해당 동영상 클립이 회전하면서 1:1 스케일로 커지면서 동영상이 실행된다.
 
사용자 삽입 이미지
그림4. 실행 결과


아쉬운 것들
-JavaFX Document의 부실함
 
작업을 위해 다운 받아 펼쳐본 Document는 한마디로 부실함이었다. 물론 아직 초기 버전이라 그런 것이라 생각되지만 현재의 Document는 그냥 어떤 것이 있다는 것을 설명하기 위해 만들어 놓은 수준이라고 봐야 할 것 같다. JDK SE 버전의 Document에 비하면 정말 완성도가 떨어졌다.
-NetBeans JavaFX plugin 
미디어 이름에 한글이 들어가거나 공백이 있을 경우 실행시 에러가 발생
실시간으로 코딩시 에러 내용이 표시되는데 간혹 이런 에러 내용이 잘못 표시되는 문제
미디어의 이름을 바꿀 경우 간혹 미디어 적용이 안되 Rebuild 해야하는 불편함
소스에서 사용되는 클래스에 대해 자동으로 import 되지 않는 불편함
 
이런 문제들은 앞으로 JavaFX 1.0 정식 버전이 나오고 plugin이 업그레이드 되면서 점점 좋아질 것이라고 생각하지만 아직까지는 약간의 불편함을 감수하고 개발해야 한다.

JavaFX로 간단한 동영상 실행 Application을 만들면서 어떻게 이렇게 간단할 수 있지? 하는 생각이 들었다. 이해하기 쉬운 코드 몇 줄로 꽤 대단한 애니메이션 효과를 줄 수 있었고 동영상 플레이도 코드 몇 줄로 끝났다. 자바 Application으로 만들려 했으면 엄청난 소스 코드 속에서 헤메야 간신히 나올만한 프로그램을 JavaFX의 스크립트 몇 줄이 해낸것이다. 물론 이런 힘이 JavaFX 에만 있는 것이 아니다. JavaFX가 경쟁하고 있는 Flex나 Silverlight도 비슷한 수준으로 지원하고 있거나 더 월등히 앞선 기능과 툴을 지원하고 있기도 하다. 지금 현재도 간단한 Application은 만들기 너무 쉽지만 정식 버전이 나올 시점이 되면 지금보다 모든 면에서 더욱 편하고 강력해져야 할 거라고 생각한다. 위쪽에서 지적한대로 Document도 현재 JDK 만큼 완성도가 높아져야 하며 IDE의 발전과 JavaFX가 적용될 수 있는 Platform도 더욱 넓어진다면 다양한 분야에서 JavaFX 가 활약하는 것을 볼 수 있을거라 생각한다.

참조사이트 
http://www.javapassion.com/javafx/
http://www.javafx.com
http://java.sun.com/javafx/index.jsp
http://openjfx.dev.java.net


글쓴이에 대해..
 


양사열

현재 JCO 부회장이며 프리랜서를 하고 있다. 주로 웹 프로젝트와 Database 동기화 솔루션을 개발했으며RIA 쪽에 관심을 갖고 있다. 10년째 개발자로 살면서 앞으로 남은 몇십년도 개발자로 살고 싶은 욕심을 갖고 있다.
블로그 이미지

맨오브파워

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

,