Running a simple openjfx application from kotlin and intellij without the javafx plugin (but with the javafx sdk)

Answered

Hi

I had been struggling to create and run a simple openjfx application from Kotlin, which I plan on using in the future as a starting point for building a more complete openjfx application with Kotlin. Having figured out how to do so (after an extensive search on the internet), if appropriate, I would like to share the code and settings that worked for me in IntelliJ (community) 2023/10/16.

The code itself is a simple translation from similar Java code, with three small changes:

import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import javafx.stage.Stage

class HelloWorld : Application()
{   override fun start(stage: Stage)
    {   val javaVersion = System.getProperty("java.version")
        val javafxVersion = System.getProperty("javafx.version")
        val l = Label("Hello, JavaFX $javafxVersion, running on Java $javaVersion.")
        val scene = Scene(StackPane(l), 640.0, 480.0)
        stage.setScene(scene)
        stage.show()
    }
}

// The three changes are below:
// - I do not use a companion object
// - I call Application.launch (since otherwise the main function will not know where to find launch
// - I pass the java version of the class of the above class name as a parameter to Application.launch
fun main(args: Array<String>)
{
    Application.launch(HelloWorld::class.java)
}
 

My build.gradle is

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.9.0'
    id 'org.openjfx.javafxplugin' version '0.1.0'
    id 'application'
}

repositories {
    mavenCentral()
}

javafx {
    version = "17.0.8"
    modules = [ 'javafx.controls' ]
}

application {
    mainClass = 'HelloWorld.kt'
}

I also followed the following steps (which I have posted elsewhere):
- I downloaded the openjfx sdk
- I unzipped it
- I stored the contents (the legal and lib folders and the src.zip file) in the folder /opt/javafx-sdk-17.0.8
- I added a global library (using file/project structure) named lib that points to /opt/javafx-sdk-17.0.8/lib
- I added lib as a dependency to project structure modules
- I added the following vm options to a new run configuration 
   --module-path /opt/javafx-sdk-17.0.8/lib --add-modules javafx.controls

I found the following link to be very helpful 
https://charts-kt.io/documentation/1.0/tutorial/creating-your-first-javafx-chart-app-part-1/

Having gotten this to work, it seems trivial.  But the rason I am posting it is that it took me hours to get it to work.

PhilTroy

0
10 comments

Hi, Phil. Thank you for letting us know about your solution. 

Please, feel free to contact us in case of any questions or requests.

0

Mr. Phil Troy,

Could you please post a link of the code/app/project so others can run it and examine it in detail?

Kotlin+JavaFX+Gradle has been a time-consuming adventure that your contribution may accelerate. 

0

Hi

I have a whole project file that I can send to you.  I think you can access a copy of the whole project at https://drive.google.com/file/d/1yI4qPhH5aqtBex6734sH7IkiXmwrZqvf/view?usp=drive_link

Phil

 

0

Hey Phil,

I tried to download your project with the link https://drive.google.com/file/d/1yI4qPhH5aqtBex6734sH7IkiXmwrZqvf/view?usp=drive_link, but access is denied. Can you give me permission or choose another way to send me the file?

0

Hi

I think that I gave you permission, and also gave everyone permission.

Please let me know if you are now able to access the file.

Phil

0

Hi

I've put together a larger example.  I am not at liberty to post all the code, but there werre a few things that I did that might be of use to others.  I'll post each “thing” in a separate comment.  The first of these was to display a number of details about a specific row of a table view.  To do so, instead of somehow creating acustom form (stage), I adapted an idea of my colleague Elizabeth to put each piece of the information into a separate menu item for a context menu.  Then to ensure that on the left I had field descriptions and on the right I had field values, and these were all lined up, I used CustomMenuItems, each having a grid pane, to specify column widths.

Here is the code to create the CustomMenuItem

class KeyValueMenuItem(val keyTextFieldWidth: Double, val valueTextFieldWidth: Double, key: String, value: String): CustomMenuItem()
{
    init
    {
        this.setContent(GridPane().apply()
                                   {   add(Label(Translator.translate(key) + ".".repeat(100)), 0, 0)
                                       add(Label(" "),1,0)
                                       add(Label(value),2,0)
                                       getColumnConstraints().add(ColumnConstraints(keyTextFieldWidth))
                                       getColumnConstraints().add(ColumnConstraints(10.0))
                                       getColumnConstraints().add(ColumnConstraints(valueTextFieldWidth))
                                   }
                       )
    }
}

I then added menu items to the menu by

add(KeyValueMenuItem(300.0, 400.0, fieldDescriptionVariable, fieldValueVariable))

Phil

 

0

Hi again

I needed to create multiple instance of the same table view, and include code for transferring items between the table views.  I had a lot of trouble determining which row and column  was selected, and this information was needed to determine the contents of the menu discussed in my previous post.  Also, I was having trouble forcing the table view to be more than 500 pixels high.  So I put together the following code:


import javafx.event.EventHandler
import javafx.scene.control.Label
import javafx.scene.control.TableColumn
import javafx.scene.control.TableView
import javafx.scene.control.cell.PropertyValueFactory
import javafx.scene.input.MouseEvent
import javafx.scene.layout.Pane


class XTableView(val tableViewNo: Int): TableView<X>()
{
   var targetHeight = -1.0

   init
   {
       // For privacy reasons, we do not display all of the fields in the grid
       this.getColumns().setAll(columnGenerator<X>("YNameAbbreviation", 40.0),
                                columnGenerator<X>("YNo", 85.0),
                                columnGenerator<X>("YAge", 40.0),
                                columnGenerator<X>("XTime", 50.0),
                                columnGenerator<X>("YArriveTime", 50.0),
                                columnGenerator<X>("zCode", 65.0),
                                columnGenerator<X>("zDescription", 120.0),
                                columnGenerator<X>("YNeed1", 20.0),
                                columnGenerator<X>("YNeed2", 20.0),
                                columnGenerator<X>("YNeed3", 20.0),
                                columnGenerator<X>("YNeed4", 20.0),
                                columnGenerator<X>("XState", 25.0)
                               )

       // Only allow one cell to be selected at a time
       this.getSelectionModel().setCellSelectionEnabled(true);

       class TableViewEventHandler(val tableView: TableView<X>, val tableViewNo: Int): EventHandler<MouseEvent>
       {   override fun handle(e: MouseEvent)
           {
               // Close any menus that are already open
               XStateMenu.hide()
               XInformationDisplay.hide()

               // Determine the tableview, row, and column that was clicked
               val column = tableView.getSelectionModel().getSelectedCells()[0].column
               val row = tableView.getSelectionModel().getSelectedCells()[0].row
               println("")
               println("*** Cell selected")
               println("*** TableViewNo: $tableViewNo")
               println("*** Column:      $column")
               println("*** Row:         $row")

               // If user clicks on rightmost column (which contains the state letter
               // - Display state menu to allow user to pick state for X to be moved to
               // - Otherwise display X information
               if (column == (XTableView.fieldCount - 1))
               {   XStateMenu.show(tableView, tableViewNo, row, e.getScreenX(), e.getScreenY())
               }
               else
               {   val X = XManager.XTableViewDatasets[tableViewNo].get(row)
                   XInformationDisplay.show(X, tableView, e.getScreenX(), e.getScreenY())
               }
           }
       }

       // To understand why a mouse clicked event handler is used to to identify selected cells, see comment by ptroy at
       // https://stackoverflow.com/questions/24441175/how-detect-which-column-selected-in-javafx-tableview/77617849#77617849\
       this.setOnMouseClicked(TableViewEventHandler(this, tableViewNo))

       // Turn off message that there is no content in the tableview
       // https://stackoverflow.com/questions/32418653/javafx-tableview-message-when-empty
       this.setPlaceholder(Label(""));

       this.setStyle("-fx-font-size: 15; -fx-font-weight: bold; -fx-font-smoothing-type: lcd; -fx-font-family: \"DejaVu Sans\";");
   }

   // This is called by JavaFX;
   // - I set height here since every other place/approach seems to be protected
   // - If I do not set height here then it seems to be limited to a value that is sometimes smaller than needed
   override fun resize(width: Double, height: Double)
   {   super.resize(width, if (targetHeight > -1) targetHeight else height)
       val header = lookup("TableHeaderRow") as Pane
       header.fixHeight(0.0)
       header.isVisible = false
   }

   fun tryToOverrideHeight(targetHeight: Double)
   {   this.targetHeight = targetHeight
   }

   companion object
   {   val fieldCount = 12
   }
}

class columnGenerator<T>(fieldName: String, width: Double): TableColumn<T, String>("")
{
   init
   {   this.setCellValueFactory(PropertyValueFactory<T, String>(fieldName))
       this.minWidth = width
       this.maxWidth = width
   }
}

 

Phil

 

0

Hi

I needed to work with both French and English, so I needed a translator capability.  I believe that there are standards for this, but I needed to build something quick and dirty.  So I created a translator class that looks like this:

import java.io.BufferedReader
import java.io.FileReader

enum class Language()
{   ENGLISH,
    FRANCAIS
}

object Translator: IObservable
{   // Please see the following web pages to determine if there is a better and simpler  way to do translation
    // https://stackoverflow.com/questions/32464974/javafx-change-application-language-on-the-run
    // https://stevenschwenke.de/howToI18nInJavaFXAndIntelliJIDEA

    var currentLanguage = Language.FRANCAIS
    var translationMap = importMap(currentLanguage.toString() + ".map");

    override val observers = java.util.ArrayList<IObserver>()

    fun setLanguage(language: Language)
    {   this.currentLanguage = language
        translationMap = importMap(currentLanguage.toString() + ".map");
        observers.forEach{observer -> observer.update("languageChangeEvent")}
    }

    fun importMap(fileName: String): Map<String, String>
    {   val data = mutableMapOf<String, String>()
        BufferedReader(FileReader(fileName)).forEachLine {line -> data[line.split(":")[0]] = line.split(":")[1]}
        return data
    }

    fun translate(key: String) = translationMap.get(key)
}

I also created an English.map and a French.map file where I put keywords on the left and translation on the right.

 

I used the following for the observer:

interface IObserver
{
    fun update(eventInformation: String)
}


interface IObservable
{
    val observers: ArrayList<IObserver>

    fun add(observer: IObserver)
    {   observers.add(observer)
    }

    fun remove(observer: IObserver)
    {   observers.remove(observer)
    }

    fun sendUpdateEvent(eventInformation: String)
    {   observers.forEach { it.update(eventInformation) }
    }
}

 

I then implemented the IObsrver interface for each label and menu item that I wanted to translate, and I created a menu item on the main screen that allowed specifying the current language.

I am guessing there is a better way, but this approach works (well and fast).

 

Phil

 

0

Hi

I needed a message box that  could be updated by updating the underlying text (or string buffer) containing the text.  Unfortunately, the TextArea controls provided by JavaFX do not allow you (I do not think they do) to store the data elsewhere directly, so I created a StringBufferObservingTextArea class that I allowed text areas to observe.  The code follows:

import javafx.scene.control.TextArea

class StringBufferObservingTextArea(val observableStringBuffer: ObservableStringBuffer): TextArea(), IObserver
{
    init
    {   observableStringBuffer.add(this)
    }
    override fun update(eventInformation: String)
    {   if (eventInformation == "TextChanged") this.setText(observableStringBuffer.getText())
    }
}

Phil

0

Hi

 

The last big piece of this puzzle was the class that created the GUI.  I used JavaFX and not TornadoFX, but I tried to take advantage of some of Kotlin's special capabilities.  The following is the result:

import javafx.application.Application
import javafx.geometry.Insets
import javafx.scene.Scene
import javafx.scene.control.ToolBar
import javafx.scene.layout.BorderPane
import javafx.scene.layout.Pane
import javafx.scene.layout.VBox
import javafx.stage.Stage


fun Pane.fixHeight(height: Double)
{   this.setMinHeight(height)
   this.setMaxHeight(height)
   this.setPrefHeight(height)
}


fun Pane.fixWidth(width: Double)
{   this.setMinWidth(width)
   this.setMaxWidth(width)
   this.setPrefWidth(width)
}


fun Pane.fixSize(width: Double, height: Double)
{   this.fixWidth(width)
   this.fixHeight(height)
}


class DHMRIStateTrackerGraphicalUserInterface: Application()
{
   lateinit var stage: Stage

   val xGridHeaderKeys  = arrayOf( "headerKey1",
                                             "headerKey2",
                                             "headerKey. . ."
                                            )

   val tableViews = Array<XTableView>(10)
   { i -> xTableView(i).apply()
       {   setItems(xManager.getxDataset(i))
       }
   }

   val messageBox = StringBufferObservingTextArea(xManager.xManagerMessages)

   override fun start(stage: Stage)
   {
       this.stage = stage

       // TODO: This binding is not working - text is not being displayed even when text is appended to xManager.xManagerMessages
       // See https://stackoverflow.com/questions/48369562/whats-a-good-observable-appendable-base-for-a-textarea

 

       val borderPane = BorderPane().apply()
       {   fixSize(1920.0, 1080.0)
           top = ToolBar( MultiLingualButton("buttonTextSendMessage"),
                          MultiLingualButton("buttonTextDisconnect"),
                          MultiLingualButton("buttonTextChangeLanguage").apply()
                          {    setOnAction{Translator.setLanguage(if(Translator.currentLanguage == Language.FRANCAIS) Language.ENGLISH 
                                                                  else Language.FRANCAIS)}
                          }
                        ).apply()  {   minHeight = 40.0
                                       borderProperty()
                                   }
           left = VBox().apply()
           {   fixSize(600.0, 900.0)
               padding = Insets(0.0, 10.0, 0.0, 10.0)
               children.add(xTableViewButton(xGridHeaderKeys.get(0)).apply{maxWidth = Double.MAX_VALUE})
               children.add(tableViews[0].apply{tryToOverrideHeight(800.0)})
           }
           center = VBox().apply()
           {   padding = Insets(0.0, 50.0, 0.0, 50.0)
               for (i in 1..8)
               {   children.add(xTableViewButton(xGridHeaderKeys.get(i)).apply{maxWidth = Double.MAX_VALUE})
                   children.add(tableViews[i].apply{maxWidth = Double.MAX_VALUE})
               }
               children.add(xTableViewButton(" ").apply{maxWidth = Double.MAX_VALUE})
           }
           right = VBox().apply()
           {   fixSize(600.0, 900.0)
               padding = Insets(0.0, 10.0, 0.0, 10.0)
               children.add(xTableViewButton(xGridHeaderKeys.get(9)).apply{maxWidth = Double.MAX_VALUE})
               children.add(tableViews[9].apply{tryToOverrideHeight(800.0)})
           }
           bottom = messageBox.apply()
           {   prefHeight(200.0)
               setStyle("-fx-font-size: 14; -fx-font-smoothing-type: lcd; -fx-font-family: \"Segoe UI\";")
           }
       }

       messageBox.appendText("*** This is where messages will be seen")

       with(stage)
       {   setTitle("DH MRI x State Tracker")
           setScene(Scene(borderPane))
           show()
       }
   }
}

 

Phil

0

Please sign in to leave a comment.