Mixing Swing Components with JavaFX Graphics
With JavaFX you can seamlessly mix existing Swing components, even custom components, with graphics and effects. This example combines the Swing JXMapViewer component from SwingLabs.org with a custom overlay and text in JavaFX graphics. Notice that you can still pan and zoom the map around with the mouse even though it is inside of a JavaFX scene.
Map data provided by OpenStreetMap.org
Understanding the Code
The JXMapViewer class is a complex Swing component from the SwingLabs.org web site. It renders images from tile-based mapping servers like OpenStreetMap.org in Swing applications, complete with pan and zoom behavior. Because this class already exists, it would be great to use it in JavaFX programs rather than having to completely rewrite it in JavaFX.
Most of the standard Swing components like JButton and JSlider have JavaFX wrappers in the javafx.ext.swing package. For those components you can use the JavaFX wrapper class. You might occasionally have an existing custom Swing component that you would like to use in a JavaFX program. For these components you can use the SwingComponent.wrap() utility function to automatically create a Swing wrapper.
This example has a static method on the SetupMap Java class which creates the mapping component by using normal Java code. This is the same code that you would write in a plain Java desktop application, as shown in Figure 1.
public class SetupMap {
public static JComponent create() {
JXMapViewer map = new JXMapViewer();
final int max = 17;
TileFactoryInfo info = new TileFactoryInfo(1,max-2,max,
256, true, true, // tile size is 256 and x/y orientation is normal
"http://tile.openstreetmap.org",
"x","y","z") {
public String getTileUrl(int x, int y, int zoom) {
zoom = max-zoom;
String url = this.baseURL +"/"+zoom+"/"+x+"/"+y+".png";
return url;
}
};
TileFactory tf = new DefaultTileFactory(info);
map.setTileFactory(tf);
map.setZoom(11);
map.setAddressLocation(new GeoPosition(51.5,0));
return map;
}
}
Figure 1: Creating the JXMapViewer Component
To use the JXMapViewer component in a JavaFX graphic scene create an instance of SwingComponent from the original component by using the SwingComponent.wrap() function. Then you must give the component a size manually (400x200 in this case) as shown in Figure 2.
var map = SetupMap.create(); map.setPreferredSize(new java.awt.Dimension(400,300)); var mapComp = SwingComponent.wrap(map);
Figure 2: Wrapping the JXMapViewer in a SwingComponent Class
Because a SwingComponent is a subclass of javafx.scene.Node you can do the cool things that you could do on other nodes, such as animation, transforms, and special effects. In this example I put a rounded rectangle clip over the component to give it round corners, then I added a border on top of the component. See Figure 3.
mapComp.clip = Rectangle {
width: 400 height: 300
arcHeight: 30 arcWidth: 30
};
var mapGroup = Group {
opacity: 0.01
translateX: 300-400/2
translateY: 200-300/2
scaleX: bind scale
scaleY: bind scale
content: [
mapComp,
Rectangle { width: 400 height: 300
arcHeight: 30 arcWidth: 30
stroke: Color.WHITE
strokeWidth: 3
fill: null
}
]
};
Figure 3: Adding Round Corners and a Border
Finally I used a scale effect to make the map zoom in and put animated text on top of the entire scene, as Figure 4 shows. During all of these transitions the Swing component is still "live", meaning it can receive mouse events and repaint itself the way any normal Swing component would do. The text above the map has its blocksMouse variable set to true so that any clicks on the text won't penetrate to the map. However, if you start dragging on the map and move over the text the map will still receive the drag events correctly. The JavaFX runtime handles all of the conversion necessary to make Swing interact seamlessly with JavaFX graphics.
var bigText:Text = Text {
content: "Swing + JavaFX"
font: Font.font("Verdana",FontWeight.BOLD,50)
fill: Color.BLACK
stroke: Color.ORANGE strokeWidth: 2
y: 130 x: -600
blocksMouse: true
onMouseEntered:function(e:MouseEvent) {
bigText.fill = Color.WHITE
}
onMouseExited:function(e:MouseEvent) {
bigText.fill = Color.BLACK
}
};
var scale = 0.3;
var zoomIn = Timeline {
keyFrames: [
at(0s) { scale=>0.3 tween Interpolator.LINEAR},
at(0s) { mapGroup.opacity=>0.0 tween Interpolator.LINEAR},
at(1s) { scale=>1.0 tween Interpolator.LINEAR},
at(1s) { mapGroup.opacity=>1.0 tween Interpolator.LINEAR},
at(0.5s) { bigText.x => -300 tween Interpolator.LINEAR},
at(2s) { bigText.x => 85 tween Interpolator.LINEAR},
]
};
Stage {
width: 600
height: 400
scene: Scene {
fill: Color.BLACK
content: [
Rectangle { x: 35 width: 40 height: 20 fill: Color.web("#444444") stroke: Color.web("#888888")
onMousePressed: function(e:MouseEvent) {
zoomIn.playFromStart();
}
},
Text { content: "Show" x: 40 y: 15 fill: Color.BLACK},
mapGroup,
bigText,
]
}
}
Figure 4: Putting Text Above the Map
Josh Marinacci