Une loupe en JavaFX - 30 août 2009
Dans le forum JavaFX de Sun, quelqu'un a demandé How to implement magnify glass effect? (Comment implémenter un effet de loupe ?). J'ai trouvé le challenge intéressant, et après quelques fausses pistes et périodes où j'ai fait autre chose, j'ai fini par atteindre un stade utilisable.
Il y avait l'échantillon Screen Shot Zoom (zoomant sur une copie d'écran) mais la personne qui avait fait la demande était ennuyée que la vue n'était pas dynamique, ie. mise à jour quand on bouge la lentille.
J'ai commencé à coder mon propre composant, délibérément évitant de regarder au code ci-dessus, pour augmenter le challenge et surtout pour améliorer la valeur d'apprentissage...
Et bien, ce fut plus dur que je pensais. J'ai d'abord joué avec la variable viewport de la classe ImageView, vu qu'ils disent qu'il peut « effectuer un effet de zoom », mais quelque part j'ai raté mon bidouillage et échoué à avoir la bonne zone affichée quand la loupe était à une position donnée sur l'image.
J'ai finalement trouvé la bonne combinaison en utilsant fitWidth (et preserveRatio) et le décalage (x et y) approprié de l'ImageView par rapport au composant, laissant le découpage (clipping) faire le job de n'affiche qu'un cercle.
Voici le code, vous pouvez aussi le trouver sur mon dépôt Launchpad de code JavaFX.
/* * 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();
J'affiche le grossissement courant, qu'on peut changer avec le clavier numérique : touches + et - (ajouter/soustraire 25 %) ainsi que * et * (multiplier/diviser par 25 %). Vous pouvez aussi bouger la loupe en la trainant avec la souris, ou avec les touches de direction.
J'ai délibérément laissé l'aspect assez sobre, me concentrant plutôt sur la fonctionnalité. Vous pouvez l'utiliser en améliorant l'aspect !
Et maintenant la copie d'écran obligatoire :
J'ai récemment vu un article officiel (dans les trucs techniques) décrivant une loupe similaire : Zoom Images With Magnifying Glass. Apparemment, ils ont réussi dans la voie du viewport... C'est bien, vous avez le choix maintenant.
I first wrote the article in English, you can find it at A magnifiying glass in JavaFX.