Currency conversion is a common task for travelers, businesses, and anyone dealing with international financial transactions. In this article, we’ll explore how to build a real-time currency converter in JavaFX. JavaFX is a robust framework for building graphical user interfaces (GUIs) in Java, making it an excellent choice for creating a user-friendly currency converter application.
Our currency converter application will allow users to input an amount in one currency, select another currency for conversion, and see the converted amount along with the current exchange rate. To create this application, we’ll use JavaFX for the user interface and leverage the Exchange Rates API for currency exchange rate information.
Country Selector
The CountrySelector class is responsible for displaying the list of countries with their currencies and flags. It uses a custom ComboBox control with a custom cell factory to render each item in the ComboBox.
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
public class CountrySelector extends ComboBox<CountrySelector.Country> {
public CountrySelector() {
this(false);
}
public CountrySelector(boolean showCurrencies) {
initialize(showCurrencies);
}
/**
* Initializes the CountrySelector with the specified display mode.
*
* @param showCurrencies True to display currencies, false to display country names.
*/
private void initialize(boolean showCurrencies) {
final List<Country> countries = List.of(
new Country("Zambia", "Zambian Kwacha", "ZMW", "/flags/zm.png"),
new Country("United States", "United States Dollar", "USD", "/flags/us.png")
);
// Countries to the ComboBox
getItems().addAll(countries);
// Select the first country
selectionModelProperty().get().selectFirst();
setShowCurrencies(showCurrencies);
}
/**
* Sets the display mode of the CountrySelector.
*
* @param showCurrencies True to display currencies, false to display country names.
*/
public void setShowCurrencies(boolean showCurrencies) {
setupCellFactory(showCurrencies ? Country::getCurrency : Country::getName);
}
private void setupCellFactory(Function<Country, String> propertyExtractor) {
setCellFactory(param -> new CustomListCell(propertyExtractor));
setButtonCell(new CustomListCell(propertyExtractor));
}
// Create a custom ListCell for displaying the country information
private static class CustomListCell extends ListCell<Country> {
private final Function<Country, String> propertyExtractor;
public CustomListCell(Function<Country, String> propertyExtractor) {
this.propertyExtractor = propertyExtractor;
}
@Override
protected void updateItem(Country item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
ImageView flag = new ImageView(item.getFlag());
flag.setFitHeight(20);
flag.setFitWidth(20);
setText(propertyExtractor.apply(item));
setGraphic(flag);
} else {
setText(null);
setGraphic(null);
}
}
}
public static class Country {
private final String name;
private final String currency;
private final String currencyCode;
private final Image flag;
public Country(String name, String currency, String currencyCode, String flagPath) {
this.name = name;
this.currency = currency;
this.currencyCode = currencyCode;
this.flag = new Image(Objects.requireNonNull(getClass().getResourceAsStream(flagPath)));
}
public String getName() {
return name;
}
public String getCurrency() {
return currency;
}
public String getCurrencyCode() {
return currencyCode;
}
public Image getFlag() {
return flag;
}
}
}
ExchangeRatesClient
The ExchangeRatesClient class serves as a utility for managing HTTP requests to the Exchange Rates API to retrieve exchange rate information. It offers functionality for currency conversion and accessing real-time exchange rates. Furthermore, the class utilizes the Gson library for parsing JSON data.
To install the Gson library, you have two options: you can either download the JAR file manually, or you can use a build automation tool. Both the JAR file and the build automation tool dependencies are available on the Maven Repository.
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class ExchangeRatesClient {
private static final String API_BASE_URL = "http://api.exchangerate.host";
private static final String API_ACCESS_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXX";
/**
* Convert an amount from one currency to another.
*
* @param from The source currency code.
* @param to The target currency code.
* @param amount The amount to convert.
* @return An array containing the exchange rate and the converted result.
* @throws IOException If an error occurs while making the API request.
*/
public static double[] convert(String from, String to, double amount) throws IOException {
// Create the query string with parameters
String query = String.format("?from=%s&to=%s&amount=%.2f", from, to, amount);
// Make an API request to convert the amount and parse the JSON response
JsonObject json = apiRequest("/convert" + query);
JsonElement rateElement = json.getAsJsonObject("info").get("quote");
JsonElement resultElement = json.get("result");
// Extract the exchange rate and result from the JSON
double rate = rateElement.isJsonNull() ? 0.0: rateElement.getAsDouble();
double result = resultElement.isJsonNull() ? 0.0: resultElement.getAsDouble();
return new double[]{rate, result};
}
/**
* Make an API request to retrieve exchange rate information.
*
* @param path The API endpoint path with parameters.
* @return The JSON response from the API as a JsonObject.
* @throws IOException If an error occurs while making the API request.
*/
private static JsonObject apiRequest(String path) throws IOException {
HttpURLConnection connection = null;
try {
// Build the URL with the API base URL and path
URL url = new URL(API_BASE_URL + path + "&access_key=" + API_ACCESS_KEY);
// Open an HTTP connection
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
// Get the HTTP response code
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// Parse and return the JSON response as a JsonObject
return new Gson().fromJson(parseResponse(connection), JsonObject.class);
} else {
throw new IOException("API request failed with response code: " + responseCode);
}
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
/**
* Parse the response from an HTTP connection into a string.
*
* @param connection The HTTP connection.
* @return The response content as a string.
* @throws IOException If an error occurs while reading the response.
*/
private static String parseResponse(HttpURLConnection connection) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
StringBuilder response = new StringBuilder();
String line;
// Read the response content line by line
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
}
}
}
Main Class
The Main class is the entry point of our application. It initializes the user interface, sets up event listeners, and handles the currency conversion logic. It also includes a nested ConverterTask class that performs currency conversion in the background.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.concurrent.Task;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.function.UnaryOperator;
public class Main extends Application {
// The parent layout manager
private final BorderPane parent = new BorderPane();
// Create country selection drop-downs
private final CountrySelector from = new CountrySelector(true);
private final CountrySelector to = new CountrySelector(true);
// Create a TextField for entering the amount to convert
private final TextField amountField = new TextField();
// Create labels for result and exchange rate
private final Label result = new Label("0.00 ZMW");
private final Label rate = new Label("0 USD = 0.00 ZMW");
@Override
public void init() throws Exception {
super.init();
buildUI();
}
private void buildUI() {
// Set styles for result and rate labels
result.setStyle("-fx-font-size: 20px; -fx-font-weight: bold; -fx-font-family: 'Trebuchet MS';");
rate.setStyle("-fx-font-size: 16px; -fx-font-weight: bold; -fx-font-family: 'Trebuchet MS';");
// Create a NumericFormatter to restrict input to numeric values only
NumericFormatter numericFormatter = new NumericFormatter();
// Set an initial selection for the 'from' ComboBox
from.selectionModelProperty().get().select(1);
// Apply the NumericFormatter to the amountField
amountField.setTextFormatter(numericFormatter);
// Add listeners for changes in input, 'from' selection, and 'to' selection
amountField.textProperty().addListener(this::updateConversion);
from.valueProperty().addListener(this::updateConversion);
to.valueProperty().addListener(this::updateConversion);
// Create a container for center-aligned content
VBox centerContainer = new VBox(
15, amountField, from, to,
new VBox(50), // Empty space for spacing
new VBox(5, result, rate)
);
centerContainer.setAlignment(Pos.CENTER);
// Create an empty left VBox for left alignment
VBox leftContainer = new VBox();
leftContainer.setAlignment(Pos.TOP_LEFT);
// Combine the left and center containers in a horizontal layout
HBox mainContainer = new HBox(leftContainer, centerContainer);
mainContainer.setAlignment(Pos.CENTER);
// Set the main container as the center content of the BorderPane
parent.setCenter(mainContainer);
}
private void updateConversion(Observable observable) {
// Start a new thread (ConverterTask) to perform currency conversion asynchronously
Thread thread = new Thread(new ConverterTask());
// Daemon thread to exit when the application exits
thread.setDaemon(true);
thread.start();
}
@Override
public void start(Stage stage) throws Exception {
// Create a scene with the BorderPane as the root
Scene scene = new Scene(parent, 640, 480);
// Set the stage title
stage.setTitle("Building a Real-time Currency Converter in JavaFX");
// Set the scene for the stage
stage.setScene(scene);
// Center the stage on the screen
stage.centerOnScreen();
// Display the stage
stage.show();
}
private static class NumericFormatter extends TextFormatter<Double> {
public NumericFormatter() {
// Initialize the NumericFormatter with appropriate settings
this(change -> {
String newText = change.getControlNewText();
if (newText.matches("[0-9]*\\.?[0-9]*")) {
// Accept the change if it's a valid numeric input
return change;
}
// Reject the change if it's not a valid numeric input
return null;
});
}
private NumericFormatter(UnaryOperator<Change> unaryOperator) {
super(unaryOperator);
}
}
private class ConverterTask extends Task<Void> {
@Override
protected Void call() throws Exception {
try {
// Get source and target currency codes, input value
String fromCurrency = from.getValue().getCurrencyCode();
String toCurrency = to.getValue().getCurrencyCode();
String value = amountField.getText();
double amount = Double.parseDouble(value.isEmpty() ? "0.0" : value);
// Perform currency conversion and fetch results
double[] conversionResult = ExchangeRatesClient.convert(fromCurrency, toCurrency, amount);
double convertedAmount = conversionResult[1];
double exchangeRate = conversionResult[0];
// Update UI on the JavaFX application thread
Platform.runLater(() -> {
result.setText(String.format("%.2f %s", convertedAmount, toCurrency));
rate.setText(String.format("%d %s = %.2f %s", exchangeRate > 0 ? 1 : 0, fromCurrency, exchangeRate, toCurrency));
});
} catch (IOException | NumberFormatException ex) {
// Handle errors and update UI
Platform.runLater(() -> {
result.setText("Error");
rate.setText("");
});
ex.printStackTrace();
}
return null;
}
}
}
The real-time currency conversion happens when the user interacts with the application. Whenever the user changes the input amount or selects different source or target currencies, the updateConversion method is triggered. This method starts a new thread (ConverterTask) to perform the currency conversion asynchronously, preventing the UI from freezing.
Inside the ConverterTask, we make an API request to an external service (ExchangeRatesClient) to fetch the exchange rate and perform the currency conversion. The result is then updated on the JavaFX application thread using Platform.runLater.
Conclusion
In this article, we’ve explored how to build a real-time currency converter application in JavaFX. The application uses custom ComboBox controls for selecting source and target currencies, a TextField for inputting amounts, and labels for displaying conversion results and exchange rates. We also use a separate thread to perform currency conversion asynchronously to ensure a smooth user experience.
With this foundation, you can further enhance the application by adding more currencies, error handling, and additional features such as historical exchange rate charts or favorite currency pairs. Currency converters are valuable tools in finance and travel applications, and building one in JavaFX provides a powerful and flexible solution for your users.
I hope you found this article informative and useful. If you would like to receive more content, please consider subscribing to our newsletter.