|
  
- Thread
- 753
- Credit
- 592
- Money
- 562
- Read Perm.
- 200
- From
- Phnom Penh
- Joined
- 28-2-2009
         
|
Open source Java projects: JFXtras part 1
JavaFX is a rapidly maturing technology, but its capabilities are still limited. In this installment of Open source Java projects, Jeff Friesen introduces you to JFXtras, utilities and add-ons that fill in useful features that are absent in JavaFX 1.0.
JavaFX 1.0 is missing a lot of functionality, especially in regard to UI components and layouts. To address these deficiencies, developer Stephen Chin initiated the JFXtras project in late 2008. He released JFXtras 0.1 shortly after JavaFX 1.0 debuted. This initial JFXtras release introduced dialog boxes, more layouts, a unit-testing framework, and asynchronous thread support to JavaFX.
The JFXtras 0.2 milestone, released in January 2009, adds a new library of custom shapes via integration with Andres Almiray's jSilhouette Project, and it enhances 0.1's Grid layout, unit-testing framework, and more. With 0.3's release on February 17 (shortly after this article's completion), JFXtras is becoming an increasingly significant part of the JavaFX landscape.
This article focuses on JFXtras 0.2. After introducing you to the project's software distribution, I demonstrate how to use the software via the NetBeans and command-line versions of JavaFX. I then explore JFXtras' various features, ranging from asynchronous thread support and a custom shapes library to unit testing and utilities classes.
Getting started with JFXtras
The JFXtras project site's downloads page provides links for downloading JFXtras-0.2.jar and JFXtras 0.2.zip. If you're interested in the custom shapes library, you must download the ZIP file, which includes JFXtras-0.2.jar and two other JAR files that provide the jSilhouette scene graph infrastructure for custom shapes. If custom shapes are of no interest to you, you can download JFXtras-0.2.jar instead, but I recommend downloading JFXtras 0.2.zip. This archive's JFXtras 0.2 home directory contains the ChangeLog.txt, JFXtras-0.2.jar, and LICENSE.txt files. It also contains javadoc (API documentation), lib (jsilhouette-geom-0.3.jar and jsilhouette-scene-0.3.jar, custom shapes support JARs), src (JFXtras source code), and test (scripts for unit-testing JFXtras) subdirectories.
In addition to JFXtras 0.2, you need to install either NetBeans IDE 6.5 with JavaFX 1.0 or the JavaFX 1.0 SDK (if not already installed). Also, Windows platforms should have Java SE 6u10 or a higher update installed, whereas Macintosh platforms will benefit from Java 10.5 Update 2 (1.6.0_07). I developed this article's code with both JavaFX SDKs and Java SE 6u12 on a Windows XP SP3 platform.
A simple scriptBefore exploring JFXtras, let's play with a couple of examples, to familiarize you with developing scripts in the context of the NetBeans and command-line versions of JavaFX 1.0. For our first example, check out Listing 1.- /*
- * Main.fx
- */
- package charseq;
- import org.jfxtras.util.*;
- def digits = SequenceUtil.characterSequence ("0", "9");
- println (digits) // Output: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
Copy Code This script invokes the SequenceUtil class's characterSequence() function to generate a sequence of single-character Strings from 0 through 9. You'll learn about characterSequence() and other SequenceUtil functions later in the article.
You'll probably prefer to work in the NetBeans environment, so complete the following steps to compile and run this script using NetBeans IDE 6.5 with JavaFX 1.0:
- Use the New Project wizard to introduce a CharSeq project with charseq.Main as the main file.
- Replace the generated skeletal Main.fx with Listing 1.
- Right-click CharSeq in the Projects window and select Properties from the pop-up menu.
- Select the Libraries category on the resulting Project Properties dialog box.
- Click the Add JAR/Folder button.
- Locate the appropriate directory containing JFXtras-0.2.jar and add this JAR to the Libraries list.
- Dismiss the Project Properties dialog.
- Click the green triangle button on the main toolbar, or press F6 to run the script.
If you would rather work at the command line, complete the following (Windows-oriented) steps to compile and run this script using the command-line version of JavaFX:
- Create a charseq subdirectory of the current directory.
- Copy Listing 1 to a Main.fx file and store this file in charseq.
- Copy JFXtras-0.2.jar to the current directory.
- Compile Main.fx via javafxc -cp JFXtras-0.2.jar charseq\Main.fx.
- Run the script via javafx -cp JFXtras-0.2.jar;. charseq.Main.
A shape-oriented scriptWhen working with JFXtras, you'll often only need to access JFXtras-0.2.jar. However, if you're planning to work with custom shapes, as is the case with Listing 2, you'll also need to access jsilhouette-geom-0.3.jar and jsilhouette-scene-0.3.jar.- /*
- * Main.fx
- */
- package asterisk;
- import javafx.scene.Scene;
- import javafx.scene.paint.Color;
- import javafx.stage.Stage;
- import org.jfxtras.scene.shape.Asterisk;
- Stage
- {
- title: "Centered Asterisk"
- width: 250
- height: 250
- var sceneRef: Scene
- scene: sceneRef = Scene
- {
- content: Asterisk
- {
- centerX: bind sceneRef.width/2.0
- centerY: bind sceneRef.height/2.0
- radius: 80
- width: 20
- beams: 5
- roundness: 0.5
- fill: Color.RED
- }
- }
- }
Copy Code This script creates a scene consisting of a single five-legged red asterisk shape whose center is always bound to the center of the scene. You'll learn about Asterisk and other custom shape
classes later in this article.
- Use the New Project wizard to introduce an Asterisk project with asterisk.Main as the main file.
- Replace the generated skeletal Main.fx with Listing 2.
- Right-click Asterisk in the Projects window and select Properties from the pop-up menu.
- Select the Libraries category on the resulting Project Properties dialog box.
- Click the Add JAR/Folder button.
- Locate the appropriate directory containing JFXtras-0.2.jar and add this JAR to the Libraries list.
- Click the Add JAR/Folder button a second time.
- Change to the lib subdirectory containing jsilhouette-geom-0.3.jar and add this JAR to the Libraries list.
- Click the Add JAR/Folder button a third time.
- Add jsilhouette-scene-0.3.jar to the Libraries list.
- Dismiss the Project Properties dialog.
- Click the green triangle button on the main toolbar, or press F6 to run the script.
 Follow the steps below to build and run this script using the command-line version of JavaFX:
- Create an asterisk subdirectory of the current directory.
- Copy Listing 2 to a Main.fx file and store this file in asterisk.
- Copy JFXtras-0.2.jar to the current directory.
- Create lib as a subdirectory of the current directory.
- Copy jsilhouette-geom-0.3.jar and jsilhouette-scene-0.3.jar to lib.
- Compile Main.fx via javafxc -cp JFXtras-0.2.jar asterisk\Main.fx.
- Run the script via javafx -cp JFXtras-0.2.jar;. asterisk.Main.
Exploring JFXtrasJFXtras 0.2 consists of nearly 50 classes organized into 10 packages, with each package name beginning with the org.jfxtras prefix. This section briefly examines some of these classes. For complete details, check out the API documentation in JFXtras 0.2.zip's javadoc directory, or point your browser to the online Javadoc.
Asynchronous thread supportJavaFX Script is a single-threaded language. On Swing-based platforms, the event-dispatching thread runs JavaFX code. When this thread is delayed, such as when it's waiting for a Web-based document to download, the user interface's responsiveness suffers. To overcome this problem, you'll want to perform time-consuming operations on a background thread.
The nature of JavaFX's implementation makes it potentially dangerous to subclass Java's Thread class and start a background thread based on this subclass. Instead, you'll typically need to subclass JavaFX's AbstractAsyncOperation class (see James Clarke's JavaFX Async operations blog post for an example) if its RemoteTextDocument subclass doesn't meet your needs.
Subclassing AbstractAsyncOperation is somewhat cumbersome, especially when you find yourself coding part of the subclass's implementation in Java (to avoid unintended conflicts with binding, triggers, and the rest of the JavaFX runtime). Fortunately, you can avoid this challenge by taking advantage of JFXtras' org.jfxtras.async.JFXWorker class, which offers a general-purpose solution.
JFXWorker uses Java SE 6's SwingWorker class to execute an asynchronous operation on a background thread and make the result available on the event-dispatching thread. The asynchronous operation is described via a function assigned to JFXWorker's inBackground attribute. When the operation completes, JFXWorker invokes the function assigned to the onDone attribute.
The function assigned to inBackground returns an object (or null), which is then passed as an argument to onDone's function after inBackground's function successfully completes. However, if inBackground's function throws an exception, the function assigned to the onFailure attribute is invoked instead of onDone's function.-
- /*
- * Main.fx
- */
- package textsrch;
- import java.io.File;
- import java.io.FileReader;
- import java.io.IOException;
- import java.lang.Thread;
- import java.nio.CharBuffer;
- import java.util.regex.Pattern;
- import javafx.ext.swing.SwingButton;
- import javafx.ext.swing.SwingLabel;
- import javafx.ext.swing.SwingList;
- import javafx.ext.swing.SwingListItem;
- import javafx.ext.swing.SwingScrollPane;
- import javafx.ext.swing.SwingTextField;
- import javafx.scene.Cursor;
- import javafx.scene.Scene;
- import javafx.scene.layout.HBox;
- import javafx.scene.layout.VBox;
- import javafx.scene.paint.Color;
- import javafx.stage.Stage;
- import org.jfxtras.async.JFXWorker;
- class FindModel
- {
- // The following attribute is populated (on a JFXWorker background thread)
- // with the SwingListItem-wrapped paths of matching files during a search.
- // It's also accessed by the UI (via binding) on the event-dispatching
- // thread to present these paths in a Swing listbox.
- var items: SwingListItem [];
- // The following attribute is used to dynamically disable the Search button
- // and enable the Stop button once a search begins, and do the reverse once
- // a search ends. More importantly, it's used to prevent updating a
- // SwingList component's items attribute (via binding) until the search
- // ends. This is done to avoid a potential thread-synchronization problem.
- var searching: Boolean;
- // The rest of these attributes should not be accessed from outside this
- // class. If I factored out this class into its own file, I would make
- // sure that only the two attributes above could be accessed.
- var worker: JFXWorker;
- def LIMIT = 500; // Store no more than LIMIT items in items, to avoid
- // exhausting heap memory and generating an
- // OutOfMemoryError. How might this happen? Imagine a
- // scenario where you have 10 roots to search, and they
- // have a combined total of 10 million matching files --
- // we're searching for the empty string (which always
- // matches), for example. Also, suppose the average path
- // length for these files is 50 characters. We would need
- // approximately one billion bytes of heap memory to store
- // all of these paths in items.
- var counter: Integer;
- def BUFSIZE = 60000; // 40000-60000 seems to be optimal on my platform.
- def buffer = CharBuffer.allocate (BUFSIZE);
- function find (text: String): Void
- {
- searching = true;
- worker = JFXWorker
- {
- inBackground: function ()
- {
- // This code executes on a background thread.
- delete items;
- counter = 0;
- // This script is coded to search all files on all roots. As an
- // exercise, improve the UI and the following code to allow the
- // user to choose a range of roots, and a range of a given root
- // to search.
- def roots = File.listRoots ();
- for (i in [0..<sizeof roots])
- findall (roots [i], text);
- return null
- }
- onDone: function (result): Void
- {
- // This code executes on the event-dispatching thread.
- searching = false;
- }
- }
- }
- function kill (): Void
- {
- worker.cancel ()
- }
- // The rest of these functions should not be accessed from outside this
- // class. If I factored out this class into its own file, I would make
- // sure that only the two functions above could be accessed.
- function find (filename: String, srchtext: String): Boolean
- {
- // All files match the empty string.
- if (srchtext.length () == 0)
- return true;
- // Compile the search text as a regex pattern (for performance).
- // Treat all special regex characters as if they were literals,
- // and also select case-insensitive pattern matching.
- def pattern = Pattern.compile (srchtext, Pattern.LITERAL+
- Pattern.CASE_INSENSITIVE);
- var fr: FileReader;
- try
- {
- fr = new FileReader (filename);
- // Prepare for initial read.
- buffer.clear ();
- while (true)
- {
- // Read up to BUFSIZE characters into the buffer. If the
- // return value is -1, no characters have been read.
- // Therefore, we can safely conclude that the file is
- // either empty (if this is the first read) or that the
- // search text is not present in the file.
- if (fr.read (buffer) == -1)
- return false;
- // Confine pattern match to only those characters that have
- // been read -- not to the entire buffer.
- buffer.flip ();
- // Extract a matcher and attempt to find the search text in
- // the buffer.
- def matcher = pattern.matcher (buffer);
- if (matcher.find ())
- return true;
- // Because the text was not found, continue the search after
- // copying srchtext.length ()-1 characters from the end
- // of the buffer to the start of the buffer. The search
- // starts with these characters, which might actually be the
- // beginning of the search text.
- buffer.limit (buffer.capacity ());
- buffer.position (buffer.capacity()-srchtext.length ()+1);
- buffer.compact()
- }
- }
- catch (e: IOException)
- {
- }
- finally
- {
- if (fr != null)
- try { fr.close () } catch (e: IOException) {}
- }
- return false
- }
- function findall (file: File, srchtext: String): Void
- {
- var files: File [] = file.listFiles ();
- for (i in [0..<sizeof files])
- {
- if (Thread.currentThread ().isInterrupted ())
- return;
- if (counter == LIMIT)
- return;
- if (files [i].isDirectory ())
- findall (files [i], srchtext)
- else
- if (find (files [i].getPath (), srchtext))
- {
- insert SwingListItem { text: files [i].getPath () }
- into items;
- counter++
- }
- }
- delete files
- }
- }
- Stage
- {
- def model = FindModel {}
- title: "Text Search"
- width: 400
- height: 300
- resizable: false
- var sceneRef: Scene
- scene: sceneRef = Scene
- {
- fill: Color.GOLD
- var vboxRef: VBox
- content: vboxRef = VBox
- {
- var input: SwingTextField
- content:
- [
- HBox
- {
- content:
- [
- SwingLabel
- {
- text: "Enter search text"
- }
- input = SwingTextField
- {
- columns: 23
- }
- ]
- spacing: 10
- }
- HBox
- {
- content:
- [
- SwingButton
- {
- action: function (): Void
- {
- model.find (input.text)
- }
- enabled: bind not model.searching
- text: "Search"
- }
- SwingButton
- {
- action: function (): Void
- {
- model.searching = false;
- model.kill ()
- }
- enabled: bind model.searching
- text: "Stop"
- }
- ]
- spacing: 10
- }
- SwingLabel
- {
- text: "Results"
- }
- SwingScrollPane
- {
- cursor: bind if (model.searching)
- Cursor.WAIT
- else
- Cursor.DEFAULT
- view: SwingList
- {
- items: bind if (model.searching)
- [
- SwingListItem
- {
- text: "Scanning files..."
- }
- ]
- else
- model.items
- }
- width: bind sceneRef.width-2*vboxRef.spacing
- }
- ]
- spacing: 10
- translateX: bind (sceneRef.width-vboxRef.boundsInLocal.width)/2
- translateY: bind (sceneRef.height-vboxRef.boundsInLocal.height)/2
- }
- }
- }
Copy Code |
|