Tokee's

Sehen, wozu das führt…

Januar 20, 2013
von tokee
6 Kommentare

Get notified when working with JavaFX properties

When working with JavaFX properties most of us are interested in notifications about value changes. Getting notified about any of these changes can be realised by applying a ChangeListener or an InvalidationListener. We’re fine so far, aren’t we?
(In case of you’re not familiar with using JavaFX properties, start with reading Using JavaFX Properties and Binding at Oracle’s website.)

Have a look at the following example:

package propertyexample;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class PropertyExample {

    private StringProperty value;

    public PropertyExample() {
        this.value = new SimpleStringProperty(this, "value");
    }

    public StringProperty getValueProperty() {
        return this.value;
    }
    
    public final void setValue(String stringValue) {
        this.value.set(stringValue);
    }
    
    public final String getValue() {
        return this.value.get();
    }
    
    public static void main(String[] args) {
        final PropertyExample example = new PropertyExample();
        
        example.getValueProperty().addListener(new InvalidationListener() {

            @Override
            public void invalidated(Observable o) {
                StringProperty property = (StringProperty)o;
                System.out.println("current value: " + property.getValue());
            }
        });
              
        //assign String instance with a value of 'a' and fire InvalidationListener
        example.setValue(new String("a")); 
        //assign String instance with a value of 'a' and nothing will happen
        example.setValue(new String("a"));
    }
}

As you might have expected the applied InvalidationListener will only fire once, since new String("a").equals(new String("a")) => true. This is fine for most cases, but what when you’re interested in getting notified when any value is assigned and expect the InvalidationListener firing everytime a value is assigned, even if the new value equals the previously assigned one. I spent much time and tried many different ways

  • extending the SimpleStringProperty
  • implementing a new StringProperty and
  • fallback to good old listeners

All of these ways lead to a working solution, but the expense of time and lines of code is beyond any comparison with my finally working solution. This one is as simple as just exchanging the StringProperty with an ObjectProperty<String>. This works since value comparison and marking as invalid is implemented differently in SimpleStringProperty and SimpleObjectProperty<String>.

package propertyexample;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;

public class PropertyExample {
    
    private ObjectProperty<String> value;
    
    public PropertyExample() {
        this.value = new SimpleObjectProperty<String>(this, "value");
    }
    
    public ObjectProperty<String> getValueProperty() {
        return this.value;
    }
    
    public final void setValue(String stringValue) {
        this.value.set(stringValue);
    }
    
    public final String getValue() {
        return this.value.get();
    }
    
    public static void main(String[] args) {
        final PropertyExample example = new PropertyExample();
        
        example.getValueProperty().addListener(new InvalidationListener() {
            
            @Override
            public void invalidated(Observable o) {
                ObjectProperty<String> property = (ObjectProperty<String>) o;
                System.out.println("current value: " + property.get());
            }
        });
        
        //assign String instance with a value of 'a' and fire InvalidationListener
        example.setValue(new String("a"));
        //assign String instance with a value of 'a' and fire InvalidationListener
        example.setValue(new String("a"));
    }
}

Just a note: To get notified you have to assign different references. Assigning the same reference as shown in the following example only results in firing the InvalidationListener once.

String a = new String("a");
example.setValue(a);
example.setValue(a);

But this shouldn’t be a hurlde ;)

Dezember 16, 2012
von tokee
4 Kommentare

Building and packaging of JavaFX applications with Gradle

Lately, I changed my favoured IDE from Netbeans to IntelliJ IDEA. Happy so far, I quickly realised that building and running my JavaFX applications was limited to IDEA, only. Compared to Netbeans, IDEA 12 does not provide the ability to create self-contained and double-clickable archives out-of-the-box.

Okay, some work to do… ‘Play around with Gradle’ was an item on my todo-list, so I created an initial build.gradle, which was a really simple one-liner.

apply plugin: 'java'

But, as you might have expected, this was not the final solution, since the creation of a self-contained, double-clickable archive requires some more steps of work – at least pointing to an installed JavaFX archive, required for building.

I found the JavaFX Ant Tasks at Oracle’s website. While I was on my way leaving building projects with Ant behind, this task seemed to be the simplest way of achieving the desired aim, and – as you might know, also – Gradle can do it’s job by importing Ant tasks.

Et voilà, after some hours of studying the Gradle documentation and searching the www, I have a solution consisting of two concise files (sources can be found below):

  • build.gradle
  • javafx.xml

project_layout_inv
Presuming a project layout as shown, having these scripts in your project folder, enables you to:

  • build the project with gradle build
  • run the project with gradle run (no matter, whether from console or IDEA)
  • install the project with gradle installApp
  • run the installed application via batch-file or shell-script
  • run the installed application by double-clicking the created archive
apply plugin: 'java'
apply plugin: 'application'

mainClassName = [REPLACE WITH YOUR MAIN CLASS]

def javaFxHome = "${System.properties['java.home']}";
//There seems to be an issue with Gradle detecting the JDK.
//So, we have to help a wee bit to find the ant-javafx.jar
//This may be different on your system.
def antJavaFxJar = "$javaFxHome/../lib/ant-javafx.jar"

def vendor = [REPLACE WITH VENDOR NAME]
def version = [REPLACE WITH VERSION NUMBER]
def title = [REPLACE WITH APPLICATION TITLE]

configurations {
	//we do not want to have jfxrt.jar in the classpath when creating the jar,
	//therefore a seperate configuration is required
    providedCompile
}

//this one works with file dependencies. If you prefer 
//e.g. Maven, feel free to change.
def libFolder = 'lib'
def includePattern = '*.jar'

dependencies {
    providedCompile files("$javaFxHome/lib/jfxrt.jar")
    compile fileTree(dir: libFolder, include: includePattern)
}

compileJava {
    //add required JavaFX libs to compile classpath
    sourceSets.main.compileClasspath += configurations.providedCompile
}

run {
    //add required JavaFX libs to runtime classpath
    classpath += configurations.providedCompile
}

//ant configuration for creating double-clickable, self-contained JAR
ant.importBuild 'javafx.xml'
ant.antJavaFxJar = antJavaFxJar
ant.mainClassName = mainClassName
ant.fallbackClassName = 'com.javafx.main.NoJavaFXFallback'
ant.distDir = libsDir
ant.distName = jar.archiveName;
ant.resourceDir = libFolder
ant.resourceIncludePattern = includePattern
ant.applicationTitle = title
ant.applicationVendor = vendor
ant.applicationVersion = version
ant.applicationClasses = sourceSets.main.output.classesDir
ant.applicationResources = sourceSets.main.output.resourcesDir

//clear existing task actions and call ant task
jar {
    // reset actions
    actions = []

    doLast {
        javafxjar.execute(); // <-- the task described in javafx.xml
}

//create some smarter looking start scripts
startScripts {
    doLast {
        unixScript.text = "java -jar ../lib/$jar.archiveName"
        windowsScript.text = "java -jar ..\\lib\\$jar.archiveName"
    }
}
<project name="JavaFXApplication"
         xmlns:fx="javafx:com.sun.javafx.tools.ant">
    <target name="javafxjar">

        <taskdef resource="com/sun/javafx/tools/ant/antlib.xml"
                 uri="javafx:com.sun.javafx.tools.ant"
                 classpath="${antJavaFxJar}"/>

        <fx:application id="application"
                        name="${applicationTitle}"
                        mainClass="${mainClassName}"
                        fallbackClass="${fallbackClassName}"/>

        <!-- show some info in gradle's output-->
        <echo message="creating package: ${distName}"/>

        <fx:jar destfile="${distDir}/${distName}">
            <fx:application refid="application"/>

            <!-- define the classpath to be placed in the manifest -->
            <fx:resources>
                <fx:fileset dir="${resourceDir}"
                            includes="${resourceIncludePattern}"/>
            </fx:resources>

            <manifest>
                <attribute name="Implementation-Vendor"
                           value="${applicationVendor}"/>
                <attribute name="Implementation-Title"
                           value="${applicationTitle}"/>
                <attribute name="Implementation-Version"
                           value="${applicationVersion}"/>
            </manifest>

            <!-- describe the content to be packed -->
            <fileset dir="${applicationClasses}"/>
            <fileset dir="${applicationResources}"/>
        </fx:jar>
    </target>
</project>

Refinements for testing, integration of the Gradle Wrapper, etc. are possible, and may be described in another blog post.

Enjoy!