coordinate system of JComponent relative to JPanel - java

println in the paintComponent prints out 497 971. in the view of JPanel, the red line's left-up point is supposed to be somewhere near JPanel's middle according that number pair, but in fact it's not. Is it caused by coordinate system conversion?
Thanks in advance.
Code shows below:
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import javax.imageio.ImageIO;
import javax.swing.*;
public class ClockFrame extends JFrame {
JPanel panel;
public ClockFrame(){
panel = new JPanel();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
add(panel);
setSize(1000, 1000);
panel.setSize(getWidth(), getHeight());
//panel.setLayout(null);//!important
panel.setLayout(new GridLayout());
setVisible(true);
setResizable(false);
panel.setBackground(Color.BLACK);
Hand sHand=new Hand(panel);
panel.add(sHand);
}
class Hand extends JComponent{
private Timer timer;
public Hand(Object o){
setLocation(500,500);
((JPanel)o).add(this);
timer = new Timer(800, new ActionListener() {
public void actionPerformed(ActionEvent e) {
repaint();
}
});
timer.start();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);System.out.println(panel.getWidth()/2 +" "+panel.getHeight());
g2d.drawLine(panel.getWidth()/2, panel.getHeight()/2, 30, 30);
g2d.dispose();
}
public Dimension getPreferredSize() {
return new Dimension(600, 600);
}
}
public static void main(String[] a) {
ClockFrame c=new ClockFrame();
}
}

g2d.drawLine(0, 0, 100, 50);
g2d.setColor(Color.WHITE); // no good setting the color now!
Should be:
g2d.setColor(Color.WHITE); // correct
g2d.drawLine(0, 0, 100, 50);
Other tips:
Better to pack() the frame after all components are added. Then it will be the exact right size to display them. Setting a top-level container visible should be the last thing in the constructor (in the vast majority of cases).
setSize(1000, 1000);
The location of the component is best determined by the layout manager, padding and borders.
public Hand(Object o){
setLocation(500,500);
If the component needs to be added to a container, better to pass it as a container. Having said that, probably best not to pass the container in the constructor at all, but instead to add(..) it in the code immediately after where it is instantiated.
public Hand(Object o){
// ..
((JPanel)o).add(this);
Some of those concepts implemented in the code below. Be sure to run it to see the effect.
Note especially:
/* Note that this is translating further drawing operations
to the middle of the Hand container based on its preferred size,
which is (layout manager not withstanding) also its actual size.
All co-ordinates in custom painting are relative to the component
being painted. */
g2d.translate(middle, middle);
Code
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Line2D;
import java.util.Calendar;
import java.util.Date;
import javax.swing.*;
public class ClockFrame extends JFrame {
public ClockFrame() {
JPanel panel = new JPanel();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
add(panel);
panel.setSize(getWidth(), getHeight());
//panel.setLayout(null);//!important
panel.setLayout(new GridLayout());
setResizable(false);
panel.setBackground(Color.BLACK);
Hand sHand = new Hand(panel);
panel.add(sHand);
pack();
setVisible(true);
}
class Hand extends JComponent {
private Timer timer;
private Dimension preferredSize = new Dimension(600, 600);
private Calendar currentTime;
public Hand(Object o) {
setLocation(500, 500);
((JPanel) o).add(this);
currentTime = Calendar.getInstance();
timer = new Timer(50, new ActionListener() {
public void actionPerformed(ActionEvent e) {
currentTime.setTime(new Date(System.currentTimeMillis()));
repaint();
}
});
timer.start();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
System.out.println("x: " + this.getX() + " y: " + this.getY() + " w: " + this.getWidth() + " h: " + this.getHeight());
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.WHITE);
double angle = (currentTime.get(Calendar.SECOND)*2*Math.PI)/60d;
double middle = preferredSize.getWidth() / 2d;
/* Note that this is translating further drawing operations
to the middle of the Hand container based on its preferred size,
which is (layout manager not withstanding) also its actual size.
All co-ordinates in custom painting are relative to the component
being painted. */
g2d.translate(middle, middle);
Line2D.Double secondHand = new Line2D.Double(0, 0,
middle*.9*Math.cos(angle),
middle*.9*Math.sin(angle));
g2d.draw(secondHand);
g2d.dispose();
}
public Dimension getPreferredSize() {
return preferredSize;
}
}
public static void main(String[] a) {
ClockFrame c = new ClockFrame();
}
}
Edit: to include minute and hour hands
I find that after second hand if I add minute hand, my panel will be twice wide - I think it's because graphics cannot be superimposed...- and what I get is that the two graphics2D are separated far away from each other and repaint themselves..any good ideas to resolve this?
Because I was bored, I played around with the code a little more. I discovered the angle was off, so put an offset to correct it. Then I added minute and hour hands to that same custom component. I think the latter is the cause of the problem you describe (if not, show your latest code - though perhaps in a new question).
Try this version (note that second, minute and hour hands are all quantized to their respective time units):
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Line2D;
import java.io.IOException;
import java.net.URL;
import java.util.Calendar;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.*;
public class ClockFrame extends JFrame {
public ClockFrame() {
JPanel panel = new JPanel();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
add(panel);
panel.setSize(getWidth(), getHeight());
panel.setLayout(new GridLayout());
setResizable(false);
panel.setBackground(Color.MAGENTA.darker().darker());
Hand sHand = new Hand(panel);
panel.add(sHand);
pack();
setVisible(true);
}
class Hand extends JComponent {
private Timer timer;
private Dimension preferredSize = new Dimension(600, 600);
private Calendar currentTime;
private Image clockFace;
public Hand(Object o) {
setLocation(500, 500);
((JPanel) o).add(this);
currentTime = Calendar.getInstance();
try {
clockFace = ImageIO.read(new URL(
"http://www.clipartbest.com/cliparts/LTK/kBp/LTKkBpRGc.png"));
} catch (IOException ex) {
Logger.getLogger(ClockFrame.class.getName()).log(Level.SEVERE, null, ex);
}
timer = new Timer(50, new ActionListener() {
public void actionPerformed(ActionEvent e) {
currentTime.setTime(new Date(System.currentTimeMillis()));
repaint();
}
});
timer.start();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(
RenderingHints.KEY_DITHERING,
RenderingHints.VALUE_DITHER_ENABLE);
g2d.setColor(Color.LIGHT_GRAY);
int size = preferredSize.width;
g2d.fillOval((int)(size*.01), (int)(size*.01), (int)(size*.98), (int)(size*.98));
if (clockFace!=null) {
g2d.drawImage(clockFace, 0, 0, this);
}
double middle = size / 2d;
/* Note that this is translating further drawing operations
to the middle of the Hand container based on its preferred size,
which is (layout manager not withstanding) also its actual size.
All co-ordinates in custom painting are relative to the component
being painted. */
g2d.translate(middle, middle);
g2d.setColor(Color.CYAN.darker().darker());
double angleHour = ((currentTime.get(Calendar.HOUR)*2*Math.PI)/12d)-(Math.PI/2);
g2d.setStroke(new BasicStroke(6.5f));
Line2D.Double hourHand = new Line2D.Double(0, 0,
middle*.83*Math.cos(angleHour),
middle*.83*Math.sin(angleHour));
g2d.draw(hourHand);
g2d.setColor(Color.CYAN.darker());
double angleMin = ((currentTime.get(Calendar.MINUTE)*2*Math.PI)/60d)-(Math.PI/2);
g2d.setStroke(new BasicStroke(4.5f));
Line2D.Double minuteHand = new Line2D.Double(0, 0,
middle*.85*Math.cos(angleMin),
middle*.85*Math.sin(angleMin));
g2d.draw(minuteHand);
g2d.setColor(Color.CYAN);
double angleSec = ((currentTime.get(Calendar.SECOND)*2*Math.PI)/60d)-(Math.PI/2);
g2d.setStroke(new BasicStroke(2.5f));
Line2D.Double secondHand = new Line2D.Double(0, 0,
middle*.87*Math.cos(angleSec),
middle*.87*Math.sin(angleSec));
g2d.draw(secondHand);
g2d.dispose();
}
public Dimension getPreferredSize() {
return preferredSize;
}
}
public static void main(String[] a) {
ClockFrame c = new ClockFrame();
}
}

Related

Prevent swing animation from jumping

I've made this basic example, using a JPanel and a javax.swing.Timer and I would expect the animation to be relatively smooth.
If I keep the mouse moving over the window, the animation is smooth. If I don't interact with the window at all, then the animation starts to jump.
import javax.swing.*;
import java.awt.*;
public class SmoothSwing{
double x = 0;
double y = 0;
double theta = 0;
public void step(){
x = 175 + 175*Math.sin( theta );
y = 175 + 175*Math.cos( theta );
theta += 0.02;
if(theta > 6.28) theta = 0;
}
public void buildGui(){
JFrame frame = new JFrame("smooth");
JPanel panel = new JPanel(){
Dimension dim = new Dimension(400, 400);
#Override
public void paintComponent(Graphics g){
super.paintComponent(g);
g.drawOval((int)x, (int)y, 50, 50);
}
#Override
public Dimension getMinimumSize(){
return dim;
}
#Override
public Dimension getPreferredSize(){
return dim;
}
};
frame.add(panel, BorderLayout.CENTER);
frame.setVisible(true);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(30, evt->{
step();
panel.repaint();
});
timer.start();
}
public static void main(String[] args){
EventQueue.invokeLater( new SmoothSwing()::buildGui );
}
}
What it looked like to me was that the repaint's are accumulating and getting repainted in clumps. So I commented out the line super.paintComponent(g); which would cause the panel to not get cleared.
When I do that, I can see that paintComponent is getting run because all of the circles are being drawn, but the display is still only updating after multiple circles have been drawn.
I have this issue on jdk-14+36, jdk-11.0.6 and 1.8.0_181 using Linux. Ubuntu 18.04.
Here are graphics comparing the difference. I had trouble controlling the speed of the gifs, but this illustrates what is happening.
Left to right, if I move the mouse over the window I get the behavior on the left, after I stop it gets a bit jumpy (middle) then it gets more jumpy (right).
One thing I can do to avoid the problem is to split my timer into two tasks.
Timer t1 = new Timer(30, evt->step());
Timer t2 = new Timer(3, evt->panel.repaint());
t1.start();
t2.start();
You can achieve smoother animation by:
- Using finer theta step
- Update in higher frequency
- Use double values to paint:
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class SmoothSwing{
double x = 0;
double y = 0;
double theta = 0;
public void step(){
x = 175 + 175*Math.sin( theta );
y = 175 + 175*Math.cos( theta );
theta += 0.002; //finer theta step
if(theta > 6.28) {
theta = 0;
}
}
public void buildGui(){
JFrame frame = new JFrame("smooth");
JPanel panel = new JPanel(){
Dimension dim = new Dimension(400, 400);
#Override
public void paintComponent(Graphics g){
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
g2d.draw(new Ellipse2D.Double(x, y, 50, 50));
}
#Override
public Dimension getMinimumSize(){
return dim;
}
#Override
public Dimension getPreferredSize(){
return dim;
}
};
frame.add(panel, BorderLayout.CENTER);
frame.setVisible(true);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(3, evt->{ //higher frequency update
step();
panel.repaint();
});
timer.start();
}
public static void main(String[] args){
EventQueue.invokeLater( new SmoothSwing()::buildGui );
}
}
When I run either you example or cOder's example I don't notice any change in motion over time. It remains constant as best as I can tell.
What it looked like to me was that the repaint's are accumulating and getting repainted in clumps.
The only way I now to bypass the RepaintManager in Swing is to use the paintImmediately(…) method. This causes the component to be painted right away without being added to the end of the EDT. It is not recommended to be used because it can decrease overall painting performance.
Any methods that updates the state of your class should be part of the class, not external to the class. So, I modified you original code. This allows me to play with the painting logic in the step() method:
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class SmoothSwing2 extends JPanel
{
double x = 0;
double y = 0;
double theta = 0;
Dimension dim = new Dimension(400, 400);
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
g2d.draw(new Ellipse2D.Double(x, y, 50, 50));
}
#Override
public Dimension getMinimumSize()
{
return dim;
}
#Override
public Dimension getPreferredSize()
{
return dim;
}
public void step()
{
x = 175 + 175*Math.sin( theta );
y = 175 + 175*Math.cos( theta );
theta += 0.008; //finer theta step
if(theta > 6.28) {
theta = 0;
}
repaint();
// paintImmediately( getBounds() ); // repaints the entire panel
// paintImmediately(x - 5, y - 5, 60, 60); // repaint only the circle
}
public void buildGui()
{
SmoothSwing panel = new SmoothSwing();
JFrame frame = new JFrame("smooth");
frame.add(panel, BorderLayout.CENTER);
frame.setVisible(true);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(3, evt-> panel.step)
timer.start();
}
public static void main(String[] args){
EventQueue.invokeLater( new SmoothSwing2()::buildGui );
}
}
I don't notice any difference using either of the 3 approaches.
If you continue to notice repaints be "accumulated", then maybe is your OS that is not giving the application CPU constantly?
I use JDK 11, on Windows 10.

Using Layout Manager with JPanel and Graphics 2D

I want to draw lines and more on a JPanel, add that to a JFrame and using .pack() afterwards. My problem is that I dont get how to use a Layout Manager in that particular case. Usually I add a button or something to the panel by using a gridBagLayout and I totally understand that. But with graphics 2D I kind of just draw directly to the panel. Therfore I cant use .pack() properly. Does somebody know how to pack() that jPanel the right way? My code looks like that:
public class NetworkViewPanel extends JPanel implements KeyListener, ActionListener {
public NetworkViewPanel(NetworkAI network) {
this.network = network;
this.netList = network.getLayerList();
addKeyListener(this);
setFocusable(true);
this.setLayout(new GridLayout(2, 2, 2, 2)); // does that even make sense ?
}
public void paint(Graphics g) {
super.paint(g);
g2 = (Graphics2D) g;
if (showStandardView) {
drawRectangles();
drawLines();
} else {
drawRectangles();
drawLinesSpecial(listIndex, xIndex);
}
}
Greetings :)
You can layout a JPanel with a layout manager, and do custom painting on top of it.
This does not prevent you from using pack()1.
The following mre 2 demonstrates painting a line on a JPanel using a GridLayout:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.util.ArrayList;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class NetworkViewPanel extends JPanel{
private final List<JLabel> labels;
public NetworkViewPanel() {
this.setLayout(new GridLayout(2, 2, 2, 2));
this.setPreferredSize(new Dimension(400,300));//used by pack()
labels = new ArrayList<>();
addLabels(new String[]{ "A", "B" , "C" , "D"});
}
private void addLabels(String[] text){
for(String t: text){
JLabel label = new JLabel(t);
label.setBorder(BorderFactory.createLineBorder(Color.BLUE));
label.setHorizontalAlignment(JLabel.CENTER);
add(label);
labels.add(label);
}
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g); //draw panel as layed out by layout manager
drawLines(g);
}
private void drawLines(Graphics g) {
//draw line between centers of first and last components
int x1 = labels.get(0).getBounds().x + labels.get(0).getBounds().width /2;
int y1 = labels.get(0).getBounds().y + labels.get(0).getBounds().height /2;
int x2 = labels.get(labels.size()-1).getBounds().x + labels.get(labels.size()-1).getBounds().width/2;
int y2 = labels.get(labels.size()-1).getBounds().y + labels.get(labels.size()-1).getBounds().height/2;
g.setColor(Color.RED);
g.drawLine(x1, y1, x2, y2);
}
public static void main(String[] args) {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setLocationRelativeTo(null);
f.add(new NetworkViewPanel());
f.pack();
f.setVisible(true);
}
}
1 See: What does .pack() do?
2 Consider posting mre when asking or answering

Trying to create a program that draws an ellipse using parametric equation, but the lines aren't showing. Why? (Java)

I am trying to create a program that, using parametric equation, can draw lines between lines and create an ellipse. Here's my code:
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import javax.swing.JFrame;
public class test extends JFrame{
ArrayList<Line2D> lines = new ArrayList<>();
public test(){
this.setTitle("test");
this.setSize(800, 600);
this.setLocationRelativeTo(null);
this.setLayout(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
drawCircle(3,3.0,3.0);
this.setVisible(true);
System.out.println(lines.size());
}
public void drawCircle(int radio, double xCenter, double yCenter) {
double t = 0;
double xPoint;
double yPoint;
double xActual = xCenter;
double yActual = yCenter + radio*Math.sin(0);
t += 0.01;
while(t < 360) {
xPoint = xCenter + radio*Math.cos(t);
yPoint = yCenter + radio*Math.sin(t);
lines.add(new Line2D.Double(xActual, yActual, xPoint, yPoint));
this.repaint();
t += 0.01;
xActual = xPoint;
yActual = yPoint;
}
}
public void paint(Graphics g) {
super.paint(g);
for(Line2D s : lines){
((Graphics2D) g).draw(s);
}
}
}
I am not getting any errors, but the lines don't seem to appear in the frame. What am I doing wrong?
Welcome to the wonderful world of why you shouldn't extend from JFrame and why you shouldn't override paint of top level containers like JFrame
The main problem is, the frame has borders. If you override paint of a JFrame and paint at 0x0, you will actually be painting UNDER the frame. Your circle (at a ratio of 3) is small enough to be painted entirely under the window's title bar.
See How to get the EXACT middle of a screen, even when re-sized, How can I set in the midst? and Java AWT drawString() does not display on window for examples of this problem and why you shouldn't override paint of top level containers
Instead, use a component like JPanel and override it's paintComponent and place your custom painting there. Then place this within an instance of a JFrame, for example...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.HeadlessException;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test extends JFrame {
public static void main(String[] args) {
new Test();
}
public Test() throws HeadlessException {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Test");
frame.add(new TestPane());
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
ArrayList<Line2D> lines = new ArrayList<>();
public TestPane() {
drawCircle(3, 3.0, 3.0);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);
System.out.println(lines.size());
for (Line2D s : lines) {
g2d.draw(s);
}
g2d.dispose();
}
public void drawCircle(int radio, double xCenter, double yCenter) {
double t = 0;
double xPoint;
double yPoint;
double xActual = xCenter;
double yActual = yCenter + radio * Math.sin(0);
t += 0.01;
while (t < 360) {
xPoint = xCenter + radio * Math.cos(t);
yPoint = yCenter + radio * Math.sin(t);
// System.out.println(xActual + "x" + yActual + "x" + xPoint + "x" + yPoint);
lines.add(new Line2D.Double(xActual, yActual, xPoint, yPoint));
t += 0.01;
xActual = xPoint;
yActual = yPoint;
}
}
}
}
Your next problem will be, 0x0 (or close enough to it) is the center point of the circle. You need to be able to calculate a x/y offset when you generate the shape so that the top/left corner of the circle is at 0x0 instead
For example, in testing your code, I increased the radio to 100 (to see if it made any difference, which is how I discovered that you were painting under the frame) and it generated something like...
Drawing in swing should under all circumstances be done in paintComponent(Graphics), not paint as described in this article. I'd recommend you use a JPanel or any some other nontoplevel component to paint on - which is common habit.
And
...
lines.add(new Line2D.Double(xActual, yActual, xPoint, yPoint));
this.repaint();
...
This repaint call will cause some really ugly behaviour and is quite a waste of resources (36000 useless repaints per added shape).
Create a Graphics2D object. It helps. For example:
Graphics2D g2 = (Graphics2D)g;
Once you have that, you need to set the color. Which you didn't do. Example:
g2.setColor(Color.BLACK);
Also just a tip, override paintComponent(Graphics g) and do NOT override paint().

Java - how do I resize my 2D Game?

I'm developing a 2D game but i get stuck on the resizing. So as you can see in the code below, I draw all the game things into a BufferedImage. After that i get the graphics from my JPanel and draw the image on it.
So my question is how can i resize my game now? I think there are many possibilities: 1 would be that the image is just resizing with the jpanel as well and the other method could be that if you have a 2d world and you resize the frame, the game will be still in the same size but you can see now more of the 2d world, like that your viewing distance increased.
Does someone knows a solution for my code?
package de.avarion.theenchanter;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Game implements Runnable{
public static int WIDTH = 800;
public static int HEIGHT = 600;
public static int FPS = 30;
private JFrame frame;
private JPanel panel;
private BufferedImage image;
private Graphics2D g;
private Thread thread;
private boolean running
public Game() {
panel = new JPanel();
panel.setPreferredSize(new Dimension(WIDTH, HEIGHT));
panel.setFocusable(true);
panel.requestFocus();
frame = new JFrame("The Enchanter - pre Alpha");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(panel, BorderLayout.CENTER);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setResizable(true);
frame.setVisible(true);
image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
g = (Graphics2D) image.getGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
running = true;
thread = new Thread(this);
thread.start();
}
#Override
public void run() { //game loop
while(running) {
update();
render();
try {
Thread.sleep(1000/FPS);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
private void update() {
if(frame.getWidth() != WIDTH || frame.getHeight() != HEIGHT) {
frame.pack();
}
}
private void render() {
g.setColor(Color.black);//drawing on my image
g.fillRect(0, 0, WIDTH, HEIGHT);
Graphics g2 = panel.getGraphics(); // get graphics from panel
g2.drawImage(image, 0, 0, null); //drawing on my panel
g2.dispose();
}
}
Let's start with Graphics g2 = panel.getGraphics(); - This is not how painting is done in Swing. Swing uses a passive rendering algorithm, this means that a paint cycle may occur at any time and for any reason, many of which are out of your control or knowledge.
Using your current approach, at best, you run the risk of the introducing flickering, as your rendering loop and Swing compete with each other or having partial updates due to a thread race condition between your thread and the EDT.
You should also never dispose of a Graphics context you did not create yourself, on some systems, this could actually prevent anything from been painted.
Take a look at Painting in AWT and Swing, Performing Custom Painting and Concurrency in Swing for more details.
A simple solution would be to override the paintComponent method of a Swing component, like JPanel and perform your custom painting directly within it. This allows you to not only ascertain the current size of the component, but the paintComponent method will be called automatically when the component is resized.
Swing components are double buffered by default, so you get that automatically for free
You could use a Swing Timer to act as the "game loop", updating the current state of the game and scheduling a repaint request with the RepaintManager, for example...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private int xPos, yPos;
private int xDelta = 2;
public TestPane() {
Timer timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
int x = (int) (getWidth() * 0.1);
int y = (int) (getHeight() * 0.1);
int width = getWidth() - (x * 2);
int height = getHeight()- (y * 2);
xPos += xDelta;
yPos = y + ((height - 5) / 2);
if (xPos + 10 > x + width) {
xPos = x + (width - 10);
xDelta *= -1;
} else if (xPos < x) {
xPos = x;
xDelta *= -1;
}
repaint();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
int x = (int) (getWidth() * 0.1);
int y = (int) (getHeight() * 0.1);
g2d.drawRect(x, y, getWidth() - (x * 2), getHeight() - (y * 2));
g2d.setColor(Color.RED);
g2d.drawOval(xPos, yPos, 10, 10);
g2d.dispose();
}
}
}
If you want something that gives you more control over the rendering process, then you will need consider using something like a BufferStrategy. This allows you to employee a active painting process, painting to the buffer when you want to, without fear of some other thread painting to it as well.
See BufferStrategy and BufferCapabilities for more details

How to add MouseListener to item on Java Swing Canvas

I'd like to make a Java panel that creates objects where the user clicks. Since my actual application uses a MVC approach I'd like also for these objects to be able to repaint themselves when a model is changed, and provide menus to change their properties.
I think that the best way to control their x and y locations would be to take a canvas based approach whereby the JPanel calls a draw method on these objects from the paintComponent method. This however will only draw the shape on the canvas and does not add the object itself loosing all abilities to control object properties. I'd be very grateful if someone could tell me the best approach for what I want to do.
I've created some sample code which can be seen below. When clicked I'd like the circle to change colour, which is implemented using a MouseListener (it basically represents changing the models properties in this small example). Also I'd just like to make sure that zooming in/out still works with any sample code/advice can provide so I've added buttons to zoom the objects in and out as a quick test.
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import java.awt.geom.Ellipse2D;
public class Main {
public static void main(String args[]) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ExamplePanel panel = new ExamplePanel();
frame.add(panel);
frame.pack();
frame.setVisible(true);
}
});
}
//I could not get this to with when it extended JLayeredPane
private static class ExamplePanel extends JPanel {
private static final int maxX = 500;
private static final int maxY = 500;
private static double zoom = 1;
private static final Circle circle = new Circle(100, 100);
public ExamplePanel() {
this.setPreferredSize(new Dimension(maxX, maxY));
this.setFocusable(true);
Button zoomIn = new Button("Zoom In");
zoomIn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
zoom += 0.1;
repaint();
}
});
add(zoomIn);
Button zoomOut = new Button("Zoom Out");
zoomOut.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
zoom -= 0.1;
repaint();
}
});
add(zoomOut);
// add(circle); // Comment back in if using JLayeredPane
}
#Override
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.scale(zoom, zoom);
super.paintComponent(g);
circle.paint(g); // Comment out if using JLayeredPane
}
}
static class Circle extends JPanel {
private Color color = Color.RED;
private final int x;
private final int y;
private static final int DIMENSION = 100;
public Circle(int x, int y) {
// setBounds(x, y, DIMENSION, DIMENSION);
this.x = x;
this.y = y;
addMouseListener(new MouseAdapter() {
#Override
public void mousePressed(MouseEvent e) {
color = Color.BLUE;
}
#Override
public void mouseReleased(MouseEvent e) {
}
});
}
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setPaint(color);
g2.fillOval(x, y, DIMENSION, DIMENSION);
}
// I had some trouble getting this to work with JLayeredPane even when setting the bounds
// In the constructor
// #Override
// public void paintComponent(Graphics g) {
// Graphics2D g2 = (Graphics2D) g;
// g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// g2.setPaint(color);
// g2.fillOval(x, y, DIMENSION, DIMENSION);
// }
#Override
public Dimension getPreferredSize(){
return new Dimension(DIMENSION, DIMENSION);
}
}
}
As an aside I did try using a JLayeredPane(useful because I'd also like to layer my objects) but could not get my objects to even render. I know it has no default layout manager so tried calling setBounds in the circle in the constructor, but sadly it did not work. I know it's better to use a layout manager but can't seem to find one suitable for my needs!
Thanks in advance.
Don't override paint components, use paintComponent and don't forget to call super.paintComponent
A component already has a concept of "location", so when painting, the top left position of your component is actually 0x0
What you are doing is actually painting beyond the boundaries of you component
For example, if you place your Circle at 100x100 and then did...
g2.fillOval(x, y, DIMENSION, DIMENSION);
You would actually start painting at 200x200 (100 for the actual location of the component and 100 for you additional positioning).
Instead use
g2.fillOval(x, y, DIMENSION, DIMENSION);
And go back and try using JLayeredPane.
You could actually write your own layout manager that takes the location of the component and it's preferred size and updates the components bounds and then apply this to a JLayeredPane. This gives you the "benefits" of an absolute layout, but keeps you within how Swing works to update its components when things change.
You should also be careful with doing anything like...
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
The Graphics context is a shared resource. That means, anything you apply to, will still be in effect when the next component is painted. This may produce some strange results.
Instead try using...
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//...
g2.dispose();
Updated
For zooming I would take a closer look at JXLayer (or JLayer in Java 7)
The JXLayer (and excellent PBar extensions) have gone quite on the net, so you can grab a copy from here
(I tried finding a better example, but this is the best I could do with the limited time I have available)
Updated with working zooming example
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.RenderingHints;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.jdesktop.jxlayer.JXLayer;
import org.pbjar.jxlayer.demo.TransformUtils;
import org.pbjar.jxlayer.plaf.ext.transform.DefaultTransformModel;
public class TestJLayerZoom {
public static void main(String[] args) {
new TestJLayerZoom();
}
public TestJLayerZoom() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private JXLayer<JComponent> layer;
private DefaultTransformModel transformModel;
private JPanel content;
public TestPane() {
content = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridy = 0;
JLabel label = new JLabel("Hello");
JTextField field = new JTextField("World", 20);
content.add(label, gbc);
content.add(field, gbc);
gbc.gridy++;
gbc.gridwidth = GridBagConstraints.REMAINDER;
final JSlider slider = new JSlider(50, 200);
slider.addChangeListener(new ChangeListener() {
#Override
public void stateChanged(ChangeEvent e) {
int value = slider.getValue();
double scale = value / 100d;
transformModel.setScale(scale);
}
});
content.add(slider, gbc);
transformModel = new DefaultTransformModel();
transformModel.setScaleToPreferredSize(true);
Map<RenderingHints.Key, Object> hints = new HashMap<>();
//hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//hints.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
//hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
layer = TransformUtils.createTransformJXLayer(content, transformModel, hints);
setLayout(new BorderLayout());
add(layer);
}
}
}
I've left the rendering hints in to demonstrate their use, but I found that they screwed with the positing of the cursor within the text field, but you might like to have a play
I'd just like to add that I fixed the zooming issue not in the way suggested by the answer, but just by keeping the line that applied a scaled transform call in the ExamplePanel paintComponent method:
g2.scale(zoom, zoom);
I thought that this was the nicest implementation since none of the components require any knowledge about zooming and it seemed far simpler than JLayer since I only required basic zooming functionalities.

Categories

Resources