Question

RESUME

Good day StackOverflow community.

I've been trying for some time to develop a program that enables users to put objects in an area, allowing this area to be moved by the mouse. For this type of program, I decided to use a ScrollPane, because the user can add various contents in the area which I call the canvas. For some reason, something strange is happening in my program.

EXPLANATION OF PROGRAM

What I basically did was create a group of objects, and define this group as the ScrollPane content. Within the group, there is a Rectangle object that was added to serve as canvas boundaries. This object has larger dimensions (such as 1500 x 1000, for example), and is used in calculations that prevent nodes from moving beyond its limits. This is just the logical behind the existing large rectangle in my program, but in reality, there is no Node object with the mouse movement. What exists is the random distribution of Shape objects by the rectangle area.

For ScrollPane has its scrollbars moved, I use the setHvalue setVvalue methods. Unfortunately for my purposes, this method does not change the position of the ScrollPane's viewport with pixel values​​, but values ​​that are in a range between 0f and 1f. So I can move the viewport with the mouse, I used a equation known as Rule of 3 (here in my Country, as I know), which we equate values ​​and cross multiply.

For example, say I want to move the viewport of the ScrollPane with the mouse horizontally, and that my canvas area has a width of 2000 pixels. Finding how far (in pixels) the mouse was dragged from one point to another, I need to know how this value represents in a range 0f to 1f. Suppose I have dragged the mouse in 3 pixels, I could find the representation of the 0f to 1f with the following comparison:

2000 px ---- 1f
   3 px ---- xf

Multiplying crossed, I'll get the following result:

xf = 3 / 2000
xf = 0.0015

Note: I believe you all know that. I'm not teaching math to anyone, just want to explain the logic of my problem.

SOURCE CODE

Here is my program class:

import testes.util.TestesUtil;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;

public class ScrollTest4 extends Application
{
// #########################################################################################################
//                                                                                                      MAIN
// #########################################################################################################

public static void main(String[] args) 
{
    Application.launch(args);
}

// #########################################################################################################
//                                                                                                INSTÂNCIAS
// #########################################################################################################

// OUTSIDE

private BorderPane root;
private Button but_moreH;
private Button but_lessH;
private Button but_moreV;
private Button but_lessV;

// LOG

private VBox vbox_south;
private Label lab_hValue;
private Label lab_vValue;
private Label lab_viewport;

// INSIDE

private Rectangle rec_canvas;
private ScrollPane scroll;
private Group grp_objects;

// MOUSE

private double mouse_x = 0;
private double mouse_y = 0;

// MISC

private AnimationTimer timer;

// EDITED - 08/02/2014
private boolean moreH = false;
private boolean moreV = false;  // Purposely unused.
private boolean lessH = false;
private boolean lessV = false;  // Purposely unused.

// #########################################################################################################
//                                                                                                 INÍCIO FX
// #########################################################################################################

@Override public void start(Stage estagio) throws Exception 
{
    this.iniFX();
    this.confFX();
    this.adFX();
    this.evFX();

    Scene cenario = new Scene(this.root , 640 , 480);

    estagio.setScene(cenario);
    estagio.setTitle("Programa JavaFX");
    estagio.show();
}

protected void iniFX()
{
    // OUTSIDE

    this.root = new BorderPane();
    this.but_moreH = new Button();
    this.but_lessH = new Button();
    this.but_moreV = new Button();
    this.but_lessV = new Button();

    // LOG

    this.vbox_south = new VBox();
    this.lab_hValue = new Label();
    this.lab_vValue = new Label();
    this.lab_viewport = new Label();

    // INSIDE

    this.scroll = new ScrollPane();
    this.grp_objects = new Group();
    this.rec_canvas = new Rectangle();

    // MISC

    this.timer = new AnimationTimer()
    {
        @Override public void handle(long now) 
        {
            // EDITED - 08/02/2014
            if(but_moreH.isArmed() || moreH)
            {
                // scroll.hvalueProperty().set(scroll.hvalueProperty().get() + 0.003f);
                scroll.setHvalue(scroll.getHvalue() + 0.003f);
            }
            // EDITED - 08/02/2014
            if(but_lessH.isArmed() || lessH)
            {
                // scroll.hvalueProperty().set(scroll.hvalueProperty().get() - 0.003f);
                scroll.setHvalue(scroll.getHvalue() - 0.003f);
            }
            if(but_moreV.isArmed())
            {
                scroll.setVvalue(scroll.getVvalue() + 0.003f);
            }
            if(but_lessV.isArmed())
            {
                scroll.setVvalue(scroll.getVvalue() - 0.003f);
            }
        }
    };
    this.timer.start();
}

protected void confFX() 
{
    // OUTSIDE

    this.but_moreH.setText("More H");
    this.but_moreH.setMaxHeight(Double.MAX_VALUE);

    this.but_lessH.setText("Less H");
    this.but_lessH.setMaxHeight(Double.MAX_VALUE);

    this.but_moreV.setText("More V");
    this.but_moreV.setMaxWidth(Double.MAX_VALUE);

    this.but_lessV.setText("Less V");
    this.but_lessV.setMaxWidth(Double.MAX_VALUE);

    // LOG

    this.updateHvalue();
    this.updateVvalue();
    this.updateViewport();

    // INSIDE

    this.rec_canvas.setWidth(1200);
    this.rec_canvas.setHeight(1000);
    this.rec_canvas.setFill(Color.INDIANRED);
    this.rec_canvas.setStroke(Color.RED);
    this.rec_canvas.setStrokeType(StrokeType.INSIDE);
    this.rec_canvas.setStrokeWidth(1);
}

protected void adFX() 
{
    // LOG

    this.vbox_south.getChildren().add(this.but_moreV);
    this.vbox_south.getChildren().addAll(this.lab_hValue , this.lab_vValue , this.lab_viewport);

    // OUTSIDE

    this.root.setCenter(this.scroll);
    this.root.setTop(this.but_lessV);
    this.root.setBottom(this.vbox_south);
    this.root.setRight(this.but_moreH);
    this.root.setLeft(this.but_lessH);

    // INSIDE

    this.grp_objects.getChildren().add(this.rec_canvas);
    this.scroll.setContent(this.grp_objects);

    // MISC

    StrokeType[] strokes = {StrokeType.CENTERED , StrokeType.INSIDE , StrokeType.OUTSIDE};

    for(int cont = 0 ; cont < 20 ; cont++)
    {
        Rectangle node = new Rectangle(Math.random() * 100 + 50 , Math.random() * 100 + 50);
        node.setFill(TestesUtil.getCorAleatoria(false));
        node.setStroke(TestesUtil.getCorAleatoria(false));
        node.setStrokeType(strokes[(int) (Math.random() * 2)]);
        node.setStrokeWidth(Math.random() * 9 + 1);
        node.setRotate(Math.random() * 360);
        node.setMouseTransparent(true);

        // EDITED - 08/02/2014
        TestsUtil.putRandomlyIn(
                node , 
                rec_canvas.getBoundsInParent().getMinY() ,
                rec_canvas.getBoundsInParent().getMinY() + rec_canvas.getBoundsInParent().getHeight() , 
                rec_canvas.getBoundsInParent().getMinX() + rec_canvas.getBoundsInParent().getWidth() , 
                rec_canvas.getBoundsInParent().getMinX() );

        this.grp_objects.getChildren().add(node);
    }
}

protected void evFX() 
{
    // ##########################
    //     SCROLL PROPERTIES
    // ##########################

    this.scroll.hvalueProperty().addListener(new ChangeListener<Number>()
    {
        @Override public void changed(ObservableValue<? extends Number> observable,Number oldValue, Number newValue) 
        {
            updateHvalue();
            updateViewport();
        }
    });

    this.scroll.vvalueProperty().addListener(new ChangeListener<Number>()
    {
        @Override public void changed(ObservableValue<? extends Number> observable,Number oldValue, Number newValue) 
        {
            updateVvalue();
            updateViewport();
        }
    });

    this.scroll.setOnKeyPressed(new EventHandler<KeyEvent>()
    {
        @Override public void handle(KeyEvent e) 
        {
            if(e.getCode() == KeyCode.RIGHT)
            {
                moreH = true;
            }
            else if(e.getCode() == KeyCode.LEFT)
            {
                lessH = true;
            }
        }
    });

    this.scroll.setOnKeyReleased(new EventHandler<KeyEvent>()
    {
        @Override public void handle(KeyEvent e) 
        {
            if(e.getCode() == KeyCode.RIGHT)
            {
                moreH = false;
            }
            else if(e.getCode() == KeyCode.LEFT)
            {
                lessH = false;
            }
        }
    });

    // ##########################
    //          CANVAS
    // ##########################

    this.rec_canvas.setOnMousePressed(new EventHandler<MouseEvent>() 
    {
        @Override public void handle(MouseEvent e) 
        {
            // The XY distance from the upper left corner of the canvas.
            mouse_x = e.getX();
            mouse_y = e.getY();
        }
    });

    this.rec_canvas.setOnMouseDragged(new EventHandler<MouseEvent>() 
    {
        @Override public void handle(MouseEvent e) 
        {
            // ##########################
            //           PIXELS 
            // ##########################

            // The distance between mouse movements (drag events).
            double xPixelsMoved = e.getX() - mouse_x;
            // double yPixelsMoved = e.getY() - mouse_y;

            // ##########################
            //           TO 1F
            // ##########################

            double h_of_1f = xPixelsMoved / rec_canvas.getBoundsInParent().getWidth();
            double h_of_1f_inverted = h_of_1f * -1;

            double currentH = scroll.getHvalue();
            scroll.setHvalue(currentH + h_of_1f);

            // scroll.hvalueProperty().set(scroll.getHvalue() + h_de_x);
            // scroll.vvalueProperty().set(scroll.getVvalue() + v_de_y);

            // ##########################
            //           DEBUG
            // ##########################

            System.out.printf("xPixelsMoved: %f , h_of_1f: %f , h_of_1f_inverted: %f %n", 
                    xPixelsMoved , h_of_1f , h_of_1f_inverted);

            // ##########################
            //        UPDATE FROM 
            //       EVENT TO EVENT
            // ##########################

            // Writes last mouse position to update on new motion event.
            mouse_x = e.getX();
            mouse_y = e.getY();
        }
    });
}

// #########################################################################################################
//                                                                                                     MISC.
// #########################################################################################################

protected void updateViewport()
{
    Bounds vport = this.scroll.getViewportBounds();
    this.lab_viewport.setText(String.format("Viewport - [X: %f , Y: %f , W: %f , H: %f]", 
            vport.getMinX() , vport.getMinY() , vport.getWidth() , vport.getHeight() ));
}

protected void updateHvalue()
{
    this.lab_hValue.setText("H value: " + this.scroll.getHvalue() );
}

protected void updateVvalue()
{
    this.lab_vValue.setText("V value: " + this.scroll.getVvalue() );
}

}

THE PROBLEM

Clicking the mouse button on the canvas area and drag it, you can see that the program moves the ScrollPane viewport horizontally. The program seems to work perfectly (or not). However, something goes wrong at the time when the mouse is dragged sometimes abruptly (...or not!). At certain times the ScrollPane Viewport is not visually updated. This is a strange behavior, because even if viewport is not visually updated, the scrollbars are still updated.

I put other ways to move the ScrollPane viewport horizontally using the same method, and for some reason, only the approach using the mouse makes it happen. I thought this could be solved by making a request for layout using requestLayout, also causing a request to a pulse, but it does not work.

THE TEST OUTPUT

The odd thing is that everything returns to normal when the window of my application is resized. Here's a video that shows what happens to my program:

VIDEO & MIRROR 1

I no longer know what else to do. Can anyone help me with this please?

EDIT (08/02/2014 10:08 AM GMT - 3:00)

The original source code of my application is found written in Portuguese, so you may be seeing something unknown. Basically TestesUtil is a utility class with static methods that define shortcuts to other client classes. I changed the call from my source code shown here previously and am now putting some methods of my class TestesUtil, translated into English as TestsUtil:

public static void putRandomlyIn(Node node , double northPoint , double southPoint , double eastPoint , double westPoint)
{
    node.setLayoutX(Math.random() * pontoLeste);
    node.setLayoutY(Math.random() * pontoSul);

    fixEasternBoundary(node , eastPoint);
    fixNorthernBoundary(node , northPoint);
    fixWesternBoundary(node , westPoint);
    fixSouthernBoundary(node , southPoint);
}

There is no mystery here. This method simply calculates a value from an interval, and defines the LayoutXY properties for the Node argument. Methods "fix ..." just check the boundsInParent bounds of the node compared to the point in the argument, and then adjust the layoutXYproperties from the Node object. Even if I remove the random distribution of objects, the problem still happens. So I'm sure this problem is not being caused by this.

The source code of the original post was changed with the addition of the ability to move the scroll bars with the arrow keys. Even if it is already an existing function of ScrollPane, adding that could reproduce the error seen with the mouse (now with arrows). Some things were also translated into English for better understanding by the community.

Please, I ask for help. I'm getting dizzy not knowing what to do. This type of situation could be happening because of some bug in JavaFX? Ahhrr... Please somebody help me in this. :'(

Thank you for your attention anyway.

EDIT (09/02/2014 10:50 AM GMT - 3:00)

Forgot to mention... My program was initially written and tested using JDK 8 b123. Currently I installed the JDK 8 b128 version and am still getting the same problem. My operating system is Windows 7 64x.

I am almost certain that this is a bug. Are you guys getting the same result as me? Or am I the only one to find this kind of problem? If this is a bug, which procedure should be taken?

Thank you for your attention.

EDIT (10/02/2014 09:45 AM GMT - 3:00)

A bounty was started.

Was it helpful?

Solution

UPDATE

This bug has now been fixed for JavaFX 8u20.

Bug description

This is a bug, that can be easily verified by executing the following code with JavaFx JRE 8:

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

final ScrollPane sp = new ScrollPane();
final Image[] images = new Image[5];
final ImageView[] pics = new ImageView[5];
final VBox vb = new VBox();
final Label fileName = new Label();
final String [] imageNames = new String [] {"fw1.jpg", "fw2.jpg",
        "fw3.jpg", "fw4.jpg", "fw5.jpg"};

@Override
public void start(Stage stage) {
    VBox box = new VBox();
    Scene scene = new Scene(box, 180, 180);
    stage.setScene(scene);
    stage.setTitle("Scroll Pane");
    box.getChildren().addAll(sp, fileName);
    VBox.setVgrow(sp, Priority.ALWAYS);

    fileName.setLayoutX(30);
    fileName.setLayoutY(160);

    for (int i = 0; i < 5; i++) {
        images[i] = new Image(getClass().getResourceAsStream(imageNames[i]));
        pics[i] = new ImageView(images[i]);
        pics[i].setFitWidth(100);
        pics[i].setPreserveRatio(true);
        vb.getChildren().add(pics[i]);
    }

    sp.setVmax(440);
    sp.setPrefSize(115, 150);
    sp.setContent(vb);
    sp.vvalueProperty().addListener(new ChangeListener<Number>() {
        public void changed(ObservableValue<? extends Number> ov,
            Number old_val, Number new_val) {
                fileName.setText(imageNames[(new_val.intValue() - 1)/100]);
        }
    });

    stage.show();
}

public static void main(String[] args) {
    launch(args);
}
}

This code comes directly from the JavaFX ScrollPane Tutorial.

If one randomly moves the vertical scroll bar with the mouse very rapidly, then at some time the screen will freeze and no longer get updated. Although one is still able to move the scroll bar around, the displayed images will stay fixed. Only if one resizes the frame, the display of the images will be updated and the ScrollPane reverts to its previous state. Note, that this bug will only happen in JRE 8, it is not reproducible in JRE 7.

The only workaround for the problem, that I could find, is adding

sp.snapshot(new SnapshotParameters(), new WritableImage(1, 1));

to the listener:

    sp.vvalueProperty().addListener(new ChangeListener<Number>() {
        public void changed(ObservableValue<? extends Number> ov,
            Number old_val, Number new_val) {
                fileName.setText(imageNames[(new_val.intValue() - 1)/100]);
                sp.snapshot(new SnapshotParameters(), new WritableImage(1, 1));
        }
    });

Calling snapshot on the ScrollPane seems to force the update every time the vvalueProperty changes. This seems to be a known workaround for several update problems with JavaFX - see here.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top