Related
I'm creating a Tic Tac Toe game where I'm using MouseListener to add functionality to my game panel. When a user clicks one of the cells its suppose to generate an X or O graphic depending on whose turn it is. I've tried adding the MouseListener to the pane, but when I run it nothing happens when I click. Any ideas on how I can fix this?
Here's my game panel:
public GameMain() {
Handler handler = new Handler();
this.addMouseListener(handler);
// setup JLabel
label = new JLabel(" ");
label.setBorder(BorderFactory.createEmptyBorder(2, 5, 4, 5));
label.setOpaque(true);
label.setBackground(Color.LIGHT_GRAY);
setLayout(new BorderLayout());
add(label, BorderLayout.SOUTH);
setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT + 30));
board = new Board();
initGame();
}
}
Here's my Handler class with the mouseClick() method that's suppose to run:
public class Handler extends MouseAdapter {
public void mouseClick(MouseEvent e) {
int mouseX = e.getX();
int mouseY = e.getY();
// Get the row and column clicked
int rowSelected = mouseY / CELL_SIZE;
int colSelected = mouseX / CELL_SIZE;
if (currentState == GameState.PLAYING) {
if (rowSelected >= 0 && rowSelected < board.cells.length && colSelected >= 0 && colSelected < board.cells.length &&
board.cells[rowSelected][colSelected].content == Seed.EMPTY) {
board.cells[rowSelected][colSelected].content = currentPlayer; // move
updateGame(currentPlayer, rowSelected, colSelected); // update currentState
currentPlayer = (currentPlayer == Seed.X) ? Seed.O : Seed.X;
}
} else {
initGame();
}
repaint();
}
public void handleButtonPress(Object o) {
if (o == singlePlayer) {
singlePlayerGame();
}
if (o == multiPlayer) {
multiPlayerGame();
}
}
}
Your question inspired me to take up the challenge of writing a tic-tac-toe game, since I don't have much experience in handling custom painting. The below code is copiously commented, so I hope that those comments will serve as a good explanation of the code.
I'm presuming that people with more experience than me will find flaws in the below code, which is to be expected, since, as I already mentioned, I don't have a lot of experience in this kind of programming. Nonetheless, I hope it is good enough in order to be of help you.
Note that the below code makes use of a new java feature that was introduced with JDK 14, namely Records. Hence it may also be helpful to people as a simple example of how to integrate java records into their code. If you aren't using JDK 14 or if you are but you haven't enabled preview features, you can simply replace the record definition with a simple class. Just replace the word record with class and add a constructor and "getter" methods to the code. By default, the name of a "getter" method in a record is simply the name of the field, e.g. for member minX in record Square (in below code), the "getter" method is minX().
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.WindowConstants;
/**
* A simple tic-tac-toe (a.k.a. noughts and crosses) game. User clicks mouse on an empty square of
* the game board and either a nought or a cross is drawn in that square, depending on whose turn
* it is. Each square has an index as shown below.
* <pre>
* 0 1 2
*
* 3 4 5
*
* 6 7 8
* </pre>
* See Tic-tac-toe on <i>Wikipedia</i>.
*/
public class GameMain extends JPanel implements ActionListener, MouseListener, Runnable {
/** For serializing instances of this class. */
private static final long serialVersionUID = 7014608855599083001L;
/** A cross. */
private static final char CROSS = 'X';
/** A nought. */
private static final char NOUGHT = 'O';
/** Dimension of each square in tic-tac-toe board. */
private static final int SQUARE_DIMENSION = 80;
/** Number of consecutive squares required for a win. */
private static final int REQUIRED_SQUARES = 3;
/** Total number of squares in tic-tac-toe board. */
private static final int TOTAL_SQUARES = 9;
/** Each of the 9 squares in tic-tac-toe board. */
private final Square[] SQUARES = new Square[]{new Square(1, 1, 99, 99),
new Square(101, 1, 199, 99),
new Square(201, 1, 299, 99),
new Square(1, 101, 99, 199),
new Square(101, 101, 199, 199),
new Square(201, 101, 299, 199),
new Square(1, 201, 99, 299),
new Square(101, 201, 199, 299),
new Square(201, 201, 299, 299)};
/** Text for {#link #turnLabel} at start of a new tic-tac-toe game. */
private static final String FIRST_MOVE = "X goes first";
/** Text for <i>new game</i>. button. */
private static final String NEW_GAME = "New Game";
/** Indicates start of a new game. */
private boolean newGame;
/** <tt>true</tt> means O turn and <tt>false</tt> means X turn. */
private boolean oTurn;
/** Records occupied squares, either 'O' or 'X' */
private char[] occupied = new char[TOTAL_SQUARES];
/** Number of unoccupied squares in tic-tac-toe board. */
private int freeCount;
/** Displays whose turn it currently is. */
private JLabel turnLabel;
/** Location of last mouse click. */
private Point clickPoint;
/**
* Creates and returns instance of this class.
*/
public GameMain() {
setPreferredSize(new Dimension(300, 300));
addMouseListener(this);
freeCount = TOTAL_SQUARES;
}
private static boolean isValidRequirement(int index) {
return index >= 0 && index < REQUIRED_SQUARES;
}
/**
* Determines whether <var>square</var> is a valid index of a square in tic-tac-toe board.
*
* #param square - will be validated.
*
* #return <tt>true</tt> if <var>square</var> is valid.
*/
private static boolean isValidSquare(int square) {
return square >= 0 && square < TOTAL_SQUARES;
}
#Override // java.awt.event.ActionEvent
public void actionPerformed(ActionEvent actnEvnt) {
String actionCommand = actnEvnt.getActionCommand();
switch (actionCommand) {
case NEW_GAME:
startNewGame();
break;
default:
JOptionPane.showMessageDialog(this,
actionCommand,
"Unhandled",
JOptionPane.WARNING_MESSAGE);
}
}
#Override // java.awt.event.MouseListener
public void mouseClicked(MouseEvent mousEvnt) {
// Do nothing.
}
#Override // java.awt.event.MouseListener
public void mouseEntered(MouseEvent mousEvnt) {
// Do nothing.
}
#Override // java.awt.event.MouseListener
public void mouseExited(MouseEvent mousEvnt) {
// Do nothing.
}
#Override // java.awt.event.MouseListener
public void mousePressed(MouseEvent mousEvnt) {
// Do nothing.
}
#Override // java.awt.event.MouseListener
public void mouseReleased(MouseEvent mousEvnt) {
System.out.println("mouse clicked");
clickPoint = mousEvnt.getPoint();
repaint();
EventQueue.invokeLater(() -> {
String text;
if (isWinner()) {
text = oTurn ? "O has won!" : "X has won!";
removeMouseListener(this);
JOptionPane.showMessageDialog(this,
text,
"Winner",
JOptionPane.PLAIN_MESSAGE);
}
else if (freeCount <= 0) {
text = "Drawn game.";
removeMouseListener(this);
JOptionPane.showMessageDialog(this,
text,
"Draw",
JOptionPane.PLAIN_MESSAGE);
}
else {
oTurn = !oTurn;
text = oTurn ? "O's turn" : "X's turn";
}
turnLabel.setText(text);
});
}
#Override // java.lang.Runnable
public void run() {
createAndShowGui();
}
#Override // javax.swing.JComponent
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.drawLine(100, 0, 100, 300);
g2d.drawLine(200, 0, 200, 300);
g2d.drawLine(0, 100, 300, 100);
g2d.drawLine(0, 200, 300, 200);
if (!newGame) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setStroke(new BasicStroke(3));
for (int i = 0; i < TOTAL_SQUARES; i++) {
if (occupied[i] == NOUGHT) {
drawNought(i, g2d);
}
else if (occupied[i] == CROSS) {
drawCross(i, g2d);
}
}
int square = getSquare(clickPoint);
if (isFreeSquare(square)) {
if (oTurn) {
drawNought(square, g2d);
occupied[square] = NOUGHT;
}
else {
drawCross(square, g2d);
occupied[square] = CROSS;
}
freeCount--;
}
}
else {
newGame = false;
}
}
private void createAndShowGui() {
JFrame frame = new JFrame("O & X");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.add(createTurnPanel(), BorderLayout.PAGE_START);
frame.add(this, BorderLayout.CENTER);
frame.add(createButtonsPanel(), BorderLayout.PAGE_END);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private JButton createButton(String text, int mnemonic, String tooltip) {
JButton button = new JButton(text);
if (mnemonic > 0) {
button.setMnemonic(mnemonic);
}
if (tooltip != null && !tooltip.isBlank()) {
button.setToolTipText(tooltip);
}
button.addActionListener(this);
return button;
}
private JPanel createButtonsPanel() {
JPanel buttonsPanel = new JPanel();
buttonsPanel.setBorder(BorderFactory.createMatteBorder(2, 0, 0, 0, Color.GRAY));
buttonsPanel.add(createButton(NEW_GAME, KeyEvent.VK_N, "Start a new game."));
return buttonsPanel;
}
private JPanel createTurnPanel() {
JPanel turnPanel = new JPanel();
turnPanel.setBorder(BorderFactory.createMatteBorder(0, 0, 2, 0, Color.GRAY));
turnLabel = new JLabel(FIRST_MOVE);
turnPanel.add(turnLabel);
return turnPanel;
}
/**
* Draws a {#link #CROSS} in <var>square</var> of tic-tac-toe board.
*
* #param square - index of square in tic-tac-toe board.
* #param g2d - facilitates drawing.
*/
private void drawCross(int square, Graphics2D g2d) {
if (isValidSquare(square) && g2d != null) {
g2d.setColor(Color.BLUE);
g2d.drawLine(SQUARES[square].minX() + 10,
SQUARES[square].minY() + 10,
SQUARES[square].maxX() - 10,
SQUARES[square].maxY() - 10);
g2d.drawLine(SQUARES[square].maxX() - 10,
SQUARES[square].minY() + 10,
SQUARES[square].minX() + 10,
SQUARES[square].maxY() - 10);
}
}
/**
* Draws a {#link #NOUGHT} in <var>square</var> of tic-tac-toe board.
*
* #param square - index of square in tic-tac-toe board.
* #param g2d - facilitates drawing.
*/
private void drawNought(int square, Graphics2D g2d) {
if (isValidSquare(square) && g2d != null) {
g2d.setColor(Color.RED);
g2d.drawOval(SQUARES[square].minX() + 10,
SQUARES[square].minY() + 10,
SQUARE_DIMENSION,
SQUARE_DIMENSION);
}
}
/**
* Returns the square of the tic-tac-toe board that contains <var>pt</var>.
*
* #param pt - point on tic-tac-toe board.
*
* #return index of square in tic-tac-toe board containing <var>pt</var> or -1 (negative one)
* if <var>pt</var> not in tic-tac-toe board.
*
* #see Square#contains(int, int)
*/
private int getSquare(Point pt) {
int ndx = -1;
if (pt != null) {
for (int i = 0; i < 9; i++) {
if (SQUARES[i].contains(pt.x, pt.y)) {
ndx = i;
break;
}
}
}
return ndx;
}
/**
* Determines whether <var>column</var> of tic-tac-toe board contains all {#link #CROSS} or all
* {#link #NOUGHT}.
*
* #param column - index of column in tic-tac-toe board.
*
* #return <tt>true</tt> if <var>column</var> contains all {#link #CROSS} or all {#link #NOUGHT}.
*
* #see #isValidRequirement(int)
*/
private boolean isColumnWin(int column) {
boolean isWin = false;
if (isValidRequirement(column)) {
isWin = isOccupied(column) &&
occupied[column] == occupied[column + REQUIRED_SQUARES] &&
occupied[column] == occupied[column + (REQUIRED_SQUARES * 2)];
}
return isWin;
}
/**
* Determines whether diagonal of tic-tac-toe board contains all {#link #CROSS} or all
* {#link #NOUGHT}. The board contains precisely two diagonals where each one includes the
* board's center square, i.e. the square with index 4. The other squares that constitute
* diagonals are the corner squares, including the indexes 0 (zero), 2, 6 and 8.
*
* #return <tt>true</tt> if one of the tic-tac-toe board diagonals contains all {#link #CROSS}
* or all {#link #NOUGHT}.
*
* #see #isValidRequirement(int)
*/
private boolean isDiagonalWin() {
boolean isWin = false;
isWin = (isOccupied(0) &&
occupied[0] == occupied[4] &&
occupied[0] == occupied[8])
||
(isOccupied(2) &&
occupied[2] == occupied[4] &&
occupied[2] == occupied[6]);
return isWin;
}
/**
* Determines whether <var>square</var> in tic-tac-toe board does not contain a {#link #CROSS}
* nor a {#link #NOUGHT}.
*
* #param square - index of square in tic-tac-toe board.
*
* #return <tt>true</tt> if <var>square</var> does not contain a {#link #CROSS} nor a {#link
* #NOUGHT}.
*/
private boolean isFreeSquare(int square) {
boolean freeSquare = false;
if (isValidSquare(square)) {
freeSquare = occupied[square] != CROSS && occupied[square] != NOUGHT;
}
return freeSquare;
}
/**
* Determines whether <var>row</var> of tic-tac-toe board contains all {#link #CROSS} or all
* {#link #NOUGHT}.
*
* #param row - index of row in tic-tac-toe board.
*
* #return <tt>true</tt> if <var>row</var> contains all {#link #CROSS} or all {#link #NOUGHT}.
*
* #see #isValidRequirement(int)
*/
private boolean isLineWin(int row) {
boolean isWin = false;
if (isValidRequirement(row)) {
int index = row * REQUIRED_SQUARES;
isWin = isOccupied(index) &&
occupied[index] == occupied[index + 1] &&
occupied[index] == occupied[index + 2];
}
return isWin;
}
/**
* Determines whether square at <var>index</var> in tic-tac-toe board contains either a {#link
* #CROSS} or a {#link #NOUGHT}.
*
* #param index - index of square in tic-tac-toe board.
*
* #return <tt>true</tt> if square at <var>index</var> in tic-tac-toe board contains either a
* {#link #CROSS} or a {#link #NOUGHT}.
*
* #see #isValidSquare(int)
*/
private boolean isOccupied(int index) {
boolean occupied = false;
if (isValidSquare(index)) {
occupied = this.occupied[index] == CROSS || this.occupied[index] == NOUGHT;
}
return occupied;
}
/**
* Determines if there is a winner.
*
* #return <tt>true</tt> if someone has won the game.
*
* #see #isColumnWin(int)
* #see #isDiagonalWin()
* #see #isLineWin(int)
*/
private boolean isWinner() {
return isLineWin(0) ||
isLineWin(1) ||
isLineWin(2) ||
isColumnWin(0) ||
isColumnWin(1) ||
isColumnWin(2) ||
isDiagonalWin();
}
/**
* Initializes the GUI in order to start a new game.
*/
private void startNewGame() {
freeCount = TOTAL_SQUARES;
newGame = true;
oTurn = false;
occupied = new char[TOTAL_SQUARES];
repaint();
EventQueue.invokeLater(() -> {
removeMouseListener(this);
addMouseListener(this);
turnLabel.setText(FIRST_MOVE);
});
}
/**
* This method is the first one called when this class is launched via the <tt>java</tt>
* command. It ignores the method parameter <var>args</var>.
*
* #param args - <tt>java</tt> command arguments.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new GameMain());
}
}
/**
* Represents the geometrical shape known as a square. For the purposes of this {#code record}, a
* square is defined by two points that indicate its top, left corner and its bottom, right corner.
*/
record Square (int minX, int minY, int maxX, int maxY) {
/**
* Determines whether the supplied point lies within this square.
*
* #param x - x coordinate of a point.
* #param y - y coordinate of same point (as x).
*
* #return <tt>true</tt> if supplied point lies within this square.
*/
public boolean contains(int x, int y) {
return minX <= x && x <= maxX && minY <= y && y <= maxY;
}
}
I am having trouble. I need to rotate an equilateral triangle around it's centre by using the drag listener and click listener. The triangle should grow but now change angles and be rotated by a point while being centred at the middle of the triangle. This is my problem, it is currently dragging by the point 3 and rotating around point 1. I have an array of values x and y and it stores 4 values each containing the initial point first at ordinal value 0 and point 1 2 and 3 at the corresponding values.
`
public class DrawTriangle extends JFrame {
enter code here
/** The Constant NUMBER_3. */
private static final int NUMBER_3 = 3;
/** The Constant EQUL_ANGLE. */
#SuppressWarnings("unused")
private static final double EQUL_ANGLE = 1;
/** The Constant TRIANGLE_POINTS. */
private static final int TRIANGLE_POINTS = 4;
/** The Constant _400. */
private static final int SIZE = 400;
/** The x points. */
private int [] xPoints = new int[TRIANGLE_POINTS];
/** The y points. */
private int [] yPoints = new int[TRIANGLE_POINTS];
private int xInitial;
private int yInitial;
/** The x. */
private double x = EQUL_ANGLE;
/** The new x. */
private double newX;
/** The new y. */
private double newY;
/**
* Instantiates a new draw triangle.
*/
public DrawTriangle() {
super("Dimitry Rakhlei");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setContentPane(new DrawTrianglePanel());
setSize(SIZE, SIZE); // you can change this size but don't make it HUGE!
setVisible(true);
}
/**
* The Class DrawTrianglePanel.
*/
private class DrawTrianglePanel extends JPanel implements MouseListener,
MouseMotionListener {
/**
* Instantiates a new draw triangle panel.
*/
public DrawTrianglePanel() {
addMouseListener(this);
addMouseMotionListener(this);
}
/**
* Drawing the triangle.
*
* #param g
* the g
* #see javax.swing.JComponent#paintComponent(java.awt.Graphics)
*/
public void paintComponent(Graphics g) {
super.paintComponent(g);
// DRAWING CODE HERE
g.drawPolygon(xPoints, yPoints, 3);
System.out.println("Paint called");
}
/**
* (non-Javadoc).
*
* #param e
* the e
* #see java.awt.event.MouseListener#mousePressed
* (java.awt.event.MouseEvent)
*/
public void mousePressed(MouseEvent e) {
System.out.println("Mouse pressed called");
e.getPoint();
xPoints[0] = e.getPoint().x;
yPoints[0] = e.getPoint().y;
repaint();
}
/**
* (non-Javadoc).
*
* #param e
* the e
* #see java.awt.event.MouseListener#mouseReleased
* (java.awt.event.MouseEvent)
*/
public void mouseReleased(MouseEvent e) {
System.out.println("Mouse released called");
}
/**
* (non-Javadoc).
*
* #param e
* the e
* #see java.awt.event.MouseMotionListener#mouseDragged
* (java.awt.event.MouseEvent)
*/
public void mouseDragged(MouseEvent e) {
System.out.println("Mouse dragged called");
newX = e.getPoint().x;
newY = e.getPoint().y;
xPoints[1] = (int) newX;
yPoints[1] = (int) newY;
newX = xPoints[0] + (xPoints[1]-xPoints[0])*Math.cos(x) - (yPoints[1]-yPoints[0])*Math.sin(x);
newY = yPoints[0] + (xPoints[1]-xPoints[0])*Math.sin(x) + (yPoints[1]-yPoints[0])*Math.cos(x);
xPoints[2] = (int) newX;
yPoints[2] = (int) newY;
newX = xPoints[0] + (xPoints[1]-xPoints[0])*Math.cos(x) - (yPoints[1]-yPoints[0])*Math.sin(x);
newY = yPoints[0] + (xPoints[1]-xPoints[0])*Math.sin(x) + (yPoints[1]-yPoints[0])*Math.cos(x);
xPoints[3] = (int) newX;
yPoints[3] = (int) newY;
repaint();
}
/**
* (non-Javadoc).
*
* #param e
* the e
* #see java.awt.event.MouseListener#mouseEntered
* (java.awt.event.MouseEvent)
*/
public void mouseEntered(MouseEvent e) {
System.out.println("Mouse Entered.");
}
/**
* (non-Javadoc).
*
* #param e
* the e
* #see java.awt.event.MouseListener#mouseExited
* (java.awt.event.MouseEvent)
*/
public void mouseExited(MouseEvent e) {
System.out.println("Mouse exited.");
}
/**
* (non-Javadoc).
*
* #param e
* the e
* #see java.awt.event.MouseListener#mouseClicked
* (java.awt.event.MouseEvent)
*/
public void mouseClicked(MouseEvent e) {
}
/**
* (non-Javadoc).
*
* #param e
* the e
* #see java.awt.event.MouseMotionListener#mouseMoved
* (java.awt.event.MouseEvent)
*/
public void mouseMoved(MouseEvent e) {
}
}
/**
* The main method.
*
* #param args
* the arguments
*/
public static void main(String[] args) {
new DrawTriangle();
}
};`
My issue is that this code basically runs correctly but I am told the vertex point of rotation has to be in the middle of the triangle. Mine is the first point.
Start by taking a look at 2D Graphics, in particular Transforming Shapes, Text, and Images.
Basically, your "polygon" will have a definable size (the maximum x/y point), from this, you can determine the center position of the "polygon", for example...
protected Dimension getTriangleSize() {
int maxX = 0;
int maxY = 0;
for (int index = 0; index < xPoints.length; index++) {
maxX = Math.max(maxX, xPoints[index]);
}
for (int index = 0; index < yPoints.length; index++) {
maxY = Math.max(maxY, yPoints[index]);
}
return new Dimension(maxX, maxY);
}
This just returns the maximum x and y bounds of your polygon. This allows you to calculate the center position of the polygon. You'll see why in a second why you don't need to actually specify the origin point...
Next, we calculate a AffineTransform, which is the applied to the Graphics context directly...
Graphics2D g2d = (Graphics2D) g.create();
AffineTransform at = new AffineTransform();
Dimension size = getTriangleSize();
int x = clickPoint.x - (size.width / 2);
int y = clickPoint.y - (size.height / 2);
at.translate(x, y);
at.rotate(Math.toRadians(angle), clickPoint.x - x, clickPoint.y - y);
g2d.setTransform(at);
g2d.drawPolygon(xPoints, yPoints, 3);
// Guide
g2d.setColor(Color.RED);
g2d.drawLine(size.width / 2, 0, size.width / 2, size.height / 2);
g2d.dispose();
This not only translates the triangle position, but will also rotate it. What this means you can create a normalised polygon (whose origin point is 0x0) and allow the Graphics context to place it where you want it, this makes life SO much easier...
Now, the rotation calculation is based on calculating the angle between two points, the "click" point and the "drag" point...
angle = -Math.toDegrees(Math.atan2(e.getPoint().x - clickPoint.x, e.getPoint().y - clickPoint.y)) + 180;
Which is based on the solution in this question
For example...
The red line is simple a guide to show that the tip of the triangle is point towards the mouse...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class DrawTriangle extends JFrame {
/**
* The x points.
*/
private int[] xPoints = new int[]{0, 25, 50};
/**
* The y points.
*/
private int[] yPoints = new int[]{50, 0, 50};
double angle = 0f;
/**
* Instantiates a new draw triangle.
*/
public DrawTriangle() {
super("Dimitry Rakhlei");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setContentPane(new DrawTrianglePanel());
pack();
setLocationRelativeTo(null);
setVisible(true);
}
/**
* The Class DrawTrianglePanel.
*/
private class DrawTrianglePanel extends JPanel implements MouseListener,
MouseMotionListener {
private Point clickPoint;
/**
* Instantiates a new draw triangle panel.
*/
public DrawTrianglePanel() {
addMouseListener(this);
addMouseMotionListener(this);
clickPoint = new Point(100, 100);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
protected Dimension getTriangleSize() {
int maxX = 0;
int maxY = 0;
for (int index = 0; index < xPoints.length; index++) {
maxX = Math.max(maxX, xPoints[index]);
}
for (int index = 0; index < yPoints.length; index++) {
maxY = Math.max(maxY, yPoints[index]);
}
return new Dimension(maxX, maxY);
}
/**
* Drawing the triangle.
*
* #param g the g
* #see javax.swing.JComponent#paintComponent(java.awt.Graphics)
*/
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
AffineTransform at = new AffineTransform();
Dimension size = getTriangleSize();
int x = clickPoint.x - (size.width / 2);
int y = clickPoint.y - (size.height / 2);
at.translate(x, y);
at.rotate(Math.toRadians(angle), clickPoint.x - x, clickPoint.y - y);
g2d.setTransform(at);
g2d.drawPolygon(xPoints, yPoints, 3);
// Guide
g2d.setColor(Color.RED);
g2d.drawLine(size.width / 2, 0, size.width / 2, size.height / 2);
g2d.dispose();
}
/**
* (non-Javadoc).
*
* #param e the e
* #see java.awt.event.MouseListener#mousePressed (java.awt.event.MouseEvent)
*/
#Override
public void mousePressed(MouseEvent e) {
System.out.println("Mouse pressed called");
// clickPoint = e.getPoint();
repaint();
}
/**
* (non-Javadoc).
*
* #param e the e
* #see java.awt.event.MouseListener#mouseReleased (java.awt.event.MouseEvent)
*/
#Override
public void mouseReleased(MouseEvent e) {
System.out.println("Mouse released called");
}
/**
* (non-Javadoc).
*
* #param e the e
* #see java.awt.event.MouseMotionListener#mouseDragged (java.awt.event.MouseEvent)
*/
public void mouseDragged(MouseEvent e) {
System.out.println("Mouse dragged called");
angle = -Math.toDegrees(Math.atan2(e.getPoint().x - clickPoint.x, e.getPoint().y - clickPoint.y)) + 180;
repaint();
}
/**
* (non-Javadoc).
*
* #param e the e
* #see java.awt.event.MouseListener#mouseEntered (java.awt.event.MouseEvent)
*/
public void mouseEntered(MouseEvent e) {
System.out.println("Mouse Entered.");
}
/**
* (non-Javadoc).
*
* #param e the e
* #see java.awt.event.MouseListener#mouseExited (java.awt.event.MouseEvent)
*/
public void mouseExited(MouseEvent e) {
System.out.println("Mouse exited.");
}
/**
* (non-Javadoc).
*
* #param e the e
* #see java.awt.event.MouseListener#mouseClicked (java.awt.event.MouseEvent)
*/
public void mouseClicked(MouseEvent e) {
}
/**
* (non-Javadoc).
*
* #param e the e
* #see java.awt.event.MouseMotionListener#mouseMoved (java.awt.event.MouseEvent)
*/
public void mouseMoved(MouseEvent e) {
}
}
/**
* The main method.
*
* #param args the arguments
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
new DrawTriangle();
}
});
}
}
Now, before you jump all over me and complain that the solution is "too complex", understand that I'm an idiot, seriously, my 2 year old has a better grasp on basic mathematics then I do, this is the most simplistic solution I can come up with that doesn't melt my brain and uses the dual array polygon API. Personally, I'd use the Shape API, but that's not what you started with...
I'm trying to create a custom layout that will show child Views in a dynamic grid fashion. Kind of like pinterest, but I haven't gotten that far yet.
Here is my layout class called PostsLayout
/**
* Displays Post records in different sizes.
*/
public class PostsLayout extends RelativeLayout
{
private class PostPos
{
public Button view;
public int Row;
public int RowSpan;
public int Column;
public int ColSpan;
public int Size;
public PostPos(Context context, int row, int row_span, int column, int col_span)
{
view = new Button(context);
view.setText("btn");
Row = row;
RowSpan = row_span;
Column = column;
ColSpan = col_span;
}
public void update(int size)
{
Size = size;
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(size, size);
lp.leftMargin = size * Column;
lp.topMargin = size * Row;
view.setLayoutParams(lp);
}
}
private ArrayList<PostPos> _posts;
/**
* The number of columns.
*/
private int _columns;
/**
* Constructor.
*
* #param context
*/
public PostsLayout(Context context, int columns)
{
super(context);
_posts = new ArrayList<PostPos>();
_columns = columns;
int rows = 3;
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < _columns; c++)
{
PostPos pp = new PostPos(context, r, 1, c, 1);
_posts.add(pp);
addView(pp.view);
}
}
}
#Override
protected void onSizeChanged(int width, int height, int old_width, int old_height)
{
for (PostPos pos : _posts)
{
pos.update(width / _columns);
}
super.onSizeChanged(width, height, old_width, old_height);
}
}
When I use this Layout I see a grid of 3x3 buttons that fill up the view. That's what I wanted, but it doesn't scroll. So I tried to use a ScrollView but when I do that everything disappears. The view is just blank.
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
ScrollView scroll = new ScrollView(this);
ScrollView.LayoutParams params = new ScrollView.LayoutParams(ScrollView.LayoutParams.MATCH_PARENT, ScrollView.LayoutParams.WRAP_CONTENT);
scroll.setLayoutParams(params);
PostsLayout posts = new PostsLayout(this, 4);
scroll.addView(posts);
setContentView(scroll);
}
I'm not sure what's going wrong.
EDIT:
If I change the PostsLayout constructor so just create 1 button with defaults, then that button appears with the ScrollView. So I'm must be doing something wrong with my other buttons.
Here is the code.
public PostsLayout(Context context, int columns)
{
super(context);
_posts = new ArrayList<PostPos>();
_columns = columns;
int rows = 3;
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < _columns; c++)
{
//PostPos pp = new PostPos(context, r, 1, c, 1);
//_posts.add(pp);
//addView(pp.view);
}
}
// this button does show!?!?
addView(new Button(context));
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
this.setLayoutParams(params);
}
EDIT:
onSizeChanged called with wrong values. When there is no ScrollView the PostsLayout is resized to fill the full size of the device, but when it's in a ScrollView the onSizeChanged method is called with very small size values. This is the reason nothing is being shown on the device.
I added this to the constructor for PostsLayout but it didn't fix it.
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
this.setLayoutParams(params);
A Custom Layout
I was able to resolve the above issues by implementing my own ViewGroup class. This is a Tiles layout that positions the children in a grid. The child views can span across a rows and columns. The difference between this is the GridLayout is it fills holes left by spanning children.
Here is the source code.
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.cgtag.R;
/**
* This class positions child Views in a grid pattern, and allows some Views to
* span across more then one grid cell.
* <p>
* The layout works by absolutely position each child in a row/column position.
* As each child is positioned next to it's sibling the layout keeps track of
* the depth for that column. This is used to fill holes that are created by
* larger Views.
*/
public class TilesLayout extends ViewGroup
{
private static String TAG = TilesLayout.class.getName();
/**
* The parameters for this layout.
*/
public static class LayoutParams extends ViewGroup.LayoutParams
{
/**
* Number of Rows to span.
*/
private int _row_span;
/**
* Number of Columns to span.
*/
private int _column_span;
/**
* X location after measurements.
*/
private int _x;
/**
* Y location after measurements.
*/
private int _y;
/**
* Width of the child after measurements.
*/
private int _width;
/**
* Height of the child after measurements.
*/
private int _height;
/**
* #return the row_span
*/
public int getRowSpan()
{
return _row_span;
}
/**
* #param row_span
* the row_span to set
*/
public void setRowSpan(int row_span)
{
this._row_span = Math.max(1, row_span);
}
/**
* #return the column_span
*/
public int getColumnSpan()
{
return _column_span;
}
/**
* #param column_span
* the column_span to set
*/
public void setColumnSpan(int column_span)
{
this._column_span = Math.max(1, column_span);
}
/**
* Constructor
*/
public LayoutParams()
{
super(0, 0);
Init();
}
/**
* Constructor
*
* #param context
* The view context.
* #param attrs
* The attributes to read.
*/
public LayoutParams(Context context, AttributeSet attrs)
{
super(context, attrs);
Init();
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.TilesLayout_Layout, 0, 0);
try
{
setRowSpan(a.getInt(R.styleable.TilesLayout_Layout_row_span, 1));
setColumnSpan(a.getInt(R.styleable.TilesLayout_Layout_column_span, 1));
}
finally
{
a.recycle();
}
}
/**
* Width/Height constructor
*
* #param w
* #param h
*/
public LayoutParams(int w, int h)
{
super(w, h);
Init();
}
/**
* Set default values.
*/
private void Init()
{
_row_span = 1;
_column_span = 1;
_x = _y = _width = _height = 0;
}
}
/**
* The width of a column in pixels.
*/
private int _column_width;
/**
* Layout constructor.
*
* #param context
* #param attrs
*/
public TilesLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.TilesLayout, 0, 0);
try
{
_column_width = a.getDimensionPixelSize(R.styleable.TilesLayout_column_width, 200);
}
finally
{
a.recycle();
}
}
/**
* #return the column_width
*/
public int getColumnWidth()
{
return this._column_width;
}
/**
* #param pColumnWidth
* the column_width to set
*/
public void setColumnWidth(int pColumnWidth)
{
this._column_width = pColumnWidth;
this.invalidate();
this.requestLayout();
}
/**
* A simple measurement using the padding and suggested sizes.
*
* #see android.view.View#onMeasure(int, int)
*/
#SuppressLint("DrawAllocation")
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
Log.d(TAG, "onMeasure: width - " + MeasureSpec.toString(widthMeasureSpec));
Log.d(TAG, "onMeasure: height - " + MeasureSpec.toString(heightMeasureSpec));
// compute the width we need
int width = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
width = Math.max(width, MeasureSpec.getSize(widthMeasureSpec));
// compute the layout of all the children.
LayoutProcessor processor = new LayoutProcessor(width);
final int height = processor.MeasureAllChildren();
// measure all the children (required so children render correctly).
for(int i=0, c = getChildCount(); i < c; i++)
{
final View child = getChildAt(i);
final TilesLayout.LayoutParams params = (TilesLayout.LayoutParams)child.getLayoutParams();
final int cellWidthSpec = MeasureSpec.makeMeasureSpec(params._width, MeasureSpec.AT_MOST);
final int cellHeightSpec = MeasureSpec.makeMeasureSpec(params._height, MeasureSpec.AT_MOST);
child.measure(cellWidthSpec, cellHeightSpec);
}
// set out measure
setMeasuredDimension(width, height);
}
/**
* The minimum width should be at least the width of 1 column.
*
* #see android.view.View#getSuggestedMinimumWidth()
*/
#Override
protected int getSuggestedMinimumWidth()
{
return Math.max(super.getSuggestedMinimumWidth(), this._column_width);
}
/**
* The minimum height should be at least the height of 1 column.
*
* #see android.view.View#getSuggestedMinimumHeight()
*/
#Override
protected int getSuggestedMinimumHeight()
{
return Math.max(super.getSuggestedMinimumHeight(), this._column_width);
}
/**
* This layout positions the child views in the onSizeChanged event. Since
* the position of children is directly related to the size of this view.
*
* #see android.view.ViewGroup#onLayout(boolean, int, int, int, int)
*/
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
{
for (int i = 0, c = getChildCount(); i < c; i++)
{
final View child = getChildAt(i);
TilesLayout.LayoutParams params = (TilesLayout.LayoutParams) child.getLayoutParams();
child.layout(params._x, params._y, params._x + params._width, params._y + params._height);
}
}
/**
* Check if the parameters are our LayoutParam class. If this returns False
* then the generateDefaultLayoutParams() method will be called.
*
* #see android.view.ViewGroup#checkLayoutParams(android.view.ViewGroup.LayoutParams)
*/
#Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p)
{
return p instanceof LayoutParams;
}
/**
* Creates default layout parameters for child Views added to this
* ViewGroup.
*
* #see android.view.ViewGroup#generateDefaultLayoutParams()
*/
#Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams()
{
return new TilesLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
/**
* This is used to create LayoutParams for child views. After this is called
* the checkLayoutParams() is called to verify if the LayoutParams is
* something this view can use. If not, then defaults will be created using
* generateDefaultLayoutParams().
*
* #see android.view.ViewGroup#generateLayoutParams(android.util.AttributeSet)
*/
#Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)
{
return new TilesLayout.LayoutParams(getContext(), attrs);
}
/**
* This is called to convert a LayoutParams that this View doesn't support
* into one it does. This is called when the checkLayoutParams() false, and
* LayoutParams need to be converted.
*
* #see android.view.ViewGroup#generateLayoutParams(android.view.ViewGroup.LayoutParams)
*/
#Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p)
{
return new TilesLayout.LayoutParams(p.width, p.height);
}
/**
* This class represents the algorithm used to position child views. The
* algorithm uses counters for each column so fill gaps as child views span
* across rows/columns.
*/
private class LayoutProcessor
{
/**
* Number of columns.
*/
private int _column_count;
/**
* The width of each column.
*/
private int _column_size;
/**
* The number of items in a column.
*/
private int _columns[];
/**
* Constructor
*
* #param width
* The width of the layout.
*/
public LayoutProcessor(int width)
{
// calculates the widest width a column can be so that they spac
// across the entire View, but not so large 1 column exceeds the
// width of the View.
_column_count = Math.max(1, ((int) (width / _column_width)));
_column_size = width / _column_count;
_columns = new int[_column_count];
}
/**
* Checks the spanning of a child to see if it fits in the current
* cursor position across multiple columns.
*
* #param child
* The view to check.
* #param row
* The row offset.
* #param column
* The column offset.
* #return True if it fits.
*/
private boolean childFits(View child, int row, int column)
{
TilesLayout.LayoutParams params = (TilesLayout.LayoutParams) child.getLayoutParams();
for (int i = column, c = column + Math.min(_column_count, params.getColumnSpan()); i < c; i++)
{
if (i == _column_count || _columns[i] > row)
{
return false;
}
}
return true;
}
/**
* Positions the child by setting it's LayoutParams. The position is
* calculated by the size of a column, and the row/column offset.
*
* #param child
* The child to position.
* #param row
* The row offset.
* #param column
* The column offset.
*
* #return The bottom of the view.
*/
private int MeasureChild(View child, int row, int column)
{
TilesLayout.LayoutParams params = (TilesLayout.LayoutParams) child.getLayoutParams();
final int col_span = Math.min(_column_count, params.getColumnSpan());
final int row_span = params.getRowSpan();
params._x = _column_size * column;
params._y = _column_size * row;
params._width = _column_size * col_span;
params._height = _column_size * row_span;
params._x += 4;
params._y += 4;
params._width -= 8;
params._height -= 8;
// increment the counters for each column this view covers
for (int r = row, rc = row + row_span; r < rc; r++)
{
for (int c = column, cc = Math.min(column + col_span, _column_count + 1); c < cc; c++)
{
_columns[c]++;
}
}
child.setLayoutParams(params);
return params._y + params._height;
}
/**
* Processes all the child for the View until they are positioned into a
* tile layout.
*
* #return The total height for the view.
*/
public int MeasureAllChildren()
{
// a list of children marked True if it has been used.
final int child_count = getChildCount();
boolean done[] = new boolean[child_count];
int offset = 0, row = 0, column = 0, height = 0;
// Continue to layout children until all the children
// have been positioned.
while (offset < child_count)
{
// Starting from the offset try to find a child
// that will fit. Continue to start from offset
// until that child is positioned, and skip
// over other child by checking `used[i]`
int i;
for (i = offset; i < child_count; i++)
{
if (!done[i])
{
View child = getChildAt(i);
// Does this child have room to span across
// columns/rows?
if (childFits(child, row, column))
{
// position the child
final int bottom = MeasureChild(child, row, column);
height = Math.max(bottom, height);
// don't position this child again
done[i] = true;
break;
}
}
}
// move the offset to the next available child.
while (offset < child_count && done[offset])
{
offset++;
}
// move to the next column
column++;
// wrap to next row
if (column == _column_count)
{
column = 0;
row++;
}
}
return height;
}
}
}
Here are the custom attrs.xml settings.
<declare-styleable name="TilesLayout">
<attr name="column_width" format="dimension" />
</declare-styleable>
<declare-styleable name="TilesLayout_Layout">
<attr name="column_span" format="integer"/>
<attr name="row_span" format="integer"/>
</declare-styleable>
I have a table (JTable) of values which is updated in real time about every 10 seconds (last 2 columns out of 4 updated, for each row). I want this table to continuous scroll up so I used the example from:
http://wiki.byte-welt.net/wiki/MarqueePanelV
The scrolling wraps around, first line is shown after last line and works fine except one thing:
When I do the update of the DefaultTableModel (I'm using setValueAt() function), the values are printed at their original position (like the table is not scrolled). Only the portion with the 2 columns is refreshed, created some fracture in the view. The view is corrected at the next scroll (in 100 milliseconds) but is still visible on the screen. More, since I'm stopping the scroll when mouse is over the table, the update will create a visible fracture in the table view.
Any idea on how can I fix this problem?
Here is a code example showing the problem:
package scrollingtable;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import javax.swing.BoxLayout;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.table.DefaultTableModel;
class MarqueePanelV extends JPanel
implements ActionListener, AncestorListener, WindowListener {
private boolean paintChildren;
private boolean scrollingPaused;
private int scrollOffset;
private int wrapOffset;
private int preferredHeight = -1;
private int scrollAmount;
private int scrollFrequency;
private boolean wrap = false;
private int wrapAmount = 0;
private boolean scrollWhenFocused = true;
private Timer timer = new Timer(100, this);
/**
* Convenience constructor that sets both the scroll frequency and
* scroll amount to a value of 5.
*/
public MarqueePanelV() {
this(20, 1);
}
/**
*
* #param scrollFrequency
* #param scrollAmount
*/
#SuppressWarnings("LeakingThisInConstructor")
public MarqueePanelV(int scrollFrequency, int scrollAmount) {
setScrollFrequency(scrollFrequency);
setScrollAmount(scrollAmount);
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
addAncestorListener(this);
}
/*
* Translate the location of the children before they are painted so it
* appears they are scrolling bottom to top
*/
#Override
public void paintChildren(Graphics g) {
// Need this so we don't see a flicker of the text before scrolling
if (!paintChildren) {
return;
}
int x = super.getPreferredSize().height;
// Normal painting as the components scroll bottom to top
Graphics2D g2d = (Graphics2D) g;
g2d.translate(0, -scrollOffset);
super.paintChildren(g);
g2d.translate(0, scrollOffset);
// Repaint the start of the components on the bottom edge of the panel once
// all the components are completely visible on the panel.
// (Its like the components are in two places at the same time)
if (isWrap()) {
//wrapOffset = scrollOffset - super.getPreferredSize().height - wrapAmount;
wrapOffset = scrollOffset - x - wrapAmount;
g2d.translate(0, -wrapOffset);
super.paintChildren(g);
g2d.translate(0, wrapOffset);
}
}
/*
* The default preferred size will be half the size of the components added to
* the panel. This will allow room for components to be scrolled on and off
* the panel.
*
* The default height can be overriden by using the setPreferredHeight() method.
*/
#Override
public Dimension getPreferredSize() {
Dimension d = super.getPreferredSize();
d.height = (preferredHeight == -1) ? d.height / 2 : preferredHeight;
return d;
}
#Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
public int getPreferredHeight() {
return preferredHeight;
}
/**
* Specify the preferred height on the panel. A value of -1 will cause the
* default preferred with size calculation to be used.
*
* #param preferredHeight preferred height of the panel in pixels
*/
public void setPreferredHeight(int preferredHeight) {
this.preferredHeight = preferredHeight;
revalidate();
}
/**
* Get the scroll amount.
*
* #return the scroll amount in pixels
*/
public int getScrollAmount() {
return scrollAmount;
}
/**
* Specify the scroll amount. The number of pixels to scroll every time
* scrolling is done.
*
* #param scrollAmount scroll amount in pixels
*/
public void setScrollAmount(int scrollAmount) {
this.scrollAmount = scrollAmount;
}
/**
* Get the scroll frequency.
*
* #return the scroll frequency
*/
public int getScrollFrequency() {
return scrollFrequency;
}
/**
* Specify the scroll frequency. That is the number of times scrolling
* should be performed every second.
*
* #param scrollFrequency scroll frequency
*/
public void setScrollFrequency(int scrollFrequency) {
this.scrollFrequency = scrollFrequency;
int delay = 1000 / scrollFrequency;
timer.setInitialDelay(delay);
timer.setDelay(delay);
}
/**
* Get the scroll only when visible property.
*
* #return the scroll only when visible value
*/
public boolean isScrollWhenFocused() {
return scrollWhenFocused;
}
/**
* Specify the scrolling property for unfocused windows.
*
* #param scrollWhenVisible when true scrolling pauses when the window
* loses focus. Scrolling will continue when
* the window regains focus. When false
* scrolling is continuous unless the window
* is iconified.
*/
public void setScrollWhenFocused(boolean scrollWhenFocused) {
this.scrollWhenFocused = scrollWhenFocused;
}
/**
* Get the wrap property.
*
* #return the wrap value
*/
public boolean isWrap() {
return wrap;
}
/**
* Specify the wrapping property. Normal scrolling is such that all the text
* will scroll from bottom to top. When the last part of the text scrolls off
* the bottom edge scrolling will start again from the bottom edge. Therefore
* there is a time when the component is blank as nothing is displayed.
* Wrapping implies that as the end of the text scrolls off the top edge
* the beginning of the text will scroll in from the bottom edge. So the end
* and the start of the text is displayed at the same time.
*
* #param wrap when true the start of the text will scroll in from the bottom
* edge while the end of the text is still scrolling off the top
* edge. Otherwise the panel must be clear of text before it
* will begin again from the bottom edge.
*/
public void setWrap(boolean wrap) {
this.wrap = wrap;
}
/**
* Get the wrap amount.
*
* #return the wrap amount value
*/
public int getWrapAmount() {
return wrapAmount;
}
/**
* Specify the wrapping amount. This specifies the space between the end of the
* text on the top edge and the start of the text from the bottom edge when
* wrapping is turned on.
*
* #param wrapAmount the amount in pixels
*/
public void setWrapAmount(int wrapAmount) {
this.wrapAmount = wrapAmount;
}
/**
* Start scrolling the components on the panel. Components will start
* scrolling from the bottom edge towards the top edge.
*/
public void startScrolling() {
paintChildren = true;
scrollOffset = -getSize().height;
timer.start();
}
/**
* Stop scrolling the components on the panel. The conponents will be
* cleared from the view of the panel
*/
public void stopScrolling() {
timer.stop();
paintChildren = false;
repaint();
}
/**
* The components will stop scrolling but will remain visible
*/
public void pauseScrolling() {
if (timer.isRunning()) {
timer.stop();
scrollingPaused = true;
}
}
/**
* The components will resume scrolling from where scrolling was stopped.
*/
public void resumeScrolling() {
if (scrollingPaused) {
timer.restart();
scrollingPaused = false;
}
}
/**
* Adjust the offset of the components on the panel so it appears that
* they are scrolling from bottom to top.
*/
#Override
public void actionPerformed(ActionEvent e) {
scrollOffset += scrollAmount;
int height = super.getPreferredSize().height;
if (scrollOffset > height) {
scrollOffset = isWrap() ? wrapOffset + scrollAmount : -getSize().height;
}
//System.out.println("scroll offset: " + scrollOffset);
repaint();
}
/**
* Get notified when the panel is added to a Window so we can use a
* WindowListener to automatically start the scrolling of the components.
*/
#Override
public void ancestorAdded(AncestorEvent event) {
SwingUtilities.windowForComponent(this).addWindowListener(this);
}
#Override
public void ancestorRemoved(AncestorEvent event) {
}
#Override
public void ancestorMoved(AncestorEvent event) {
}
// Implement WindowListener
#Override
public void windowOpened(WindowEvent e) {
startScrolling();
}
#Override
public void windowClosing(WindowEvent e) {
stopScrolling();
}
#Override
public void windowClosed(WindowEvent e) {
stopScrolling();
}
#Override
public void windowIconified(WindowEvent e) {
pauseScrolling(); }
#Override
public void windowDeiconified(WindowEvent e) {
resumeScrolling(); }
#Override
public void windowActivated(WindowEvent e) {
if (isScrollWhenFocused()) {
resumeScrolling();
}
}
#Override
public void windowDeactivated(WindowEvent e) {
if (isScrollWhenFocused()) {
pauseScrolling();
}
}
}
public class Main extends JDialog implements ActionListener {
private JTable table;
private DefaultTableModel model;
MarqueePanelV mpv = new MarqueePanelV();
private Timer timer2;
public static void main(String[] args) {
// TODO code application logic here
new Main();
}
Main() {
setSize(600, 400);
this.setLocation(300, 300);
table = new JTable();
model = new DefaultTableModel(20, 2);
table.setModel(model);
table.getColumnModel().getColumn(0).setPreferredWidth(280);
table.getColumnModel().getColumn(1).setPreferredWidth(280);
for(int i = 0; i < 20; i++) {
model.setValueAt(i, i, 0);
model.setValueAt(100 + i, i, 1);
}
mpv.add(table);
add(mpv);
mpv.setWrap(true);
mpv.setWrapAmount(0);
mpv.startScrolling();
table.addMouseListener(new MouseListener() {
// Implement MouseListener
#Override
public void mouseClicked(MouseEvent e) {}
#Override
public void mousePressed(MouseEvent e) {}
#Override
public void mouseReleased(MouseEvent e) {}
#Override
public void mouseEntered(MouseEvent e) {
mpv.pauseScrolling();
}
#Override
public void mouseExited(MouseEvent e) {
mpv.resumeScrolling();
}
});
setVisible(true);
timer2 = new Timer(2000, this);
timer2.setInitialDelay(1000);
timer2.setDelay(2000);
timer2.start();
}
public void actionPerformed(ActionEvent e) {
for(int i = 0; i < 20; i++) {
model.setValueAt(100 + i, i, 1);
}
}
}
Problem was solved by calling mpv.repaint(); in the timer2 action:
package scrollingtable;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import javax.swing.BoxLayout;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.table.DefaultTableModel;
class MarqueePanelV extends JPanel
implements ActionListener, AncestorListener, WindowListener {
private boolean paintChildren;
private boolean scrollingPaused;
private int scrollOffset;
private int wrapOffset;
private int preferredHeight = -1;
private int scrollAmount;
private int scrollFrequency;
private boolean wrap = false;
private int wrapAmount = 0;
private boolean scrollWhenFocused = true;
private Timer timer = new Timer(100, this);
/**
* Convenience constructor that sets both the scroll frequency and
* scroll amount to a value of 5.
*/
public MarqueePanelV() {
this(20, 1);
}
/**
*
* #param scrollFrequency
* #param scrollAmount
*/
#SuppressWarnings("LeakingThisInConstructor")
public MarqueePanelV(int scrollFrequency, int scrollAmount) {
setScrollFrequency(scrollFrequency);
setScrollAmount(scrollAmount);
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
addAncestorListener(this);
}
/*
* Translate the location of the children before they are painted so it
* appears they are scrolling bottom to top
*/
#Override
public void paintChildren(Graphics g) {
// Need this so we don't see a flicker of the text before scrolling
if (!paintChildren) {
return;
}
int x = super.getPreferredSize().height;
// Normal painting as the components scroll bottom to top
Graphics2D g2d = (Graphics2D) g;
g2d.translate(0, -scrollOffset);
super.paintChildren(g);
g2d.translate(0, scrollOffset);
// Repaint the start of the components on the bottom edge of the panel once
// all the components are completely visible on the panel.
// (Its like the components are in two places at the same time)
if (isWrap()) {
//wrapOffset = scrollOffset - super.getPreferredSize().height - wrapAmount;
wrapOffset = scrollOffset - x - wrapAmount;
g2d.translate(0, -wrapOffset);
super.paintChildren(g);
g2d.translate(0, wrapOffset);
}
}
/*
* The default preferred size will be half the size of the components added to
* the panel. This will allow room for components to be scrolled on and off
* the panel.
*
* The default height can be overriden by using the setPreferredHeight() method.
*/
#Override
public Dimension getPreferredSize() {
Dimension d = super.getPreferredSize();
d.height = (preferredHeight == -1) ? d.height / 2 : preferredHeight;
return d;
}
#Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
public int getPreferredHeight() {
return preferredHeight;
}
/**
* Specify the preferred height on the panel. A value of -1 will cause the
* default preferred with size calculation to be used.
*
* #param preferredHeight preferred height of the panel in pixels
*/
public void setPreferredHeight(int preferredHeight) {
this.preferredHeight = preferredHeight;
revalidate();
}
/**
* Get the scroll amount.
*
* #return the scroll amount in pixels
*/
public int getScrollAmount() {
return scrollAmount;
}
/**
* Specify the scroll amount. The number of pixels to scroll every time
* scrolling is done.
*
* #param scrollAmount scroll amount in pixels
*/
public void setScrollAmount(int scrollAmount) {
this.scrollAmount = scrollAmount;
}
/**
* Get the scroll frequency.
*
* #return the scroll frequency
*/
public int getScrollFrequency() {
return scrollFrequency;
}
/**
* Specify the scroll frequency. That is the number of times scrolling
* should be performed every second.
*
* #param scrollFrequency scroll frequency
*/
public void setScrollFrequency(int scrollFrequency) {
this.scrollFrequency = scrollFrequency;
int delay = 1000 / scrollFrequency;
timer.setInitialDelay(delay);
timer.setDelay(delay);
}
/**
* Get the scroll only when visible property.
*
* #return the scroll only when visible value
*/
public boolean isScrollWhenFocused() {
return scrollWhenFocused;
}
/**
* Specify the scrolling property for unfocused windows.
*
* #param scrollWhenVisible when true scrolling pauses when the window
* loses focus. Scrolling will continue when
* the window regains focus. When false
* scrolling is continuous unless the window
* is iconified.
*/
public void setScrollWhenFocused(boolean scrollWhenFocused) {
this.scrollWhenFocused = scrollWhenFocused;
}
/**
* Get the wrap property.
*
* #return the wrap value
*/
public boolean isWrap() {
return wrap;
}
/**
* Specify the wrapping property. Normal scrolling is such that all the text
* will scroll from bottom to top. When the last part of the text scrolls off
* the bottom edge scrolling will start again from the bottom edge. Therefore
* there is a time when the component is blank as nothing is displayed.
* Wrapping implies that as the end of the text scrolls off the top edge
* the beginning of the text will scroll in from the bottom edge. So the end
* and the start of the text is displayed at the same time.
*
* #param wrap when true the start of the text will scroll in from the bottom
* edge while the end of the text is still scrolling off the top
* edge. Otherwise the panel must be clear of text before it
* will begin again from the bottom edge.
*/
public void setWrap(boolean wrap) {
this.wrap = wrap;
}
/**
* Get the wrap amount.
*
* #return the wrap amount value
*/
public int getWrapAmount() {
return wrapAmount;
}
/**
* Specify the wrapping amount. This specifies the space between the end of the
* text on the top edge and the start of the text from the bottom edge when
* wrapping is turned on.
*
* #param wrapAmount the amount in pixels
*/
public void setWrapAmount(int wrapAmount) {
this.wrapAmount = wrapAmount;
}
/**
* Start scrolling the components on the panel. Components will start
* scrolling from the bottom edge towards the top edge.
*/
public void startScrolling() {
paintChildren = true;
scrollOffset = -getSize().height;
timer.start();
}
/**
* Stop scrolling the components on the panel. The conponents will be
* cleared from the view of the panel
*/
public void stopScrolling() {
timer.stop();
paintChildren = false;
repaint();
}
/**
* The components will stop scrolling but will remain visible
*/
public void pauseScrolling() {
if (timer.isRunning()) {
timer.stop();
scrollingPaused = true;
}
}
/**
* The components will resume scrolling from where scrolling was stopped.
*/
public void resumeScrolling() {
if (scrollingPaused) {
timer.restart();
scrollingPaused = false;
}
}
// Implement ActionListener
/**
* Adjust the offset of the components on the panel so it appears that
* they are scrolling from bottom to top.
*/
#Override
public void actionPerformed(ActionEvent e) {
scrollOffset += scrollAmount;
int height = super.getPreferredSize().height;
if (scrollOffset > height) {
scrollOffset = isWrap() ? wrapOffset + scrollAmount : -getSize().height;
}
repaint();
}
// Implement AncestorListener
/**
* Get notified when the panel is added to a Window so we can use a
* WindowListener to automatically start the scrolling of the components.
*/
#Override
public void ancestorAdded(AncestorEvent event) {
SwingUtilities.windowForComponent(this).addWindowListener(this);
}
#Override
public void ancestorRemoved(AncestorEvent event) {
}
#Override
public void ancestorMoved(AncestorEvent event) {
}
// Implement WindowListener
#Override
public void windowOpened(WindowEvent e) {
startScrolling();
}
#Override
public void windowClosing(WindowEvent e) {
stopScrolling();
}
#Override
public void windowClosed(WindowEvent e) {
stopScrolling();
}
#Override
public void windowIconified(WindowEvent e) {
pauseScrolling(); }
#Override
public void windowDeiconified(WindowEvent e) {
resumeScrolling(); }
#Override
public void windowActivated(WindowEvent e) {
if (isScrollWhenFocused()) {
resumeScrolling();
}
}
#Override
public void windowDeactivated(WindowEvent e) {
if (isScrollWhenFocused()) {
pauseScrolling();
}
}
}
public class Main extends JDialog implements ActionListener {
private JTable table;
private DefaultTableModel model;
MarqueePanelV mpv = new MarqueePanelV();
private Timer timer2;
public static void main(String[] args) {
// TODO code application logic here
new Main();
}
Main() {
setSize(600, 400);
this.setLocation(300, 300);
table = new JTable();
model = new DefaultTableModel(20, 2);
table.setModel(model);
table.getColumnModel().getColumn(0).setPreferredWidth(280);
table.getColumnModel().getColumn(1).setPreferredWidth(280);
for(int i = 0; i < 20; i++) {
model.setValueAt(i, i, 0);
model.setValueAt(100 + i, i, 1);
}
mpv.add(table);
add(mpv);
mpv.setWrap(true);
mpv.setWrapAmount(0);
mpv.startScrolling();
table.addMouseListener(new MouseListener() {
// Implement MouseListener
#Override
public void mouseClicked(MouseEvent e) {}
#Override
public void mousePressed(MouseEvent e) {}
#Override
public void mouseReleased(MouseEvent e) {}
#Override
public void mouseEntered(MouseEvent e) {
mpv.pauseScrolling();
}
#Override
public void mouseExited(MouseEvent e) {
mpv.resumeScrolling();
}
});
setVisible(true);
timer2 = new Timer(2000, this);
timer2.setInitialDelay(1000);
timer2.setDelay(2000);
timer2.start();
}
int k = 0;
public void actionPerformed(ActionEvent e) {
for(int i = 0; i < 20; i++) {
model.setValueAt(100 + i + k, i, 1);
}
k++;
mpv.repaint();
}
}
I have an application that bounces Shapes around inside a JPanel. Whenever shapes hit a side they will bounce off in the other direction. I am trying to add a new shape called a NestingShape that contains zero or more Shapes that bounce around inside it, while the NestingShape bounces around in the JPanel. The children of a NestingShape instance can be either simple Shapes or other NestingShape instances.
Right now I am having trouble with moving the children of a NestingShape within a NestingShape with the move(width, height) method in the NestingShape subclass. I am also having trouble with developing a method in the Shape superclass that can find the parent any given shape. I'll copy and paste the code I have come up with so far for the Shape superclass and NestingShape subclass below and the test cases I am using to test the code so far:
Shape superclass:
NOTE: parent() and path() methods are the most relevant methods for this task and the parent() method is the one I'm having trouble implementing. There are a lot of little details such as fFill and count and such that are related to different Shapes that I have developed, and those can be ignored.
package bounce;
import java.awt.Color;
import java.util.List;
/**
* Abstract superclass to represent the general concept of a Shape. This class
* defines state common to all special kinds of Shape instances and implements
* a common movement algorithm. Shape subclasses must override method paint()
* to handle shape-specific painting.
*
* #author wadfsd
*
*/
public abstract class Shape {
// === Constants for default values. ===
protected static final int DEFAULT_X_POS = 0;
protected static final int DEFAULT_Y_POS = 0;
protected static final int DEFAULT_DELTA_X = 5;
protected static final int DEFAULT_DELTA_Y = 5;
protected static final int DEFAULT_HEIGHT = 35;
protected static final int DEFAULT_WIDTH = 25;
protected static final Color DEFAULT_COLOR = Color.black;
protected static final String DEFAULT_STRING = "";
// ===
// === Instance variables, accessible by subclasses.
protected int fX;
protected int fY;
protected int fDeltaX;
protected int fDeltaY;
protected int fWidth;
protected int fHeight;
protected boolean fFill;
protected Color fColor;
protected int count;
protected int fState;
protected int before;
protected String fString;
// ===
/**
* Creates a Shape object with default values for instance variables.
*/
public Shape() {
this(DEFAULT_X_POS, DEFAULT_Y_POS, DEFAULT_DELTA_X, DEFAULT_DELTA_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_COLOR, DEFAULT_STRING);
}
/**
* Creates a Shape object with a specified x and y position.
*/
public Shape(int x, int y) {
this(x, y, DEFAULT_DELTA_X, DEFAULT_DELTA_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_COLOR, DEFAULT_STRING);
}
public Shape(int x, int y, String str) {
this(x, y, DEFAULT_DELTA_X, DEFAULT_DELTA_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_COLOR, str);
}
/**
* Creates a Shape object with specified x, y, and color values.
*/
public Shape(int x, int y, Color c) {
this(x, y, DEFAULT_DELTA_X, DEFAULT_DELTA_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT, c, DEFAULT_STRING);
}
public Shape(int x, int y, Color c, String str) {
this(x, y, DEFAULT_DELTA_X, DEFAULT_DELTA_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT, c, str);
}
/**
* Creates a Shape instance with specified x, y, deltaX and deltaY values.
* The Shape object is created with a default width, height and color.
*/
public Shape(int x, int y, int deltaX, int deltaY) {
this(x, y, deltaX, deltaY, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_COLOR, DEFAULT_STRING);
}
public Shape(int x, int y, int deltaX, int deltaY, String str) {
this(x, y, deltaX, deltaY, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_COLOR, str);
}
/**
* Creates a Shape instance with specified x, y, deltaX, deltaY and color values.
* The Shape object is created with a default width and height.
*/
public Shape(int x, int y, int deltaX, int deltaY, Color c) {
this(x, y, deltaX, deltaY, DEFAULT_WIDTH, DEFAULT_HEIGHT, c, DEFAULT_STRING);
}
public Shape(int x, int y, int deltaX, int deltaY, Color c, String str) {
this(x, y, deltaX, deltaY, DEFAULT_WIDTH, DEFAULT_HEIGHT, c, str);
}
/**
* Creates a Shape instance with specified x, y, deltaX, deltaY, width and
* height values. The Shape object is created with a default color.
*/
public Shape(int x, int y, int deltaX, int deltaY, int width, int height) {
this(x, y, deltaX, deltaY, width, height, DEFAULT_COLOR, DEFAULT_STRING);
}
public Shape(int x, int y, int deltaX, int deltaY, int width, int height, String str) {
this(x, y, deltaX, deltaY, width, height, DEFAULT_COLOR, str);
}
public Shape(int x, int y, int deltaX, int deltaY, int width, int height, Color c) {
this(x, y, deltaX, deltaY, width, height, c, DEFAULT_STRING);
}
/**
* Creates a Shape instance with specified x, y, deltaX, deltaY, width,
* height and color values.
*/
public Shape(int x, int y, int deltaX, int deltaY, int width, int height, Color c, String str) {
fX = x;
fY = y;
fDeltaX = deltaX;
fDeltaY = deltaY;
fWidth = width;
fHeight = height;
fFill = false;
fColor = c;
count = 0;
fState = 1;
before = 0;
fString = str;
}
/**
* Moves this Shape object within the specified bounds. On hitting a
* boundary the Shape instance bounces off and back into the two-
* dimensional world and logs whether a vertical or horizontal wall
* was hit for the DynamicRectangleShape.
* #param width width of two-dimensional world.
* #param height height of two-dimensional world.
*/
public void move(int width, int height) {
int nextX = fX + fDeltaX;
int nextY = fY + fDeltaY;
if (nextY <= 0) {
nextY = 0;
fDeltaY = -fDeltaY;
fFill = false;
count++;
} else if (nextY + fHeight >= height) {
nextY = height - fHeight;
fDeltaY = -fDeltaY;
fFill = false;
count++;
}
// When Shape hits a corner the vertical wall fFill value overrides the horizontal
if (nextX <= 0) {
nextX = 0;
fDeltaX = -fDeltaX;
fFill = true;
count++;
} else if (nextX + fWidth >= width) {
nextX = width - fWidth;
fDeltaX = -fDeltaX;
fFill = true;
count++;
}
fX = nextX;
fY = nextY;
}
public void text(Painter painter, String str) {
painter.drawCentredText(str, fX, fY, fWidth, fHeight);
}
/**
* Returns the NestingShape that contains the Shape that method parent
* is called on. If the callee object is not a child within a
* NestingShape instance this method returns null.
*/
public NestingShape parent() {
// Related to NestingShape
}
/**
* Returns an ordered list of Shape objects. The first item within the
* list is the root NestingShape of the containment hierarchy. The last
* item within the list is the callee object (hence this method always
* returns a list with at least one item). Any intermediate items are
* NestingShapes that connect the root NestingShape to the callee Shape.
* E.g. given:
*
* NestingShape root = new NestingShape();
* NestingShape intermediate = new NestingShape();
* Shape oval = new OvalShape();
* root.add(intermediate);
* intermediate.add(oval);
*
* a call to oval.path() yields: [root,intermediate,oval]
*/
public List<Shape> path() {
// Related to NestingShape
}
/**
* Method to be implemented by concrete subclasses to handle subclass
* specific painting.
* #param painter the Painter object used for drawing.
*/
public abstract void paint(Painter painter);
/**
* Returns this Shape object's x position.
*/
public int x() {
return fX;
}
/**
* Returns this Shape object's y position.
*/
public int y() {
return fY;
}
/**
* Returns this Shape object's speed and direction.
*/
public int deltaX() {
return fDeltaX;
}
/**
* Returns this Shape object's speed and direction.
*/
public int deltaY() {
return fDeltaY;
}
/**
* Returns this Shape's width.
*/
public int width() {
return fWidth;
}
/**
* Returns this Shape's height.
*/
public int height() {
return fHeight;
}
/**
* Returns a String whose value is the fully qualified name of this class
* of object. E.g., when called on a RectangleShape instance, this method
* will return "bounce.RectangleShape".
*/
public String toString() {
return getClass().getName();
}
}
NestingShape subclass:
NOTE: Having trouble with the move() method
package bounce;
import java.util.ArrayList;
import java.util.List;
public class NestingShape extends Shape {
private List<Shape> nest = new ArrayList<Shape>();
/**
* Creates a NestingShape object with default values for state.
*/
public NestingShape() {
super();
}
/**
* Creates a NestingShape object with specified location values, default values for other
* state items.
*/
public NestingShape(int x, int y) {
super(x,y);
}
/**
* Creates a NestingShape with specified values for location, velocity and direction.
* Non-specified state items take on default values.
*/
public NestingShape(int x, int y, int deltaX, int deltaY) {
super(x,y,deltaX,deltaY);
}
/**
* Creates a NestingShape with specified values for location, velocity, direction, width, and
* height.
*/
public NestingShape(int x, int y, int deltaX, int deltaY, int width, int height) {
super(x,y,deltaX,deltaY,width,height);
}
/**
* Moves a NestingShape object (including its children) with the bounds specified by arguments
* width and height.
*/
public void move(int width, int height) {
int nextX = fX + fDeltaX;
int nextY = fY + fDeltaY;
if (nextY <= 0) {
nextY = 0;
fDeltaY = -fDeltaY;
fFill = false;
count++;
} else if (nextY + fHeight >= height) {
nextY = height - fHeight;
fDeltaY = -fDeltaY;
fFill = false;
count++;
}
if (nextX <= 0) {
nextX = 0;
fDeltaX = -fDeltaX;
fFill = true;
count++;
} else if (nextX + fWidth >= width) {
nextX = width - fWidth;
fDeltaX = -fDeltaX;
fFill = true;
count++;
}
fX = nextX;
fY = nextY;
// Move children
for (int i = 0; i < shapeCount(); i++) {
Shape shape = shapeAt(i);
int nextXChild = shape.fX + shape.fDeltaX;
int nextYChild = shape.fY + shape.fDeltaY;
if (nextYChild <= 0) {
nextYChild = 0;
shape.fDeltaY = -shape.fDeltaY;
} else if (nextYChild + shape.fHeight >= fHeight) {
nextYChild = fHeight - shape.fHeight;
shape.fDeltaY = -shape.fDeltaY;
}
if (nextXChild <= 0) {
nextXChild = 0;
shape.fDeltaX = -shape.fDeltaX;
} else if (nextXChild + fWidth >= width) {
nextXChild = fWidth - shape.fWidth;
shape.fDeltaX = -shape.fDeltaX;
}
shape.fX = nextXChild;
shape.fY = nextYChild;
}
}
/**
* Paints a NestingShape object by drawing a rectangle around the edge of its bounding box.
* The NestingShape object's children are then painted.
*/
public void paint(Painter painter) {
painter.drawRect(fX,fY,fWidth,fHeight);
painter.translate(fX,fY);
for (int i = 0; i < shapeCount(); i++) {
Shape shape = shapeAt(i);
shape.paint(painter);
}
painter.translate(0,0);
}
/**
* Attempts to add a Shape to a NestingShape object. If successful, a two-way link is
* established between the NestingShape and the newly added Shape. Note that this method
* has package visibility - for reasons that will become apparent in Bounce III.
* #param shape the shape to be added.
* #throws IllegalArgumentException if an attempt is made to add a Shape to a NestingShape
* instance where the Shape argument is already a child within a NestingShape instance. An
* IllegalArgumentException is also thrown when an attempt is made to add a Shape that will
* not fit within the bounds of the proposed NestingShape object.
*/
void add(Shape shape) throws IllegalArgumentException {
if (contains(shape)) {
throw new IllegalArgumentException();
} else if (shape.fWidth > fWidth || shape.fHeight > fHeight) {
throw new IllegalArgumentException();
} else {
nest.add(shape);
}
}
/**
* Removes a particular Shape from a NestingShape instance. Once removed, the two-way link
* between the NestingShape and its former child is destroyed. This method has no effect if
* the Shape specified to remove is not a child of the NestingShape. Note that this method
* has package visibility - for reasons that will become apparent in Bounce III.
* #param shape the shape to be removed.
*/
void remove(Shape shape) {
int index = indexOf(shape);
nest.remove(index);
}
/**
* Returns the Shape at a specified position within a NestingShape. If the position specified
* is less than zero or greater than the number of children stored in the NestingShape less
* one this method throws an IndexOutOfBoundsException.
* #param index the specified index position.
*/
public Shape shapeAt(int index) throws IndexOutOfBoundsException {
if (index < 0 || index >= shapeCount()) {
throw new IndexOutOfBoundsException();
} else {
Shape shape = nest.get(index);
return shape;
}
}
/**
* Returns the number of children contained within a NestingShape object. Note this method is
* not recursive - it simply returns the number of children at the top level within the callee
* NestingShape object.
*/
public int shapeCount() {
int number = nest.size();
return number;
}
/**
* Returns the index of a specified child within a NestingShape object. If the Shape specified
* is not actually a child of the NestingShape this method returns -1; otherwise the value
* returned is in the range 0 .. shapeCount() - 1.
* #param shape the shape whose index position within the NestingShape is requested.
*/
public int indexOf(Shape shape) {
int index = nest.indexOf(shape);
return index;
}
/**
* Returns true if the shape argument is a child of the NestingShape object on which this method
* is called, false otherwise.
*/
public boolean contains(Shape shape) {
boolean child = nest.contains(shape);
return child;
}
}
TestNestingShape test cases:
package bounce;
import java.util.List;
import junit.framework.TestCase;
/**
* Class to test class NestingShape according to its specification.
*/
public class TestNestingShape extends TestCase {
private NestingShape topLevelNest;
private NestingShape midLevelNest;
private NestingShape bottomLevelNest;
private Shape simpleShape;
public TestNestingShape(String name) {
super(name);
}
/**
* Creates a Shape composition hierarchy with the following structure:
* NestingShape (topLevelNest)
* |
* --- NestingShape (midLevelNest)
* |
* --- NestingShape (bottomLevelNest)
* |
* --- RectangleShape (simpleShape)
*/
protected void setUp() throws Exception {
topLevelNest = new NestingShape(0, 0, 2, 2, 100, 100);
midLevelNest = new NestingShape(0, 0, 2, 2, 50, 50);
bottomLevelNest = new NestingShape(5, 5, 2, 2, 10, 10);
simpleShape = new RectangleShape(1, 1, 1, 1, 5, 5);
midLevelNest.add(bottomLevelNest);
midLevelNest.add(simpleShape);
topLevelNest.add(midLevelNest);
}
/**
* Checks that methods move() and paint() correctly move and paint a
* NestingShape's contents.
*/
public void testBasicMovementAndPainting() {
Painter painter = new MockPainter();
topLevelNest.move(500, 500);
topLevelNest.paint(painter);
assertEquals("(rectangle 2,2,100,100)(rectangle 2,2,50,50)(rectangle 7,7,10,10)(rectangle 2,2,5,5)", painter.toString());
}
/**
* Checks that method add successfuly adds a valid Shape, supplied as
* argument, to a NestingShape instance.
*/
public void testAdd() {
// Check that topLevelNest and midLevelNest mutually reference each other.
assertSame(topLevelNest, midLevelNest.parent());
assertTrue(topLevelNest.contains(midLevelNest));
// Check that midLevelNest and bottomLevelNest mutually reference each other.
assertSame(midLevelNest, bottomLevelNest.parent());
assertTrue(midLevelNest.contains(bottomLevelNest));
}
/**
* Check that method add throws an IlegalArgumentException when an attempt
* is made to add a Shape to a NestingShape instance where the Shape
* argument is already part of some NestingShape instance.
*/
public void testAddWithArgumentThatIsAChildOfSomeOtherNestingShape() {
try {
topLevelNest.add(bottomLevelNest);
fail();
} catch(IllegalArgumentException e) {
// Expected action. Ensure the state of topLevelNest and
// bottomLevelNest has not been changed.
assertFalse(topLevelNest.contains(bottomLevelNest));
assertSame(midLevelNest, bottomLevelNest.parent());
}
}
/**
* Check that method add throws an IllegalArgumentException when an attempt
* is made to add a shape that will not fit within the bounds of the
* proposed NestingShape object.
*/
public void testAddWithOutOfBoundsArgument() {
Shape rectangle = new RectangleShape(80, 80, 2, 2, 50, 50);
try {
topLevelNest.add(rectangle);
fail();
} catch(IllegalArgumentException e) {
// Expected action. Ensure the state of topLevelNest and
// rectangle has not been changed.
assertFalse(topLevelNest.contains(rectangle));
assertNull(rectangle.parent());
}
}
/**
* Check that method remove breaks the two-way link between the Shape
* object that has been removed and the NestingShape it was once part of.
*/
public void testRemove() {
topLevelNest.remove(midLevelNest);
assertFalse(topLevelNest.contains(midLevelNest));
assertNull(midLevelNest.parent());
}
/**
* Check that method shapeAt returns the Shape object that is held at a
* specified position within a NestingShape instance.
*/
public void testShapeAt() {
assertSame(midLevelNest, topLevelNest.shapeAt(0));
}
/**
* Check that method shapeAt throws a IndexOutOfBoundsException when called
* with an invalid index argument.
*/
public void testShapeAtWithInvalidIndex() {
try {
topLevelNest.shapeAt(1);
fail();
} catch(IndexOutOfBoundsException e) {
// Expected action.
}
}
/**
* Check that method shapeCount returns zero when called on a NestingShape
* object without children.
*/
public void testShapeCountOnEmptyParent() {
assertEquals(0, bottomLevelNest.shapeCount());
}
/**
* Check that method shapeCount returns the number of children held within
* a NestingShape instance - where the number of children > 0.
*/
public void testShapeCountOnNonEmptyParent() {
assertEquals(2, midLevelNest.shapeCount());
}
/**
* Check that method indexOf returns the index position within a
* NestingShape instance of a Shape held within the NestingShape.
*/
public void testIndexOfWith() {
assertEquals(0, topLevelNest.indexOf(midLevelNest));
assertEquals(1, midLevelNest.indexOf(simpleShape));
}
/**
* Check that method indexOf returns -1 when called with an argument that
* is not part of the NestingShape callee object.
*/
public void testIndexOfWithNonExistingChild() {
assertEquals(-1, topLevelNest.indexOf(bottomLevelNest));
}
/**
* Check that Shape's path method correctly returns the path from the root
* NestingShape object through to the Shape object that path is called on.
*/
public void testPath() {
List<Shape> path = simpleShape.path();
assertEquals(3, path.size());
assertSame(topLevelNest, path.get(0));
assertSame(midLevelNest, path.get(1));
assertSame(simpleShape, path.get(2));
}
/**
* Check that Shape's path method correctly returns a singleton list
* containing only the callee object when this Shape object has no parent.
*/
public void testPathOnShapeWithoutParent() {
List<Shape> path = topLevelNest.path();
assertEquals(1, path.size());
assertSame(topLevelNest, path.get(0));
}
}
With the code I have so far when I run the test cases, I am unable to test the testAdd and testRemove related test cases to make sure I'm adding shapes properly because I haven't yet developed the parent() method in the Shape class. But I can't think of a way of implementing the parent method.
Whenever I testBasicMovementAndPainting, I also get a failed test because my current move() method (in the NestingShape class) only moves the children in the first NestingShape and doesn't move the children of the midLevelNest.
It's a bit of a long read and I'm not sure if that provides enough context as there are a lot of other classes in the package that I didn't include but if anyone could help I would really appreciate it.
Thanks.
For the "parent" problem: a Shape needs an extra Shape attribute that points to the outer nesting shape (it's container / parent):
private Shape parent = null;
you can either set it with a constructor or simply add getter/setter methods:
public Shape(Shape parent) {
this.parent = parent;
}
public void setParent(Shape parent) {
this.parent = parent;
}
public Shape parent() {
return parent;
}
Note the problem now is that any shape can be container for other shapes - it is not restricted to NestingShape. But if I declare the parent as a NestingShape, then we have the ugly situation, that Shape depends on NestingShape, its subclass.
Maybe you simply define an extra interface named ShapeContainer which adds container functionality to a Shape, like
public interface ShapeContainer {
public List<Shape> getChildren();
// .. more?
}
Then your class signature would look like this
public class NestingShape extends Shape implements ShapeContainer
and the type of the parent field in Shape would be ShapeContainer.