JavaFX Controls in py5

The following source code demonstrates using javafx controls in py5. This demo may be run in the Thonny editor using ‘Imported mode for py5’. In addition, copies of the seven javafx.xxxx.jar files and a copy of javafx.jar must be in a folder entitled ‘jars’ in the py5 sketch folder. These may be copy/pasted from your Processing libraries folder; on MacOS the jar files are located at the following path: Documents/Processing/libraries/javafx/library/yourOSFolder/modules/seven javafx.xxxxx.jar files. The javafx.jar is found in the ‘library’ folder (making eight jar files copy/pasted to the ‘jars’ folder). Disclaimer: May not work in Windows.

# Uses Imported mode for py5
import javafx.stage.Stage

global pane

def toggleBtnAction(event):
  global toggleBtn
  if(toggleBtn.getText() == "Off"):
    toggleBtn.setText("On")
  else:
    toggleBtn.setText("Off")

def dialogBtnAction(self):
  global dialog
  dialog.showAndWait()
  
def sliderBtnAction(self):
  global myCircle
  global slider
  myCircle.setRadius(slider.getValue())
  
def colorPickerAction(self):
  global colorPicker
  global label
  global myCircle
  label.setTextFill(colorPicker.getValue())
  myCircle.setFill(colorPicker.getValue())

def datePickerAction(self):
  global datePicker
  value = datePicker.getValue()
  
def listViewListener(self):
  global listView
  label.setText(str(listView.getSelectionModel().getSelectedItem()))
  
def addControls():
  global toggleBtn
  global dialog
  global myCircle
  global slider
  global colorPicker
  global label
  global datePicker
  global listView
  
# Pane
  pane = javafx.scene.layout.Pane()

# Label
  label = javafx.scene.control.Label("Label")
  font = javafx.scene.text.Font.font("Menlo", javafx.scene.text.FontWeight.BOLD, javafx.scene.text.FontPosture.REGULAR, 15)
  label.setFont(font)
  label.setTextFill(javafx.scene.paint.Color.RED)
  label.setTranslateX(60)
  label.setTranslateY(65)
  pane.getChildren().add(label)

# Toggle Button
  toggleBtn = javafx.scene.control.ToggleButton("Off")
  toggleBtn.setLayoutX(390)
  toggleBtn.setLayoutY(140)
  toggleBtn.isSelected() == False
  toggleBtn.setOnAction(toggleBtnAction)
  pane.getChildren().add(toggleBtn)

# Button with Dialog
  dialog = javafx.scene.control.Dialog()
  dialog.setTitle("Error Dialog")
  btnType = javafx.scene.control.ButtonType("Ok")
  dialog.setContentText("Message goes here.")
  dialog.getDialogPane().getButtonTypes().add(btnType)
  btn = javafx.scene.control.Button("Push Me")
  btn.setLayoutX(80)
  btn.setLayoutY(20)
  btn.setOnAction(dialogBtnAction)
  pane.getChildren().add(btn)

# Circle
  myCircle = javafx.scene.shape.Circle(400, 60, 20)
  myCircle.setFill(javafx.scene.paint.Color.GREEN)
  pane.getChildren().add(myCircle)

# Slider
  slider = javafx.scene.control.Slider(1, 50, 10)
  slider.setLayoutX(190)
  slider.setLayoutY(20)
  tooltip1 = javafx.scene.control.Tooltip("Sets radius")
  slider.setTooltip(tooltip1)
  slider.setOnMouseDragged(sliderBtnAction)
  pane.getChildren().add(slider)

# Spinner
  spinner = javafx.scene.control.Spinner(0, 255, 0)
  spinner.setPrefSize(70, 25)
  spinner.setLayoutX(480)
  spinner.setLayoutY(60)
  pane.getChildren().add(spinner)

# ComboBox
  combo = javafx.scene.control.ComboBox()
  combo.setPromptText("Select One")
  myList = combo.getItems()
  myList.add("Java")
  myList.add("C++")
  myList.add("Python")
  myList.add("Processing")
  combo.setLayoutX(560)
  combo.setLayoutY(70)
  pane.getChildren().add(combo)

# ListView
  items = javafx.collections.FXCollections.observableArrayList("Item 1", "Item 2", "Item 3")
  listView = javafx.scene.control.ListView(items)
  listView.setMaxSize(120, 80)
  listView.setLayoutX(60)
  listView.setLayoutY(90)
  listView.getSelectionModel().selectedItemProperty().addListener(listViewListener)
  pane.getChildren().add(listView)

# TabPane
  tabPane = javafx.scene.control.TabPane()
  tabPane.setLayoutX(30)
  tabPane.setLayoutY(190)
  tab1 = javafx.scene.control.Tab("Tab 1")
  tab2 = javafx.scene.control.Tab("Tab 2")
  tab3 = javafx.scene.control.Tab("Tab 3")
  tab1.setContent(javafx.scene.shape.Rectangle(200, 100, javafx.scene.paint.Color.BLUE))
  tab2.setContent(javafx.scene.shape.Rectangle(200, 100, javafx.scene.paint.Color.RED))
  tab3.setContent(javafx.scene.shape.Rectangle(200, 100, javafx.scene.paint.Color.GREEN))
  tabPane.getTabs().addAll(tab1, tab2, tab3)
  pane.getChildren().add(tabPane)
  
# TextArea
  txtArea = javafx.scene.control.TextArea()
  txtArea.setLayoutX(250)
  txtArea.setLayoutY(190)
  txtArea.setMaxSize(180, 130)
  pane.getChildren().add(txtArea)

# RadioButton Group
  radioPane = javafx.scene.layout.TilePane(15, 15)
  group = javafx.scene.control.ToggleGroup()
  radio1 = javafx.scene.control.RadioButton("A")
  radio1.setToggleGroup(group)
  radio1.setSelected(True)
  radio2 = javafx.scene.control.RadioButton("B")
  radio2.setToggleGroup(group)
  radio3 = javafx.scene.control.RadioButton("C")
  radio3.setToggleGroup(group)
  radioPane.getChildren().addAll(radio1, radio2, radio3)
  radioPane.setLayoutX(200)
  radioPane.setLayoutY(120)
  pane.getChildren().add(radioPane)

# CheckBox
  cBox = javafx.scene.control.CheckBox("Checkbox")
  cBox.setIndeterminate(False)
  cBox.setLayoutX(200)
  cBox.setLayoutY(90)
  pane.getChildren().add(cBox)

# TextField
  txtFld = javafx.scene.control.TextField()
  txtFld.setLayoutX(200)
  txtFld.setLayoutY(150)
  pane.getChildren().add(txtFld)

# MenuBar **** //
  menu = javafx.scene.control.Menu("File")
  menuItem1 = javafx.scene.control.MenuItem("Open")
  menuItem2 = javafx.scene.control.MenuItem("Save")
  menu.getItems().addAll(menuItem1, menuItem2)
  menuBar = javafx.scene.control.MenuBar()
  menuBar.getMenus().add(menu)
  pane.getChildren().add(menuBar)

# ToolBar **** //
  toolBar = javafx.scene.control.ToolBar()
  openBtn = javafx.scene.control.Button("Build")
  saveBtn = javafx.scene.control.Button("Run")
  toolBar.getItems().addAll(openBtn, saveBtn)
  toolBar.setLayoutX(450)
  tbarTip = javafx.scene.control.Tooltip("ToolBar")
  toolBar.setTooltip(tbarTip)
  pane.getChildren().add(toolBar)

# ProgressBar
  pBar = javafx.scene.control.ProgressBar()
  pBar.setLayoutX(560)
  pBar.setLayoutY(20)
  pane.getChildren().add(pBar)

# TreeView **** //
  rootItem = javafx.scene.control.TreeItem("Pets")
  dogItem = javafx.scene.control.TreeItem("Dogs")
  dogItem.getChildren().add(javafx.scene.control.TreeItem("Poodle"))
  dogItem.getChildren().add(javafx.scene.control.TreeItem("Collie"))
  dogItem.getChildren().add(javafx.scene.control.TreeItem("Bulldog"))
  rootItem.getChildren().add(dogItem)

  catItem = javafx.scene.control.TreeItem("Cats")
  catItem.getChildren().add(javafx.scene.control.TreeItem("Siamese"))
  catItem.getChildren().add(javafx.scene.control.TreeItem("Ragdoll"))
  catItem.getChildren().add(javafx.scene.control.TreeItem("Persian"))
  rootItem.getChildren().add(catItem)

  treeView = javafx.scene.control.TreeView()
  treeView.setRoot(rootItem)
  treeView.setLayoutX(460)
  treeView.setLayoutY(110)
  treeView.setMaxSize(140, 230)
  treeView.setShowRoot(True)
  pane.getChildren().add(treeView)

# ColorPicker
  colorPicker = javafx.scene.control.ColorPicker()
  colorPicker.setLayoutX(200)
  colorPicker.setLayoutY(50)
  colorPicker.setValue(javafx.scene.paint.Color.GREEN)
  colorPicker.setOnAction(colorPickerAction) 
  pane.getChildren().add(colorPicker)

# DatePicker
  datePicker = javafx.scene.control.DatePicker()
  datePicker.setLayoutX(30)
  datePicker.setLayoutY(350)
  datePicker.setOnAction(datePickerAction) 
  pane.getChildren().add(datePicker)

# Line Chart
  xAxis = javafx.scene.chart.NumberAxis()
  xAxis.setLabel("No. of months")
  yAxis = javafx.scene.chart.NumberAxis()
  yAxis.setLabel("Dollars Per Dozen")
  lineChart = javafx.scene.chart.LineChart(xAxis, yAxis)
  dataSeries1 = javafx.scene.chart.XYChart.Series()
  dataSeries1.setName("2024")
  dataSeries1.getData().add(javafx.scene.chart.XYChart.Data( 1, 2))
  dataSeries1.getData().add(javafx.scene.chart.XYChart.Data( 2, 3))
  dataSeries1.getData().add(javafx.scene.chart.XYChart.Data(3, 4))
  dataSeries1.getData().add(javafx.scene.chart.XYChart.Data(4, 5))
  dataSeries1.getData().add(javafx.scene.chart.XYChart.Data(5, 6))
  dataSeries1.getData().add(javafx.scene.chart.XYChart.Data(6, 6))
  lineChart.getData().add(dataSeries1)
  lineChart.setTitle("Price of Eggs")
  lineChart.setLayoutX(280)
  lineChart.setLayoutY(350)
  lineChart.setMaxSize(320, 240)
  pane.getChildren().add(lineChart)
  
  return pane

def settings():
  size(1, 1, FX2D)
  
def setup():  
  stage = javafx.stage.Stage()
  stage.setTitle("JavaFX Controls")
  pane = addControls()
  scene = javafx.scene.Scene(pane, 700, 600)
  stage.setScene(scene)
  stage.show()

Output:

1 Like

I’d love to find a way to combine these controls with an FX2D sketch, with the controls in the same window as the drawing surface.

I presume that by ‘drawing surface’ you mean canvas. A canvas is a control just like a button; the programmer can control its size, location, and function. In the demo below the canvas occupies almost the entire window except for a strip at the top reserved for other controls.

# Uses Imported mode for py5
import javafx.stage.Stage;

_wndW = 600
_wndH = 400
_topMargin = 60

def settings():
  size(1, 1, FX2D)
    
def setup():
  global gc
  global colorPicker
  
  stage = javafx.stage.Stage()
  stage.setTitle("JavaFX Drawing Demo")
  pane = javafx.scene.layout.Pane()
  
  # **** ColorPicker ****
  colorPicker = javafx.scene.control.ColorPicker()
  colorPicker.setLayoutX(200)
  colorPicker.setLayoutY(10)
  colorPicker.setValue(javafx.scene.paint.Color.GREEN) 
  pane.getChildren().add(colorPicker)

# **** Canvas ****
  canvas = javafx.scene.canvas.Canvas(_wndW, _wndH - _topMargin)
  gc = canvas.getGraphicsContext2D()
  gc.setLineWidth(2)
  gc.strokeRect(0, 0, _wndW, _wndH -_topMargin);
  canvas.setLayoutY(_topMargin)
  canvas.setOnMousePressed(MOUSE_PRESSED)
  canvas.setOnMouseDragged(MOUSE_DRAGGED)
  pane.getChildren().add(canvas)

  scene = javafx.scene.Scene(pane, _wndW, _wndH);
  stage.setScene(scene);
  stage.show();

def MOUSE_PRESSED(evnt):
  gc.moveTo(evnt.getX(), evnt.getY())

def MOUSE_DRAGGED(evnt):
  global colorPicker
  
  gc.lineTo( evnt.getX(), evnt.getY())
  gc.setStroke(colorPicker.getValue())
  gc.stroke()
  gc.setLineCap(javafx.scene.shape.StrokeLineCap.ROUND)
  gc.setLineJoin(javafx.scene.shape.StrokeLineJoin.ROUND)

  blur = javafx.scene.effect.BoxBlur()
  blur.setWidth(10)
  blur.setHeight(1)
  blur.setIterations(1)
  gc.setEffect(blur)

Output:

1 Like

My guess is that py5.get_surface().get_native() would return a JavaFX object of some kind that could somehow be embedded in a JavaFX window? It would be neat if you could use py5 drawing commands to draw something that is inside a JavaFX window that also has those controls on it.

It would be neat if you could use py5 drawing commands to draw something that is inside a JavaFX window that also has those controls on it.

To the best of my knowledge it is not possible to have JavaFX controls in a window which supports standard py5 drawing commands. A py5 window with a FX2D renderer returns PSurfaceFX$ResizableCanvas@703580bf for ‘get_surface().get_native()’, not a JavaFX object. You can draw into the FX2D renderer window using standard Processing primitives (circle, rect, triangle, etc) in the draw() loop as we are accustomed to, but it will not accept JavaFX controls without using a stage, scene, and pane (which creates another window). If you used a P2D renderer you could mix cp5 controls with standard drawing but the graphics is not quite as sharp as those with a FX2D renderer. The only way to have higher resolution graphics with snazzier controls is to use FX2D with JavaFX all the way. JavaFX has its own set of graphic shapes, but of course it’s more code. Examples are shown below.

Py5 window with FX2D renderer:

# Uses Imported mode for py5

_wndW = 450
_wndH = 350

def setup():
  size(_wndW, _wndH, FX2D)
  window_title("Sketch Drawing")
  print(get_surface().get_native())

def draw():
  fill(0)
  text_size(32)
  text("FX2D Renderer Text",80,60)
  fill(255,0,0)
  circle(100,150,100)
  fill(0,0,255)
  triangle(220,100,170,200,270,200)
  fill(0,255,0)
  rect(300,100,100,100)

Output:

Py5 window with P2D renderer and cp5 controls:

# Uses Imported mode for py5
from controlP5 import *

_wndW = 450
_wndH = 350

def setup():
  global btn1
  
  size(_wndW, _wndH, P2D)
  window_title("Sketch Drawing")
  print(get_surface().get_native())
  cp5 = ControlP5(get_current_sketch())
  btn1 = Button(cp5,"Hello") 

def draw():
  fill(0)
  text_size(32)
  text("P2D Renderer Text",80,60)
  fill(255,0,0)
  circle(100,150,100)
  fill(0,0,255)
  triangle(220,100,170,200,270,200)
  fill(0,255,0)
  rect(300,100,100,100)
  
def mouse_pressed(e):
  if(btn1.isPressed()):
    print("btn1 was pressed.")

Output:

Py5 window with JavaFX and FX2D renderer:

# Uses Imported mode for py5
import javafx.stage.Stage;

_wndW = 600
_wndH = 300

def myBtnAction(event):
    print("btn hit.")
    
def settings():
  size(1,1,FX2D)
  print(get_surface())
  
def setup():
  stage = javafx.stage.Stage()
  print("stage =",stage)
  stage.setTitle("JavaFX Graphics Demo")
#   stage.setResizable:true  
  pane = javafx.scene.layout.Pane()

# Button
  btn = javafx.scene.control.Button("Button")
  btn.setLayoutX(90)
  btn.setLayoutY(10)
  btn.setOnAction(myBtnAction)
  pane.getChildren().add(btn)
  
# Text
  txt = javafx.scene.text.Text(130, 80, "Welcome to JavaFX!")
  txt.setFont(javafx.scene.text.Font(35))
  pane.getChildren().add(txt)
  
# Rectangle
  r = javafx.scene.shape.Rectangle(50, 130, 100, 50)
  r.setStroke(javafx.scene.paint.Color.color(0.0, 1.0, 0.0, 0.95))
  r.setStrokeWidth(5)
  r.setArcWidth(15)
  r.setArcHeight(15)
  pane.getChildren().add(r);
  
# Circle
  myCircle = javafx.scene.shape.Circle(250, 160, 40)
  myCircle.setFill(javafx.scene.paint.Color.DARKRED)
  myCircle.setStrokeWidth(6)
  myCircle.setStroke(javafx.scene.paint.Color.DARKSLATEBLUE)
  pane.getChildren().add(myCircle)

# Triangle
  polygon = javafx.scene.shape.Polygon();
  polygon.getPoints().addAll([ 380.0, 100.0, 330.0, 200.0, 430.0, 200.0 ])
  polygon.setFill(javafx.scene.paint.Color.GREEN)
  pane.getChildren().add(polygon)
  
# Line
  ln = javafx.scene.shape.Line(450,150,550,150)
  ln.setStroke(javafx.scene.paint.Color.color(1.0, 0.0, 0.0, 1.0))
  ln.setStrokeWidth(10)
  pane.getChildren().add(ln)
  
  scene = javafx.scene.Scene(pane, _wndW, _wndH, javafx.scene.paint.Color.ALICEBLUE)
  stage.setScene(scene)
  stage.show()

Output:

If we rummage around in PSurfaceFX there is a ‘stage’ so the runtime is already using JavaFX for the FX2D renderer. It is possible to get a javafx control added to a pane container and to make the pane part of a scene, but it chokes when it comes time to set the scene on the stage and show it.

# https://github.com/processing/processing/blob/master/core/src/processing/javafx/PSurfaceFX.java
import javafx.stage.Stage

_wndW = 300
_wndH = 300

def setup():
    size(_wndW,_wndH,FX2D)
    frame = get_surface()
    print("frame =",frame)
    pane = javafx.scene.layout.Pane()
    btn = javafx.scene.control.Button("btn")
    btn.setLayoutX(100)
    btn.setLayoutY(30)
    pane.getChildren().add(btn)
    print("pane =",pane)
    print("btn =",pane.getChildren())
    canvas = get_surface().get_native()
    canvas.prefHeight(240)
    print("canvas =",canvas)
    pane.setVisible(True)
    scene = javafx.scene.Scene(pane)
    print("scene =",scene)
    stage = canvas.getStage()
    print("stage =",stage)
#     stage.setScene(scene)
#     stage.show()

def draw():
  circle(100,200,50) 

Error message:

Output: (attempt to put a JavaFX button in the window failed)

Addendum:
If we pause the thread right before stage.setScene() the button is shown but the circle is lost.
New Output:

A glimmer of hope! I bet it could be done by modifying the library’s PGraphicsFX class to use a replacement PSurfaceFX class that can better cooperate with these goals. That’s likely a hard thing to do though. But still possible.

Observation: Notice the difference in the background colors. My interpretation of this is that there are two potential canvases: one for drawing and one for javafx controls. One or the other winds up being used but not both at the same time. With Swing controls I have slid the default canvas down to create a vacant strip at the top and then added Swing components to the strip. I would like to use a similar technique here, but I’m not sure javafx controls can be added to the bare frame using frame.add(control). It seems that they can only go in a stage and that creates a whole new window.

Another Observation: this is what shuts down drawing
processing/core/src/processing/javafx/PSurfaceFX.java at master · processing/processing · GitHub line 88

        try {
          sketch.handleDraw();
        } catch (Throwable e) {
          // Let exception handler thread crash with our exception
          drawExceptionQueue.offer(e);
          // Stop animating right now so nothing runs afterwards
          // and crash frame can be for example traced by println()
          animation.stop();
          return;
        }

Interestingly the draw loop keeps right on going; if you add print(“x”) to draw() output shows up in the console.

I see. Processing’s JavaFX renderer has its own animation thread. Any attempt to commandeer the drawing surface and integrate it with controls would have to take over that also.

I wonder what would happen if we forked the editor, remmed out that line and recompiled it. Would be interesting to see the effect; there likely was some good reason why the author thought it was necessary to stop animation in the first place. The FX2D window appears to be chimeric; on one hand it can take regular Processing drawing commands and on the other hand it can be turned into a JavaFX window (stage).

Processing’s renderers are fascinating to dive into but this requires more time than I have right now. I did make a note to explore it further later, when I have more time.

I’m unable to find PSurfaceFX on current website: https://github.com/processing/processing4 ; I did several searches.

Thanks. I thought that the current editor was coming from the website that I listed, but I’ll give this one a try.

Addendum: I don’t see any way to fork this one.

Oh look, you can get two windows at one time:

# Uses Imported mode for py5
import javafx.stage.Stage;

_wndW = 600
_wndH = 250

def myBtnAction(event):
    print("btn hit.")
    
def settings():
  size(_wndW, _wndH, FX2D)
    
def setup():
 
  window_title("Default window")
  
  stage = javafx.stage.Stage()
  stage.setTitle("JavaFX Drawing Demo")
  pane = javafx.scene.layout.Pane()
  
# Button
  btn = javafx.scene.control.Button("Button")
  btn.setLayoutX(90)
  btn.setLayoutY(10)
  btn.setOnAction(myBtnAction)
  pane.getChildren().add(btn)
  
# Text
  txt = javafx.scene.text.Text(130, 80, "Welcome to JavaFX!")
  txt.setFont(javafx.scene.text.Font(35))
  pane.getChildren().add(txt)
  
# Rectangle
  r = javafx.scene.shape.Rectangle(50, 130, 100, 50)
  r.setStroke(javafx.scene.paint.Color.color(0.0, 1.0, 0.0, 0.95))
  r.setStrokeWidth(5)
  r.setArcWidth(15)
  r.setArcHeight(15)
  pane.getChildren().add(r);
  
# Circle
  myCircle = javafx.scene.shape.Circle(250, 160, 40)
  myCircle.setFill(javafx.scene.paint.Color.DARKRED)
  myCircle.setStrokeWidth(6)
  myCircle.setStroke(javafx.scene.paint.Color.DARKSLATEBLUE)
  pane.getChildren().add(myCircle)

# Triangle
  polygon = javafx.scene.shape.Polygon();
  polygon.getPoints().addAll([ 380.0, 100.0, 330.0, 200.0, 430.0, 200.0 ])
  polygon.setFill(javafx.scene.paint.Color.GREEN)
  pane.getChildren().add(polygon)
  
# Line
  ln = javafx.scene.shape.Line(450,150,550,150)
  ln.setStroke(javafx.scene.paint.Color.color(1.0, 0.0, 0.0, 1.0))
  ln.setStrokeWidth(10)
  pane.getChildren().add(ln)
  
  scene = javafx.scene.Scene(pane, _wndW, _wndH, javafx.scene.paint.Color.ALICEBLUE)
  stage.setScene(scene)
  stage.show()

def draw():
  fill(0)
  text_size(32)
  text("FX2D Renderer Text",80,60)
  fill(255,0,0)
  circle(100,150,100)
  fill(0,0,255)
  triangle(220,100,170,200,270,200)
  fill(0,255,0)
  rect(300,100,100,100)

Output:

It’s possible to have controls in the JavaFX window control drawing in the Default FX2D window using standard Processing drawing calls. Note that size() parameters control the size of the default window and scene() parameters control the size of the JavaFX window.

# Uses Imported mode for py5
import javafx.stage.Stage

_wndW = 700
_wndH = 600

global pane

def settings():
  size(_wndW, _wndH, FX2D)
  
def setup():
  global slider
  global colorPicker
  
  window_title("Processing Drawing")
  stage = javafx.stage.Stage()
  stage.setTitle("JavaFX Controls")
  pane = javafx.scene.layout.Pane()
  
  # Slider
  slider = javafx.scene.control.Slider(1, 500, 75)
  slider.setLayoutX(60)
  slider.setLayoutY(20)
  pane.getChildren().add(slider)
  
  # ColorPicker
  colorPicker = javafx.scene.control.ColorPicker()
  colorPicker.setLayoutX(70)
  colorPicker.setLayoutY(80)
  colorPicker.setValue(javafx.scene.paint.Color.GREEN)
  pane.getChildren().add(colorPicker)
  
  scene = javafx.scene.Scene(pane, 300, 200)
  stage.setScene(scene)
  stage.show()

def draw():
  global slider
  global colorPicker
  
  background(209)
  Color = colorPicker.getValue()
  fill(int(Color.getRed()*255),int(Color.getGreen()*255),int(Color.getBlue()*255))
  circle(_wndW/2,_wndH/2, float(slider.getValue()))

Output:

Right, this is a straightforward approach that is easy to understand.