Java custom JComponent: Why is getMinimumSize() ignored? - java

I've overridden getPreferredSize() and getMinimumSize() in my custom JComponent. While getPreferredSize() is working as expected, getMinimumSize() instead is ignored as I am able to resize the JFrame until my custom component disappears. Why? How to avoid it?
Here is a complete runnable program that shows my point. What am I doing wrong?
Main.java
package swing.minimumsize;
import javax.swing.JFrame;
#SuppressWarnings("serial")
public class Main extends JFrame {
public Main() {
setTitle("Custom Component Test");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void display() {
add(new CustomComponent());
pack();
setVisible(true);
}
/**
* #param args
*/
public static void main(String[] args) {
Main main = new Main();
main.display();
}
}
CustomComponent.java
package swing.minimumsize;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JComponent;
#SuppressWarnings("serial")
public class CustomComponent extends JComponent {
#Override
public Dimension getMinimumSize() {
return new Dimension(100, 100);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
public void paintComponent(Graphics g) {
int margin = 10;
Dimension dim = getSize();
super.paintComponent(g);
g.setColor(Color.red);
g.fillRect(margin, margin, dim.width-margin*2, dim.height-margin*2);
}
}

..getMinimumSize() instead is ignored as I am able to resize the JFrame until my custom component disappears. Why? How to avoid it?
Check the 1st comment.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class Main extends JFrame {
public Main() {
setTitle("Custom Component Test");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void display() {
add(new CustomComponent());
pack();
// enforces the minimum size of both frame and component
setMinimumSize(getSize());
setVisible(true);
}
public static void main(String[] args) {
Main main = new Main();
main.display();
}
}
class CustomComponent extends JComponent {
CustomComponent() {
setBackground(Color.YELLOW);
}
#Override
public Dimension getMinimumSize() {
return new Dimension(100, 100);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// paint the BG.
g.setColor(getBackground());
g.fillRect(0,0,getWidth(),getHeight());
int margin = 10;
Dimension dim = getSize();
g.setColor(Color.red);
g.fillRect(margin, margin, dim.width-margin*2, dim.height-margin*2);
}
}

What is ignored or listened to will depend on the layout of the container that holds the JComponent. Since you're adding your comopnent to a JFrame BorderLayout.CENTER, it makes sense that setMinimumSize would be ignored.

The preferred, minimum and maximum sizes are just "suggestions" that the layout manager can use or ignore when laying out components.
Edit:
In addition to Andrew's suggestion you can try using a layout manager that respects the minimum size:
Container contentPane = getContentPane();
contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.X_AXIS));
add(new CustomComponent());
pack();
setMinimumSize(getMinimumSize());
setVisible(true);

Related

Why is JPanel size is wrong?

There is constructor of class extending JFrame:
import java.awt.*;
import javax.swing.*;
public class ChessFrame extends JFrame {
public ChessFrame () {
setSize(520, 520);
setResizable(false);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new GridLayout(1, 1));
// Add components
getContentPane().add(new Board());
pack();
setVisible(true);
}
}
And class extending JPanel:
import javax.swing.*;
public class Board extends JPanel {
public Board() {
setSize(new Dimension(520, 520));
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.fillRect(0, 0, 520, 520);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(520, 520);
}
}
As a result rectangle smaller then 520x520.
Size of black rectangle is about 496x510. There is more:
getWidth() and getHegiht() written inside the Board class, returns 0 and 0 (so size of this JPanel into JFrame is 0x0)
If I remove pack(), size of frame becomes 496x510 (black rectangle size)
It's actually copypaste of official java tutorial: https://docs.oracle.com/javase/tutorial/uiswing/painting/step2.html.
Do I do something wrong or it's something related with java? If it's second, why does this happen?
Any help would be appreciated.
This example only tries to establish the panel size once the frame (and panel) are visible on-screen. It returns the exact size (300 pixels) set in the code.
import java.awt.*;
import javax.swing.*;
public class ChessBoard extends JPanel {
int size = 300;
JLabel sizeLabel = new JLabel();
ChessBoard() {
setBackground(Color.CYAN);
setLayout(new GridBagLayout());
add(sizeLabel);
}
public void showSize() {
sizeLabel.setText(String.format("%1sx%1s", getWidth(), getHeight()));
}
#Override
public Dimension getPreferredSize() {
return new Dimension(size,size);
}
public static void main(String[] args) {
Runnable r = () -> {
ChessBoard cb = new ChessBoard();
JFrame f = new JFrame(cb.getClass().getSimpleName());
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
f.setLocationByPlatform(true);
f.setContentPane(cb);
f.pack();
f.setMinimumSize(f.getSize());
f.setVisible(true);
// delay showing size until app. is on-screen
Runnable r1 = cb::showSize;
SwingUtilities.invokeLater(r1);
};
SwingUtilities.invokeLater(r);
}
}

Repaint() does not work after adding more components to JPanel in Swing, Java

I am starting with Java and want to make a simple pong game to get into the ways of displaying stuff in java. I have created 2 classes that both extend JPanel and call the repaint() function every 16.6 milliseconds. I added both to a JPanel which I added to the frame, but only the component I added first displays.
I've tried to revalidate() after repaint() in both classes and made sure that the Timer actually works and that it's actionPerformed() method is actually called.
This is the main method:
public class Main {
public static void main(String[] args){
JFrame frame = new JFrame("Pong");
//...
JPanel mainPanel = new JPanel();
mainPanel.add(new Player());
mainPanel.add(new Ball(frameWidth/2, frameHeight/2 -40));
frame.add(mainPanel);
}
}
This is the important code for the classes (It's basically the same for both of them):
public class Player extends JPanel {
public Player(){
setPreferredSize(new Dimension(Main.frameWidth, Main.frameHeight));
setBackground(Color.BLACK);
new Timer(1000/60, new ActionListener(){
public void actionPerformed(ActionEvent e){
update();
repaint();
}
}).start();
}
//...
public void paintComponent(Graphics g){
super.paintComponent(g);
g.setColor(Color.WHITE);
g.fillRect(x, y, width, height);
}
}
(Of coure I left things like #Override or unneseccary functions out to shorten the code)
This code only paints the Player, altough I want it to display all the components of mainPanel.
This is an example that you can (hopefully) run. I had to split it up into 3 files, since I suck at anonymus classes:
Main.java
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Main {
public static void main(String[] args){
JFrame frame = new JFrame("Pong");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800, 800);
JPanel mainPanel = new JPanel();
mainPanel.add(new Player());
mainPanel.add(new Ball());
frame.add(mainPanel);
frame.setVisible(true);
}
}
///////
Player.java
//////
import javax.swing.JPanel;
import javax.swing.Timer;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class Player extends JPanel{
private int x = 20, y = 300, width = 20, height = 100;
public Player(){
setPreferredSize(new Dimension(800, 800));
setBackground(Color.BLACK);
new Timer(1000/60, new ActionListener(){
#Override
public void actionPerformed(ActionEvent e){
repaint();
}
}).start();
}
#Override
public void paintComponent(Graphics g){
super.paintComponent(g);
g.setColor(Color.WHITE);
g.fillRect(x, y, width, height);
}
}
//////
Ball.java
//////
import javax.swing.JPanel;
import javax.swing.Timer;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class Ball extends JPanel{
private int x = 20, y = 300, width = 20, height = 100;
public Ball(){
setPreferredSize(new Dimension(800, 800));
setBackground(Color.BLACK);
new Timer(1000/60, new ActionListener(){
#Override
public void actionPerformed(ActionEvent e){
repaint();
}
}).start();
}
#Override
public void paintComponent(Graphics g){
super.paintComponent(g);
g.setColor(Color.WHITE);
g.fillRect(x, y, width, height);
}
}
In method paintComponent() of class Player, you paint the exact same Rectangle each time. In order to achieve animation, each time you paint the rectangle you need to change either its size or location or both. What do you expect the Player to do? Should it move up and down along the left edge of the window? Have you seen the lesson entitled How to Use Swing Timers which is part of Oracle's Java Tutorial?
EDIT
I see now that Player hides Ball, because of the default layout manager of JPanel. The below code is essentially the code you posted but I set GridLayout as the layout manager for mainPanel. Also, a java source code file may contain more than one class but exactly one class must be public. Hence in the below code only class Main is public.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Main {
public static void main(String[] args) {
JFrame frame = new JFrame("Pong");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel mainPanel = new JPanel(new GridLayout(0, 2));
mainPanel.add(new Player());
mainPanel.add(new Ball());
frame.add(mainPanel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
}
class Player extends JPanel {
public Player() {
setBorder(BorderFactory.createLineBorder(Color.RED, 2, false));
setPreferredSize(new Dimension(800, 800));
setBackground(Color.BLACK);
new Timer(1000 / 60, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
repaint();
}
}).start();
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.WHITE);
g.fillRect(20, 100, 20, 100);
}
}
class Ball extends JPanel {
public Ball() {
setBorder(BorderFactory.createLineBorder(Color.CYAN, 2, false));
setPreferredSize(new Dimension(800, 800));
setBackground(Color.BLACK);
new Timer(1000 / 60, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
repaint();
}
}).start();
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.WHITE);
g.fillRect(300, 300, 10, 10);
}
}
And here is a screen capture of the GUI...
I just realized that if I extend my window manually, the second JPanel shows up under the one responsible for the Player! This means that I'll need to set the Panels position somehow, right?
#Abra

Prevent automatic JScrollPane viewport change on content resize

I have a JScrollPane with a custom JPanel as the content. The scrollpane has a mouse wheel handler, so when the user scrolls, the content is 'zoomed', and the JPanel becomes larger in height.
I need to ensure that the user's viewport maintains position on the current row of pixels on the JPanel that are currently under the mouse. However, when I manually try and set the viewport in the wheel handler, I can't ensure consistent focus, as sometimes the JScrollPane overrides my changes with the default behaviour for when JScrollPane's content becomes larger.
I've made a simple example showing the structure of my program as below, however the example only tries to ensure the viewport focus remains on the bottom of the JPanel when scrolling in as the panel doubles in size. The scroll wheel causes the JPanel to double/halve in height.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import javax.swing.*;
public class Test extends JFrame{
public static void main(String[] args){
Test t= new Test();
t.show();
}
public Test()
{
BorderLayout blm = new BorderLayout();
setLayout(blm);
getContentPane().add(new MyScroller(), BorderLayout.CENTER);
this.setPreferredSize(new Dimension(300, 300));
this.setMinimumSize(new Dimension(300, 300));
pack();
setLocationRelativeTo(null);
setVisible(true);
}
static class MyScroller extends JScrollPane implements MouseWheelListener
{
static InnerPanel controlGrid = new InnerPanel();
int panelHeight = 200;
public MyScroller()
{
super(controlGrid, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
this.setBackground(Color.blue);
this.addMouseWheelListener(this);
this.setPreferredSize(new Dimension(300, 300));
this.setMinimumSize(new Dimension(300, 300));
}
#Override
public void revalidate()
{
super.revalidate();
if (controlGrid != null)
{
controlGrid.revalidate();
}
}
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
draw(g);
}
public void draw(Graphics g)
{
}
public void mouseWheelMoved(MouseWheelEvent e) {
String message;
int direction = 1;
int notches = e.getWheelRotation();
if (notches < 0) {
panelHeight += panelHeight;
} else {
panelHeight -= (panelHeight > panelHeight ? panelHeight : 0);
}
controlGrid.setPreferredSize(new Dimension(20, panelHeight));
controlGrid.setMinimumSize(new Dimension(20, panelHeight));
repaint();
this.getViewport().setViewPosition(new java.awt.Point(0, panelHeight));
}
static class InnerPanel extends JPanel
{
public InnerPanel(){
this.setBackground(Color.red);
this.setPreferredSize(new Dimension(100, 600));
this.setMinimumSize(new Dimension(100, 600));
}
}
}
}

No line drawn at mouseClick

I have the following java programm:
import java.awt.*;
import java.awt.Graphics;
import javax.swing.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
public class Lapex extends JPanel implements MouseListener{
JPanel p = new JPanel();
Lapex(){
JFrame f = new JFrame();
p.addMouseListener(this);
p.setPreferredSize(new Dimension(600, 600));
f.add(p);
f.pack();
f.show(true);
}
public void paint(Graphics g){
paintComponents(g);
g.setColor(Color.RED);
g.drawLine(10, 10, 100, 100);
}
public void mouseClicked(MouseEvent me){
System.out.println("AAAA");
repaint();
}
}
public static void main(String[] args){
new Lapex();
}
}
Clicking the mouse, at the console is displayied "AAAAA", but draws no line.(I deleted the other mouse event)
How to modify ?
You have to call the super method and use paintComponent.
public void paintComponent(Graphics g){
super.paintComponent(g);
g.setColor(Color.RED);
g.drawLine(10, 10, 100, 100);
}
A few things to note:
Your class extends JPanel, but then you create another JPanel inside the class that you actually add to the frame. Add the instance of your class instead.
Override paintComponent instead of paint.
Use a call to invokeLater to start your program on the EDT. See Event Dispatch Thread for more info.
Override getPreferredSize rather than call setPreferredSize.
Here is a complete example that toggles the line on/off when the mouse button is clicked:
import java.awt.*;
import java.awt.Graphics;
import javax.swing.*;
import java.awt.event.*;
public class Lapex extends JPanel {
boolean drawLine = false;
Lapex(){
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
this.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent me){
drawLine = !drawLine;
repaint();
}
});
f.add(this);
f.pack();
f.setVisible(true);
}
#Override
protected void paintComponent(Graphics g){
super.paintComponent(g);
if(drawLine) {
g.setColor(Color.RED);
g.drawLine(10, 10, 100, 100);
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(600, 600);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run()
{
new Lapex();
}});
}
}
No!, don't override paint() leave this up to Swing itself. All you should do is override paintComponent().

paint method draws outside jFrame if first coord 0,0

First coord in this case should be 0,0 and not 8,30. What am i doing wrong(i am using NetBeans)
import java.awt.Color;
import java.awt.Graphics;
public class TEST extends javax.swing.JFrame {
#Override
public void paint(Graphics g){
super.paint(g);
g.setColor(Color.blue);
g.drawRect(8, 30, 200, 200);
repaint();
}}
Add a JPanel to the frame and paint in that. The frame's coordinates include the decorations (title bar, borders, etc.). It would look something like this:
public class Test extends JFrame {
public static void main(String[] args) {
new Test();
}
private Test() {
add(new MyPanel());
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(600, 600);
setVisible(true);
}
private class MyPanel extends JPanel {
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.blue);
g.drawRect(8, 30, 200, 200);
}
}
}
Also, don't call repaint(); in paint();. That will cause an infinite loop and will freeze the entire program.
The problem is your paint(..) method is not taking into account the JFrame Insets by calling getInsets which as docs state:
If a border has been set on this component, returns the border's
insets.
this code works fine:
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class Test {
public Test() {
createAndShowGui();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new Test();
}
});
}
private void createAndShowGui() {
JFrame frame = new JFrame() {
#Override
public void paint(Graphics g) {
super.paint(g);
g.setColor(Color.blue);
g.drawRect(0 + getInsets().left, 0 + getInsets().top, 200, 200);
}
};
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
}
however this is not best practice.
Rather add JPanel to JFrame and override paintComponent(Graphics g) of JPanel dont forget call to super.paintComponent(g) as first call in the overridden method and than draw there (dont forget to override getPreferredSize() and return correct Dimensions so the JPanel will fit its drawing/graphic content) this problem will no longer persist as JPanel is added at correct co-ordinates on contentPane like so:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class Test {
public Test() {
createAndShowGui();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new Test();
}
});
}
private void createAndShowGui() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel() {
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.addRenderingHints(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
g2d.setColor(Color.blue);
g2d.drawRect(0, 0, 200, 200);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(300, 300);
}
};
frame.add(panel);
frame.pack();
frame.setVisible(true);
}
}
The above includes Graphics2D and RenderingHints i.e anti-aliasing. Just for some better looking drawings :)

Categories