Unable to Update Display in Swing - java

I'm sure I'm going about this all wrong but...
I have a 2D-Array of custom Tile objects that extend JComponent. They contain model information, as well as an override of the paintComponent() method. The tiles really only contain an Image, a String representing their type, and two boolean values indicating whether they contain a player and whether they are able to be walked upon. This Tile class is a superclass for all my Tile objects, which include NormalTile, ActionTile, and EmptyTile currently.
The 2D array itself is contained within a TilePanel class that extends JPanel, and this class has several methods for modifying this array of tiles.
At the top of this chain is the MapManager, which doesn't explicitly extend anything, and instead contains both an instance of TilePanel and JScrollPane, and the TilePanel is added to the JScrollPane.
When my TilePanel is first created, it initializes the 2D array with a series of NormalTiles, which easily display themselves with the test image they use. This all displays just perfectly.
However, immediately after the creation of the TilePanel, I call its addTileBlock(int width, int height, int x, int y, String type) method to change the tiles at a particular point in the 2D array. This is where the problem in updating the display arises.
The Tiles in the specified location do appear to change, according to the data I gather by printing them out, and their behavior when the player steps on them does work according to how their changed type would indicate, yet the change in display is never reflected. The new Tiles (in this case, I have used EmptyTiles) instead simply display the same image the previous NormalTiles used, despite their underlying objects having changed.
As it stands now, "updating" the tiles actually makes the old Tile references point to a new Tile instance, since my Tile objects themselves are immutable. Whenever the update is being done, the TilePanel class simply class an abstract class TileFactory to use its generateTile(String type) method to create the correct Tile instance. It simply uses a switch/case block to return the tiles.
It was suggested to me to attempt using the revalidate() method on the tile objects to fix the problem, which you can see where I've implemented it in the Tile class's constructor. I also previous attempted using it in its paintComponent method, but that did nothing as well.
Here is the addTileBlock method and Tile classes in full:
addTileBlock:
public void addTileBlock(int width, int height, int x, int y, String type)
{
for(int i = 0; i < width; i++)
for(int j = 0; j < height; j++)
{
tiles[i][j] = null;
tiles[i][j] = TileFactory.generateTile(type);
tiles[i][j].repaint();
}
}
Tile Class:
package games.tile.tiles;
import games.tile.Player;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.JComponent;
abstract public class Tile extends JComponent implements TileConstants
{
private String type;
private Image tileImage;
private boolean containsPlayer = false;
private boolean walkable;
public Tile(String type, int size, boolean walkable)
{
this.setSize(size, size);
this.type = type;
this.walkable = walkable;
this.setPreferredSize(new Dimension(this.getHeight(), this.getWidth()));
tileImage = null;
revalidate();
}
public Tile(String type, int size, boolean walkable, Image image)
{
this.setSize(size, size);
this.type = type;
this.walkable = walkable;
tileImage = image;
this.setPreferredSize(new Dimension(this.getHeight(), this.getWidth()));
this.revalidate();
}
public boolean getWalkable() { return walkable; }
public void setWalkable(boolean val) { walkable = val; }
public boolean containsPlayer() { return containsPlayer; }
public void setContainsPlayer(boolean val) { containsPlayer = val;}
public Image getImage() { return tileImage; }
public void setImage(Image image) { tileImage = image; }
public String getType() { return type; }
abstract public void applyEffect(Player player);
#Override
public void paintComponent(Graphics g)
{
if(type.equals(EMPTY) || tileImage == null)
{
g.setColor(Color.black);
g.fillRect(0, 0, 32, 32);
}
else
{
g.drawImage(tileImage, 0, 0, null);
}
if(containsPlayer)
{
g.drawImage(Player.PLAYER_IMAGE, 0, 0, null);
}
}
}
Can anyone inform me as to what I've probably done wrong?

It was suggested to me to attempt using the revalidate() method on the tile objects to fix the problem,
revalidate() is done when you add/remove a component from a panel. So the basic code is:
panel.add(...);
panel.revalidate();
panel.repaint();
This assumes you are using a proper layout manager. In your case I would guess a GridLayout.
As it stands now, "updating" the tiles actually makes the old Tile references point to a new Tile instance,
You can't just change the reference of a variable to point to a new Swing component. You need to actually add the component to the panel. So in your case because your are using a grid, I would guess you need to remove the component at the current point on the grid and then add another component at that point.
That is a lot of work. Since you say your components basically just contain an image, an easier approach is to probably create a changeImage(...) image and then invoke repaint() from within that method and the component will repaint itself and you don't need to worry about creating new components and adding them to the panel.

Related

Java: JFrame Graphics not drawing rectangle

Hello fellow programmers,
I've ran into a little issue in my code that I can't seem to crack. It has to do with the Jframe; Graphics area of Java. The code that I'll post below, is over a drawing method. Which purpose is to draw the "rooms" that are in a ArrayList roomList which is located in another class hence lvl. before. This off-course doesn't happen, hence the post on here.
public class LevelGUI implements Observer {
private Level lv;
private Display d;
public LevelGUI(Level level, String name) {
this.lv = level;
JFrame frame = new JFrame(name);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
d = new Display(lv, 500, 500);
frame.getContentPane().add(d);
frame.pack();
frame.setLocation(0, 0);
frame.setVisible(true);
}
private class Display extends JPanel {
public Display(Level fp, int x, int y) {
addKeyListener(new Listener());
setBackground(Color.GRAY);
setPreferredSize(new Dimension(x + 20, y + 20));
setFocusable(true);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
draw(g);
}
private void draw(Graphics g) {
Level lvl = new Level();
for(int i = 0; i < lvl.roomList.size(); i++) {
Room room = lvl.roomList.get(i);
g.setColor(room.floorColor);
g.drawRect(room.posX, room.posY, room.roomWidth, room.roomHeight);
}
}
}
}
To get some background info on the program. roomList is the ArrayList, and it is filled with various different sized and colored rooms. The rooms themselves are objects.
Here comes first Level class:
public class Level extends Observable {
private boolean Switch = true;
public ArrayList<Room> roomList = new ArrayList<Room>();
(...)
}
Here is the Class Room() that is used to create the rooms.
public class Room {
Color floorColor;
int roomWidth;
int roomHeight;
int posX;
int posY;
public Room(int dx, int dy, Color color) {
this.floorColor = color;
this.roomHeight = dy;
this.roomWidth = dx;
this.posY = 0;
this.posX = 0;
}
(...)
}
I've managed to locate where the problem is thought to occur, and it's the code in the for-loop. I tried switching the roomList.size() for an integer to test if it was the loop., But it wasn't. It is possible to draw a figure outside of the for-loop.
and again, the problem isn't an error message, the program simply doesn't draw the rooms that I've instructed it to draw in the method draw().
The display output looks like this:
Thanks beforehand!
Be aware that the paintComponent() method is invoked by Swing whenever the framework thinks the component needs to be rendered on screen. This usually is when the window is getting visible - initially or because some other window no longer hides the component. Such events are out of your control.
So your application should create a state and be ready to draw it anytime. Therefore you do not create state (like a level) inside the paint() or paintComponent() method. Put that elsewhere - if need be into the constructor.
Looking at you code:
As you are creating a new level inside paintComponent()/draw(), is it correct to assume that this level has no rooms associated? In that case the method is right to return without having painted anything.
If your application thinks the screen should be updated call repaint(), knowing that the paint() method will be called by the framework soon.

Java Drawing Multiple Squares in Same JFrame

I am trying to make an animation with multiple thread. I want to paint n squares where this n comes from commend-line argument. Every square has their x-y coordinates, colors and speed. They are moving to the right of the frame with different speed, color and coordinates. Since I am using multi thread I assume I have to control each squares. So I have to store each square object in the ArrayList. However, I am having trouble with painting those squares. I can paint one square but when I try to paint multiple squares, it does not show. Here what I have done so far:
DrawSquare.java
import java.awt.Graphics;
import javax.swing.JPanel;
public class DrawSquare extends JPanel {
public Square square;
public DrawSquare() {
square = new Square();
}
#Override
public void paintComponents(Graphics g) {
// TODO Auto-generated method stub
super.paintComponents(g);
}
#Override
public void paint(Graphics g) {
// TODO Auto-generated method stub
super.paint(g);
g.setColor(square.getC());
g.fillRect(square.getX(), square.getY(), square.getR(), square.getR());
}
}
Square.java
import java.awt.Color;
import java.util.Random;
public class Square {
private int x,y,r,s;
private Color c;
private Random random;
public Square() {
random = new Random();
x = random.nextInt(100) + 30;
y = random.nextInt(100) + 30;
r = random.nextInt(50) + 20;
s = random.nextInt(20) + 5;
c = new Color(random.nextInt(255),random.nextInt(255),random.nextInt(255));
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getR() {
return r;
}
public int getS() {
return s;
}
public Color getC() {
return c;
}
}
Animation.java
import java.awt.BorderLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Animation extends JFrame implements Runnable {
private JPanel panel;
private DrawSquare square;
public Animation() {
}
public static void main(String[] args) {
Animation w = new Animation();
DrawSquare square = new DrawSquare();
JFrame f = new JFrame("Week 9");
int n = Integer.parseInt(args[0]);
f.setVisible(true);
f.setSize(700,700);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setResizable(false);
for(int i=0; i<n; i++) {
f.getContentPane().add(square);
}
}
#Override
public void run() {
// TODO Auto-generated method stub
}
}
So, starting with...
public class DrawSquare extends JPanel {
public Square square;
public DrawSquare() {
square = new Square();
}
#Override
public void paintComponents(Graphics g) {
// TODO Auto-generated method stub
super.paintComponents(g);
}
#Override
public void paint(Graphics g) {
// TODO Auto-generated method stub
super.paint(g);
g.setColor(square.getC());
g.fillRect(square.getX(), square.getY(), square.getR(), square.getR());
}
}
As general recommendation, it's preferred to put custom painting in the paintComponent method (note, there's no s at the end)
When paint is called, the Graphics context has already been translated to the component coordinate position. This means that 0x0 is the top/left corner of the component, this also means that...
g.fillRect(square.getX(), square.getY(), square.getR(), square.getR());
is painting the rect at x + x x y + y, which will, at the very least, paint the rect in the wrong position, at worst paint it beyond the visible scope of the component.
You're also not providing any sizing hints for the component, so it's default size will be 0x0, which prevent it from been painted.
Since I am using multi thread I assume I have to control each squares.
Well, since I can't really see what's driving the animation, I imagine that when you say "multi thread" you're suggesting that each square has it's own Thread. In this case, that's a bad idea. Let's put aside the thread synchronisation issues for a moment, more threads doesn't equate to more work you can do, at some point, it will begin to degrade the system performance.
In most cases, a single, well managed thread, is all you really need. You also have to understand that Swing is NOT thread safe. This means that you shouldn't update the UI (or states that the UI relies on) from outside the context of the Event Dispatching Thread.
So, while you're thread can update the position of the rects, you need to take care to ensure that they are not been painted while they are been update. Once you've updated the state, you then need to trigger a paint pass (which is trivial in of itself)
So I have to store each square object in the ArrayList.
Yep, good start
However, I am having trouble with painting those squares. I can paint one square but when I try to paint multiple squares, it does not show.
Okay, so instead of using multiple components, use one. Run through your ArrayList within the paintComponent method of this component and paint all the rects to it. This provides a much simpler way to manage things like bounds detection, as you have only one container to worry about.
I'd highly recommend you have a look at:
Java Bouncing Ball which demonstrates many of the concepts discussed here
Concurrency in Swing
How to use Swing Timers
Performing Custom Painting
Painting in AWT and Swing

How to access/change a variable in the initialization? JAVA

Hello fellow programmers!
So to be honest here, i'm not sure if the title question is correct, and you will see why.
Before i explain what i do, and why, here is the code snippet:
JPanel playerPanel = new JPanel() {
public void paint(Graphics g) {
X = 1;
Y = 1;
g.drawImage(player.getScaledInstance(player.getHeight()/2, player.getWidth()/2, Image.SCALE_DEFAULT), X, Y, null);
}
};
So this a snippet from a custom class i made, and my question would be that, you see there is an X and Y variable, i can change their values , but that changes nothing on the impact of the actual program, my first question would be that can i change the X, and Y of this JPanel's image, and if so , how can i "refresh" the actual JPanel/Image so that it looks like it moved?
Some notes:
-the X, Y are global variables
-playerPanel is inside a procedure, and a global variable
-i can access X, Y since they are global variables from outside the class
I'm having a hard time actually writing down my problem... Hopefully you understand what i would like to accomplish.
You're main problem: Don't use an anonymous inner class if you want to give the class new mutable fields. Instead, create a separate class, it can be an inner class, but it can't be anonymous, give it fields that are needed with getters and setters. Also all that Luxx recommends is correct -- override paintCompoent, call the super method, don't declare the fields within a method...
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.JPanel;
public class PlayerDrawingPanel extends JPanel {
private int playerX;
private int playerY;
private Player player;
public PlayerDrawingPanel(int playerX, int playerY, Player player) {
this.playerX = playerX;
this.playerY = playerY;
this.player = player;
}
public void setPlayerX(int playerX) {
this.playerX = playerX;
repaint();
}
public void setPlayerY(int playerY) {
this.playerY = playerY;
repaint();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(player.getImage(), playerX, playerY, this);
}
}
There is no need to create global variables.
You can use setBounds(x, y, w, h) from Swing's JComponent to move and resize the JPanel.
Though, you have to keep in mind that a Component cannot draw outside its borders. Meaning that the Graphics object that is passed into paint(Graphics g) comes clipped and translated to fit the Component from it's parent.
So, to solve your case, you can either make your JPanel take over the whole area in which you want to draw by using setBounds() or you can you the LayeredLayout from your root panel to draw anywhere.
Let me exemplify the last solution. Consider frame to be your JFrame and playerPanel the JPanel that you overwrote the paint() method:
frame.setGlassPane(playerPanel);
frame.getGlassPane().setVisible(true);
Now your playerPanel is at the topmost layer of your application, covering the whole area. This means you can draw anywhere over anything.

How can I add a long text to my fillRect?

I'm working on a program that add pins to a map, the pins are are subclasses to a class that draws a triangle to the map and are clickable, and if you click it shall unfold and show different things like name, text or a picture.
I've one working subclass that creates a rectangle out of the triangle and shows what the name of the place is. For this I used the drawString. But now, to my second subclass, it shall show an description over the place, and the description could be quite long and for this I can't use the drawString, because it only shows on one row, and it will clip my text..
I tried to add the description to a JTextArea, and add that one to a JScrollPane and then I tried to add the scrollpane to the rect area, but that didn't seem to work, because "The method add(JScrollPane) is undefined for the type Graphics"
Here is my super class:
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
abstract public class Place extends JComponent {
private String name;
private int x,y;
boolean highlighted = false;
boolean hidden = false;
boolean showed = false;
public Place(int x, int y, String name){
setBounds(x,y,30,30);
this.name=name;
this.x=x-15;
this.y=y-30;
Dimension d = new Dimension(30,30);
setPreferredSize(d);
setMaximumSize(d);
setMinimumSize(d);
addMouseListener(new MouseLis());
}
abstract protected void show(Graphics g);
protected void paintComponent(Graphics g){
super.paintComponent(g);
// g.setColor(Color.BLACK);
if(!showed){
setBounds(x,y,30,30);
int[] xes = {0,15,30};
int[] yes = {0,30,0};
g.fillPolygon(xes, yes, 3);
} else {
show(g);
}
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public String getName() {
return name;
}
class MouseLis extends MouseAdapter{
#Override
public void mouseClicked(MouseEvent mev){
showed = ! showed;
repaint();
}
}
}
and here is my subclass that doesn't work..
class DescPlace extends Place{
private String Description;
private JTextArea desc = new JTextArea(Description);
public DescPlace(int x, int y, String name, String descr){
super(x,y,name);
this.Description = descr;
}
protected void show(Graphics g){
setBounds(getX(), getY(),150,200);
g.setColor(Color.YELLOW);
g.fillRect(0, 0, 150, 200);
//g.add(new JScrollPane(desc));
}
}
You can use the same JTextArea and just paint it using Graphics instance
desc.setSize(width, height); //define size
desc.paintAll(g); //paint
You can use a JLabel to do this, using it to display HTML-formatted content.
From Oracle's docs:
If you want to mix fonts or colors within the text, or if you want formatting such as multiple lines, you can use HTML. HTML formatting can be used in all Swing buttons, menu items, labels, tool tips, and tabbed panes, as well as in components such as trees and tables that use labels to render text.
Source: https://docs.oracle.com/javase/tutorial/uiswing/components/html.html
EDIT
No time to write a thousand words, so here's an example:
new JLabel("<html><p>This will</p><br /><p>appear over multiple</p><br /><p>lines</p></html>")
The same applies to JToolTip if you go down that route.

Resize an image to fit the entirety of a JLabel adapting to its size

before you ask, I've looked up this issue in the website and the solutions provided have, unfortunately, not worked for me, so I must resort to asking it once more to see what could I be doing wrong.
Closest achievement I've had with the code I've got is this (I should most definitely use a try and catch when I retrieve the image, I'll save that for later on):
private void asignarTile(Tile tile, JPanel panel){
if(tile.getTipo() == 0){
ImageIcon ii = new ImageIcon("pasto.png");
Image image = ii.getImage();
Image newimg = image.getScaledInstance(32, 32, java.awt.Image.SCALE_SMOOTH);
ii = new ImageIcon(newimg);
tile.setIcon(ii);
panel.add(tile);
}
}
Now, as oblivious as it is, I must mention that the code does work for a specific size, but it won't adapt the size of the image to the JLabel afterwards, that means that first I'll have this:
But after I resize it I'll have this:
I think it would be useful to note that Tile extends JLabel, and these are the changes (columna means column, fila means row, maybe I should start writing my code entirely in english)
package gui;
import javax.swing.JLabel;
public class Tile extends JLabel{
private int fila, columna, tipo;
public int getFila() {
return fila;
}
public void setFila(int x) {
this.fila = x;
}
public int getColumna() {
return columna;
}
public void setColumna(int y) {
this.columna = y;
}
public int getTipo(){
return tipo;
}
public void setTipo(int tipo){
if(tipo >= 0 || tipo <= 6)
this.tipo = tipo;
}
public Tile(int x, int y, int tipo) {
this.setFila(x);
this.setColumna(y);
this.setTipo(tipo);
}
}
As a conclusion I must say that I have considered adding a componentListener to the Tile since it extends a JLabel, but I have also tried to resize the image to the label's dimensions to no avail, as it gave me an exception saying that its dimensions were 0, and well, they can't be 0.
Thank you for reading!
You can try Darryl's Stretch Icon for dynamic resizing.
Otherwise you would extend JComponent (instead of JLabel) and do you own custom painting of the image. See Background Panel for an example of how to paint a scaled image.
I would recommend that you try to create or find a larger tile image. If you try to stretch the image, you will degrade it, but this is not so for shrinking.
This code looks good.
Here's a code review suggestion:
The serializable class Tile.java does not declare a static final serialVersionUID field of type long. Solution: Add a generated serial version ID to the selected type.

Categories