Related
I'm using double-buffered graphics in my JLayer subclass to implement a simple swipe animation in a Java Swing application. It works fine on the older displays, but when I run it on a Retina display, the screen loses the doubled-resolution when the animation starts, and gets it back when it ends. I'm not sure how to maintain the higher resolution during the animation.
My animate method originally looked like this:
private void animate() {
Timer timer = new Timer(frameMillis, null);
final ActionListener actionListener = (evt) -> { /* omitted for brevity */ };
timer.addActionListener(actionListener);
int imageType = BufferedImage.TYPE_INT_ARGB;
upcomingScreen = new BufferedImage(liveComponent.getWidth(), liveComponent.getHeight(), imageType);
Graphics2D graphics2D = (Graphics2D) upcomingScreen.getGraphics();
liveComponent.paint(graphics2D); // liveComponent is a JComponent
graphics2D.dispose();
timer.start();
}
I tried doubling the image size, but that didn't help.
upcomingScreen = new BufferedImage(liveComponent.getWidth()*2, liveComponent.getHeight()*2, imageType);
To reflect these changes, I changed my drawing code in LayerUI by doubling xLimit, width, height:
public void paint(final Graphics g, final JComponent c) {
if (isAnimating) {
int xLimit = (c.getWidth()*2 * frame) / maxFrames;
int width = c.getWidth()*2;
int height = c.getHeight()*2;
g.drawImage(uScreen, 0, 0, xLimit, height, 0, 0, xLimit, height, c);
g.drawImage(pScreen, xLimit, 0, width, height, xLimit, 0, width, height, c);
} else {
super.paint(g, c);
}
}
This doesn't help. It draws the same with or without this last change, which makes no sense.
Here is a class that illustrates the problem:
/**
* <p>Created by IntelliJ IDEA.
* <p>Date: 5/2/20
* <p>Time: 10:25 AM
*
* #author Miguel Mu\u00f1oz
*/
#SuppressWarnings({"HardcodedLineSeparator", "StringConcatenation", "HardCodedStringLiteral", "DuplicatedCode"})
public final class SwipeViewTest extends JPanel {
public static final String text1 = "Demo of Swipe View.\n\nThe swipe button will toggle between two pages of text. It has a built-in " +
"special effect, which is a swipe. When you hit the swipe button, it should flip between two pages of text. This worked fine on " +
"the older displays, but for some reason, on a Retina display, the text briefly switches to low resolution as the swipe proceeds, " +
"then switches back once it has finished. This code is written for retina displays. I don't know if it will work for the older, " +
"low resolution displays.\n\nYou can watch it swipe by hitting the space bar or by clicking the swipe button.";
public static final String text2 = "Demo of Swipe View.\n\nThis is the second page of the swipe-text demo. The change in resolution is " +
"most easily noticed when watching the line at the top, which doesn't change as the swipe is performed.";
private final SwipeView<TestView> swipeView;
private final TestView testView;
public static void main(String[] args) {
JFrame frame = new JFrame("SwipeView demo");
SwipeViewTest comp = new SwipeViewTest();
comp.install();
frame.add(comp);
frame.setLocationByPlatform(true);
frame.pack();
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
private boolean page1 = true;
private SwipeViewTest() {
super(new BorderLayout());
testView = new TestView();
swipeView = SwipeView.wrap(testView, 1000);
add(BorderLayout.CENTER, swipeView.getLayer());
}
private void install() {
JButton jButton = new JButton("Swipe");
jButton.addActionListener(this::doSwipe);
add(jButton, BorderLayout.PAGE_END);
AncestorListener ancestorListener = new AncestorListener() {
#Override
public void ancestorAdded(final AncestorEvent event) {
JComponent button = event.getComponent();
button.requestFocus();
button.removeAncestorListener(this); // execute only once.
}
#Override public void ancestorRemoved(final AncestorEvent event) { }
#Override public void ancestorMoved(final AncestorEvent event) { }
};
jButton.addAncestorListener(ancestorListener);
}
private void doSwipe(ActionEvent ignored) {
swipeView.swipeLeft(this::flipPage);
}
private void flipPage() {
page1 = !page1;
if (page1) {
testView.setText(text1);
} else {
testView.setText(text2);
}
}
private static class TestView extends JPanel {
private final JTextArea textArea;
TestView() {
super(new BorderLayout());
textArea = new JTextArea(20, 40);
JScrollPane scrollPane = new JScrollPane(textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
textArea.setEditable(false);
textArea.setText(text1);
add(scrollPane, BorderLayout.CENTER);
}
private void setText(String text) {
textArea.setText(text);
}
}
/**
* SwipeView adds a swipe special effect to a Component. This draws a swipe-right or swipe-left effect on a chosen
* action. It also optionally supports a repeated action when the mouse is held down.
* <p>
* This class is very specific right now, but I hope to generalize it for other special effects later.
* <p>Created by IntelliJ IDEA.
* <p>Date: 4/4/18
* <p>Time: 12:38 AM
*
* #author Miguel Mu\u00f1oz
*/
#SuppressWarnings("MagicNumber")
public static final class SwipeView<C extends JComponent> extends LayerUI<C> {
public static <J extends JComponent> SwipeView<J> wrap(J view, int durationMillis) {
JLayer<J> jLayer = new JLayer<>(view);
final SwipeView<J> ui = new SwipeView<>(view, jLayer, durationMillis);
jLayer.setUI(ui);
return ui;
}
private final C liveComponent;
private Image priorScreen = null;
private Image upcomingScreen = null;
private final JLayer<C> layer;
private boolean isAnimating = false;
private SwipeDirection swipeDirection = SwipeDirection.SWIPE_RIGHT;
private final int maxFrames;
// Calculated:
#SuppressWarnings("FieldCanBeLocal")
private final int frameMillis;
private int frame = 0;
private final long startTime = System.currentTimeMillis();
private SwipeView(C view, JLayer<C> theLayer, int animationDurationMillis) {
super();
liveComponent = view;
layer = theLayer;
maxFrames = (30 * animationDurationMillis) / 1000;
frameMillis = animationDurationMillis / maxFrames;
}
public JLayer<C> getLayer() { return layer; }
/**
* Perform the specified operation with a swipe-right special effect. This is often used in an ActionListener:
* <pre>
* first.addActionListener((e) -> swipeView.swipeRight(recordModel::goFirst));
* </pre>
* Here, the Action listener will perform a Swipe-right after executing the goFirst() method of recordModel.
*
* #param operation The operation
*/
#SuppressWarnings("WeakerAccess")
public void swipeRight(Runnable operation) {
swipe(operation, SwipeDirection.SWIPE_RIGHT);
}
/**
* Perform the specified operation with a swipe-left special effect. This is often used in an ActionListener:
* <pre>
* first.addActionListener((e) -> swipeView.swipeLeft(recordModel::goFirst));
* </pre>
* Here, the Action listener will perform a Swipe-Left after executing the goFirst() method of recordModel.
*
* #param operation The operation
*/
#SuppressWarnings("WeakerAccess")
public void swipeLeft(Runnable operation) {
swipe(operation, SwipeDirection.SWIPE_LEFT);
}
private void swipe(Runnable operation, SwipeDirection swipeDirection) {
prepareToAnimate(swipeDirection);
operation.run();
animate();
}
// #SuppressWarnings({"HardCodedStringLiteral", "HardcodedFileSeparator"})
#Override
public void paint(final Graphics g, final JComponent c) {
if (isAnimating) {
int xLimit = (c.getWidth() * 2 * frame) / maxFrames;
if (swipeDirection == SwipeDirection.SWIPE_LEFT) {
xLimit = (c.getWidth() * 2) - xLimit;
}
int width = c.getWidth() * 2;
int height = c.getHeight() * 2;
// //noinspection UseOfSystemOutOrSystemErr
// System.out.printf("Dimensions: Frame: %d/%d (at %d) xLimit: %4d (%4d x %4d) (from %4d x %4d) Animating: %b%n",
// frame, maxFrames, System.currentTimeMillis() - startTime, xLimit, width, height, c.getWidth(), c.getHeight(), isAnimating);
assert upcomingScreen != null;
assert priorScreen != null;
Image pScreen = Objects.requireNonNull(priorScreen);
Image uScreen = Objects.requireNonNull(upcomingScreen);
if (swipeDirection == SwipeDirection.SWIPE_RIGHT) {
g.drawImage(uScreen, 0, 0, xLimit, height, 0, 0, xLimit, height, c);
g.drawImage(pScreen, xLimit, 0, width, height, xLimit, 0, width, height, c);
} else {
g.drawImage(uScreen, xLimit, 0, width, height, xLimit, 0, width, height, c);
g.drawImage(pScreen, 0, 0, xLimit, height, 0, 0, xLimit, height, c);
}
} else {
super.paint(g, c);
}
}
private void prepareToAnimate(SwipeDirection swipeDirection) {
this.swipeDirection = swipeDirection;
isAnimating = true;
frame = 0;
// Save current state
priorScreen = new BufferedImage(liveComponent.getWidth() * 2, liveComponent.getHeight() * 2, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2D = (Graphics2D) priorScreen.getGraphics();
liveComponent.paint(graphics2D);
graphics2D.dispose();
}
private void animate() {
Timer timer = new Timer(frameMillis, null);
final ActionListener actionListener = (evt) -> {
frame++;
layer.repaint();
if (frame == maxFrames) {
frame = 0;
isAnimating = false;
timer.stop(); // Investigate: Am I leaking timers?
}
};
timer.addActionListener(actionListener);
upcomingScreen = new BufferedImage(liveComponent.getWidth() * 2, liveComponent.getHeight() * 2, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2D = (Graphics2D) upcomingScreen.getGraphics();
liveComponent.paint(graphics2D);
graphics2D.dispose();
timer.start();
}
}
public static enum SwipeDirection {
#SuppressWarnings("JavaDoc") SWIPE_RIGHT,
#SuppressWarnings("JavaDoc") SWIPE_LEFT
}
}
I don't use a retina display, but I did notice a slight painting difference when the animation started.
I changed both of your BufferedImage to get rid of the alpha value and I no longer notice the painting difference:
//priorScreen = new BufferedImage(liveComponent.getWidth() * 2, liveComponent.getHeight() * 2, BufferedImage.TYPE_INT_ARGB);
priorScreen = new BufferedImage(liveComponent.getWidth() * 2, liveComponent.getHeight() * 2, BufferedImage.TYPE_INT_RGB);
It turns out I needed to change the way I animated the frame to account for the doubling of the scale.
First, I needed to detect the scale. I added this code, which requires Java 9 or greater to work correctly. (It compiles under java 8, but fails to execute correctly, always returning 1 for any screen.)
private static final int SCALE = calculateScaleForDefaultScreen();
private static int calculateScaleForDefaultScreen() {
// scale will be 2.0 for a Retina screen, and 1.0 for an older screen
double scale = GraphicsEnvironment.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration()
.getDefaultTransform()
.getScaleX(); // Requires Java 9+ to work. Compiles under Java 8 but always returns 1.0.
//noinspection NumericCastThatLosesPrecision
return (int) Math.round(scale);
}
When I prepared my two off-screen graphics, I needed to do so at twice the scale:
Graphics2D graphics2D = (Graphics2D) priorScreen.getGraphics();
graphics2D.scale(SCALE, SCALE);
liveComponent.paint(graphics2D); // paint the current state of liveComponent into the image
graphics2D.dispose();
And…
Graphics2D graphics2D = (Graphics2D) upcomingScreen.getGraphics();
graphics2D.scale(SCALE, SCALE);
liveComponent.paint(graphics2D); // paint the upcoming state of liveComponent into the image
graphics2D.dispose();
Then, when I did my animation, I needed to include the SCALE in the drawing.
if (swipeDirection == SwipeDirection.SWIPE_RIGHT) {
g.drawImage(uScreen, 0, 0, xLimit, height, 0, 0, xLimit*SCALE, height*SCALE, c);
g.drawImage(pScreen, xLimit, 0, width, height, xLimit*SCALE, 0, width*SCALE, height*SCALE, c);
} else {
g.drawImage(uScreen, xLimit, 0, width, height, xLimit*SCALE, 0, width*SCALE, height*SCALE, c);
g.drawImage(pScreen, 0, 0, xLimit, height, 0, 0, xLimit*SCALE, height*SCALE, c);
}
There are several other places where I multiplied widths and heights by 2. I changed those to SCALE as well.
I wish there were a more elegant solution, but this works.
I looked through some of the other questions similar to mine, but none of them seemed of help when i tried their fixes, How would i go about fixing this as its the only thing stopping my programme from printing rectangles which is vital.
Below is the code for the entire programme as im not sure in where the problem lies, only what the problem is. The stuff that is commented I'm currently not using, but I'm keeping it in there just in case I find a use for it later on. Many thanks
Currently, my programme does not draw any sort of rectangle as it should, and its because repaint doesn't invoke the paintcomponent. My question is basically asking as to why nothing is printed when I start it.
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.logging.Logger;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
/**
* Write a description of class Game here.
*
* #author Adam Steele
* #version 1.0.0
*/
public class Game extends JPanel implements MouseListener
{
// logger
private static final Logger LOGGER = Logger.getLogger(Game.class.getName());
// variables
private boolean isGolden; // Class wide boolean for whether rectangle is golden
private int score; // Stores the score
private int noOfAttempts; // stores number of rectangles that are created
private int goldTimer; // attempts before fail & new rectangle is created
// graphics
private Rectangle box;
private JFrame frame;
private JPanel panel;
// testing
private ArrayList<Rect> rects = new ArrayList<Rect>();
/**
* Constructor for objects of class Game
*/
public Game()
{
setup();
}
public void setup()
{
LOGGER.info("setup has been called");
// initalise frame
frame = new JFrame();
final int FRAME_WIDTH = 800;
final int FRAME_HEIGHT = 600;
// set frame attributes
frame.setSize(FRAME_WIDTH, FRAME_HEIGHT);
frame.setTitle("Gold Picker");
// initalise panel
panel = new JPanel();
panel.addMouseListener(this);
// add things to panel - ie. score, time limit, etc.
// panel.add();
JLabel amLabel = new JLabel("I am a GUI label.");
panel.add(amLabel);
JButton button = new JButton("Button");
button.setBackground(Color.YELLOW);
button.setForeground(Color.GREEN);
panel.add(button);
// add panel to frame
frame.add(panel);
// Make frame visible
showFrame();
LOGGER.info("Game window has been created");
// (re)set variables
score = 0;
noOfAttempts = 0;
goldTimer = 30;
// this is to check variable initalisation
LOGGER.info("Variables have been initalised at: score = " + score + ", noOfAttempts = " + noOfAttempts + ", goldTimer = " + goldTimer);
// start game
decideGolden();
}
public void showFrame()
{
frame.setVisible(true);
}
/**
* A method that decides if the rectangle will be golden based on a random chance
*/
public void decideGolden()
{
double goldProportion = Math.random() * 1;
double goldChance = Math.random() * 1;
//System.out.println(goldChance + " " + goldProportion);
if(goldChance <= goldProportion) {
isGolden = true;
} else {
isGolden = false;
}
LOGGER.info("isGolden has been set to " + isGolden);
timedRect();
}
/**
* Method for generating rectangled on timed intervals
*/
public void timedRect()
{
/*
Timer timer = new Timer();
timer.schedule(new TimerTask() {
#Override
public void run()
{
}
}, 20000, 20000 );
/*
if(isGolden) {
repaint();
} else {
createNormalRectangle();
}
new java.util.Timer().schedule(
new java.util.TimerTask() {
#Override
public void run() {
}
},
20000); */
LOGGER.info("timedRect has been called");
for(int i = 0; i < goldTimer; i++) {
try {
LOGGER.info("try has been reached");
//repaint();
drawRectangle();
Thread.sleep(20000); // wait 20 seconds..
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
#Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
LOGGER.info("paintComponent has been called with " + g);
/*
int x = 400;
int y = 300;
// Using Math.random() or .nextInt() we could make random gold rects
int width = 100;
int height = 100;
LOGGER.info("x = " + x + ", y = " + y + ", width = " + width + ", height = " + height);
if(isGolden) {
//g.drawRect(x, y, width, height);
g.setColor(Color.BLACK);
g.fillRect(x, y, 100, 100); // g.fillRect(x, y, size, size);
} else {
//g.drawRect(x, y, width, height);
g.setColor(Color.BLACK);
g.fillRect(x, y, 100, 100);
}
*/
Graphics2D g2d = (Graphics2D) g;
for(Rect rectangle : rects) { // for each Rect object in rect ArrayList ..
rectangle.paint(g2d);
}
}
/**
*
*/
public void drawRectangle()
{
int x = (int) (Math.random() * getWidth());
int y = (int) (Math.random() * getHeight());
int width = (int) (Math.random() * (getWidth() / 4));
int height = (int) (Math.random() * (getHeight() / 4));
// LOGGER.info("x = " + x + ", y = " + y + ", width = " + width + ", height = " + height);
if (x + width > getWidth()) {
x = getWidth() - width;
}
if (y + height > getHeight()) {
y = getHeight() - height;
}
Color color = new Color(
(int) (Math.random() * 255),
(int) (Math.random() * 255),
(int) (Math.random() * 255));
rects.add(new Rect(x, y, width, height, color));
repaint();
// LOGGER.info("repaint has been called, check if paintComponent has been called..");
}
/**
* these methods are needed to override the MouseListener
* ..and hence needed to implement the MouseListener
* ..which will probably only be used for testing
* ..actionlistener maybe more appropriate
*/
#Override
public void mouseClicked(MouseEvent e) {
LOGGER.info("Mouse has been clicked");
drawRectangle();
}
public void mouseExited(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
}
public void mouseReleased(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
}
Rect class in case its needed
Rect class
import java.awt.*;
/**
* Deaals with misc properties of rect
* Inherits the Rectangle class from java.awt.Rectangle
public class Rect extends Rectangle
{
private Color color;
/**
* Constructor for objects of class Rect
*/
public Rect(int x, int y, int width, int height, Color color)
{
super(x, y, width, height);
this.color = color;
}
public void paint(Graphics2D g2d)
{
g2d.setColor(color);
g2d.fill(this);
}
}
Basically, you never actually add Game to anything which can display it, therefore it will never be painted.
Before a component can be painted, it must be added to a container which is realised on the screen.
One of the issues you're having is your Game class is taking on to much responsibility, it should be focused on displaying and managing the game state, not also creating the basic UI.
Another issue you're going to have is your timedRect method will block the EDT, preventing anything from getting painted anyway
This is a "basic" example (I've not tested it because I don't have your Rect class), but conceptually it should get you closer to your goal.
Realistically, I'd have a separate "main" class which started the app, setup the initial state, created the UI and gets the ball rolling.
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.logging.Logger;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.w3c.dom.css.Rect;
public class Game extends JPanel implements MouseListener {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
LOGGER.info("setup has been called");
// initalise frame
JFrame frame = new JFrame();
final int FRAME_WIDTH = 800;
final int FRAME_HEIGHT = 600;
// set frame attributes
frame.setSize(FRAME_WIDTH, FRAME_HEIGHT);
frame.setTitle("Gold Picker");
// initalise panel
JPanel panel = new Game();
// add things to panel - ie. score, time limit, etc.
// panel.add();
JLabel amLabel = new JLabel("I am a GUI label.");
panel.add(amLabel);
JButton button = new JButton("Button");
button.setBackground(Color.YELLOW);
button.setForeground(Color.GREEN);
panel.add(button);
// add panel to frame
frame.add(panel);
frame.setVisible(true);
LOGGER.info("Game window has been created");
}
});
}
// logger
private static final Logger LOGGER = Logger.getLogger(Game.class.getName());
// variables
private boolean isGolden; // Class wide boolean for whether rectangle is golden
private int score; // Stores the score
private int noOfAttempts; // stores number of rectangles that are created
private int goldTimer; // attempts before fail & new rectangle is created
private int gameLoops = 0;
// graphics
private Rectangle box;
// testing
private ArrayList<Rect> rects = new ArrayList<Rect>();
/**
* Constructor for objects of class Game
*/
public Game() {
addMouseListener(this);
// (re)set variables
score = 0;
noOfAttempts = 0;
goldTimer = 30;
// this is to check variable initalisation
LOGGER.info("Variables have been initalised at: score = " + score + ", noOfAttempts = " + noOfAttempts + ", goldTimer = " + goldTimer);
decideGolden();
}
/**
* A method that decides if the rectangle will be golden based on a random
* chance
*/
public void decideGolden() {
double goldProportion = Math.random() * 1;
double goldChance = Math.random() * 1;
//System.out.println(goldChance + " " + goldProportion);
if (goldChance <= goldProportion) {
isGolden = true;
} else {
isGolden = false;
}
LOGGER.info("isGolden has been set to " + isGolden);
timedRect();
}
/**
* Method for generating rectangled on timed intervals
*/
public void timedRect() {
Timer timer = new Timer(2000, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (gameLoops < goldTimer) {
LOGGER.info("try has been reached");
//repaint();
drawRectangle();
gameLoops++;
} else {
((Timer) (e.getSource())).stop();
}
}
});
timer.start();
LOGGER.info("timedRect has been called");
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
LOGGER.info("paintComponent has been called with " + g);
Graphics2D g2d = (Graphics2D) g;
for (Rect rectangle : rects) { // for each Rect object in rect ArrayList ..
rectangle.paint(g2d);
}
}
/**
*
*/
public void drawRectangle() {
int x = (int) (Math.random() * getWidth());
int y = (int) (Math.random() * getHeight());
int width = (int) (Math.random() * (getWidth() / 4));
int height = (int) (Math.random() * (getHeight() / 4));
// LOGGER.info("x = " + x + ", y = " + y + ", width = " + width + ", height = " + height);
if (x + width > getWidth()) {
x = getWidth() - width;
}
if (y + height > getHeight()) {
y = getHeight() - height;
}
Color color = new Color(
(int) (Math.random() * 255),
(int) (Math.random() * 255),
(int) (Math.random() * 255));
rects.add(new Rect(x, y, width, height, color));
repaint();
LOGGER.info("repaint has been called, check if paintComponent has been called..");
}
/**
* these methods are needed to override the MouseListener ..and hence needed
* to implement the MouseListener ..which will probably only be used for
* testing ..actionlistener maybe more appropriate
*/
#Override
public void mouseClicked(MouseEvent e) {
LOGGER.info("Mouse has been clicked");
drawRectangle();
}
public void mouseExited(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
}
public void mouseReleased(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public class Rect extends Rectangle {
private Color color;
/**
* Constructor for objects of class Rect
*/
public Rect(int x, int y, int width, int height, Color color) {
super(x, y, width, height);
this.color = color;
}
public void paint(Graphics2D g2d) {
g2d.setColor(color);
g2d.fill(this);
}
}
}
Why are you extending JPanel when you have an instance of JPanel? When you extend a class, you inherit all of its functionality.
Instead of using your instance "panel.add" whatever, get rid of your JPanel instance and use this.add.
Which leads to my next statement:
Your class should extend JComponent, you're not overriding paintcomponent because JPanel doesn't have a paintcomponent() method to override.
Read up on how to properly implement JComponent and JFrame and how they go together. Normally you add all your drawings to the JComponent, then you say frame.add(component), where component is of type JComponent.
Try reimplementing your code in this fashion and seperate your classes, now you don't have to follow this exact format but from what you've posted, I think you're lost.
Implement a class that extends JFrame, this will be your frame.
Implement a class that "knows" how to draw your rectangle shapes. You must put it in a method called draw(Graphics2D g2){}.
Implement a class that extends JComponent, this class may have an arraylist of your "rectangle" objects.
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
// initialize your rectangle objects here
for(Rectangles rects: whateveryourarraylistvariable) {
rects.draw(g2);
}
}
good luck
I want to create a button that has an icon with text on the top and arrow on the corner. Upon clicking on it, a menu is displayed. I was able to achieve all of above except for the arrow part. I know I can have an an image file that has the icon and arrow saved in one png file. However, I don't want to alter the icon file. Here is the code I have so far.
How can I add an arrow ( whether programmtically or from another arrow image file to the button)
public class JButtonMenu extends JToggleButton {
JPopupMenu popup;
public JButtonMenu(ImageIcon img, String title, String []list) {
super(name);
this.popup = new JPopupMenu();
this.buttonId = buttonId;
this.setMenuList(list); //This is another method
setIcon(img);
setVerticalTextPosition(SwingConstants.TOP);
setHorizontalTextPosition(SwingConstants.CENTER);
}
public void setMenuList(String[]list){
if(list == null){
return;
}
for(String item:list){
popup.add(new JMenuItem(new AbstractAction(item) {
public void actionPerformed(ActionEvent e) {
JMenuItem menuItem = (JMenuItem)e.getSource();
int index= popup.getComponentIndex(menuItem);
menuItemListener.itemSelectedListener(buttonId,index, menuItem.getText());
}
}));
}
}
}
This is basically a watered down version of this implementation of a split button but which focuses on the need for painting an additional image as well as some of the other functionality you'll need to implement to ensure that the original text and icon are offset accurtaly.
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
SplitButton btn = new SplitButton();
btn.setText("This is a split button");
JFrame frame = new JFrame("Testing");
frame.setLayout(new GridBagLayout());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(btn);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class SplitButton extends JButton {
private int separatorSpacing = 4;
private int splitWidth = 30;
private int arrowSize = 8;
private Rectangle splitRectangle;
private Color arrowColor = Color.BLACK;
private Color disabledArrowColor = Color.GRAY;
private Image image;
public SplitButton() {
super();
}
#Override
public Insets getInsets() {
Insets insets = (Insets) super.getInsets().clone();
insets.right += splitWidth;
return insets;
}
#Override
public Insets getInsets(Insets insets) {
Insets insets1 = getInsets();
insets.left = insets1.left;
insets.right = insets1.right;
insets.bottom = insets1.bottom;
insets.top = insets1.top;
return insets1;
}
/**
* Returns the separatorSpacing. Separator spacing is the space above and
* below the separator( the line drawn when you hover your mouse over the
* split part of the button).
*
* #return separatorSpacingimage = null; //to repaint the image with the new
* size
*/
public int getSeparatorSpacing() {
return separatorSpacing;
}
/**
* Sets the separatorSpacing.Separator spacing is the space above and below
* the separator( the line drawn when you hover your mouse over the split
* part of the button).
*
* #param spacing
*/
public void setSeparatorSpacing(int spacing) {
if (spacing != separatorSpacing && spacing >= 0) {
int old = separatorSpacing;
this.separatorSpacing = spacing;
image = null;
firePropertyChange("separatorSpacing", old, separatorSpacing);
revalidate();
repaint();
}
}
/**
* Gets the color of the arrow.
*
* #return arrowColor
*/
public Color getArrowColor() {
return arrowColor;
}
/**
* Set the arrow color.
*
* #param color
*/
public void setArrowColor(Color color) {
if (arrowColor != color) {
Color old = arrowColor;
this.arrowColor = color;
image = null;
firePropertyChange("arrowColor", old, arrowColor);
repaint();
}
}
/**
* gets the disabled arrow color
*
* #return disabledArrowColor color of the arrow if no popup attached.
*/
public Color getDisabledArrowColor() {
return disabledArrowColor;
}
/**
* sets the disabled arrow color
*
* #param color color of the arrow if no popup attached.
*/
public void setDisabledArrowColor(Color color) {
if (disabledArrowColor != color) {
Color old = disabledArrowColor;
this.disabledArrowColor = color;
image = null; //to repaint the image with the new color
firePropertyChange("disabledArrowColor", old, disabledArrowColor);
}
}
/**
* Splitwidth is the width of the split part of the button.
*
* #return splitWidth
*/
public int getSplitWidth() {
return splitWidth;
}
/**
* Splitwidth is the width of the split part of the button.
*
* #param width
*/
public void setSplitWidth(int width) {
if (splitWidth != width) {
int old = splitWidth;
this.splitWidth = width;
firePropertyChange("splitWidth", old, splitWidth);
revalidate();
repaint();
}
}
/**
* gets the size of the arrow.
*
* #return size of the arrow
*/
public int getArrowSize() {
return arrowSize;
}
/**
* sets the size of the arrow
*
* #param size
*/
public void setArrowSize(int size) {
if (arrowSize != size) {
int old = arrowSize;
this.arrowSize = size;
image = null; //to repaint the image with the new size
firePropertyChange("setArrowSize", old, arrowSize);
revalidate();
repaint();
}
}
/**
* Gets the image to be drawn in the split part. If no is set, a new image
* is created with the triangle.
*
* #return image
*/
public Image getImage() {
if (image == null) {
Graphics2D g = null;
BufferedImage img = new BufferedImage(arrowSize, arrowSize, BufferedImage.TYPE_INT_RGB);
g = (Graphics2D) img.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, img.getWidth(), img.getHeight());
g.setColor(isEnabled() ? arrowColor : disabledArrowColor);
//this creates a triangle facing right >
g.fillPolygon(new int[]{0, 0, arrowSize / 2}, new int[]{0, arrowSize, arrowSize / 2}, 3);
g.dispose();
//rotate it to face downwards
img = rotate(img, 90);
BufferedImage dimg = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB);
g = (Graphics2D) dimg.createGraphics();
g.setComposite(AlphaComposite.Src);
g.drawImage(img, null, 0, 0);
g.dispose();
for (int i = 0; i < dimg.getHeight(); i++) {
for (int j = 0; j < dimg.getWidth(); j++) {
if (dimg.getRGB(j, i) == Color.WHITE.getRGB()) {
dimg.setRGB(j, i, 0x8F1C1C);
}
}
}
image = Toolkit.getDefaultToolkit().createImage(dimg.getSource());
}
return image;
}
/**
*
* #param g
*/
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
//Graphics gClone = g.create();//EDIT: Hervé Guillaume
Color oldColor = g.getColor();
splitRectangle = new Rectangle(getWidth() - splitWidth, 0, splitWidth, getHeight());
g.translate(splitRectangle.x, splitRectangle.y);
int mh = getHeight() / 2;
int mw = splitWidth / 2;
g.drawImage(getImage(), mw - arrowSize / 2, mh + 2 - arrowSize / 2, null);
if (getModel().isRollover() || isFocusable()) {
g.setColor(UIManager.getLookAndFeelDefaults().getColor("Button.background"));
g.drawLine(1, separatorSpacing + 2, 1, getHeight() - separatorSpacing - 2);
g.setColor(UIManager.getLookAndFeelDefaults().getColor("Button.shadow"));
g.drawLine(2, separatorSpacing + 2, 2, getHeight() - separatorSpacing - 2);
}
g.setColor(oldColor);
g.translate(-splitRectangle.x, -splitRectangle.y);
}
/**
* Rotates the given image with the specified angle.
*
* #param img image to rotate
* #param angle angle of rotation
* #return rotated image
*/
private BufferedImage rotate(BufferedImage img, int angle) {
int w = img.getWidth();
int h = img.getHeight();
BufferedImage dimg = dimg = new BufferedImage(w, h, img.getType());
Graphics2D g = dimg.createGraphics();
g.rotate(Math.toRadians(angle), w / 2, h / 2);
g.drawImage(img, null, 0, 0);
return dimg;
}
}
}
Swing has a well defined and documented painting process, in order to perform custom painting you need to work within the constraints of the API otherwise you will end up with no end ot issues.
Take a look at Painting in AWT and Swing and Performing Custom Painting for more details
A quick way to do it would be
Image img1=imageIcon1.getImage();
Image img2=imageIcon2.getImage();
Graphics g=img1.getGraphics();
g.drawImage(img2, x, y, sizex, sizey, null)
where x, y is where you put the second icon (arrow) on the first icon and sizex, sizey is the reduced size of the second icon. You can change these by trying.
You have to have another method
public JButtonMenu(ImageIcon imageIcon1, ImageIcon imageIcon2, String title, String []list) {
// above segment here
// continue with the rest of the code
}
How can I quickly and efficiently set all pixels of a BufferedImage to transparent so that I can simply redraw what sprite graphics I want for each frame?
I am designing a simple game engine in java that updates a background and foreground BufferedImage and draws them to a composite VolatileImage for efficient scaling, to be drawn to a JPanel. This scalable model allows me to add more layers and iterate over each drawing layer.
I simplified my application into one class given below that demonstrates my issue. Use the arrow keys to move a red square over the image. The challenge is I want to decouple updating the game graphics from drawing the composite graphics to the game engine. I have studied seemingly thorough answers to this question but cannot figure out how to apply them to my application:
Java: Filling a BufferedImage with transparent pixels
Here is the critical section that does not clear the pixels correctly. The commented out section is from stack-overflow answers I have read already, but they either draw the background as a non-transparent black or white. I know the foregroundImage begins with transparent pixels in my implementation as you can see the random pixel noise of the backgroundImage behind the red sprite when the application begins. Right now, the image is not cleared, so the previous drawn images remain.
/** Update the foregroundGraphics. */
private void updateGraphics(){
Graphics2D fgGraphics = (Graphics2D) foregroundImage.getGraphics();
// set image pixels to transparent
//fgGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR));
//fgGraphics.setColor(new Color(0,0,0,0));
//fgGraphics.clearRect(0, 0, width, height);
//fgGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
// draw again.
fgGraphics.setColor(Color.RED);
fgGraphics.fillRect(sx, sy, spriteSize, spriteSize);
fgGraphics.dispose();
}
Here is my entire example code:
/**
* The goal is to draw two BufferedImages quickly onto a scalable JPanel, using
* a VolatileImage as a composite.
*/
public class Example extends JPanel implements Runnable, KeyListener
{
private static final long serialVersionUID = 1L;
private int width;
private int height;
private Object imageLock;
private Random random;
private JFrame frame;
private Container contentPane;
private BufferedImage backgroundImage;
private BufferedImage foregroundImage;
private VolatileImage compositeImage;
private Graphics2D compositeGraphics;
private int[] backgroundPixels;
private int[] foregroundPixels;
// throttle the framerate.
private long prevUpdate;
private int frameRate;
private int maximumWait;
// movement values.
private int speed;
private int sx;
private int sy;
private int dx;
private int dy;
private int spriteSize;
/** Setup required fields. */
public Example(){
width = 512;
height = 288;
super.setPreferredSize(new Dimension(width, height));
imageLock = new Object();
random = new Random();
frame = new JFrame("BufferedImage Example");
frame.addKeyListener(this);
contentPane = frame.getContentPane();
contentPane.add(this, BorderLayout.CENTER);
// used to create hardware-accelerated images.
GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
backgroundImage = gc.createCompatibleImage(width, height,Transparency.TRANSLUCENT);
foregroundImage = gc.createCompatibleImage(width, height,Transparency.TRANSLUCENT);
compositeImage = gc.createCompatibleVolatileImage(width, height,Transparency.TRANSLUCENT);
compositeGraphics = compositeImage.createGraphics();
compositeGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
compositeGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
backgroundPixels = ((DataBufferInt) backgroundImage.getRaster().getDataBuffer()).getData();
foregroundPixels = ((DataBufferInt) foregroundImage.getRaster().getDataBuffer()).getData();
//initialize the background image.
for(int i = 0; i < backgroundPixels.length; i++){
backgroundPixels[i] = random.nextInt();
}
// used to throttle frames per second
frameRate = 180;
maximumWait = 1000 / frameRate;
prevUpdate = System.currentTimeMillis();
// used to update sprite state.
speed = 1;
dx = 0;
dy = 0;
sx = 0;
sy = 0;
spriteSize = 32;
}
/** Renders the compositeImage to the Example, scaling to fit. */
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// draw the composite, scaled to the JPanel.
synchronized (imageLock) {
((Graphics2D) g).drawImage(compositeImage, 0, 0, super.getWidth(), super.getHeight(), 0, 0, width, height, null);
}
// force repaint.
repaint();
}
/** Update the BufferedImage states. */
#Override
public void run() {
while(true){
updateSprite();
updateGraphics();
updateComposite();
throttleUpdateSpeed();
}
}
/** Update the Sprite's position. */
private void updateSprite(){
// update the sprite state from the inputs.
dx = 0;
dy = 0;
if (Command.UP.isPressed()) dy -= speed;
if (Command.DOWN.isPressed()) dy += speed;
if (Command.LEFT.isPressed()) dx -= speed;
if (Command.RIGHT.isPressed()) dx += speed;
sx += dx;
sy += dy;
// adjust to keep in bounds.
sx = sx < 0 ? 0 : sx + spriteSize >= width ? width - spriteSize : sx;
sy = sy < 0 ? 0 : sy + spriteSize >= height ? height - spriteSize : sy;
}
/** Update the foregroundGraphics. */
private void updateGraphics(){
Graphics2D fgGraphics = (Graphics2D) foregroundImage.getGraphics();
// set image pixels to transparent
//fgGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR));
//fgGraphics.setColor(new Color(255, 255, 255, 255));
//fgGraphics.clearRect(0, 0, width, height);
//fgGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
// draw again.
fgGraphics.setColor(Color.RED);
fgGraphics.fillRect(sx, sy, spriteSize, spriteSize);
fgGraphics.dispose();
}
/** Draw the background and foreground images to the volatile composite. */
private void updateComposite(){
synchronized (imageLock) {
compositeGraphics.drawImage(backgroundImage, 0, 0, null);
compositeGraphics.drawImage(foregroundImage, 0, 0, null);
}
}
/** Keep the update rate around 60 FPS. */
public void throttleUpdateSpeed(){
try {
Thread.sleep(Math.max(0, maximumWait - (System.currentTimeMillis() - prevUpdate)));
prevUpdate = System.currentTimeMillis();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
/** Ignore key typed events. */
#Override
public void keyTyped(KeyEvent e) {}
/** Handle key presses. */
#Override
public void keyPressed(KeyEvent e) {
setCommandPressedFrom(e.getKeyCode(), true);
}
/** Handle key releases. */
#Override
public void keyReleased(KeyEvent e) {
setCommandPressedFrom(e.getKeyCode(), false);
}
/** Switch over key codes and set the associated Command's pressed value. */
private void setCommandPressedFrom(int keycode, boolean pressed){
switch (keycode) {
case KeyEvent.VK_UP:
Command.UP.setPressed(pressed);
break;
case KeyEvent.VK_DOWN:
Command.DOWN.setPressed(pressed);
break;
case KeyEvent.VK_LEFT:
Command.LEFT.setPressed(pressed);
break;
case KeyEvent.VK_RIGHT:
Command.RIGHT.setPressed(pressed);
break;
}
}
/** Commands are used to interface with key press values. */
public enum Command{
UP, DOWN, LEFT, RIGHT;
private boolean pressed;
/** Press the Command. */
public void press() {
if (!pressed) pressed = true;
}
/** Release the Command. */
public void release() {
if (pressed) pressed = false;
}
/** Check if the Command is pressed. */
public boolean isPressed() {
return pressed;
}
/** Set if the Command is pressed. */
public void setPressed(boolean pressed) {
if (pressed) press();
else release();
}
}
/** Begin the Example. */
public void start(){
try {
// create and display the frame.
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
Example e = new Example();
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
// start updating from key inputs.
Thread t = new Thread(this);
t.start();
}
catch (InvocationTargetException e) {
e.printStackTrace();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
/** Start the application. */
public static void main(String[] args){
Example e = new Example();
e.start();
}
}
Edits:
- Fixed a typo in the for-loop initializing the backgroundPixels to random.
Turns out I goofed in my method selection. I noticed I was clearing a one-pixel wide box that was the outline of my graphics. This is because I accidentally used drawRect() instead of fillRect(). Upon changing my code it works now. Here are examples I was able to get to work.
Example using AlphaComposite.CLEAR (draw with any opaque color):
// clear pixels
fgGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR));
fgGraphics.setColor(new Color(255,255,255,255));
fgGraphics.fillRect(0, 0, width, height);
fgGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
// draw new graphics
Example using AlphaComposite.SRC_OUT (draw with any color with alpha zero):
// clear pixels
fgGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OUT));
fgGraphics.setColor(new Color(255,255,255,0));
fgGraphics.fillRect(0, 0, width, height);
fgGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
// draw new graphics
Trying the exercise in lesson2 of the Udacity course. Despite importing the classes (I'm at java.awt.* now, but I also tried java.awt.Color and java.awt.Canvas separately (also need Shape))..
package com.jul.udacity.lesson2;
public class TestRectangle {
public static void main(String[] args) {
// TODO Auto-generated method stub
Rectangle rect1 = new Rectangle(100.0, 100.0, 200.0, 100.0);
rect1.draw();
}
}
And the class is copied from there and java.awt import added. Any help will be great. Thanks!
package com.jul.udacity.lesson2;
//HIDE
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
//import java.awt.Color;
//import java.awt.Shape;
//import java.awt.Canvas;
import java.awt.*;
public class Rectangle implements Shape
{
private Color color = Color.BLACK;
private boolean filled = false;
private double x;
private double y;
private double width;
private double height;
/**
Constructs an empty rectangle.
*/
public Rectangle()
{
x = 0;
y = 0;
width = 0;
height = 0;
}
/**
Constructs a rectangle.
#param x the leftmost x-coordinate
#param y the topmost y-coordinate
#param width the width
#param height the height
*/
public Rectangle(double x, double y, double width, double height)
{
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
/**
Gets the leftmost x-position of this rectangle.
#return the leftmost x-position
*/
public int getX()
{
return (int) Math.round(x);
}
/**
Gets the topmost y-position of this rectangle.
#return the topmost y-position
*/
public int getY()
{
return (int) Math.round(y);
}
/**
Gets the width of this rectangle.
#return the width
*/
public int getWidth()
{
return (int) Math.round(width);
}
/**
Gets the height of this rectangle.
#return the height
*/
public int getHeight()
{
return (int) Math.round(height);
}
/**
Moves this rectangle by a given amount.
#param dx the amount by which to move in x-direction
#param dy the amount by which to move in y-direction
*/
public void translate(double dx, double dy)
{
x += dx;
y += dy;
Canvas.getInstance().repaint();
}
/**
Resizes this rectangle both horizontally and vertically.
#param dw the amount by which to resize the width on each side
#param dw the amount by which to resize the height on each side
*/
public void grow(double dw, double dh)
{
width += 2 * dw;
height += 2 * dh;
x -= dw;
y -= dh;
Canvas.getInstance().repaint();
}
/**
Sets the color of this rectangle.
#param newColor the new color
*/
public void setColor(Color newColor)
{
color = newColor;
Canvas.getInstance().repaint();
}
/**
Draws this rectangle.
*/
public void draw()
{
filled = false;
Canvas.getInstance().show(this);
}
/**
Fills this rectangle.
*/
public void fill()
{
filled = true;
Canvas.getInstance().show(this);
}
public String toString()
{
return "Rectangle[x=" + getX() + ",y=" + getY() + ",width=" + getWidth() + ",height=" + getHeight() + "]";
}
public void paintShape(Graphics2D g2)
{
Rectangle2D.Double rect = new Rectangle2D.Double(getX(), getY(),
getWidth(), getHeight());
g2.setColor(new java.awt.Color((int) color.getRed(), (int) color.getGreen(), (int) color.getBlue()));
if (filled)
{
g2.fill(rect);
}
else
{
g2.draw(rect);
}
}
}
You might want to check the lesson directions carefully. java.awt.Canvas has no getInstance() method. You just use new to make a Canvas. So you either didn't read carefully and are using the wrong Canvas, or there's something else going on.
Also the show() methods are deprecated, so I'm leaning towards you are supposed to be using a different Canvas class.
Also, Swing is not thread safe. Read up on how to use Swing objects
markspace is correct: Canvas here is not the class from java.awt - the Udacity instructors are using their own class called Canvas. I suggest grabbing the lesson files and using those. Here's the Canvas class from that Intro To Java Course:
import java.awt.image.BufferedImage;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.RescaleOp;
import java.io.IOException;
import java.io.File;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
public class Canvas
{
private static Canvas canvas = new Canvas();
private ArrayList<Shape> shapes = new ArrayList<Shape>();
private BufferedImage background;
private JFrame frame;
private CanvasComponent component;
private static final int MIN_SIZE = 100;
private static final int MARGIN = 10;
private static final int LOCATION_OFFSET = 120;
class CanvasComponent extends JComponent
{
public void paintComponent(Graphics g)
{
g.setColor(java.awt.Color.WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(java.awt.Color.BLACK);
if (background != null)
{
g.drawImage(background, 0, 0, null);
}
for (Shape s : new ArrayList<Shape>(shapes))
{
Graphics2D g2 = (Graphics2D) g.create();
s.paintShape(g2);
g2.dispose();
}
}
public Dimension getPreferredSize()
{
int maxx = MIN_SIZE;
int maxy = MIN_SIZE;
if (background != null)
{
maxx = Math.max(maxx, background.getWidth());
maxy = Math.max(maxx, background.getHeight());
}
for (Shape s : shapes)
{
maxx = (int) Math.max(maxx, s.getX() + s.getWidth());
maxy = (int) Math.max(maxy, s.getY() + s.getHeight());
}
return new Dimension(maxx + MARGIN, maxy + MARGIN);
}
}
private Canvas()
{
component = new CanvasComponent();
if (System.getProperty("com.horstmann.codecheck") == null)
{
frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(component);
frame.pack();
frame.setLocation(LOCATION_OFFSET, LOCATION_OFFSET);
frame.setVisible(true);
}
else
{
final String SAVEFILE ="canvas.png";
final Thread currentThread = Thread.currentThread();
Thread watcherThread = new Thread()
{
public void run()
{
try
{
final int DELAY = 10;
while (currentThread.getState() != Thread.State.TERMINATED)
{
Thread.sleep(DELAY);
}
saveToDisk(SAVEFILE);
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
};
watcherThread.start();
}
}
public static Canvas getInstance()
{
return canvas;
}
public void show(Shape s)
{
if (!shapes.contains(s))
{
shapes.add(s);
}
repaint();
}
public void repaint()
{
if (frame == null) return;
Dimension dim = component.getPreferredSize();
if (dim.getWidth() > component.getWidth()
|| dim.getHeight() > component.getHeight())
{
frame.pack();
}
else
{
frame.repaint();
}
}
/**
* Pauses so that the user can see the picture before it is transformed.
*/
public void pause()
{
if (frame == null) return;
JOptionPane.showMessageDialog(frame, "Click Ok to continue");
}
/**
* Takes a snapshot of the screen, fades it, and sets it as the background.
*/
public static void snapshot()
{
Dimension dim = getInstance().component.getPreferredSize();
java.awt.Rectangle rect = new java.awt.Rectangle(0, 0, dim.width, dim.height);
BufferedImage image = new BufferedImage(rect.width, rect.height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
g.setColor(java.awt.Color.WHITE);
g.fillRect(0, 0, rect.width, rect.height);
g.setColor(java.awt.Color.BLACK);
getInstance().component.paintComponent(g);
float factor = 0.8f;
float base = 255f * (1f - factor);
RescaleOp op = new RescaleOp(factor, base, null);
BufferedImage filteredImage
= new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
op.filter(image, filteredImage);
getInstance().background = filteredImage;
getInstance().component.repaint();
}
public void saveToDisk(String fileName)
{
Dimension dim = component.getPreferredSize();
java.awt.Rectangle rect = new java.awt.Rectangle(0, 0, dim.width, dim.height);
BufferedImage image = new BufferedImage(rect.width, rect.height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) image.getGraphics();
g.setColor(java.awt.Color.WHITE);
g.fill(rect);
g.setColor(java.awt.Color.BLACK);
component.paintComponent(g);
String extension = fileName.substring(fileName.lastIndexOf('.') + 1);
try
{
ImageIO.write(image, extension, new File(fileName));
}
catch(IOException e)
{
System.err.println("Was unable to save the image to " + fileName);
}
g.dispose();
}
}