I have a PackingCanvas that draws elements on the screen, which is an extension of the JavaFX Canvas control. I want my elements to be drawn on resize, but only when the resize is done, as redrawing the entire canvas on every resize event might be too costly in some situations.
What I have now is the following:
widthProperty().addListener(evt -> draw());
heightProperty().addListener(evt -> draw());
I have tried doing this with threads; add a small delay, see if new a new thread is created, if not; draw(), else abort current thread.
However, this doesn't seem like an elegant solution to me. Is there an idiom to deal with these problems?
you can cache width & height inside draw() function.
void draw()
{
cacheHeight = getHeight();
cacheWidth = getWidth();
}
then
heightProperty().addListener(evt -> {if(getHeight()!=cacheHeight) draw();});
Related
So I was experimenting with javaFX a bit since it is a part of one of my subjects.
I have a catapult that shoots a ball in certain direction. I was wondering if you could somehow change the color of a canvas background in a runtime on a certain occasion - when a ball hits the wall for example.
I already figured out how to make the ball bounce of the wall, but I cannot figure out how to change the bg color in runtime.
I'm using import javafx.scene.canvas.GraphicsContext; as it is what we kinda "have" to work with.
I thought something like this would work, I found some threads about sleep so I gave it a try.
public void flashCanvas() {
try
{
gc.setFill(Color.WHITESMOKE);
Thread.sleep(100);
gc.setFill(Color.BLACK);
Thread.sleep(100);
}
catch(InterruptedException ex)
{
Thread.currentThread().interrupt();
}
}
I thought that this would just change the color, then wait and over again.
Thanks for help!
Short Answer
You can make your desired behaviour using this part of code
Runnable collide = new Runnable() {
#Override
public void run() {
Platform.runLater(() ->
damageEffect()
);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Platform.runLater(() ->
resetEffect()
);
}
};
new Thread(collide).start();
Explanation
you can't use Thread.Sleep() on the main thread that will cause your UI to freeze so you need to make it on another thread and for more safety when changing anything in the UI/Main thread you should use Platform.runLater()
you can write whatever the effect in these function boomEffect() and resetEffect()
if you want to change the canvas fill color you can use the code provided by #Barudar in the comment like this
private void resetEffect() {
canvas.getGraphicsContext2D().setFill(Color.RED);
canvas.getGraphicsContext2D().fillRect(0,0,canvas.getWidth(),canvas.getHeight());
}
where fillRect() take startingX , startingY , endX , endY as a parameters
You're blocking the JavaFX application thread with Thread.sleep which prevents the change from being rendered. (In your code there actually is no change, since you're simply replacing the color used to draw, assuming the color is not used anywhere else, but this is what would happen, if you did draw something in addition to modifying the fill property.)
I recommend using one of the classes from the javafx.animation package to change this.
In this case a PauseTransition seems the simplest way to go, but if you want to include a update loop, I recommend going with AnimationTimer or Timeline instead and integrating this kind of logic into the update "loop". Using one of the latter possiblities you could simply save the last time the ball hit a wall and decide based on this info which color to use to draw the background.
Example using PauseTransition:
gc.setFill(Color.WHITESMOKE);
gc.fillRect(x, y, w, h);
// schedule update without blocking
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(evt -> {
gc.setFill(Color.BLACK);
gc.fillRect(x, y, w, h);
});
pause.play();
Note that you'll probably need to redraw more than just the background, since using fillRect replaces all the content currently in the rectangle.
So I want to add elements or items to a canvas through a GraphicsContext.
For example, to add a Rectangle, I don't want to use fillRect(...), I want to create a Rectangle rect = new Rectangle(...) and add it to my GraphicsContext,
So I can perform changes on that rect.
Something like this :
Rectangle rect = new Rectangle();
rect.setHeight(100);
rect.setWidth(100);
rect.setFill(Color.BLACK);
DragResizeMod.makeResizable(rect, null);
then added it to
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.add(rect); //somehow
There is a solution which is to use a Pane instead of Canvas but I don't think it the best way, since a GraphicsContext run on a single thread.
Thanks in advance.
Adding Rectangles to a properly configured Pane is the way to go. I don't understand what your problem with this approach is. This is the way JavaFX works and you will probably not be able to change this.
so i am dabbling in making a game and i am a fairly messy worker and then i go back and tidy it up. So please ignore any stupid naming conventions or methods or anything. As I go back later and tidy up.
So basically I am trying to make a main menu, i just want to draw something to the screen and have it stay there. I am writing the graphics to a image ATM and displaying the image as i tried just drawing to the canvas and it just flickered up onto the screen and then it went blank. I changed it to a runnable so i could put in sleep() to try and error hunt. I've removed all my error hunting code now. It turns out when i have a sleep() before or after the image is put on the screen for longer than 50ms the rectangle stays on screen and doesn't flicker away like 1ms after it was drawn.
I'm not sure what's going on. I am probably not that clear and I apologies.
import java.awt.Canvas;
import java.awt.Graphics;
import javax.swing.JFrame;
public class test extends Thread{
public static void main(String[] args){
test t = new test();
t.start();
}
#Override
public void run() {
JFrame f = new JFrame("Test");
Canvas c = new Canvas();
Graphics g;
c.setSize(400, 400);
f.add(c);
f.pack();
f.setVisible(true);
c.setVisible(true);
try {
sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
g = c.getGraphics();
g.drawRect(10, 10, 40, 68);
}
}
Basically, this is not how painting in AWT works. Painting should be done within the context of a component's paint method. In AWT Canvas seems to be the preferred base component to use.
You should never use getGraphics, apart from been able to return null, it will only represent the Graphics context from the last paint cycle, anything added to it could be wiped about by newer paint events...which is probably happening to you.
Take a look at Painting in AWT and Swing for details about how painting works.
Having said all that, I would discourage you from using AWT based API as it was replaced by Swing some 15 years ago.
Take a look at Performing Custom Painting for more details about painting in Swing
You're kind of getting the idea, but there are a few standard design patterns of creating a game, for starters check out this code.
Basically, you'll need something called a game loop which will update and render to the display a certain amount of times every second (framerate or fps). When it renders, it will clear and redraw the image in a new location at a certain interval, if your fps was 60 you would be rendering (clearing, and redrawing) 60 times a second. We can also introduce more advanced concepts such as buffer strategies (shown in the example above) to reduce tearing and flickering.
To sum it up, you're kind of there, you just need to constantly do that g = c.getGraphics() so...
while (true) {
g = c.getGraphics();
// set color
// draw a rectangle
g.dispose();
}
Note how I added the dispose, this will just free up any unused memory. Just to clarify, this is by no means good code, but it will give you a place to start :)
The flickering is due to only having 1 screen, to stop this flickering you need a buffer strategy. Basically having two buffers is like having two screens, whilst you render on one 'screen' you can clear and draw the next step on the second screen and switch between the two buffers; this reduces the flickering.
I have a piece of code designed to take a screenshot of a node in JavaFX:
public BufferedImage getSnapshot(final Node... hideNodes) {
Window window = getScene().getWindow();
Bounds b = localToScene(getBoundsInLocal());
int x = (int) Math.round(window.getX() + getScene().getX() + b.getMinX());
int y = (int) Math.round(window.getY() + getScene().getY() + b.getMinY());
int w = (int) Math.round(b.getWidth());
int h = (int) Math.round(b.getHeight());
try {
Robot robot = new Robot();
for(Node node : hideNodes) {
node.setOpacity(0);
node.getParent().requestLayout();
}
BufferedImage image = robot.createScreenCapture(new java.awt.Rectangle(x, y, w, h));
for(Node node : hideNodes) {
node.setOpacity(1);
node.getParent().requestLayout();
}
return image;
}
catch(AWTException ex) {
return null;
}
}
It has a twist, and that is it should hide the given nodes before taking the screenshot (in case they overlap with the node, which in some cases is definite.)
However, I'm stuck finding a way to force a redraw to include the opacity change before taking the screenshot - the only reference I found was to requestLayout(), but no joy there.
What method(s) should I call to force and wait for a redraw to complete?
I find your code quite strange:
Why use node.setOpacity(0) to make it invisible, rather than node.setVisible(false)?
Why return an AWT BufferedImage rather than a JavaFX Image?
Why use a robot to capture of the screen rather than taking a snapshot of the scene?
Why mix Swing and JavaFX and end up having to worry about rendering order?
Perhaps there are reasons for these things which I don't understand, but I'd just do it this way:
public Image takeSnapshot(Scene scene, final Node... hideNodes) {
for (Node node: hideNodes) node.setVisible(false);
Image image = scene.snapshot(null);
for (Node node: hideNodes) node.setVisible(true);
return image;
}
I created a small sample app which uses the above routine.
The primary window includes a group with a circle and a rectangle. When a snapshot command is issued, the rectangle is hidden in the primary, a snapshot of the primary is taken, then the rectangle is made visible in the primary again.
To answer your question's title about forcing a UI update - you can't really. The JavaFX application thread and JavaFX rendering thread are to be treated as two separate things. What you need to do is run your processing on the JavaFX application thread, seed control back to the JavaFX system, wait for it to do it's rendering, then examine the results. The scene.snapshot method will take care of that synchronization for you so you don't need to worry about it.
If, for whatever reason, scene.snapshot won't work for you and you wanted to maintain something similar to your original strategy, then what you would do is:
Issue some update commands (e.g. setting node opacity to 0) on the JavaFX application thread.
Issue a Platform.runLater call and take your robotic snapshot in the runLater body.
Once the snapshot has really been taken (notification in some awt callback), issue another Platform.runLater command to get back on the JavaFX application thread.
Back in the JavaFX application thread, issue some more update commands (e.g. setting node opacity back to 1).
This should work as it will allow the JavaFX system to perform another pulse which performs a rendering layout of the screen with the opacity changes before your robot actually takes the snapshot. An alternate mechanism is to use a JavaFX AnimationTimer which will provide you with a callback whenever a JavaFX pulse occurs. Maintaining proper synchronization of all of this between the AWT and JavaFX threads, would be annoying.
How can I draw something in JPanel that will stay the same and not be repainted, I am doing a traffic simulation program and I want the road to be drawn once because It will not change.
Thanks
I'm not sure you actually want your road to never be repainted - repaint events fire (for example) when your window is resized, or when it becomes visible following another window obstructing it. If your panel never repaints then it'll look peculiar.
As far as I remember, Swing will only fire appropriate paint events for these circumstances, so you should be OK following the usual method of subclassing JPanel with a suitable override:
public class RoadPanel extends JPanel {
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// your drawing code here
}
}
If you cache your road into an image or another graphics format (to save calculating the display data multiple times) once drawn once, this might save you some time on subsequent paints.
To my knowledge, no, unless there is a trick with transparent overlays.
Most graphical applications I saw (and did) just re-draw the whole panel on each repaint. Now, you can do that once, in a graphic buffer, and then just paint the whole background at once, quickly, by copying the graphic buffer to the JPanel. It should be faster than calling all graphical primitives to draw the road.
Or, the way some 2D games do, perhaps paint it once and update the moving parts, like sprites: it needs to erase the old place used by the sprites (restore the background there) and re-draw the sprites at the new place. So you still have a copy of the road in a graphic buffer but instead of re-drawing it whole each time, you update only some small parts. Can be slightly faster.
The component will need to be repainted every time that the panel is obscured (ie frame minimized/another window put on top). Therefore drawing something only once will not work as you want it to. To make parts that do not change be drawn more efficiently you can draw them once to a 'buffer' image, and then just draw this buffer each time that the panel or component needs to be redrawn.
// Field that stores the image so it is always accessible
private Image roadImage = null;
// ...
// ...
// Override paintComponent Method
public void paintComponent(Graphics g){
if (roadImage == null) {
// Create the road image if it doesn't exist
roadImage = createImage(width, height);
// draw the roads to the image
Graphics roadG = roadImage.getGraphics();
// Use roadG like you would any other graphics
// object to draw the roads to an image
} else {
// If the buffer image exists, you just need to draw it.
// Draw the road buffer image
g.drawImage(roadImage, 0, 0, null);
}
// Draw everything else ...
// g.draw...
}
What I do is set a boolean value to whether or not a certain part needs to be redrawn. Then, in the paintComponent() method I can check the value and redraw the certain thing, or not.
protected void paintComponent(Graphics g){
super.paintComponent(g);
if (drawRoad) {
drawRoadMethod(g);
}
drawTheRest(g);
}
Kinda like that.