When writing desktop applications in Java, we want them to look and feel native. A good application blends in. It provides an experience already familiar to the user.
Swing GUI look and feel: Metal vs. Native
On the desktop, the user journey doesn’t start within an application itself. It starts with an installer. This is where the Java world used to fall behind. But not anymore.
Starting from Java 16, JDK comes with
jpackage
. This tool packages an application into a bundle with
a built-in JRE and wraps it into a native installer and executable.
In this article, I’ll demonstrate how to use jpackage with a JxBrowser application. I will create a simple JxBrowser application, wrap it into a native installer and executable program. I’ll give you code snippets that you can copy straight into your project.
Configuring Gradle
My goal here is to create a simple Pomodoro tracker, install it, and launch it as a native OS application. It will use Gradle for the build configuration and Swing for the user interface.
Let’s start from scratch by creating an empty Gradle project:
$ gradle init --dsl kotlin --type basic --project-name jxbrowser-installer
Open build.gradle.kts and apply the Java plug-in:
plugins {
java
}
group = "com.teamdev.examples"
version = "1.0"
java {
sourceCompatibility = JavaVersion.VERSION_16
targetCompatibility = JavaVersion.VERSION_16
}
repositories {
mavenCentral()
}
Our application requires two JxBrowser dependencies. One contains the core API with Chromium binaries. And another one implements the Swing toolkit support.
Let’s use the JxBrowser Gradle Plug-in to add the necessary dependencies.
plugins {
…
id("com.teamdev.jxbrowser") version "2.0.0"
}
jxbrowser {
version = "8.5.1"
}
dependencies {
implementation(jxbrowser.swing)
implementation(jxbrowser.currentPlatform)
}
In the code snippet above, I use jxbrowser.currentPlatform
that detects the
current platform and cherry-picks only necessary Chromium binaries. If you’re
building, let’s say, a Windows-only application, you can specify the
Chromium binaries for Windows explicitly:
dependencies {
implementation(jxbrowser.swing)
implementation(jxbrowser.win64)
}
Application code
Our application is very simple. It opens a JFrame
, adds the BrowserView
component that loads and displays a web page with the Pomodoro Tracker.
package com.teamdev.examples;
import com.teamdev.jxbrowser.engine.Engine;
import com.teamdev.jxbrowser.view.swing.BrowserView;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static com.teamdev.jxbrowser.engine.RenderingMode.HARDWARE_ACCELERATED;
import static javax.swing.SwingConstants.CENTER;
import static javax.swing.WindowConstants.DISPOSE_ON_CLOSE;
/**
* A Pomodoro tracker.
*
* This app displays a window with the integration browser component that loads and displays
* the Pomodoro Tracker web application.
*/
public final class PomodoroTracker {
public static final String URL = "https://teamdev.com/jxbrowser/docs/tutorials/jpackage/pomodoro/";
public static void main(String[] args) {
var splash = showSplashScreen();
showBrowser();
splash.dispose();
}
private static void showBrowser() {
var engine = Engine.newInstance(HARDWARE_ACCELERATED);
var browser = engine.newBrowser();
var frame = new JFrame("Pomodoro Tracker");
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
engine.close();
}
});
var view = BrowserView.newInstance(browser);
frame.add(view, BorderLayout.CENTER);
frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
frame.setSize(1280, 900);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
browser.navigation().loadUrl(URL);
}
private static JWindow showSplashScreen() {
var splash = new JWindow();
splash.getContentPane().add(new JLabel("Loading...", CENTER));
splash.setBounds(500, 150, 300, 200);
splash.setVisible(true);
return splash;
}
}
Packaging the application
Desktop applications written in Java need to carry all necessary libraries they require at runtime. The usual approach is to merge libraries together with an application into a big Uber JAR. It requires an extra bit of configuration and is not suitable for modular projects.
Now there’s a simpler option: gather a bunch of JAR files in one folder and let jpackage handle the rest.
First, let’s configure building the main application JAR:
val jarDirectory = file("$buildDir/jars")
tasks {
jar {
manifest {
attributes["Main-Class"] = "com.teamdev.examples.PomodoroTracker"
}
archiveFileName.set("main.jar")
destinationDirectory.set(jarDirectory)
}
}
Then let’s create a task to gather JAR files of the dependencies:
val jarDirectory = file("$buildDir/jars")
tasks {
…
register<Copy>("gatherDependencies") {
from(configurations.runtimeClasspath).into(jarDirectory)
}
}
These two tasks are enough to launch the application from the command line:
$ ./gradlew jar gatherDependencies
$ java -cp "build/jars/*" com.teamdev.examples.PomodoroTracker
Now everything is ready to configure jpackage.
This is a command-line tool. However, I prefer to keep everything in Gradle scripts. They are easier to read and maintain than a heap of .sh and .bat files.
I recommend the org.panteleyev.jpackage
plug-in. This plug-in wraps
the command-line API of jpackage into Gradle DSL. Here’s how to apply it:
plugins {
...
id("org.panteleyev.jpackageplugin") version "1.3.1"
}
And here’s how I configured it to generate the installers:
val jarDirectory = file("$buildDir/jars")
tasks {
…
jpackage {
// Gather all JAR files before packaging.
dependsOn("jar", "gatherDependencies")
appName = "Pomodoro Tracker"
appVersion = "${project.version}"
// The directory with JARs.
input = jarDirectory.absolutePath
// The name of the main, launchable JAR.
mainJar = "main.jar"
// The list of necessary modules to include into a bundled JRE.
// The "java.logging" is required by JxBrowser.
addModules = listOf("java.base", "java.desktop", "java.logging")
// The path to the JRE modules.
modulePaths = listOf("${System.getProperty("java.home")}/jmods")
// The directory where to put the installers.
destination = "$buildDir/dist"
linux {
type = org.panteleyev.jpackage.ImageType.DEB
linuxPackageName = "pomodoro"
}
windows {
type = org.panteleyev.jpackage.ImageType.MSI
winDirChooser = true
winMenu = true
}
mac {
type = org.panteleyev.jpackage.ImageType.DMG
}
}
}
Once everything is configured, the only thing left to do is to call the jpackage task. Once the task is complete, we’ll find the installers in build/dist directory:
$ ./gradlew jpackage
For Windows, you’re going to need https://wixtoolset.org/releases/
Installer in action
Source code
You can find the source code of the application in the GitHub repository.