Java Layout Proportions: Creating a scalable square Panel - java

I am making a GUI component to represent something like a Chess board in a window. Normally it will be a grid of 8x8 squares, although some variants require a 10x8 board etc. The first step is to make a panel that contains a grid of 8x8 components.
The class Board extends JPanel and uses a GridLayout to model a grid of 8x8 components. In an effort to get something done these are simply of class Square which extends JButton. The trouble is that they're not squares!
The Board has been added to a freshly instantiated JFrame, packed and rendered on the screen. Of course, right now the board takes up the entire frame as it is resized by the user. The grid scales with the board and this distorts the squares into rectangles.
This is not entirely undesired behaviour. I would like the board to scale with the frame. However, I would like to ensure that the squares remain square at all times. The board could be rectangular (10x8) but should maintain a fixed proportion.
How do I get square squares?

You can choose to use a LayoutManager that honors the preferred size of the cells instead. GridLayout will provide a equal amount of the available space to each cell, which doesn't appear to be quite what you want.
For example, something like GridBagLayout
public class TestChessBoard {
public static void main(String[] args) {
new TestChessBoard();
}
public TestChessBoard() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
}
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new ChessBoardPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class ChessBoardPane extends JPanel {
public ChessBoardPane() {
int index = 0;
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
Color color = index % 2 == 0 ? Color.BLACK : Color.WHITE;
gbc.gridx = col;
gbc.gridy = row;
add(new Cell(color), gbc);
index++;
}
index++;
}
}
}
public class Cell extends JButton {
public Cell(Color background) {
setContentAreaFilled(false);
setBorderPainted(false);
setBackground(background);
setOpaque(true);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(25, 25);
}
}
}
Updated with proportional example
Now, if you want to do a proportional layout (so that each cell of the grid remains proportional to the other regardless of the available space), things begin to get ... fun ...
public class TestChessBoard {
public static void main(String[] args) {
new TestChessBoard();
}
public TestChessBoard() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
}
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestChessBoard.ChessBoardPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class ChessBoardPane extends JPanel {
public ChessBoardPane() {
int index = 0;
setLayout(new ChessBoardLayoutManager());
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
Color color = index % 2 == 0 ? Color.BLACK : Color.WHITE;
add(new TestChessBoard.Cell(color), new Point(col, row));
index++;
}
index++;
}
}
}
public class Cell extends JButton {
public Cell(Color background) {
setContentAreaFilled(false);
setBorderPainted(false);
setBackground(background);
setOpaque(true);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(25, 25);
}
}
public class ChessBoardLayoutManager implements LayoutManager2 {
private Map<Point, Component> mapComps;
public ChessBoardLayoutManager() {
mapComps = new HashMap<>(25);
}
#Override
public void addLayoutComponent(Component comp, Object constraints) {
if (constraints instanceof Point) {
mapComps.put((Point) constraints, comp);
} else {
throw new IllegalArgumentException("ChessBoard constraints must be a Point");
}
}
#Override
public Dimension maximumLayoutSize(Container target) {
return preferredLayoutSize(target);
}
#Override
public float getLayoutAlignmentX(Container target) {
return 0.5f;
}
#Override
public float getLayoutAlignmentY(Container target) {
return 0.5f;
}
#Override
public void invalidateLayout(Container target) {
}
#Override
public void addLayoutComponent(String name, Component comp) {
}
#Override
public void removeLayoutComponent(Component comp) {
Point[] keys = mapComps.keySet().toArray(new Point[mapComps.size()]);
for (Point p : keys) {
if (mapComps.get(p).equals(comp)) {
mapComps.remove(p);
break;
}
}
}
#Override
public Dimension preferredLayoutSize(Container parent) {
return new CellGrid(mapComps).getPreferredSize();
}
#Override
public Dimension minimumLayoutSize(Container parent) {
return preferredLayoutSize(parent);
}
#Override
public void layoutContainer(Container parent) {
int width = parent.getWidth();
int height = parent.getHeight();
int gridSize = Math.min(width, height);
CellGrid grid = new CellGrid(mapComps);
int rowCount = grid.getRowCount();
int columnCount = grid.getColumnCount();
int cellSize = gridSize / Math.max(rowCount, columnCount);
int xOffset = (width - (cellSize * columnCount)) / 2;
int yOffset = (height - (cellSize * rowCount)) / 2;
Map<Integer, List<CellGrid.Cell>> cellRows = grid.getCellRows();
for (Integer row : cellRows.keySet()) {
List<CellGrid.Cell> rows = cellRows.get(row);
for (CellGrid.Cell cell : rows) {
Point p = cell.getPoint();
Component comp = cell.getComponent();
int x = xOffset + (p.x * cellSize);
int y = yOffset + (p.y * cellSize);
comp.setLocation(x, y);
comp.setSize(cellSize, cellSize);
}
}
}
public class CellGrid {
private Dimension prefSize;
private int cellWidth;
private int cellHeight;
private Map<Integer, List<Cell>> mapRows;
private Map<Integer, List<Cell>> mapCols;
public CellGrid(Map<Point, Component> mapComps) {
mapRows = new HashMap<>(25);
mapCols = new HashMap<>(25);
for (Point p : mapComps.keySet()) {
int row = p.y;
int col = p.x;
List<Cell> rows = mapRows.get(row);
List<Cell> cols = mapCols.get(col);
if (rows == null) {
rows = new ArrayList<>(25);
mapRows.put(row, rows);
}
if (cols == null) {
cols = new ArrayList<>(25);
mapCols.put(col, cols);
}
Cell cell = new Cell(p, mapComps.get(p));
rows.add(cell);
cols.add(cell);
}
int rowCount = mapRows.size();
int colCount = mapCols.size();
cellWidth = 0;
cellHeight = 0;
for (List<Cell> comps : mapRows.values()) {
for (Cell cell : comps) {
Component comp = cell.getComponent();
cellWidth = Math.max(cellWidth, comp.getPreferredSize().width);
cellHeight = Math.max(cellHeight, comp.getPreferredSize().height);
}
}
int cellSize = Math.max(cellHeight, cellWidth);
prefSize = new Dimension(cellSize * colCount, cellSize * rowCount);
System.out.println(prefSize);
}
public int getRowCount() {
return getCellRows().size();
}
public int getColumnCount() {
return getCellColumns().size();
}
public Map<Integer, List<Cell>> getCellColumns() {
return mapCols;
}
public Map<Integer, List<Cell>> getCellRows() {
return mapRows;
}
public Dimension getPreferredSize() {
return prefSize;
}
public int getCellHeight() {
return cellHeight;
}
public int getCellWidth() {
return cellWidth;
}
public class Cell {
private Point point;
private Component component;
public Cell(Point p, Component comp) {
this.point = p;
this.component = comp;
}
public Point getPoint() {
return point;
}
public Component getComponent() {
return component;
}
}
}
}
}

This got a bit long, so here is the quick answer: You can't maintain a square board with square squares given your board dimensions (8x8, 10x8) and fully fill the screen if the user can resize it. You should limit the size of the board so that it maintains the aspect ratio even if that means you have some blank space in your frame. OK, read on for the long-winded explanation...
There are two ways you can make this work. Either you can limit the possible sizes of the JFrame, or you can limit the size of your Board so it doesn't always fill the frame. Limiting the size of the board is the more common method, so let's start with that.
Option 1: Limiting the Board
If you are working with a fixed set of board dimensions (8x8, 10x8, and a couple others maybe), and assuming each square has some minimum size (1 pixel squares on a chess board don't sound too practical), there are only so many frame dimensions that the board can fully fill. If your frame is 80pixels by 80pixels, your 8x8 board fits perfectly. But as soon as the user resizes to something like 85x80 you're stuck because you can't fully fill that while maintaining squares with the board dimensions you gave.
In this case you want to leave 5 pixels empty, whether it's 5 above or below, or 2.5 above and below, or whatever, doesn't matter. This should sound familiar - it's an aspect ratio problem and basically why you can get black bars on the edges of your TV depending on TV vs. movie dimensions.
Option 2: Limiting the Frame
If you want the board to always fully fill the frame, probably not what you want, then you have to adjust the size of the frame after a user resizes it. Say you are using a 10x8 board, and the user sets the frame to 107x75. That's not too bad, and with a little math you can figure out 100x80 is your closest aspect ratio that works, and fix the window. It will probably a bit frustrating for the user if the window keeps jumping around on them though, especially if they tried to make it something way off like 50x200.
Last thoughts / Example
Limiting the board is most likely the correct solution. Everything from games to desktop apps follows that principle. Take the ribbon in MS Office products for example. As you make the window larger, the ribbon will expand (maintaining its proportions) until it hits it max size, and then you just get more space for your document. When you make the window smaller the ribbon gets smaller (again maintaining its proportions) until it hits a minimum size and then you start losing parts of it (remember, don't want 1x1 squares on your board).
On the other hand you can prevent the user from resizing the window at all. I'm pretty sure this is how MineSweeper works (don't have it on this computer to double check), and may be a better/easier solution for what you need.

Related

JPanel not showing in JFrame, but JFrame still changes size

I don't know what I did, or what went wrong, but a change I made at some point in the last while has made my JPanel completely invisible. The JFrame it's nested in still changes in size to house it, and I can still toggle the content in the combobox.
In my desperation, I tried replacing the content of the SnakeSettingsPanel class with a single button, but the same thing happened - completely invisible, yet I can still interact with it. I figured it might be a computer error, so I tried restarting, but still nothing. When I tried adding a button to the frame outside of the JPanel, it worked just fine. What am I doing wrong?
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
public class SnakeSettingsPanel extends JPanel {
public boolean quit = false;
public boolean play = false;
public int width = 20;
public int height = 15;
public Speed speed = Speed.SLOW;
public JTextField wField;
public JTextField hField;
public JComboBox<Speed> sField;
public static void main(String[] args) {
JFrame jf = new JFrame();
jf.setTitle("Snake");
SnakeSettingsPanel settings = new SnakeSettingsPanel();
jf.add(settings);
jf.pack();
jf.setVisible(true);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public SnakeSettingsPanel() {
setLayout(new GridBagLayout());
// #author Create our labels.
JLabel wLabel = new JLabel("Width:");
JLabel hLabel = new JLabel("Height:");
JLabel sLabel = new JLabel("Speed:");
GridBagConstraints p = new GridBagConstraints();
p.gridx = 0;
p.gridy = 0;
p.insets = new Insets(10, 10, 10, 10);
// #author Create the buttons, and add listeners
JButton y = new JButton("Play");
JButton n = new JButton("Quit");
y.addActionListener(new PlayListener());
n.addActionListener(new QuitListener());
// #author Create text fields for height/width
wField = new JTextField(15);
wField.setText("20");
hField = new JTextField(15);
hField.setText("15");
// #author Creates a combobox for selecting speed.
Speed[] speeds = {Speed.SLOW, Speed.MEDIUM, Speed.FAST};
sField = new JComboBox<Speed>(speeds);
// #author Stitch everything into the panel.
add(wLabel, p);
p.gridx = 1;
add(wField, p);
p.gridx = 0;
p.gridy = 1;
add(hLabel, p);
p.gridx = 1;
add(hField, p);
p.gridx = 0;
p.gridy = 2;
add(sLabel, p);
p.gridx = 1;
add(sField, p);
p.gridx = 0;
p.gridy = 3;
add(y, p);
p.gridx = 1;
add(n, p);
setVisible(true);
}
public boolean getPlay() {
return play;
}
public boolean getQuit() {
return quit;
}
// #author Returns all settings as a SnakeSettings object
public SnakeSettings getSettings() {
return new SnakeSettings(width, height, speed);
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Speed getSpeed() {
return speed;
}
// #author Sends out the word to start a new game.
public class PlayListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
quit = false;
play = true;
width = Integer.parseInt(wField.getText());
height = Integer.parseInt(hField.getText());
speed = (Speed) sField.getSelectedItem();
}
}
// #author Sends out the word to shut down the program.
public class QuitListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
quit = true;
play = false;
}
}
}
Let this be a lesson on why you should avoid mixing Model (your application's data) with View (how it is displayed). Your SnakeSettingsPanel is currently both.
As Model, it contains 3 important fields: width, height, and speed.
As View, it is a full JPanel. JPanels have a lot of fields which you should avoid touching directly. Including width and height, usually accessed via getHeight and getWidth -- which you are overwriting with a version that always returns the same built-in values of 20 and 15 (until the user changes those values through a UI that they cannot see).
The fast fix is to rename your current getWidth() and getHeight() to avoid clashing with the built-in getWidth() and getHeight() methods of the parent JPanel class. Call them getMyWidth(), getMyHeight(), and suddenly everything works.
The better fix is to remove those fields and methods entirely, and store your own model attributes in a SnakeSettings attribute. Update it when the user clicks on play, and return it when it is requested via getSettings(). Less code for you, less chance of accidental name clashes with your parent JPanel class. This would look like:
// SnakeSettingsPanel, before
public int width = 20;
public int height = 15;
public Speed speed = Speed.SLOW;
public int getWidth() { // <-- clashes with superclass
return width;
}
public int getHeight() { // <-- clashes with superclass
return height;
}
public Speed getSpeed() {
return speed;
}
public SnakeSettings getSettings() {
return new SnakeSettings(width, height, speed);
}
// SnakeSettingsPanel, after
SnakeSettings settings = new SnakeSettings();
public SnakeSettings getSettings() {
return settings;
}

How can I add a rectangle in BorderLayout.SOUTH?

I am trying to add a thing like this in my music player application in swing.
I tried to add a rectangle to BorderLayout.SOUTH, but it never appeared on screen. Here is what I did:
public class MyDrawPanel extends JPanel {
#Override
public void paintComponent(Graphics g) {
g.setColor(Color.GREEN);
g.fillRect(200,200,200,200);
}
public static void main(String[] args) {
JFrame frame = new JFrame();
MyDrawPanel a = new MyDrawPanel();
frame.getContentPane().add(BorderLayout.SOUTH,a);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(1000,1000);
frame.setVisible(true);
}
}
I just did not try 200,200,200,200, but I tried a lot of values, even with the help of a for loop, but it never appeared on screen. If I used CENTER instead of SOUTH it appeared. I read the documentation to check how fillRect works, but it simply said it added x+width and y+height. The point (0,0) is the top left corner. I checked that by adding a rectangle to CENTER layout. How cam I do it?
I did not share the output, because it was just a blank screen.
The values you give to fillRect are wrong. The first two are the top left corner's coordinates, relative to the component you're painting in; in your case the MyDrawPanel. With the code you posted, this drawing area is outside of the container the panel is placed in. You want to do
g.fillRect(0,0,200,200);
A note: You usually want to call frame.pack() after you've finished adding all components, so it can layout itself. In your case, this results in a tiny window because the system doesn't know how large it should be. You probably want to add a method
public Dimension getPreferredSize() {
System.out.println("getting pref size");
return new Dimension(200, 200);
}
to ensure it's always large enough to draw the full rectangle.
Also, you should call frame.getContentPane().setLayout(new BorderLayout()) before. You can print it out without setting it to see it is not the default. EDIT: As VGR points out, the documentation says that it is in fact a BorderLayout. I cannot confirm that is the case - it is in fact a RootLayout. That seems to behave like a BorderLayout though.
I thought this might make a quick little project. Here's the level meter I came up with.
The important parts are the DrawingPanel and the LevelMeterModel. The DrawingPanel takes the information from the LevelMeterModel and paints the bars on a JPanel.
The LevelMeterModel is an int array of levels, a minimum level, and a maximum level. The maximum level could be calculated, but I assumed music has a certain volume and frequency range.
The JFrame holds the DrawingPanel. A Swing Timer varies the levels somewhat randomly. The random numbers are in a small range so the bar heights don't change abruptly.
Here's the complete runnable code.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class LevelMeterGUI implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new LevelMeterGUI());
}
private final DrawingPanel drawingPanel;
private final LevelMeterModel model;
public LevelMeterGUI() {
this.model = new LevelMeterModel();
this.drawingPanel = new DrawingPanel(model);
}
#Override
public void run() {
JFrame frame = new JFrame("Level Meter GUI");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(drawingPanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
System.out.println("Frame size: " + frame.getSize());
Timer timer = new Timer(250, new ActionListener() {
#Override
public void actionPerformed(ActionEvent event) {
model.setRandomLevels();
drawingPanel.repaint();
}
});
timer.start();
}
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
private final int drawingWidth, drawingHeight, margin, rows;
private final Dimension barDimension;
private final LevelMeterModel model;
public DrawingPanel(LevelMeterModel model) {
this.model = model;
this.margin = 10;
this.rows = 20;
this.barDimension = new Dimension(50, 10);
int columns = model.getLevels().length;
drawingWidth = columns * barDimension.width + (columns + 1) * margin;
drawingHeight = rows * barDimension.height + (rows + 1) * margin;
this.setBackground(Color.WHITE);
this.setPreferredSize(new Dimension(drawingWidth, drawingHeight));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int maximum = model.getMaximumLevel();
double increment = (double) maximum / rows;
int peak = rows * 75 / 100;
int x = margin;
for (int level : model.getLevels()) {
int steps = (int) Math.round((double) level / increment);
int y = drawingHeight - margin - barDimension.height;
for (int index = 0; index < steps; index++) {
if (index < peak) {
g.setColor(Color.GREEN);
} else {
g.setColor(Color.RED);
}
g.fillRect(x, y, barDimension.width, barDimension.height);
y = y - margin - barDimension.height;
}
x += margin + barDimension.width;
}
}
}
public class LevelMeterModel {
private final int minimumLevel, maximumLevel;
private int[] levels;
private final Random random;
public LevelMeterModel() {
this.minimumLevel = 100;
this.maximumLevel = 999;
this.levels = new int[8];
this.random = new Random();
setRandomLevels();
}
public void setRandomLevels() {
for (int index = 0; index < levels.length; index++) {
levels[index] = getRandomLevel(levels[index]);
}
}
private int getRandomLevel(int level) {
if (level == 0) {
return random.nextInt(maximumLevel - minimumLevel) + minimumLevel;
} else {
int minimum = Math.max(level * 90 / 100, minimumLevel);
int maximum = Math.min(level * 110 / 100, maximumLevel);
return random.nextInt(maximum - minimum) + minimum;
}
}
public int[] getLevels() {
return levels;
}
public int getMinimumLevel() {
return minimumLevel;
}
public int getMaximumLevel() {
return maximumLevel;
}
}
}

GUI not updating correctly, components disappearing

I have been struggling with this problem now for over a week, and would really appreciate some help. I am developing my first Java game using a gui, and I currently have about 20 classes involved. The game is a simple grid-based representation of Star Trek, with JLabel icons that move around the galaxy grid. The problem is that usually after about 7 to 10 moves, one of two things will happen: one, the grid of sectors in my current quadrant will disappear, leaving only a single sector in the top left corner; or two, the Enterprise icon will disappear.
I have no experience dealing with threads, but after some reading I thought this was probably a result of the Event Dispatch Thread not being properly synchronized with the program logic. I read up on the proper way to update a GUI, and surrounded all my statements that had any effect on the GUI (I think) with invokeLater and invokeAndWait blocks.
However, this did not solve the problem. So, today I rewrote everything into the smallest compilable unit I could (it isn't that small, but I can't figure out how to make it smaller) while still keeping my basic game structure to see if that would change anything. It didn't. The GUI still gets corrupted after 7 to 10 moves.
I am at my wits' end here. I would be truly grateful for some help.
Here is my code. It compiles and runs as is.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.Random;
import java.util.Scanner;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SpringLayout;
import javax.swing.SwingUtilities;
import javax.swing.border.BevelBorder;
public class GUI extends JFrame
{
int screenwidth;
int screenheight;
public static void main(String[] args)
{
GUI gui = new GUI();
run(gui);
}
public static void run(final GUI gui)
{
Quadrant[][] galaxy = new Quadrant[8][8];
//populate galaxy with quadrants
for(int i = 0; i < 8; i++)
{
for(int j = 0; j < 8; j++)
{
galaxy[i][j] = new Quadrant(i, j);
}
}
//Quadrant to put in the view when game starts
Quadrant startingQuadrant = galaxy[0][0];
final QuadrantView quadrantView = startingQuadrant.getQuadrantView();
Enterprise enterprise;
Sector startingSector;
//add SectorViews to the QuadrantView
for (int i = 0; i < 8; i++)
{
for(int j = 0; j < 8; j++)
{
startingQuadrant.getQuadrantView().addSectorView(startingQuadrant.getSectorArray()[i][j].getSectorView(), i, j);
}
}
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
//initialize gui with the starting quadrant quadrantView
gui.intiGUI(quadrantView);
}
});
//start on sector (0, 0)
startingSector = startingQuadrant.getSectorArray()[0][0];
enterprise = new Enterprise(startingQuadrant, startingSector);
startingSector.setContainsEnterprise(true);
Scanner input = new Scanner(System.in);
Sector destinationSector;
int qRow; //destination quadrant row
int qCol; //destination quadrant column
int sRow; //destination sector row
int sCol; //destination sector column
while(true)
{
System.out.println("Enter quadrant row: ");
qRow = input.nextInt();
System.out.println("Enter quadrant column: ");
qCol = input.nextInt();
System.out.println("Enter sector row: ");
sRow = input.nextInt();
System.out.println("Enter sector column: ");
sCol = input.nextInt();
destinationSector = galaxy[qRow][qCol].getSectorArray()[sRow][sCol];
enterprise.move(destinationSector, galaxy[qRow][qCol], gui);
}
}
public GUI()
{
super("Star Trek");
//create an anonymous listener to close window and end game
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
dispose();
System.exit(0);
}
});
// get user's screen width and height
screenwidth = (int)Toolkit.getDefaultToolkit().getScreenSize().getWidth();
screenheight = (int)Toolkit.getDefaultToolkit().getScreenSize().getHeight();
//set layout
getContentPane().setLayout(new BorderLayout());
resizeGUI();
setVisible(true);
validate();
}
private void resizeGUI()
{
// set window size
if (screenwidth >= 1280)
setSize(1024, 768);
else if (screenwidth >= 1024)
setSize(800, 600);
else if (screenwidth >= 800)
setSize(640, 480);
// maximize window
setExtendedState(this.getExtendedState() | this.MAXIMIZED_BOTH);
}
//initialize this gui with the starting QuadrantView
public void intiGUI(QuadrantView quadrantView)
{
getContentPane().add(quadrantView, BorderLayout.CENTER);
validate();
}
//reset the gui to hold the new QuadrantView
public void resetGUI(QuadrantView newQuadrantView)
{
getContentPane().add(newQuadrantView, BorderLayout.CENTER);
validate();
}
static class Quadrant
{
private int row;
private int col;
private QuadrantView quadrantView;
private Sector[][] sectorArray;
public Quadrant(int r, int c)
{
// quadrant row
row = r;
// quadrant columns
col = c;
// the view object associated with this quadrant
setQuadrantView(new QuadrantView(8, 8));
// an array to hold the sectors in this quadrant (req. 3.1.0)
sectorArray = new Sector[8][8];
// create the 64 sectors in this quadrant and add them to the array (req. 3.1.0)
for (int i = 0; i < sectorArray.length; i ++)
{
for (int j = 0; j < sectorArray[i].length; j++)
{
sectorArray[i][j] = new Sector(i, j, this);
}
}
}
public int getRow()
{
return row;
}
public int getCol()
{
return col;
}
public void setRow(int r)
{
row = r;
}
public void setCol(int c)
{
col = c;
}
public Sector[][] getSectorArray()
{
return sectorArray;
}
public QuadrantView getQuadrantView()
{
return quadrantView;
}
public void setQuadrantView(QuadrantView quadrantView)
{
this.quadrantView = quadrantView;
}
}
static class Sector
{
//sector row
private int row;
//sector column
private int col;
//the quadrant this sector is in
private Quadrant quadrant;
//the view associated with this sector
private SectorView sectorView;
//boolean values to determine what this sector holds (Req. 4.1.0)
private boolean containsEnterprise;
//if the sector holds the Enterprise, store a reference to it
private Enterprise enterprise;
public Sector(int r, int c, Quadrant q)
{
row = r;
col = c;
quadrant = q;
setSectorView(new SectorView());
containsEnterprise = false;
//print the sector's coordinates on the gui
sectorView.setID(row + ", " + col);
}
public int getRow()
{
return row;
}
public int getCol()
{
return col;
}
public void setRow(int r)
{
row = r;
}
public void setCol(int c)
{
col = c;
}
public Quadrant getQuadrant()
{
return quadrant;
}
public boolean containsEnterprise()
{
return containsEnterprise;
}
public void setContainsEnterprise(boolean containsEnterprise)
{
this.containsEnterprise = containsEnterprise;
if (containsEnterprise)
{
sectorView.showEnterpriseIcon();
}
else
{
sectorView.hideEnterpriseIcon();
}
}
public Enterprise getEnterprise()
{
return enterprise;
}
public void addEnterprise(Enterprise enterprise)
{
this.enterprise = enterprise;
}
public void removeEnterprise()
{
enterprise = null;
}
public SectorView getSectorView()
{
return sectorView;
}
public void setSectorView(SectorView sectorView)
{
this.sectorView = sectorView;
}
public String toString()
{
return Integer.toString(row)+ "." + Integer.toString(col);
}
}
//end Sector class
static class SectorView extends JPanel
{
// default font for text
private final Font TREK_FONT = new Font("Verdana", Font.BOLD, 10);
// color for text
private final Color LABEL_COLOR = Color.BLACK;
// component layout
private SpringLayout layout;
// displays sector ID
private JLabel IDLabel;
//enterprise display
private JLabel enterpriseIcon;
/*
* create a new SectorView
*/
public SectorView()
{
super();
//create and set layout for child components
layout = new SpringLayout();
this.setLayout(layout);
//initialize child components
initComponents();
//position and display child components
layoutComponents();
//set background color
setBackground(Color.DARK_GRAY);
//set border
setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED));
//set size of sectors
setPreferredSize(new Dimension(QuadrantView.SECTOR_SIZE, QuadrantView.SECTOR_SIZE));
}
/*
* initialize components
*/
private void initComponents()
{
// displays ID of this view
IDLabel = new JLabel("");
IDLabel.setFont(TREK_FONT);
IDLabel.setForeground(Color.WHITE);
// create an enterprise icon and make it invisible
enterpriseIcon = new JLabel("E");
enterpriseIcon.setForeground(Color.WHITE);
enterpriseIcon.setVisible(false);
}
/*
* lay out components and add them to this view
*/
private void layoutComponents()
{
// position components:
// ID label
layout.putConstraint(SpringLayout.WEST, IDLabel, 1, SpringLayout.WEST, this);
layout.putConstraint(SpringLayout.NORTH, IDLabel, 1, SpringLayout.NORTH, this);
// enterprise icon
layout.putConstraint(SpringLayout.WEST, enterpriseIcon, 5, SpringLayout.WEST, this);
layout.putConstraint(SpringLayout.NORTH, enterpriseIcon, 30, SpringLayout.NORTH, this);
// add to view
this.add(IDLabel);
this.add(enterpriseIcon);
}
public void showEnterpriseIcon()
{
enterpriseIcon.setVisible(true);
}
public void hideEnterpriseIcon()
{
enterpriseIcon.setVisible(false);
}
//the sector's (row, col) coordinates within the quadrant
public void setID(String id)
{
IDLabel.setText(id);
}
}
//end SectorView class
static class QuadrantView extends JPanel
{
//size of sectors
public final static int SECTOR_SIZE = 100;
private final Color BACKGROUND_COLOR = Color.DARK_GRAY;
private SpringLayout layout;
/*
* create a new QuadrantView with the specified width
* and height
*
* #param quadrantHeight height of quad. in sectors
* #param quadrantWidth width of quad. in secors
*/
public QuadrantView(int quadrantHeight, int quadrantWidth)
{
//call JPanel constructor
super();
//create and set the layout
layout = new SpringLayout();
setLayout(layout);
//set the size of the QuadrantView we are creating using the inherited JComponent method
setPreferredSize(new Dimension(quadrantWidth * SECTOR_SIZE, quadrantHeight * SECTOR_SIZE));
//set background color using the inherited JComponent method
setBackground(BACKGROUND_COLOR);
}
/*
* add the specified Sector to this view
*
* each sector is represented by a (row, column) pair
* #param sectorView SectorView to be added to the QuadrantView
* #param row row coordinate
* #param col column coordinate
*/
public void addSectorView(SectorView sectorView, int row, int col)
{
//position the sector
layout.putConstraint(SpringLayout.WEST, sectorView, col * SECTOR_SIZE, SpringLayout.WEST, this);
layout.putConstraint(SpringLayout.NORTH, sectorView, row * SECTOR_SIZE, SpringLayout.NORTH, this);
//add sectorView to the layout using inherited method of Container class
this.add(sectorView);
}
}
static class Enterprise
{
protected Sector sectorLocation;
protected Quadrant quadrantLocation;
public Enterprise(Quadrant quadrant, Sector sector)
{
sectorLocation = sector;
quadrantLocation = quadrant;
sector.addEnterprise(this);
sector.setContainsEnterprise(true);
}
// Requirement 9.4.0
public boolean move(Sector destinationSector, final Quadrant destinationQuadrant, final GUI gui)
{
//if the destination quadrant is not our current quadrant, we need to update the gui (is updating this way causing a problem?)
if (!destinationQuadrant.equals(this.quadrantLocation))
{
//Put the new SectorViews in the new quadrant.
for (int i = 0; i < 8; i++)
{
for(int j = 0; j < 8; j++)
{
destinationQuadrant.getQuadrantView().addSectorView(destinationQuadrant.getSectorArray()[i][j].getSectorView(), i, j);
}
}
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
//initialize gui with the starting quadrant quadrantView
//replace the old quadrant view with the new one
gui.resetGUI(destinationQuadrant.getQuadrantView());
}
});
}
//remove the reference to this starship from the current sector
sectorLocation.removeEnterprise();
//sector no longer contains the Enterprise
sectorLocation.setContainsEnterprise(false);
//move to destination quadrant
quadrantLocation = destinationQuadrant;
//move to destination sector
sectorLocation = destinationSector;
//add a reference to this starship to the new sector
sectorLocation.addEnterprise(this);
//new sector now contains Enterprise
sectorLocation.setContainsEnterprise(true);
return true;
}
}//end Enterprise class
}
Limit the amount of data exchanged between threads. The only data which needs to be exchanged is the input from keyboard. Especially avoid sharing fields between threads - this leads to race conditions. Your main loop should look like this:
while(true)
{
final int qRow = input.nextInt();
final int qCol = input.nextInt();
final int sRow = input.nextInt();
final int sCol = input.nextInt();
SwingUtilities.invokeAndWait(new Runnable() {
#Override
public void run() {
move(qRow,qCol,sRow,sCol);
}
});
}
Remove all other invokeAndWait and invokeLater. Do not use invokeLater at all. It makes your program unpredictable.
Try to declare variables right before they are initialized and mark them as final. Mutable state leads to bugs.
I was not able to figure out why the table shrinks to 1x1. Try using GridLayout instead of SpringLayout. It seems better suited for this scenario.

Java JScrollPane Update Image

I'm making a program that has an image that you scroll around on, and I can't figure out how to update the image if a button is pressed (For example: Adds a Green Ellipse to the image.) It already draws the image into the JScrollPane and you can scroll around, but when you click a button it doesn't refresh the image. (more details in code)
Here is the code:
public class PegMaster extends JPanel implements ActionListener {
//Note: not complete code
public PegBox[] pegbox = new PegBox[9];
public static Dimension size = new Dimension(520, 500);
public BufferedImage canvas;
public Graphics2D g2d;
public JScrollPane scroller;
JPanel panel;
private Canvas window;
JScrollPane pictureScrollPane;
public PegMaster() {
JButton button = new JButton("test");
button.addActionListener(this);
add(button);
canvas = new BufferedImage((int)size.getWidth()-30, 75 * GUESSES, BufferedImage.TYPE_INT_RGB);
g2d = canvas.createGraphics();
for(int i = 0;i<=pegbox.length-1;i++) {
pegbox[i] = new PegBox(i, g2d);
}
window = new Canvas(new ImageIcon(toImage(canvas)), 1);
//Class Canvas is a Scrollable JLabel to draw to (the image)
pictureScrollPane = new JScrollPane(window);
pictureScrollPane.setPreferredSize(new Dimension((int)size.getWidth()-10, (int)size.getHeight()-20));
pictureScrollPane.setViewportBorder(BorderFactory.createLineBorder(Color.black));
add(pictureScrollPane);
//adds the scrollpane, but can't update the image in it
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createGUI();
//just adds the scrollpane
}
});
}
public void paint(Graphics g) {
super.paint(g);
for(int i = 0;i<=pegbox.length-1;i++) {
//pegbox[i] = new PegBox(i);
pegbox[i].draw(g2d);
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//tried re-making the scrollpane, didn't work.
//window = new Canvas(new ImageIcon(toImage(canvas)), 1);
//pictureScrollPane = new JScrollPane(window);
//pictureScrollPane.setPreferredSize(new Dimension((int)size.getWidth()-10 (int)size.getHeight()-20));
//pictureScrollPane.setViewportBorder(BorderFactory.createLineBorder(Color.black));
//tried imageupdate: pictureScrollPane.imageUpdate(canvas, 0, 0, 0 (int)size.getWidth()-10, (int)size.getHeight()-20);
//remove(pictureScrollPane);
//tried this: pictureScrollPane.revalidate();
repaint();
}
}
Firstly, don't use Canvas it's a heavy weight component, it will only cause you issues in the long run, use either JComponent or JPanel
Secondly, don't override paint, use paintComponent instead. paint does a lot of work, including painting things like the border and child components. It's better if you use paintComponent as it's at the right layer within the paint hierarchy for what you want do to.
Thirdly, NEVER call something like Thread.sleep while in the Event Dispatching Thread. This will cause the event queue to pause and stop responding to events, making you program look like it's stalled.
Fourthly, NEVER call repaint (invalidate, revalidate or any method that might cause a repaint request to occur) within a paint method. You will simply end up maxing out your CPU and you will be forced to kill the process.
Fifthly, you didn't provide the actionPerformed method, which is probably where all the action (and problems) are. I'd imagin you need to call window.repaint() and possibly window.invalidate() (in reverse order), but since you didn't provided use with this code, that's simply speculation...
Try this class which displays an Image. This can be added to a JScrollPane
public class ImagePanel extends JPanel {
public Image img;
public ImagePanel(Image img){
this.img = img;
}
public void paintComponent(Graphics g){
super.paintComponent(g);
g.drawImage(img, 0, 0, this);
}
}
Now add this class to the JScrollPane. To update it, just change the image reference and call the repaint() method on the component
The above solution didn't solved my purpose so I researched and found this.
Please follow the link for whole example. I have added the code to refer in case the link changes.
public class ScrollablePicture extends JLabel
implements Scrollable,
MouseMotionListener {
private int maxUnitIncrement = 1;
private boolean missingPicture = false;
public ScrollablePicture(ImageIcon i, int m) {
super(i);
if (i == null) {
missingPicture = true;
setText("No picture found.");
setHorizontalAlignment(CENTER);
setOpaque(true);
setBackground(Color.white);
}
maxUnitIncrement = m;
//Let the user scroll by dragging to outside the window.
setAutoscrolls(true); //enable synthetic drag events
addMouseMotionListener(this); //handle mouse drags
}
//Methods required by the MouseMotionListener interface:
public void mouseMoved(MouseEvent e) { }
public void mouseDragged(MouseEvent e) {
//The user is dragging us, so scroll!
Rectangle r = new Rectangle(e.getX(), e.getY(), 1, 1);
scrollRectToVisible(r);
}
public Dimension getPreferredSize() {
if (missingPicture) {
return new Dimension(320, 480);
} else {
return super.getPreferredSize();
}
}
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
public int getScrollableUnitIncrement(Rectangle visibleRect,
int orientation,
int direction) {
//Get the current position.
int currentPosition = 0;
if (orientation == SwingConstants.HORIZONTAL) {
currentPosition = visibleRect.x;
} else {
currentPosition = visibleRect.y;
}
//Return the number of pixels between currentPosition
//and the nearest tick mark in the indicated direction.
if (direction < 0) {
int newPosition = currentPosition -
(currentPosition / maxUnitIncrement)
* maxUnitIncrement;
return (newPosition == 0) ? maxUnitIncrement : newPosition;
} else {
return ((currentPosition / maxUnitIncrement) + 1)
* maxUnitIncrement
- currentPosition;
}
}
public int getScrollableBlockIncrement(Rectangle visibleRect,
int orientation,
int direction) {
if (orientation == SwingConstants.HORIZONTAL) {
return visibleRect.width - maxUnitIncrement;
} else {
return visibleRect.height - maxUnitIncrement;
}
}
public boolean getScrollableTracksViewportWidth() {
return false;
}
public boolean getScrollableTracksViewportHeight() {
return false;
}
public void setMaxUnitIncrement(int pixels) {
maxUnitIncrement = pixels;
}

Show ScrollBars in FlowLayout only when necessary

I will rephrase my question:**
How to prevent Java ScrollBar from being enabled in FlowLayout when there is enough space to show all items by warping them.
**
Here a screenshot of what I am trying to achieve:
Note that scrollbar is disabled when it is not necessary.
And when you resize the window scrollbar should appear if some items are out the viewplane
P.S. I am aware of things called documentation and web.
Your updated screen shots suggest that you may be looking for Wrap Layout, which "extends the FlowLayout" in a way that "will result in synchronizing the preferred size of the container with the layout of the container." See also Creating a Custom Layout Manager.
The default scroll bar policy is as needed, but I remember having to account for the FlowLayout gaps to get an even number initially. If you stretch this example out, you'll see the horizontal scroll bar disappear.
Update: It doesn't really fix your problem, but it shows how I implemented Scrollable. I wanted to get rid of setPreferredSize(), even tho' the image is just a placeholder. The Wrap Layout article talks about why FlowLayout works the way it does.
import javax.swing.*;
import java.awt.*;
import java.util.Random;
public class ImageScrollTest extends JPanel implements Scrollable {
private static final int N = 8;
private static final int W = 120;
private static final int H = 100;
private final FlowLayout layout = new FlowLayout();
private final int hGap = layout.getHgap();
private final int vGap = layout.getVgap();
private final Dimension size;
// Show n of N images in a Scrollable FlowLayout
public ImageScrollTest(int n) {
setLayout(layout);
for (int i = 0; i < N; i++) {
this.add(new ImagePanel());
}
size = new Dimension(n * W + (n + 1) * hGap, H + 2 * vGap);
}
#Override
public Dimension getPreferredScrollableViewportSize() {
return size;
}
#Override
public int getScrollableUnitIncrement(
Rectangle visibleRect, int orientation, int direction) {
return getIncrement(orientation);
}
#Override
public int getScrollableBlockIncrement(
Rectangle visibleRect, int orientation, int direction) {
return getIncrement(orientation);
}
private int getIncrement(int orientation) {
if (orientation == JScrollBar.HORIZONTAL) {
return W + hGap;
} else {
return H + vGap;
}
}
#Override
public boolean getScrollableTracksViewportWidth() {
return false;
}
#Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
private static class ImagePanel extends JPanel {
private static final Random rnd = new Random();
private Color color = new Color(rnd.nextInt());
public ImagePanel() {
this.setBackground(color);
this.setBorder(BorderFactory.createLineBorder(Color.blue));
}
#Override
public Dimension getPreferredSize() {
return new Dimension(W, H);
}
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
createAndShowGUI();
}
});
}
private static void createAndShowGUI() {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ImageScrollTest ist = new ImageScrollTest(N / 2);
JScrollPane sp = new JScrollPane(ist);
sp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
f.add(sp);
f.pack();
f.setVisible(true);
}
}
You shoud specify to the scrollPane when you want the scrollBars to apprear (possible in both horizontal and vertical direction):
myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
There is also: HORIZONTAL_SCROLLBAR_ALWAYS and HORIZONTAL_SCROLLBAR_NEVER if you need that anytime soon.
EDIT: For more information, and exactly my answer: http://docs.oracle.com/javase/tutorial/uiswing/components/scrollpane.html

Categories