I'm creating a game using Java Swing, and I'm finding the need for graphical displays of what's going on at this point. The current display uses a grid of JButtons to represent the tiles on a battlefield. Would it be possible to display floating/disappearing damage numbers over the JButtons, using perhaps custom made pixel art GIFs? If so, how would I go about implementing this?
Almost all Swing components can be extended to change its presentation.
Create an own button class extending JButton and override its paintComponent method to display the required damage. This method receives a Graphic (actually an instance of Graphics2D) on which you can draw the damage, if one is active.
Very simple example:
public class DamageButton extends JButton {
private String damage = null;
public DamageButton(String text) {
super(text);
}
public void setDamage(String damage) {
this.damage = damage;
System.out.println(damage);
repaint();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (damage != null) {
Graphics2D gg = (Graphics2D) g.create();
try {
gg.setColor(Color.RED);
gg.drawString(damage, 10, 10);
} finally {
gg.dispose();
}
}
}
}
Creating a new Graphics (gg) so the settings of g are not changed and need not to be restored.
Call setDamage() with some text to have it displayed over the button or with null to cancel the effect.
Animation or other effects can (more or less) easily be added. Just be aware that the given Graphics2D has a clipping area set to the dimension of the button.
Related
I know there's no direct replacement for java.awt.Canvas in swing, and I know I'm supposed to use a JPanel and override paintComponent, for example like so:
public void paintComponent(Graphics g) {
g.setColor(Color.black);
g.drawOval(0, 0, 100, 100);
}
And this would draw a black circle on the JPanel when it is created. The problem I have is that I want a dynamic canvas: I want to be able to draw things in response to user input, and redraw continuously, not just once when the application starts. An example would be having a moving object on a canvas, that would need to be redrawn at a rate of say 60 frames per second. How could I achieve this without using AWT components?
EDIT: what I mean is, in an actual canvas, I'd be able to arbitrarily call, say, drawOval anywhere in my code, and that would draw an oval on the canvas; is this doable with JPanel?
Store the information to be drawn (e.g. a Shape or a group of them) and call repaint() from a Swing Timer. Each time the paintComponent(..) method is called, first call the super(..) method to erase the previous drawings, then iterate the list of shapes, move them if necessary, and draw each one.
Here's one way to do it:
public class Renderer extends JComponent implements ActionListener {
private int x;
public Renderer() {
Timer timer = new Timer(1000/60, this);
timer.start();
x = 0;
}
#Override
public void paintComponent(Graphics g) {
super.paint(g);
// drawing code
g.setColor(Color.black);
g.drawOval(x, 0, 100, 100);
}
private void update() {
this.x++;
}
#Override
public void actionPerformed(ActionEvent e) {
update();
repaint();
}
}
Now just add this to your component (JPanel or whatever):
comp.add(new Renderer());
Sorry if my question doesn't adjust to the Stackoverflow requirements due to it is theorical but i don't know where else to ask.
For the past few weeks i've been trying to understand better how the Swing API works and it's components in order to create my own custom components. I've read tons of tutorials, searched in here, i am neck-deep in Java's swing source code and frankly... my mind is a mess.
As far as i understand, the swing components are composed of 3 parts:
the model: where the component state and data are stored
the UI delegate: which paints the component and
the JComponent: it ties everything together.
In this tutorial https://docs.oracle.com/javase/tutorial/uiswing/painting/step2.html there is a paragraph that says:
The paintComponent method is where all of your custom painting takes place. >This method is defined by javax.swing.JComponent and then overridden by your >subclasses to provide their custom behavior
Why is there a paintComponent method on a JComponent? Shouldn't it be an exclusive method of an UI delegate?
Swing is based on AWT. So the initial restriction is based on how AWT paints it components. Because Swing is light weight, when a components root component is painted, the child, Swing, components need to be notified that they need to update as well. This is done by calling the paint methods of all the child components affected by the update (which in turn call the paintComponent methods).
Much of the decisions about the Swing API's paint chain are based around the concept of customisation. It reduces the complexity involved (by discouraging the overriding of paint and focus the functionality down t the paintComponent method).
The Look and Feel API is based on a "delegate" model. This means that the functionality used to perform a said action is "delegated" to some other object. This means that the UI delegate UI doesn't actually know when a component "needs" to be painted, but instead is told, by the component, that it needs to be painted. This makes it much more flexible and in many cases, easier to customise.
Based on your previous question a custom ButtonUI is probably one the better choices, this way you gain much more control over how the button is painted.
Trying to get the buttons to follow the current look and feels color schemes would be very difficult, but you could try having a look at the src.jar which is installed with the JDK, which includes many implementations of look and feels (and if your on Windows you should get the Windows look and feel as well, if your on Mac, then you don't get either Mac or Windows 🙄)
I started by having a look at BasicButtonUI and ButtonUI to get a better understanding of there properties. I pulled some of more interesting methods into a custom ShapeButtonUI...
public class ShapeButtonUI extends BasicButtonUI {
private Shape shape;
public ShapeButtonUI(Shape shape) {
this.shape = shape;
}
protected Color getSelectColor() {
return UIManager.getColor(getPropertyPrefix() + "select");
}
protected Color getDisabledTextColor() {
return UIManager.getColor(getPropertyPrefix()
+ "disabledText");
}
protected Color getFocusColor() {
return UIManager.getColor(getPropertyPrefix() + "focus");
}
#Override
protected void installDefaults(AbstractButton b) {
super.installDefaults(b);
}
#Override
protected void uninstallDefaults(AbstractButton b) {
super.uninstallDefaults(b);
}
#Override
public void paint(Graphics g, JComponent c) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setClip(shape);
Rectangle bounds = shape.getBounds();
LinearGradientPaint lgp = new LinearGradientPaint(
new Point(bounds.x, bounds.y),
new Point(bounds.x, bounds.y + bounds.height),
new float[]{0, 1},
new Color[]{c.getBackground().brighter(), c.getBackground().darker()});
g2d.setPaint(lgp);
g2d.fill(shape);
g2d.dispose();
g2d = (Graphics2D) g.create();
g2d.setColor(c.getForeground());
g2d.draw(shape);
g2d.dispose();
super.paint(g, c);
}
#Override
protected void paintButtonPressed(Graphics g, AbstractButton b) {
super.paintButtonPressed(g, b);
}
#Override
protected void paintFocus(Graphics g, AbstractButton b,
Rectangle viewRect, Rectangle textRect, Rectangle iconRect) {
super.paintFocus(g, b, viewRect, textRect, iconRect);
}
#Override
protected void paintText(Graphics g, AbstractButton b, Rectangle textRect, String text) {
super.paintText(g, b, textRect, text);
// ButtonModel model = b.getModel();
// FontMetrics fm = SwingUtilities2.getFontMetrics(c, g);
// int mnemIndex = b.getDisplayedMnemonicIndex();
//
// /* Draw the Text */
// if (model.isEnabled()) {
// /**
// * * paint the text normally
// */
// g.setColor(b.getForeground());
// } else {
// /**
// * * paint the text disabled **
// */
// g.setColor(getDisabledTextColor());
// }
// SwingUtilities2.drawStringUnderlineCharAt(c, g, text, mnemIndex,
// textRect.x, textRect.y + fm.getAscent());
}
#Override
public Dimension getMinimumSize(JComponent c) {
Rectangle bounds = shape.getBounds();
return new Dimension(bounds.x + bounds.width + 1, bounds.y + bounds.height + 1);
}
#Override
public Dimension getPreferredSize(JComponent c) {
Rectangle bounds = shape.getBounds();
return new Dimension(bounds.x + bounds.width + 1, bounds.y + bounds.height + 1);
}
#Override
public Dimension getMaximumSize(JComponent c) {
Rectangle bounds = shape.getBounds();
return new Dimension(bounds.x + bounds.width + 1, bounds.y + bounds.height + 1);
}
}
Most of these you probably don't need to worry about, but you should at least know they exist as you may want to customise some of the other properties/functionality later.
This delegate is intended to be installed on a button as needed, as apposed to installing it as the default UI delegate for buttons. The reason for this is the need for the shape object. This allows each button to have it's own shape if you want.
You could seed a single shape into the UIManager's properties as well and use that instead, but I've not bothered for this example.
I then created my own shape/path...
public class PointerPath extends Path2D.Double {
public PointerPath() {
moveTo(1, 1);
lineTo(150, 1);
lineTo(198, 100);
lineTo(150, 198);
lineTo(1, 198);
lineTo(50, 100);
closePath();
}
}
And the applied it to a button...
ShapeButtonUI shapeUI = new ShapeButtonUI(new PointerPath());
JButton btn = new JButton("That way");
btn.setUI(shapeUI);
Which finally generated something like...
Now, this is a really basic example, as realistically, the button should be sizing itself around the text (with some additional padding/margin information), but this would require a multi part shape, so we knew which sections could be resized and in what directions, so, complicated.
Main class:
public Main() {
Frame f = new Frame();
final Panel p = f.p;
final Player player = new Player();
Timer t = new Timer(UPDATE_PERIOD, new ActionListener() {
public void actionPerformed(ActionEvent e) {
Graphics g = p.getGraphics();
p.render(g);
player.tick();
player.render(g);
g.dispose();
}
});
t.start();
}
Player render method:
public void render(Graphics g) {
g.drawImage(Images.get("player"), x, y, null);
}
The problem is, that all previous drawn images are still there. Example (when I change the drawn image's x or y):
To draw in Swing, you should not be getting the Graphics object directly from the JPanel. Instead, override the paintComponent method and use the parameter Graphics object to perform your custom drawing, with a call to the parent method to erase previous painting
#Override
public void paintComponent(Graphics g){
super.paintComponent(g);
//custom painting goes here
}
If you wish to trigger a repaint, use the method by that name on the JPanel:
p.repaint();
Rather than doing custom rendering your your timer, you should really be doing all your painting in your paintComponent method. Something like:
public void actionPerformed(ActionEvent e) {
player.tick();
p.repaint();
}
And then re-render the player and the background in paintComponent()
Painting like you currently are runs into issues when you resize the panel, etc
Try calling 'p.repaint()' in your ActionListener once you have changed the position of the Graphic.
I put this code together based on a lot of examples I found around here on stackoverflow. When I run the program the entire screen flickers intensely. I'm sure there is something simple I'm overlooking, but so far have been unable to track down a solution. I've been debugging this for a couple hours mostly with the help of online forum reading, so I figured it was time to ask the audience.
public class Screen extends JComponent {
#Override
public Dimension getPreferredSize(){
Dimension tempDimension = Toolkit.getDefaultToolkit().getScreenSize();
return tempDimension;
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2D = (Graphics2D)bufferStrategy.getDrawGraphics();
g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); //sprites overlap instead of overwrite
if(game==null){
drawSplash(g2D);
}else{
drawBoard(g2D);
}
g2D.dispose();
bufferStrategy.show();
}
}
If any additional code is required, I can provide it. Thank you for your help, stackoverflow!
To achieve the results you are getting, you either have another class which extends from Canvas or are using the BufferStrategy from the top level container. In either case, both must be visible on the screen.
Basically, they are fighting each other, as they are two different painting algorithms. Swing, which is a passive painting algorithm, paints updates as they are needed and the BufferStrategy, which uses an active algorithm, requiring your to schedule the updates to the buffer as required.
Both use a double buffering algorithm.
So, you should pick one or the other...
public class Screen extends JComponent {
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2D = (Graphics2D) g.create();
g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); //sprites overlap instead of overwrite
if (game == null) {
drawSplash(g2D);
} else {
drawBoard(g2D);
}
g2D.dispose();
}
}
or something like...
public void gameEngine(BufferStrategy strategy) {
// Main loop
while (!done) {
// Prepare for rendering the next frame
// ...
// Render single frame
do {
// The following loop ensures that the contents of the drawing buffer
// are consistent in case the underlying surface was recreated
do {
// Get a new graphics context every time through the loop
// to make sure the strategy is validated
Graphics2D g2D = (Graphics2D) strategy.getDrawGraphics();
// Render to graphics
g2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); //sprites overlap instead of overwrite
if (game == null) {
drawSplash(g2D);
} else {
drawBoard(g2D);
}
// Dispose the graphics
g2D.dispose();
// Repeat the rendering if the drawing buffer contents
// were restored
} while (strategy.contentsRestored());
// Display the buffer
strategy.show();
// Repeat the rendering if the drawing buffer was lost
} while (strategy.contentsLost());
}
}
Which was pretty much ripped from the JavaDocs for BufferStrategy
BTW, this...
#Override
public Dimension getPreferredSize(){
Dimension tempDimension = Toolkit.getDefaultToolkit().getScreenSize();
return tempDimension;
}
is a really bad design, you are making assumptions about the state of the component which may not meet reality. You should allow the window to decide how large it ultimately wants to be, which can be achieved by using setExtendedState and passing it JFrame.MAXIMIZED_BOTH, which will take into consideration other OS elements, like the task bar or dock
I am creating an applet that lets the user draw different shapes using the rubber band effect, letting the user see the shape while it is being drawn. What I want is for the program to draw shapes that stay on the screen. The problem is that the program draws a shape wherever the mouse is.
Take the program below, for example. Say the user clicks the applet at point (50,50) and drags the mouse to draw a rectangle with the bottom-right corner at (70,70). The program will draw several rectangles inside the final rectangle (i.e. rectangles with the bottom-right corner at (54,56), (63,61), etc.). I only want the final rectangle to be shown, but also while using the rubber band effect. If the user were to draw a second rectangle, the first one would remain on the screen while the user draws the second one.
How can I alter the code to make this work?
import java.awt.Graphics;
import java.awt.event.*;
public class Test extends java.applet.Applet implements MouseListener, MouseMotionListener {
int downX, downY, dragX, dragY;
public void init() {
downX = downY = dragX = dragY = 0;
addMouseListener(this);
addMouseMotionListener(this);
}
public void paint(Graphics g) {
g.drawRect(downX,downY,dragX-downX,dragY-downY);
}
public void update(Graphics g) {
paint(g);
}
public void mouseClicked(MouseEvent e) {
downX = e.getX();
downY = e.getY();
}
public void mouseDragged(MouseEvent e) {
dragX = e.getX();
dragY = e.getY();
repaint();
}
public void /*Other MouseEvent methods*/ {}
}
You've broken the paint chain. Failing to call super.paint is preventing the applet from preparing the Graphics context for painting, by removing anything that might have been painted to it before. There's no need to override update, as you're not doing anything with it.
Typically, you should avoid overriding paint of top level containers, as they aren't double buffered and will flicker as they are repainted
You should avoid using Applet and instead use a combination of JApplet a (for example) JPanel as your drawing surface . In fact, if you're just learning. It would be better to use JFrame as applets have a lot of additional management
Painting by its very nature is destructive. You need to maintain a list of things that you want to draw. In this, I would recommend a List of Points, which can be used to paint lines, where the last point is the current drag point
Also take a look at Painting in AWT and Swing for details about how painting works