Wednesday Aug 26, 2009

Get a swing component rendered to an image

Today my friend contacted me with an issue he faced with his program in swing. He wanted to get a swing component painted on to an image, except that he didn't want to show the component on screen. This was the code snippet he sent me:


    JPanel panel = new JPanel();
    panel.setSize(200, 200);
    panel.setBackground(Color.BLUE);
    panel.setLayout(new BorderLayout());
    panel.add(new JLabel("Hello"));

    //Get the panel rendered to an image
    BufferedImage image = new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB);
    Graphics g = image.createGraphics();
    panel.paintAll(g);
    g.dispose();
    ImageIO.write(image, "jpeg", <file>);
    image.flush();
  

Now, if the JPanel was added to a JFrame and shown, all worked well - the JPanel along with the JLabel in it was being correctly captured as image. But, when the JPanel wasn't added anywhere, only the blue background of JPanel was being captured as image. 

This seems to be a painting optimization being done in java. As per the documentation of Component.paint method


    For performance reasons, Components with zero width or height aren't 
    considered to need painting when they are first shown, and also aren't 
    considered to need repair.
  

My colleague Praveen came up with a workaround for this. The code was changed as follows and that solved the issue. Now, the JPanel along with its content is being captured correctly.


    Graphics g = image.createGraphics();
    panel.addNotify();
    panel.validate();
    panel.paintAll(g);
    g.dispose();
  

This seems to work in headless mode too.

Monday Aug 03, 2009

Reflection in JavaFX

Last week I was trying to learn how to use JavaFX's reflection APIs - I was working on a test framework and had to use reflection to run a test. My requirement was quite simple: I had to instantiate a JavaFX class using reflection and had to call one of its functions. I didn't realize its difficult until I went through JavaFX's reflection api. I was expecting an api set similar to Java's reflection api, but I was wrong. I couldn't find much documentation on using it either. The sole blog I could find on reflection, didn't talk about what I was looking for. So, I finally had to experiment with reflection API to figure out how it works. I'm just jotting down what I learned, may be it could help someone taking the same route.

Here is a sample JavaFX class.


    public class ClassA {
        public var count: Integer;
        init {
            println("ClassA init");
        }
        public function methodA(): Void {
            println("methodA called");
        }
        public function methodB(str: String, num: Integer): Integer {
            println("methodB called {str} {num}");
            return 20;
        }
        public function methodC(): Number[] {
            println("methodC");
            return [10.0, 100.6, 1000.8];
        }
        public function methodD(num: Integer, seq: String[]): Void {
            println("methodD called {num} {sizeof seq}");
        }
    }
  

The classes for JavaFX reflection api is located in javafx.reflect package. For reflective operations you need an instance of 'FXContext' to operate on. In order to access objects and types in the current JVM, you have 'FXLocal.Context', which is an implementation of 'FXContext' using Java reflection (as per the API doc). I believe FXLocal.Context is what we need to use.


    var context = FXLocal.Context.getInstance();
  

Below code is self explanatory. I've given some comments on what they do, or rather my understanding of what they do :-)


    //This returns an instance of FXLocal.ClassType, a subclass of FXClassType.
    var cls = context.findClass("ClassA");

    //Creates an instance of 'ClassA'
    var objValue = cls.newInstance();
  

The above 'newInstance' call creates an instance of 'ClassA'. But the same can be done in two steps as below


    //A raw uninitialized object is returned by 'allocate()' function.
    var objValue = cls.allocate();

    //'initialize()' function of FXObjectValue makes the 'init' of the class to be called.
    objValue = objValue.initialize();
  

This is beneficial because you may need to initialize any variables of the class before it's 'init' is called, as follows:


    var objValue = cls.allocate();
    var num: Integer = 10;
    objValue.initVar("count", context.mirrorOf(num));
    objValue = objValue.initialize();
  

Note that the above 'objValue' is not a JavaFX 'object', but an instance of 'FXLocal.ObjectValue'. You can get an object from 'objValue', cast it to a class you want. That is covered in Jim's blog.

Now, we can look at how to call functions of this class⁞. Let us see how to call 'methodA()' which neither has any arguments nor any return value. 'getFunction' method of FXClassType accepts method name and a sequence of argument types of the function. Since 'methodA' is a no args function, an empty sequence is passed.


    var fnA = cls.getFunction("methodA", []);
    fnA.invoke(objValue, []);
  
Below is how the function 'methodB' is called. The function accepts two arguments, a String and an Integer. FXContext has methods which return the 'FXType' of different data types.


    var argString = "argument";
    var number = 20;
    var fnB = cl.getFunction("methodB", [context.getStringType(), context.getIntegerType()]);
    var retVal = fnB.invoke(ni, [context.mirrorOf(argString), context.mirrorOf(number)]);
  

It can be seen that methodB returns an Integer. It is fairly simple to retrieve the return value from FXValue.


    var retInt = Integer.parseInt(retVal.getValueString());
  

We also have a function 'methodC' which returns a Number sequence. This is how the return value is obtained from the FXValue


    var fnC = cl.getFunction("methodC", []);
    var fnCVal = fnC.invoke(ni, []);
    if (fnCVal.getItemCount() > 0) {
        for (i in [0..fnCVal.getItemCount() - 1) {
            var f = Float.parseFloat(fnCVal.getItem(i).getValueString());
            println("Value at index {i} is {f}");
        }
    }
  

I couldn't get to call a function which accepts a sequence argument. Somehow, 'getFunction' throws a ClassCastException for me. I tried both these ways, but both threw CCE for me.


    var inputSeq = ["ABC", "DEF", "GHI"];
    var fnD = cl.getFunction("methodD", [context.getIntegerType(), context.getStringType().getSequenceType()]);
    
    //or
    
    var valSeq = context.makeSequenceValue(for (i in inputSeq) context.mirrorOf(i), sizeof inputSeq, context.getStringType());
    var fnD = cl.getFunction("methodD", [context.getIntegerType(), valSeq.getType()]);
  

I'm still trying to figure out the right way to do it - or it could be a bug!

Update: This in fact turned out to be a JavaFX bug: see JFXC-3381. For now, you can loop through all the functions returned by 'getFunctions' method to find the method you are looking for, though I'm not sure if calling that method will work without issues.

Monday Jul 06, 2009

Adding nodes to a scenegraph

A few of us were recently involved in porting an application written by a third party to the latest JavaFX 1.2. The application was originally written using JavaFX 1.0. The application as such was slow and some parts of the application was too slow to be usable. After porting it to 1.2, the application performance improved significantly compared to its 1.0 version, but it wasn't fast enough. Though we didn't have time to look into the general slowness, we had to look at one particular performance issue. It was kind of recreating a portion of what the app shows depending on user input. This was too slow and the performance was degrading over time. 

It wasn't tough to solve. The code originally was doing the following:

  • Delete a set of nodes present in a group
  • Depending on the input, create another set of nodes
  • Add back the new set of nodes to the group

Creating new nodes is an expensive task. Old nodes had to be gc'd too. Added to that, here we had to delete nodes from a sequence and add back another set of nodes to the sequence. The solution was to rewrite the code to get rid of the expensive node creation and sequence operations. We manipulate the existing nodes by altering its attribute values instead of creating a new set of nodes with new values.

Here is how the actual code looked like:


        delete from content;
        //some call to a java library to get a set of records 
        //depending on user input
        var records: Iterator = getRecords(value).iterator();
        for (records.hasNext()) {
            var record = records.next();
            insert Text {
                content: record.getText()
                translateX: record.getX()
                translateY: record.getY()
            } into content;
        }
  

This was changed to something like the following:


        var records: Iterator = getRecords(value).iterator();
        var index: Integer = 0;
        for (records.hasNext()) {
            var record = records.next();

            //Check if the sequence contains the required number of nodes
            if (index < sizeof content) {
                //modify existing nodes
                var text = content[index];
                text.content = record.getText();
                text.translateX = record.getX();
                text.translateY = record.getY();
                text.visible = true;
            } else {
                //create new nodes only if required
                insert Text {
                    content: record.getText()
                    translateX: record.getX()
                    translateY: record.getY()
                } into content;
            }
            index++;
        }
        //Hide any existing unwanted nodes - don't remove them
        if (index < sizeof content) {
            for (i in [index..sizeof content - 1]) {
                content[index].visible = false;
            }
        }

The above is a simplification of the actual code. The actual application was trying to remove and add back around 100 nodes - it consisted of Text and Line nodes. The modification of code resulted in a significant (2x to 3x) improvement in performance. The gradual degradation of performance was also solved. We still have to look into why the actual code resulted in the repainting becoming progressively slower. This looked like a memory leak somewhere - we have to investigate this.

So, to those who program using JavaFX, keep in mind that you might be sacrificing performance if you are trying to change the scenegraph by removing existing nodes and replacing them with new nodes.

Sunday Jul 05, 2009

A Starter

I've been planning to start blogging for quite some time now. Whenever I felt I need to write about something, I had this thought which stopped me - that I will not find any topics in the future and my blog might die. But, now I feel I have had enough of those postponements and it was just an inertia which will be tough to overcome. This is my try to get over that inertia.

So, welcome me to blogging! :)