scale - JavaFX correct scaling -
i want scale nodes in pane on scroll event.
what have tried far:
when scalex or scaley, border of pane scales respectively (seen when set pane style
-fx-border-color: black;). not every event start if i'm not borders of pane, need all.
next step tried scale each node , turned out bad, - (lines stretched through points). or if scrolling in other side, less

another method tried scale points of node. it's better, don't it. looks
point.setscalex(point.getscalex()+scalex), y , other nodes appropriately.
i created sample app demonstrate 1 approach performing scaling of node in viewport on scroll event (e.g. scroll in , out rolling mouse wheel).
the key logic sample scaling group placed within stackpane:
final double scale_delta = 1.1; final stackpane zoompane = new stackpane(); zoompane.getchildren().add(group); zoompane.setonscroll(new eventhandler<scrollevent>() { @override public void handle(scrollevent event) { event.consume(); if (event.getdeltay() == 0) { return; } double scalefactor = (event.getdeltay() > 0) ? scale_delta : 1/scale_delta; group.setscalex(group.getscalex() * scalefactor); group.setscaley(group.getscaley() * scalefactor); } }); the scroll event handler set on enclosing stackpane resizable pane expands fill empty space, keeping zoomed content centered in pane. if move mouse wheel anywhere inside stackpane zoom in or out enclosed group of nodes.

import javafx.application.application; import javafx.beans.value.*; import javafx.event.*; import javafx.geometry.bounds; import javafx.scene.*; import javafx.scene.control.*; import javafx.scene.image.*; import javafx.scene.input.*; import javafx.scene.layout.*; import javafx.scene.paint.color; import javafx.scene.shape.*; import javafx.stage.stage; public class graphicsscalingapp extends application { public static void main(string[] args) { launch(args); } @override public void start(final stage stage) { final group group = new group( createstar(), createcurve() ); parent zoompane = createzoompane(group); vbox layout = new vbox(); layout.getchildren().setall( createmenubar(stage, group), zoompane ); vbox.setvgrow(zoompane, priority.always); scene scene = new scene( layout ); stage.settitle("zoomy"); stage.geticons().setall(new image(app_icon)); stage.setscene(scene); stage.show(); } private parent createzoompane(final group group) { final double scale_delta = 1.1; final stackpane zoompane = new stackpane(); zoompane.getchildren().add(group); zoompane.setonscroll(new eventhandler<scrollevent>() { @override public void handle(scrollevent event) { event.consume(); if (event.getdeltay() == 0) { return; } double scalefactor = (event.getdeltay() > 0) ? scale_delta : 1/scale_delta; group.setscalex(group.getscalex() * scalefactor); group.setscaley(group.getscaley() * scalefactor); } }); zoompane.layoutboundsproperty().addlistener(new changelistener<bounds>() { @override public void changed(observablevalue<? extends bounds> observable, bounds oldbounds, bounds bounds) { zoompane.setclip(new rectangle(bounds.getminx(), bounds.getminy(), bounds.getwidth(), bounds.getheight())); } }); return zoompane; } private svgpath createcurve() { svgpath ellipticalarc = new svgpath(); ellipticalarc.setcontent( "m10,150 a15 15 180 0 1 70 140 a15 25 180 0 0 130 130 a15 55 180 0 1 190 120" ); ellipticalarc.setstroke(color.lightgreen); ellipticalarc.setstrokewidth(4); ellipticalarc.setfill(null); return ellipticalarc; } private svgpath createstar() { svgpath star = new svgpath(); star.setcontent( "m100,10 l100,10 40,180 190,60 10,60 160,180 z" ); star.setstrokelinejoin(strokelinejoin.round); star.setstroke(color.blue); star.setfill(color.darkblue); star.setstrokewidth(4); return star; } private menubar createmenubar(final stage stage, final group group) { menu filemenu = new menu("_file"); menuitem exitmenuitem = new menuitem("e_xit"); exitmenuitem.setgraphic(new imageview(new image(close_icon))); exitmenuitem.setonaction(new eventhandler<actionevent>() { @override public void handle(actionevent event) { stage.close(); } }); filemenu.getitems().setall( exitmenuitem ); menu zoommenu = new menu("_zoom"); menuitem zoomresetmenuitem = new menuitem("zoom _reset"); zoomresetmenuitem.setaccelerator(new keycodecombination(keycode.escape)); zoomresetmenuitem.setgraphic(new imageview(new image(zoom_reset_icon))); zoomresetmenuitem.setonaction(new eventhandler<actionevent>() { @override public void handle(actionevent event) { group.setscalex(1); group.setscaley(1); } }); menuitem zoominmenuitem = new menuitem("zoom _in"); zoominmenuitem.setaccelerator(new keycodecombination(keycode.i)); zoominmenuitem.setgraphic(new imageview(new image(zoom_in_icon))); zoominmenuitem.setonaction(new eventhandler<actionevent>() { @override public void handle(actionevent event) { group.setscalex(group.getscalex() * 1.5); group.setscaley(group.getscaley() * 1.5); } }); menuitem zoomoutmenuitem = new menuitem("zoom _out"); zoomoutmenuitem.setaccelerator(new keycodecombination(keycode.o)); zoomoutmenuitem.setgraphic(new imageview(new image(zoom_out_icon))); zoomoutmenuitem.setonaction(new eventhandler<actionevent>() { @override public void handle(actionevent event) { group.setscalex(group.getscalex() * 1/1.5); group.setscaley(group.getscaley() * 1/1.5); } }); zoommenu.getitems().setall( zoomresetmenuitem, zoominmenuitem, zoomoutmenuitem ); menubar menubar = new menubar(); menubar.getmenus().setall( filemenu, zoommenu ); return menubar; } // icons source from: http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html // icon license: cc attribution-noncommercial-no derivate 3.0 =? http://creativecommons.org/licenses/by-nc-nd/3.0/ // icon commercial usage: allowed (author approval required -> visit artist website details). public static final string app_icon = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/zoom-icon.png"; public static final string zoom_reset_icon = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/zoom-icon.png"; public static final string zoom_out_icon = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/zoom-out-icon.png"; public static final string zoom_in_icon = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/zoom-in-icon.png"; public static final string close_icon = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/button-close-icon.png"; } update zoomed node in scrollpane
the above implementation works far goes, useful able place zoomed node inside scroll pane, when zoom in making zoomed node larger available viewport, can still pan around zoomed node within scroll pane view parts of node.
i found achieving behavior of zooming in scroll pane difficult, asked on oracle javafx forum thread.
oracle javafx forum user james_d came following solution solves zooming within scrollpane problem quite well.
his comments , code below:
a couple of minor changes first: wrapped stackpane in group scrollpane aware of changes transforms, per scrollpane javadocs. , bound minimum size of stackpane viewport size (keeping content centered when smaller viewport).
initially thought should use scale transform zoom around displayed center (i.e. point on content @ center of viewport). found still needed fix scroll position afterwards keep same displayed center, abandoned , reverted using setscalex() , setscaley().
the trick fix scroll position after scaling. computed scroll offset in local coordinates of scroll content, , computed new scroll values needed after scale. little tricky. basic observation (hvalue-hmin)/(hmax-hmin) = x / (contentwidth - viewportwidth), x horizontal offset of left edge of viewport left edge of content. have centerx = x + viewportwidth/2.
after scaling, x coordinate of old centerx centerx*scalefactor. have set new hvalue make new center. there's bit of algebra figure out.
after that, panning dragging pretty easy :).
a corresponding feature request add high level apis support zooming , scaling functionality in scrollpane add scalecontent functionality scrollpane. vote or comment on feature request if see implemented.
import javafx.application.application; import javafx.beans.property.objectproperty; import javafx.beans.property.simpleobjectproperty; import javafx.beans.value.*; import javafx.event.*; import javafx.geometry.bounds; import javafx.geometry.point2d; import javafx.scene.*; import javafx.scene.control.*; import javafx.scene.image.*; import javafx.scene.input.*; import javafx.scene.layout.*; import javafx.scene.paint.color; import javafx.scene.shape.*; import javafx.stage.stage; public class graphicsscalingapp extends application { public static void main(string[] args) { launch(args); } @override public void start(final stage stage) { final group group = new group(createstar(), createcurve()); parent zoompane = createzoompane(group); vbox layout = new vbox(); layout.getchildren().setall(createmenubar(stage, group), zoompane); vbox.setvgrow(zoompane, priority.always); scene scene = new scene(layout); stage.settitle("zoomy"); stage.geticons().setall(new image(app_icon)); stage.setscene(scene); stage.show(); } private parent createzoompane(final group group) { final double scale_delta = 1.1; final stackpane zoompane = new stackpane(); zoompane.getchildren().add(group); final scrollpane scroller = new scrollpane(); final group scrollcontent = new group(zoompane); scroller.setcontent(scrollcontent); scroller.viewportboundsproperty().addlistener(new changelistener<bounds>() { @override public void changed(observablevalue<? extends bounds> observable, bounds oldvalue, bounds newvalue) { zoompane.setminsize(newvalue.getwidth(), newvalue.getheight()); } }); scroller.setprefviewportwidth(256); scroller.setprefviewportheight(256); zoompane.setonscroll(new eventhandler<scrollevent>() { @override public void handle(scrollevent event) { event.consume(); if (event.getdeltay() == 0) { return; } double scalefactor = (event.getdeltay() > 0) ? scale_delta : 1 / scale_delta; // amount of scrolling in each direction in scrollcontent coordinate // units point2d scrolloffset = figurescrolloffset(scrollcontent, scroller); group.setscalex(group.getscalex() * scalefactor); group.setscaley(group.getscaley() * scalefactor); // move viewport old center remains in center after // scaling repositionscroller(scrollcontent, scroller, scalefactor, scrolloffset); } }); // panning via drag.... final objectproperty<point2d> lastmousecoordinates = new simpleobjectproperty<point2d>(); scrollcontent.setonmousepressed(new eventhandler<mouseevent>() { @override public void handle(mouseevent event) { lastmousecoordinates.set(new point2d(event.getx(), event.gety())); } }); scrollcontent.setonmousedragged(new eventhandler<mouseevent>() { @override public void handle(mouseevent event) { double deltax = event.getx() - lastmousecoordinates.get().getx(); double extrawidth = scrollcontent.getlayoutbounds().getwidth() - scroller.getviewportbounds().getwidth(); double deltah = deltax * (scroller.gethmax() - scroller.gethmin()) / extrawidth; double desiredh = scroller.gethvalue() - deltah; scroller.sethvalue(math.max(0, math.min(scroller.gethmax(), desiredh))); double deltay = event.gety() - lastmousecoordinates.get().gety(); double extraheight = scrollcontent.getlayoutbounds().getheight() - scroller.getviewportbounds().getheight(); double deltav = deltay * (scroller.gethmax() - scroller.gethmin()) / extraheight; double desiredv = scroller.getvvalue() - deltav; scroller.setvvalue(math.max(0, math.min(scroller.getvmax(), desiredv))); } }); return scroller; } private point2d figurescrolloffset(node scrollcontent, scrollpane scroller) { double extrawidth = scrollcontent.getlayoutbounds().getwidth() - scroller.getviewportbounds().getwidth(); double hscrollproportion = (scroller.gethvalue() - scroller.gethmin()) / (scroller.gethmax() - scroller.gethmin()); double scrollxoffset = hscrollproportion * math.max(0, extrawidth); double extraheight = scrollcontent.getlayoutbounds().getheight() - scroller.getviewportbounds().getheight(); double vscrollproportion = (scroller.getvvalue() - scroller.getvmin()) / (scroller.getvmax() - scroller.getvmin()); double scrollyoffset = vscrollproportion * math.max(0, extraheight); return new point2d(scrollxoffset, scrollyoffset); } private void repositionscroller(node scrollcontent, scrollpane scroller, double scalefactor, point2d scrolloffset) { double scrollxoffset = scrolloffset.getx(); double scrollyoffset = scrolloffset.gety(); double extrawidth = scrollcontent.getlayoutbounds().getwidth() - scroller.getviewportbounds().getwidth(); if (extrawidth > 0) { double halfwidth = scroller.getviewportbounds().getwidth() / 2 ; double newscrollxoffset = (scalefactor - 1) * halfwidth + scalefactor * scrollxoffset; scroller.sethvalue(scroller.gethmin() + newscrollxoffset * (scroller.gethmax() - scroller.gethmin()) / extrawidth); } else { scroller.sethvalue(scroller.gethmin()); } double extraheight = scrollcontent.getlayoutbounds().getheight() - scroller.getviewportbounds().getheight(); if (extraheight > 0) { double halfheight = scroller.getviewportbounds().getheight() / 2 ; double newscrollyoffset = (scalefactor - 1) * halfheight + scalefactor * scrollyoffset; scroller.setvvalue(scroller.getvmin() + newscrollyoffset * (scroller.getvmax() - scroller.getvmin()) / extraheight); } else { scroller.sethvalue(scroller.gethmin()); } } private svgpath createcurve() { svgpath ellipticalarc = new svgpath(); ellipticalarc.setcontent("m10,150 a15 15 180 0 1 70 140 a15 25 180 0 0 130 130 a15 55 180 0 1 190 120"); ellipticalarc.setstroke(color.lightgreen); ellipticalarc.setstrokewidth(4); ellipticalarc.setfill(null); return ellipticalarc; } private svgpath createstar() { svgpath star = new svgpath(); star.setcontent("m100,10 l100,10 40,180 190,60 10,60 160,180 z"); star.setstrokelinejoin(strokelinejoin.round); star.setstroke(color.blue); star.setfill(color.darkblue); star.setstrokewidth(4); return star; } private menubar createmenubar(final stage stage, final group group) { menu filemenu = new menu("_file"); menuitem exitmenuitem = new menuitem("e_xit"); exitmenuitem.setgraphic(new imageview(new image(close_icon))); exitmenuitem.setonaction(new eventhandler<actionevent>() { @override public void handle(actionevent event) { stage.close(); } }); filemenu.getitems().setall(exitmenuitem); menu zoommenu = new menu("_zoom"); menuitem zoomresetmenuitem = new menuitem("zoom _reset"); zoomresetmenuitem.setaccelerator(new keycodecombination(keycode.escape)); zoomresetmenuitem.setgraphic(new imageview(new image(zoom_reset_icon))); zoomresetmenuitem.setonaction(new eventhandler<actionevent>() { @override public void handle(actionevent event) { group.setscalex(1); group.setscaley(1); } }); menuitem zoominmenuitem = new menuitem("zoom _in"); zoominmenuitem.setaccelerator(new keycodecombination(keycode.i)); zoominmenuitem.setgraphic(new imageview(new image(zoom_in_icon))); zoominmenuitem.setonaction(new eventhandler<actionevent>() { @override public void handle(actionevent event) { group.setscalex(group.getscalex() * 1.5); group.setscaley(group.getscaley() * 1.5); } }); menuitem zoomoutmenuitem = new menuitem("zoom _out"); zoomoutmenuitem.setaccelerator(new keycodecombination(keycode.o)); zoomoutmenuitem.setgraphic(new imageview(new image(zoom_out_icon))); zoomoutmenuitem.setonaction(new eventhandler<actionevent>() { @override public void handle(actionevent event) { group.setscalex(group.getscalex() * 1 / 1.5); group.setscaley(group.getscaley() * 1 / 1.5); } }); zoommenu.getitems().setall(zoomresetmenuitem, zoominmenuitem, zoomoutmenuitem); menubar menubar = new menubar(); menubar.getmenus().setall(filemenu, zoommenu); return menubar; } // icons source from: // http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html // icon license: cc attribution-noncommercial-no derivate 3.0 =? // http://creativecommons.org/licenses/by-nc-nd/3.0/ // icon commercial usage: allowed (author approval required -> visit artist // website details). public static final string app_icon = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/zoom-icon.png"; public static final string zoom_reset_icon = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/zoom-icon.png"; public static final string zoom_out_icon = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/zoom-out-icon.png"; public static final string zoom_in_icon = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/zoom-in-icon.png"; public static final string close_icon = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/button-close-icon.png"; }
Comments
Post a Comment