I can use the setVvalue(double) and setHvalue(double) methods to move a viewport in a JavaFX ScrollPane. What I'm struggling to do is center a particular node in the content of the scroll pane based on its position. I've tried all sorts of combos of localToScene() and boundsInParent(). I've read (a lot) and seen this example
How to scroll to make a Node within the content of a ScrollPane visible?
Which is close but doesn't center the objects just puts them visible. Having the built in mouse panning is brilliant but I'm making heavy weather of the programmatic panning.
Ultimately I need to be able to do a zoom too so I have put the actual shapes in a Group and added the group to the scroll pane content. I think I'm supposed to do the zooming on the group and again I need to be able to zoom around the center of the group so we are back to manipulating and identifying the current center position. Any pointers or examples that can be provided would be really really appreciated. The code sample in the link above is a good SSCCE.
Thanks in advance,
Andy
I'll try explaining this without code, because I don't think that's necessary here.
Let's say the content of your scrollpane has height h, and the height of the viewport is v. If h = v, then the content would fit perfectly into the viewport, and you would need no scrollbar. In this situation (with a non-movable scrollbar), for an element to be centered, it would need to be positioned at the center of the scrollpane's content. You can't move it to the viewport's center by means of scrolling.
Now consider h to be twice the size of v (i.e. h = 2v). In this situation the upper 1/4 and the lower 1/4 of the scrollpane's content could not be centered by scrolling.
(If you absolutely needed to center any component by scrolling you should consider padding your content pane, but we'll continue with the unpadded solution here)
When you think about it, you'll realize the possible scrollable distance of the scrollbar is h - v, and you will scroll that amount by setting vvalue to 1.0.
To center a point y (here the point y is a coordinate of the scrollpane's content pane) you can use the following vvalue:
vvalue = (y - 0.5 * v) / (h - v)
The nominator of this expression is the y-coordinate of what is shown at the top of the viewport when the point y is centered inside the viewport. The denominator is the total scrollable distance.
Edit: Adding some code anyway!
public void centerNodeInScrollPane(ScrollPane scrollPane, Node node) {
double h = scrollPane.getContent().getBoundsInLocal().getHeight();
double y = (node.getBoundsInParent().getMaxY() +
node.getBoundsInParent().getMinY()) / 2.0;
double v = scrollPane.getViewportBounds().getHeight();
scrollPane.setVvalue(scrollPane.getVmax() * ((y - 0.5 * v) / (h - v)));
}
(Please note this assumes that the node is a direct child of the scrollpane's contentpane)
Hope this helps! :)
I know, it is little bit late, but maybe someone will find it useful.:)
As regards centering, this is not hard task it takes only little bit of math. You need two things. Size of whole content of ScrollPane (width and height of scrollable area, not only of frame with scrollbars, but complete as if no scrollbars are present) and position of centered object inside of ScrollPane content.
You get size of content like this: scrollPane.getContent().getBoundsInLocal().getHeight() or .getWidth()
And position of your object: node.getBoundsInParent().getMinY() or .getMinX() - minimal position of object. You can get its height and width too.
Then calculate position of your center and move scrollbars according this formula.
double vScrollBarPosition = scrollPane.getVMax() * (yPositionOfCenter / heightOfScrollPaneContent)
Here is an example on how to center a pic that has been rotated.
The code is in Scala and therefore applicable to ScalaFX, but JavaFX and Java developers will be able to read/use it :
def turnPic(angle:Int) {
val virtualSurfaceMultiplier = 8.0;
currentAngle = ( floor((currentAngle % 360) / 90) * 90 + angle + 360 ) % 360;
imageView.rotate = currentAngle;
val orientationSwitched = (currentAngle / 90) % 2 > 0;
val width= scrollPane.getViewportBounds().getWidth();
val height= scrollPane.getViewportBounds().getHeight();
val viewPort = new Rectangle2D(0, 0, image.width.value, image.height.value);
imageView.setViewport(viewPort);
imageView.fitHeight.value = virtualSurfaceMultiplier * height;
imageView.fitWidth.value = 0 //virtualSurfaceMultiplier * width;
def centerV(picHeight:Double) {
val node = scrollPane.getContent();
val totalHeight = scrollPane.getContent().getBoundsInLocal().getHeight();
val offsetToCenter = (node.getBoundsInParent().getMaxY() + node.getBoundsInParent().getMinY()) / 2.0;
val viewportWidth = scrollPane.getViewportBounds().getHeight();
val res = (scrollPane.getVmax() * ((offsetToCenter - 0.5 * picHeight * zoomFactor) / (totalHeight - picHeight * zoomFactor)));
scrollPane.setVvalue(res);
}
def centerH(picWidth:Double) {
val node = scrollPane.getContent();
val totalWidth = scrollPane.getContent().getBoundsInLocal().getWidth();
val offsetToCenter = (node.getBoundsInParent().getMaxX() + node.getBoundsInParent().getMinX()) / 2.0;
val viewportWidth = scrollPane.getViewportBounds().getWidth();
val res = (scrollPane.getHmax() * ((offsetToCenter - 0.5 * picWidth * zoomFactor) / (totalWidth - picWidth * zoomFactor)));
scrollPane.setHvalue(res);
System.err.println(s"trace centerH> $FILE:$LINE:" + " totalWidth:" + totalWidth + " offsetToCenter=" + offsetToCenter + " vWidth=" + viewportWidth + "res=" + res);
}
if (orientationSwitched) {
zoomFactor = 1.0 / (virtualSurfaceMultiplier * image.width.value / image.height.value);
centerH(height);
centerV(width);
}
else
{
zoomFactor = 1.0 / virtualSurfaceMultiplier;
centerH(width);
centerV(height);
}
imageView.setScaleX(zoomFactor);
imageView.setScaleY(zoomFactor);
}
Related
I'm making a Conway's Game of Life simulator because I thought that would be a fun project to try out, but I got stuck when trying to implement a function to keep changing cells to alive or dead when I drag my mouse over them.
scrollPane.setOnMousePressed(e -> {
double translation = (SQ_SIZE + SPACING) * SIZE + SPACING;
double xPosition = e.getX() + scrollPane.hvalueProperty().doubleValue() * (translation - PREF_WIDTH);
double yPosition = e.getY() + scrollPane.vvalueProperty().doubleValue() * (translation - PREF_HEIGHT);
// row and column of the click on the grid
int row = (int) ((yPosition - SPACING * (int) (yPosition / (SQ_SIZE + SPACING))) / SQ_SIZE);
int col = (int) ((xPosition - SPACING * (int) (xPosition / (SQ_SIZE + SPACING))) / SQ_SIZE);
grid[row][col].setAlive();
});
scrollPane.setOnMouseReleased(e -> {
//??
});
This was just to see if I could make the mouse change the cells. I dragged it over to alive and it does not seem to work. It can change the cell where it was initially pressed, but all the other cells do not change.
There is probably a very obvious solution I'm missing, but I'm quite new to JavaFX.
Ok so basically you cant use setOnMouseDragged for a scrollpane, so i set it on the gridpane and it worked.
I need to zoom in / out on a scroll pane, relative to the mouse position.
I currently achieve the zooming functionality by wrapping my content in a Group, and scaling the group itself. I create a new Scale object with a custom pivot. (Pivot is set to the mouse position)
This works perfectly for where the Group's initial scale is 1.0, however scaling afterwards does not scale in the correct direction - I believe this is because the relative mouse position changes when the Group has been scaled.
My code:
#Override
public void initialize(URL location, ResourceBundle resources) {
Delta initial_mouse_pos = new Delta();
anchorpane.setOnScrollStarted(event -> {
initial_mouse_pos.x = event.getX();
initial_mouse_pos.y = event.getY();
});
anchorpane.setOnScroll(event -> {
double zoom_fac = 1.05;
double delta_y = event.getDeltaY();
if(delta_y < 0) {
zoom_fac = 2.0 - zoom_fac;
}
Scale newScale = new Scale();
newScale.setPivotX(initial_mouse_pos.x);
newScale.setPivotY(initial_mouse_pos.y);
newScale.setX( content_group.getScaleX() * zoom_fac );
newScale.setY( content_group.getScaleY() * zoom_fac );
content_group.getTransforms().add(newScale);
event.consume();
});
}
private class Delta { double x, y; }
How do I get the correct mouse position at different levels of scaling? Is there a completely different way to zooming the ScrollPane that is easier?
This is a scalable, pannable JavaFX ScrollPane :
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
public class ZoomableScrollPane extends ScrollPane {
private double scaleValue = 0.7;
private double zoomIntensity = 0.02;
private Node target;
private Node zoomNode;
public ZoomableScrollPane(Node target) {
super();
this.target = target;
this.zoomNode = new Group(target);
setContent(outerNode(zoomNode));
setPannable(true);
setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
setFitToHeight(true); //center
setFitToWidth(true); //center
updateScale();
}
private Node outerNode(Node node) {
Node outerNode = centeredNode(node);
outerNode.setOnScroll(e -> {
e.consume();
onScroll(e.getTextDeltaY(), new Point2D(e.getX(), e.getY()));
});
return outerNode;
}
private Node centeredNode(Node node) {
VBox vBox = new VBox(node);
vBox.setAlignment(Pos.CENTER);
return vBox;
}
private void updateScale() {
target.setScaleX(scaleValue);
target.setScaleY(scaleValue);
}
private void onScroll(double wheelDelta, Point2D mousePoint) {
double zoomFactor = Math.exp(wheelDelta * zoomIntensity);
Bounds innerBounds = zoomNode.getLayoutBounds();
Bounds viewportBounds = getViewportBounds();
// calculate pixel offsets from [0, 1] range
double valX = this.getHvalue() * (innerBounds.getWidth() - viewportBounds.getWidth());
double valY = this.getVvalue() * (innerBounds.getHeight() - viewportBounds.getHeight());
scaleValue = scaleValue * zoomFactor;
updateScale();
this.layout(); // refresh ScrollPane scroll positions & target bounds
// convert target coordinates to zoomTarget coordinates
Point2D posInZoomTarget = target.parentToLocal(zoomNode.parentToLocal(mousePoint));
// calculate adjustment of scroll position (pixels)
Point2D adjustment = target.getLocalToParentTransform().deltaTransform(posInZoomTarget.multiply(zoomFactor - 1));
// convert back to [0, 1] range
// (too large/small values are automatically corrected by ScrollPane)
Bounds updatedInnerBounds = zoomNode.getBoundsInLocal();
this.setHvalue((valX + adjustment.getX()) / (updatedInnerBounds.getWidth() - viewportBounds.getWidth()));
this.setVvalue((valY + adjustment.getY()) / (updatedInnerBounds.getHeight() - viewportBounds.getHeight()));
}
}
Did you try to remove the setOnScrollStarted-event and move its content to the setOnScroll-event?
Doing so reduces the need of your extra Delta-class and the computations of your mouse-positions are always on par with the current zoom factor.
I implemented the same thing and it works the way you are describing it.
Somehow like this:
#Override
public void initialize(URL location, ResourceBundle resources) {
anchorpane.setOnScroll(event -> {
double zoom_fac = 1.05;
if(delta_y < 0) {
zoom_fac = 2.0 - zoom_fac;
}
Scale newScale = new Scale();
newScale.setPivotX(event.getX);
newScale.setPivotY(event.getY);
newScale.setX( content_group.getScaleX() * zoom_fac );
newScale.setY( content_group.getScaleY() * zoom_fac );
content_group.getTransforms().add(newScale);
event.consume();
});
}
I believe this is a duplicate of this question which involves the same concepts at work. If you don't really care if it zooms relative to your mouse and just prefer it zoom in the center look at this question. If you need any more help comment below.
Assuming you want to have the following zoom behavior:
When the mouse wheel is pushed forward/backward the object under the cursor will be scaled up/down and the area under the cursor is now centered within the zooming area.
Eg. pushing the wheel forward while pointing at a place left from the center of the zooming area results in a 'up-scale and move right' action.
The scaling thing is as simple as you have already done so far.
The tricky part is the move action. There are some problem you have to consider within your calculations:
You have to calculate the difference from the center and the
position of the cursor. You can calculate this value by subtracting
the center point (cp) from the mouse position (mp).
If your zoom level is 1 and you point 50px left from the center you want to move your object 50px to the right, because 1px of your screen corresponds to one 1px of your object (picture). But if you doubled the size of your object, than 2 screen pixel are equal to on object pixel. You have to consider this when moving the object, because the translating part is always done before the scaling part. In other words you are moving your object in original size and the scaling is only the second independent step.
How is the scaling done? In JavaFX you simply set some scale-properties and JavaFX does the rest for you. But what does it exactly? The important thing to know is, where the fixed point is while zooming the object. If you scale an object there will be one point which stays fixed at its position while all other points are moving towards this point or moving way from it.
As the documentation of JavaFX says the center of the zoomed object will be the fixed point.
Defines the factor by which coordinates are scaled about the center of
the object along the X axis of this Node.
That means you have to ensure that your visual center point is equal to the one JavaFX uses while scaling you object. You can achieve this if you wrap your object within a container. Now zoom the container instead of the object and position the object within the container to fit your needs.
I hope this helps. If you need more help please offer a short working example project.
I created my first JavaFX app that displays images. I want it to zoom the image to full size on mouse down (centered on cursor position) and to refit the image on mouse up.
All is working fine but the i don't know how to center on cursor position. My zoom method looks like that at the moment:
private void zoom100(double cursorx, double cursory){
double centery = imageView.getLayoutBounds().getHeight()/2;
double centerx = imageView.getLayoutBounds().getWidth()/2;
imageView.setFitHeight(-1); //zooms height to 100%
imageView.setFitWidth(-1); //zooms width to 100%
imageView.setTranslateX(centerx-cursorx); //moves x
imageView.setTranslateY(centery-cursory); //moves y
}
My idea is to set an offset with translate. I am not sure if translate is the correct approach. If it is the correct approach how to calculate the correct values? (centerx-cursorx) is wrong!
I uploaded the code here: https://github.com/dermoritz/FastImageViewer (it is working now - see my answer)
I didn't test it but I think the order is wrong. you calculate the center before you set the fit size. By setting the fit size your image view will change it's preferred size. therefore the center point won't match anymore.
I found a solution, but i am still not sure if this is a good way to go:
private void zoom100(double x, double y) {
double oldHeight = imageView.getBoundsInLocal().getHeight();
double oldWidth = imageView.getBoundsInLocal().getWidth();
boolean heightLarger = oldHeight>oldWidth;
imageView.setFitHeight(-1);
imageView.setFitWidth(-1);
//calculate scale factor
double scale=1;
if(heightLarger){
scale = imageView.getBoundsInLocal().getHeight()/oldHeight;
}else {
scale = imageView.getBoundsInLocal().getWidth()/oldWidth;
}
double centery = root.getLayoutBounds().getHeight() / 2;
double centerx = root.getLayoutBounds().getWidth() / 2;
double xOffset = scale * (centerx - x);
double yOffset = scale *(centery - y);
imageView.setTranslateX(xOffset);
imageView.setTranslateY(yOffset);
}
The picture must be centered within "root". I think there should be something more direct - only using properties of imageview?!
Meanwhile the app works using this code: https://github.com/dermoritz/FastImageViewer
Ok, so I have my isometric map where the width of each tile is 64 and the height is 32.
This is the equation I came up with to place the tiles
xPos = (this.getPos().getX() - this.getPos().getY()) * (64 / 2) - Main.gameWindow.getCamera().getxOffset().intValue();
xPos -= 32;
yPos = (this.getPos().getX() + this.getPos().getY()) * (32 / 2) - Main.gameWindow.getCamera().getyOffset().intValue();
I subtract xPos by 32 to make up for the fact that the origin point of the tile is in the far left corner.
What I've been trying to do is find the tile that is beneath my mouse.
So first I tried simply reversing the equation (I was sure it would work)
And this is the code I ended up with when I reversed it.
int yMouseTile = ( (cursorY / (32 / 2) - (cursorX / 32)) / 2 );
int xMouseTile = ( (cursorX / 32) + yMouseTile);
This only sort of works. But as it turns out, this code actually treats each tile as if it were a square, not a diamond.
The next weird part is that when my mouse passes over the center of the tile, the tile changes. So what should happen, is that my mouse should go over the edge of the tile, and then it changes to the next one. But instead, it acts as if the center of tile is actually the edge.
But really, all I want is the equation that will cause my mouse to work like this
http://www.tonypa.pri.ee/tbw/tut18.html
On that link, click the "Click Here to Start" Button, and watch how the mouse interacts with the tiles. That is my goal :), thanks
P.S. I've tried a myriad of different equations, many of which have the exact same result as the equation I have above
Refactor your variable names.
int TILE_WIDTH = 64;
int TILE_HEIGHT = TILE_WIDTH / 2;
int xMap = this.getPos().getX();
int yMap = this.getPos().getY();
int xScreenCameraOffset = Main.gameWindow.getCamera().getxOffset().intValue();
int yScreenCameraOffset = Main.gameWindow.getCamera().getyOffset().intValue();
xScreen = (xMap - yMap) * (TILE_WIDTH / 2) - yScreenCameraOffset;
yScreen = (xMap + yMap) * (TILE_HEIGHT / 2) - yScreenCameraOffset;
This might seem excessive, but in my opinion is way easier to read and reason about. According to this tutorial If you try to derive The reverse equation you would get:
xMouseTile = (xCursor / TILE_WIDTH / 2 + yCursor / TILE_HEIGHT / 2) / 2;
yMouseTile = (yCursor / TILE_HEIGHT / 2 - xCursor / TILE_WIDTH / 2) / 2;
This doesn't take into account the camera offset.
I have created JTextpane and inserted components inside textpane (components like Jtextarea). (vertical scrollbar of )Jscrollpane of JTextpane is automatically set to bottom when I insert new components in that JTextpane. I want to keep it to be set to the top position. How can I do this
Thanks
Sunil Kumar Sahoo
Here's a utility class I use. It can be used to scroll to the top, bottom, left, right or horizonatal / vertical center of a JScrollPane.
public final class ScrollUtil {
public static final int NONE = 0, TOP = 1, VCENTER = 2, BOTTOM = 4, LEFT = 8, HCENTER = 16, RIGHT = 32;
private static final int OFFSET = 100; // Required for hack (see below).
private ScrollUtil() {
}
/**
* Scroll to specified location. e.g. <tt>scroll(component, BOTTOM);</tt>.
*
* #param c JComponent to scroll.
* #param part Location to scroll to. Should be a bit-wise OR of one or moe of the values:
* NONE, TOP, VCENTER, BOTTOM, LEFT, HCENTER, RIGHT.
*/
public static void scroll(JComponent c, int part) {
scroll(c, part & (LEFT|HCENTER|RIGHT), part & (TOP|VCENTER|BOTTOM));
}
/**
* Scroll to specified location. e.g. <tt>scroll(component, LEFT, BOTTOM);</tt>.
*
* #param c JComponent to scroll.
* #param horizontal Horizontal location. Should take the value: LEFT, HCENTER or RIGHT.
* #param vertical Vertical location. Should take the value: TOP, VCENTER or BOTTOM.
*/
public static void scroll(JComponent c, int horizontal, int vertical) {
Rectangle visible = c.getVisibleRect();
Rectangle bounds = c.getBounds();
switch (vertical) {
case TOP: visible.y = 0; break;
case VCENTER: visible.y = (bounds.height - visible.height) / 2; break;
case BOTTOM: visible.y = bounds.height - visible.height + OFFSET; break;
}
switch (horizontal) {
case LEFT: visible.x = 0; break;
case HCENTER: visible.x = (bounds.width - visible.width) / 2; break;
case RIGHT: visible.x = bounds.width - visible.width + OFFSET; break;
}
// When scrolling to bottom or right of viewport, add an OFFSET value.
// This is because without this certain components (e.g. JTable) would
// not scroll right to the bottom (presumably the bounds calculation
// doesn't take the table header into account. It doesn't matter if
// OFFSET is a huge value (e.g. 10000) - the scrollRectToVisible method
// still works correctly.
c.scrollRectToVisible(visible);
}
}
I have found that the easiest way to do this is the following:
public void scroll(int vertical) {
switch (vertical) {
case SwingConstants.TOP:
getVerticalScrollBar().setValue(0);
break;
case SwingConstants.CENTER:
getVerticalScrollBar().setValue(getVerticalScrollBar().getMaximum());
getVerticalScrollBar().setValue(getVerticalScrollBar().getValue() / 2);
break;
case SwingConstants.BOTTOM:
getVerticalScrollBar().setValue(getVerticalScrollBar().getMaximum());
break;
}
}
I placed this in an object which extended JScrollPane but you could also add the name of your JScrollPane before all the getVertivalScrollBar(). There is two setValue()s for CENTER because getMaximum() returns the bottom of the JScrollBar, not the lowest value it goes to. This also works for Horizontal Scrolling using getHorizontalScrollBar() in place of getverticalScrollBar().
It should be possible to set the DefaultCaret update policy to NEVER_UPDATE. See the article Text Area Scrolling for other uses.
There are various methods that you can use, depending on what is inside the scrollpane. See the tutorial, the very last section.
This works too:
JTextArea. myTextArea;
// ...
myTextArea.select(0, 0); // force the scroll value to the top
jScrollPane.getVerticalScrollBar().setValue(1);
ScrollPane's scroll value is always between ( 0.0 - 1 )
for example
0.0 = 0%
0.1 = 10%
0.2 = 20%
0.25 = 25%
.... so on
And you can adjust the scroll position using these values. For example, in JavaFX
// suppose this is the scrollpane
ScrollPane pane = new ScrollPane();
// to scroll the scrollpane horizontally 10% from its current position
pane.setHvalue(pane.getHvalue() + 0.1);
// to scroll 100%
pane.setHvalue(1);
and so on...
apply logic as you need