A magnifiying glass in JavaFX - 30 août 2009
In the Sun JavaFX forum, somebody asked How to implement magnify glass effect? I was interested by the challenge, and after some dead end trials and on/off periods of coding, I reached a usable state.
There was a Screen Shot Zoom sample but the person asking was annoyed that view wasn't dynamic, ie. updated as we move the lens.
I started to code my own custom component, deliberately not looking at above code to increase the challenge and thus enhance the learning experience...
Well, it was harder than I thought. I first played with the viewport variable of ImageView, as it is said to be "achieving a "zoom" effect", but somehow I fumbled and failed to get the right zone being displayed when the loupe was at a given position over the image.
I finally found the right combination using fitWidth (and preserveRatio) and the appropriate offset (x and y) of the ImageView with regard to the component, letting the clipping doing the job of displaying only a circle.
Here is the code, you can find it also on my JavaFX Launchpad repository.
/* * A magnifiying glass in JavaFX. * Following JavaFX forum thread <http://forums.sun.com/thread.jspa?threadID=5394690> */ /* File history: * 1.01.000 -- 2009/08/08 (PL) -- Working control * 1.00.000 -- 2009/07/23 (PL) -- Creation */ /* Author: Philippe Lhoste <PhiLho(a)GMX.net> http://Phi.Lho.free.fr Copyright notice: For details, see the following file: http://Phi.Lho.free.fr/softwares/PhiLhoSoft/PhiLhoSoftLicence.txt This program is distributed under the zlib/libpng license. Copyright (c) 2009 Philippe Lhoste / PhiLhoSoft */ import javafx.stage.*; // [...] Skipping boring boilerplate series of imports class MagnifyingGlass extends CustomNode { public-init var externalRadius = 100; public-init var internalRadius = 90; public-init var image: Image; public-init var display: Node; public-init var glassColor: Color = Color.DARKBLUE; public-init var infoColor: Color = Color.LIGHTGREEN; public var magnifyingRatio: Number = 1.0; var centerX: Number; var centerY: Number; var dbil: Bounds; init { dbil = display.boundsInLocal; // Position of display relative to display node centerX = dbil.minX + dbil.width / 2; centerY = dbil.minY + dbil.height / 2; ChangeView(); } // The look of the glass def glass = Group { content: [ Circle { centerX: bind centerX centerY: bind centerY radius: externalRadius fill: glassColor } Rectangle { x: bind centerX y: bind centerY width: externalRadius height: externalRadius arcWidth: externalRadius / 5 arcHeight: externalRadius / 5 fill: glassColor } Text { translateX: bind centerX + externalRadius * 4 / 7 translateY: bind centerY + externalRadius * 6 / 7 content: bind "x{%.2f magnifyingRatio}" fill: infoColor } ] } // The magnified image inside the glass def magnifiedView = Container { width: internalRadius content: ImageView { image: image // Manage the magnification, relative to view in scene, // not to imge original size itself. More intuitive and allow better quality // of magnified view if the scene view is smaller than image. fitWidth: bind dbil.width * magnifyingRatio preserveRatio: true } clip: Circle { centerX: bind centerX centerY: bind centerY radius: internalRadius } } override protected function create(): Node { Group { content: [ glass, magnifiedView ] } } // Dragging part var offsetX: Number; var offsetY: Number; override def onMousePressed = function (evt: MouseEvent): Void { // Compute offset between click and center offsetX = centerX - evt.sceneX; offsetY = centerY - evt.sceneY; evt.node.requestFocus(); // To handle keypresses } override def onMouseDragged = function (evt: MouseEvent): Void { // Position relative to initial click and current drag position, // adjusted for initial click offset UpdateX(evt.dragAnchorX + evt.dragX + offsetX); UpdateY(evt.dragAnchorY + evt.dragY + offsetY); ChangeView(); } override def onMouseWheelMoved = function (evt: MouseEvent): Void { magnifyingRatio += 0.25 * evt.wheelRotation; ChangeView(); } override def onKeyPressed = function (evt: KeyEvent): Void { if (evt.code == KeyCode.VK_ADD) // Numeric keypad + { magnifyingRatio += 0.25; ChangeView(); } else if (evt.code == KeyCode.VK_SUBTRACT) // Numeric keypad - { if (magnifyingRatio > 0.25) { magnifyingRatio -= 0.25; } ChangeView(); } else if (evt.code == KeyCode.VK_MULTIPLY) // Numeric keypad * { magnifyingRatio *= 1.25; ChangeView(); } else if (evt.code == KeyCode.VK_DIVIDE) // Numeric keypad / { magnifyingRatio /= 1.25; ChangeView(); } else if (evt.code == KeyCode.VK_LEFT) { UpdateX(centerX - 20); ChangeView(); } else if (evt.code == KeyCode.VK_RIGHT) { UpdateX(centerX + 20); ChangeView(); } else if (evt.code == KeyCode.VK_UP) { UpdateY(centerY - 20); ChangeView(); } else if (evt.code == KeyCode.VK_DOWN) { UpdateY(centerY + 20); ChangeView(); } } function ChangeView(): Void { // Center of glass relative to display coordinates def relCenterX = centerX - dbil.minX; def relCenterY = centerY - dbil.minY; // 0 on top-left, +1 on bottom right of the display def ratioX = relCenterX / dbil.width; def ratioY = relCenterY / dbil.height; def iv = magnifiedView.content[0] as ImageView; iv.x = dbil.minX - dbil.width * ratioX * (magnifyingRatio - 1.0); iv.y = dbil.minY - dbil.height * ratioY * (magnifyingRatio - 1.0); } function UpdateX(newPos: Number): Void { centerX = Constraints(newPos, dbil.minX, dbil.maxX); } function UpdateY(newPos: Number): Void { centerY = Constraints(newPos, dbil.minY, dbil.maxY); } } function Constraints(v: Number, min: Number, max: Number): Number { if (v < min) return min; if (v > max) return max; return v; } def bigImage = Image { url: "file:///D:/Documents/Images/References/carte-france-map.jpg" width: 800 preserveRatio: true } def smallView = ImageView { image: bigImage x: 150, y: 150 fitWidth: 500, preserveRatio: true } def magGlass = MagnifyingGlass { image: bigImage, display: smallView } Stage { title: "Magnifying Glass" scene: Scene { width: 800 height: 800 content: [ smallView, magGlass ] } } magGlass.requestFocus();
I display the current magnification, which you can change with keypad's + and - keys (add/subtract 25%), * and / (multiply/divide by 25%). You can also move the glass by dragging it, or by using arrow keys.
I deliberately left the look quite sober, concentrating on functionality instead. Feel free to use it and improve this look!
Note that the program doesn't do magic: you should take a rather large image, and display it a bit reduced, so that the magnified look is actually a slightly less reduced view, or a not-so-much enlarged view, thus reducing the fuzzy effect we get when zooming in too much (JavaFX smooths the enlarged view, avoiding a pixellized look).
And now for the mandatory screenshot:
I recently saw an official article (technical tip) describing a similar magnifying glass: Zoom Images With Magnifying Glass. Apparently, they were successful going the viewport route... Looks like you have some choice now.
J'ai écrit l'article directement en anglais, je l'ai "traduit" dans Une loupe en JavaFX.