3

I am actually working on an ScalaFX application without window decorations. What I want to do is making it draggable, so that a user can move it anyhow.

I managed moving the stage by simply writing the coordinates of the dragging mouse into the stage X/Y coordinates. However, that resulted in a laggy and flickering window.

How can dragging a stage be implemented smoothly in ScalaFX?

smoes
  • 591
  • 2
  • 17
  • I answered with an example that works for me, but it would help if you posted a code sample of what you already tried to do. – Cyäegha Nov 02 '14 at 12:25

2 Answers2

2

Here's an example that works well for me. It's adapted from one example in the book "JavaFX 8 Introduction by Example". How does it compare to your attempt?

import scalafx.Includes._
import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import scalafx.geometry.Point2D
import scalafx.scene.Scene
import scalafx.scene.input.MouseEvent
import scalafx.scene.paint.Color
import scalafx.stage.{WindowEvent, StageStyle}

object DraggableApp extends JFXApp {

  private var anchorPt: Point2D = null
  private var previousLocation: Point2D = null

  stage = new PrimaryStage {
    initStyle(StageStyle.TRANSPARENT)
    scene = new Scene {
      fill = Color.rgb(0, 0, 0, 1.0)
    }
  }

  // Initialize stage to be movable via mouse
  initMovablePlayer()

  /**
   * Initialize the stage to allow the mouse cursor to move the application
   * using dragging.
   */
  private def initMovablePlayer(): Unit = {
    val scene = stage.getScene

    scene.onMousePressed = (event: MouseEvent) => anchorPt = new Point2D(event.screenX, event.screenY)

    scene.onMouseDragged = (event: MouseEvent) =>
      if (anchorPt != null && previousLocation != null) {
        stage.x = previousLocation.x + event.screenX - anchorPt.x
        stage.y = previousLocation.y + event.screenY - anchorPt.y
      }

    scene.onMouseReleased = (event: MouseEvent) => previousLocation = new Point2D(stage.getX, stage.getY)

    stage.onShown = (event: WindowEvent) => previousLocation = new Point2D(stage.getX, stage.getY)
  }
}

Edit: About you question on stage resizing, I tried the following variant, which resizes the stage when right-clicking and dragging; I don't see any flickering (on OS X).

import javafx.scene.{input => jfxsi}
// (...)

object DraggableApp extends JFXApp {

  private var previousHeight = 0.0
  private var previousWidth = 0.0
  // (...)

  private def initMovablePlayer(): Unit = {
    val scene = stage.getScene

    scene.onMousePressed = (event: MouseEvent) => anchorPt = new Point2D(event.screenX, event.screenY)

    scene.onMouseDragged = (event: MouseEvent) =>
      if (anchorPt != null && previousLocation != null) {
        if (event.getButton == jfxsi.MouseButton.PRIMARY) {
          stage.x = previousLocation.x + event.screenX - anchorPt.x
          stage.y = previousLocation.y + event.screenY - anchorPt.y
        } else if (event.getButton == jfxsi.MouseButton.SECONDARY) {
          stage.width = previousWidth + event.screenX - anchorPt.x
          stage.height = previousHeight + event.screenY - anchorPt.y
        }
      }

    scene.onMouseReleased = (_: MouseEvent) => reset()

    stage.onShown = (_: WindowEvent) => reset()

    def reset (): Unit = {
      previousLocation = new Point2D(stage.getX, stage.getY)
      previousHeight = stage.getHeight
      previousWidth = stage.getWidth
    }
  }
}
Cyäegha
  • 4,191
  • 2
  • 20
  • 36
  • First, thank you for you answer. I am not sure why it did not work for me before, maybe my laptop was under stress and lagged like hell? Your solution works. However, your solution also works when removing the previousLocation and anchorPt, by just assigning the event.screenX/Y in the onMouseDragged handler to stage.x,y (essentially what I had before I asked this question Oo). Why do you need previousLocation and achorPt? – smoes Nov 02 '14 at 18:33
  • Just an offtopic question, if you worked with undecorated windows before: Do you know how to the rid of the flickering when resizing an undecorated stage (especially on bigger stages sizes, like maxing a window)? – smoes Nov 02 '14 at 18:35
  • 1
    With `stage.x = event.screenX; stage.y = event.screenY`, the top left point of the stage jumps to the mouse position when you start dragging. The effect is more obvious if you have a big stage and you click near the bottom right corner before starting to drag. The idea here is to apply to the stage the same movement that was applied to the mouse cursor, but the starting point isn't the same (the top left corner of the stage in its initial position vs. the point where you clicked). I'll have a look at the resizing issue, but I can't make any promise, not having a lot of experience with this. – Cyäegha Nov 02 '14 at 21:48
  • 1
    I added an example of what I did to add resizing (using the right mouse button for resizing the stage, left button for dragging the stage around), and I don't see any flickering. Do you have flickering with the same code on your computer? With different code? With similar code but a different/more complex stage? – Cyäegha Nov 02 '14 at 22:07
  • Thank you for your help. I cannot reproduce the behavior so I guess it is reasoned by another part of my program. I will repost my other question if I have small example code which reproduces the problem :) – smoes Nov 03 '14 at 21:44
0

I know that this has been answered already, but for the sake of completeness I would like to include the way I have done this in the past.

Credits:
Draggable window: Pro JavaFX 8 by example
Resizable Window: Little Child - Allow user to resize an undecorated Stage
Restore up/down: Undecorator - UndecoratorController.java

Be prepared for the wall of (messy) code

class UndecoratedWindowHelper(val target: Stage) {

   var anchorPt: Point2D = null
   var prevLoc: Point2D = null
   private var savedBounds: BoundingBox = _
   private val _maximized: ReadOnlyBooleanWrapper = new ReadOnlyBooleanWrapper {value = false}

   // begin init
   val resizeListener = new ResizeListener(target, this)
   target.scene.get.addEventHandler(jfxme.MOUSE_MOVED, resizeListener)
   target.scene.get.addEventHandler(jfxme.MOUSE_PRESSED, resizeListener)
   target.scene.get.addEventHandler(jfxme.MOUSE_DRAGGED, resizeListener)
   // end init

   def addWindowDragPoint(dragNode: Node): Unit = {
      dragNode.onMousePressed = (event: MouseEvent) => {
         anchorPt = new Point2D(event.screenX, event.screenY)
         prevLoc = new Point2D(target.getX, target.getY)
      }

      dragNode.onMouseClicked = (event: MouseEvent) =>
         if (event.clickCount == 2)
            maximizeOrRestore()

      dragNode.onMouseDragged = (event: MouseEvent) =>
         if (resizeListener.cursorEvent == Cursor.DEFAULT && !maximized) {
            target.x = prevLoc.x + event.screenX - anchorPt.x
            target.y = prevLoc.y + event.screenY - anchorPt.y
         }

      target.onShown = (event: WindowEvent) => prevLoc = new Point2D(target.getX, target.getY)
   }


   def maximizeOrRestore() {
      if (maximized) {
         restoreSavedBounds()
         savedBounds = null
         _maximized set false
      } else {
         val screensForRectangle = Screen.screensForRectangle(target.getX, target.getY, target.getWidth, target.getHeight)
         val screen = screensForRectangle.get(0)
         val visualBounds = screen.visualBounds
         savedBounds = new BoundingBox(target.getX, target.getY, target.getWidth, target.getHeight)

         target.setX(visualBounds.getMinX)
         target.setY(visualBounds.getMinY)
         target.setWidth(visualBounds.getWidth)
         target.setHeight(visualBounds.getHeight)
         _maximized set true
      }
   }
   def saveBounds() {
      savedBounds = new BoundingBox(target.getX, target.getY, target.getWidth, target.getHeight)
   }

   def restoreSavedBounds() {
      target.setX(savedBounds.getMinX)
      target.setY(savedBounds.getMinY)
      target.setWidth(savedBounds.getWidth)
      target.setHeight(savedBounds.getHeight)
      savedBounds = null
   }

   def maximized = _maximized.getValue
   def maximizedProp = _maximized.getReadOnlyProperty
}

//formatter:off
protected class ResizeListener(
                                  val stage: Stage, owner: UndecoratedWindowHelper) extends EventHandler[jfxme] {

   var cursorEvent = Cursor.DEFAULT
   val border = 4
   var startX = 0d
   var startY = 0d
   //formatter:on

   def handle(mouseEvent: jfxme) {
      val mouseEventType = mouseEvent.getEventType
      val scene = stage.getScene

      val mouseEventX = mouseEvent.getSceneX
      val mouseEventY = mouseEvent.getSceneY
      val sceneWidth = scene.getWidth
      val sceneHeight = scene.getHeight

      if (jfxme.MOUSE_MOVED == mouseEventType) {

         //@formatter:off
         cursorEvent =
               if (mouseEventX < border && mouseEventY < border) Cursor.NW_RESIZE
               else if (mouseEventX < border && mouseEventY > sceneHeight - border) Cursor.SW_RESIZE
               else if (mouseEventX > sceneWidth - border && mouseEventY < border) Cursor.NE_RESIZE
               else if (mouseEventX > sceneWidth - border && mouseEventY > sceneHeight - border) Cursor.SE_RESIZE
               else if (mouseEventX < border) Cursor.W_RESIZE
               else if (mouseEventX > sceneWidth - border) Cursor.E_RESIZE
               else if (mouseEventY < border) Cursor.N_RESIZE
               else if (mouseEventY > sceneHeight - border) Cursor.S_RESIZE
               else Cursor.DEFAULT
         //@formatter:on
         if (owner.maximized)
            cursorEvent = Cursor.DEFAULT


         scene.setCursor(cursorEvent)
      } else if (mouseEventType == jfxme.MOUSE_PRESSED) {

         startX = stage.getWidth - mouseEventX
         startY = stage.getHeight - mouseEventY

      } else if (mouseEventType == jfxme.MOUSE_DRAGGED) {
         if (Cursor.DEFAULT != cursorEvent) {


            if (Cursor.W_RESIZE != cursorEvent && Cursor.E_RESIZE != cursorEvent) {// not west or east

               val minHeight = Math.max(stage.getMinHeight, border * 2)


               if (Cursor.NW_RESIZE == cursorEvent || Cursor.N_RESIZE == cursorEvent || Cursor.NE_RESIZE == cursorEvent) {// NW or N or NE

                  val attemptedSize = stage.getY - mouseEvent.getScreenY + stage.getHeight
                  val actSize = Math.max(minHeight, attemptedSize)
                  val diff = actSize - attemptedSize

                  stage.setHeight(actSize)
                  stage.setY(mouseEvent.getScreenY - diff)

               } else {// SW or S or SE

                  val attemptedSize = mouseEventY + startY
                  val actSize = Math.max(minHeight, attemptedSize)

                  stage.setHeight(actSize)
               }
            }

            if (Cursor.N_RESIZE != cursorEvent && Cursor.S_RESIZE != cursorEvent) {

               val minWidth = Math.max(stage.getMinWidth, border * 2)

               if (Cursor.NW_RESIZE == cursorEvent || Cursor.W_RESIZE == cursorEvent || Cursor.SW_RESIZE == cursorEvent) {
                  val attemptedSize = stage.getX - mouseEvent.getScreenX + stage.getWidth
                  val actSize = Math.max(minWidth, attemptedSize)
                  val diff = actSize - attemptedSize

                  stage.setWidth(actSize)
                  stage.setX(mouseEvent.getScreenX - diff)

               } else {
                  val attemptedSize = mouseEventX + startX
                  val actSize = Math.max(minWidth, attemptedSize)

                  stage.setWidth(actSize)

               }
            }
            owner.saveBounds()
         }

      }
   }
}
Community
  • 1
  • 1
J Atkin
  • 3,080
  • 2
  • 19
  • 33