I'm working on a program that add pins to a map, the pins are are subclasses to a class that draws a triangle to the map and are clickable, and if you click it shall unfold and show different things like name, text or a picture.
I've one working subclass that creates a rectangle out of the triangle and shows what the name of the place is. For this I used the drawString. But now, to my second subclass, it shall show an description over the place, and the description could be quite long and for this I can't use the drawString, because it only shows on one row, and it will clip my text..
I tried to add the description to a JTextArea, and add that one to a JScrollPane and then I tried to add the scrollpane to the rect area, but that didn't seem to work, because "The method add(JScrollPane) is undefined for the type Graphics"
Here is my super class:
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
abstract public class Place extends JComponent {
private String name;
private int x,y;
boolean highlighted = false;
boolean hidden = false;
boolean showed = false;
public Place(int x, int y, String name){
setBounds(x,y,30,30);
this.name=name;
this.x=x-15;
this.y=y-30;
Dimension d = new Dimension(30,30);
setPreferredSize(d);
setMaximumSize(d);
setMinimumSize(d);
addMouseListener(new MouseLis());
}
abstract protected void show(Graphics g);
protected void paintComponent(Graphics g){
super.paintComponent(g);
// g.setColor(Color.BLACK);
if(!showed){
setBounds(x,y,30,30);
int[] xes = {0,15,30};
int[] yes = {0,30,0};
g.fillPolygon(xes, yes, 3);
} else {
show(g);
}
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public String getName() {
return name;
}
class MouseLis extends MouseAdapter{
#Override
public void mouseClicked(MouseEvent mev){
showed = ! showed;
repaint();
}
}
}
and here is my subclass that doesn't work..
class DescPlace extends Place{
private String Description;
private JTextArea desc = new JTextArea(Description);
public DescPlace(int x, int y, String name, String descr){
super(x,y,name);
this.Description = descr;
}
protected void show(Graphics g){
setBounds(getX(), getY(),150,200);
g.setColor(Color.YELLOW);
g.fillRect(0, 0, 150, 200);
//g.add(new JScrollPane(desc));
}
}
You can use the same JTextArea and just paint it using Graphics instance
desc.setSize(width, height); //define size
desc.paintAll(g); //paint
You can use a JLabel to do this, using it to display HTML-formatted content.
From Oracle's docs:
If you want to mix fonts or colors within the text, or if you want formatting such as multiple lines, you can use HTML. HTML formatting can be used in all Swing buttons, menu items, labels, tool tips, and tabbed panes, as well as in components such as trees and tables that use labels to render text.
Source: https://docs.oracle.com/javase/tutorial/uiswing/components/html.html
EDIT
No time to write a thousand words, so here's an example:
new JLabel("<html><p>This will</p><br /><p>appear over multiple</p><br /><p>lines</p></html>")
The same applies to JToolTip if you go down that route.
Related
Hello fellow programmers,
I've ran into a little issue in my code that I can't seem to crack. It has to do with the Jframe; Graphics area of Java. The code that I'll post below, is over a drawing method. Which purpose is to draw the "rooms" that are in a ArrayList roomList which is located in another class hence lvl. before. This off-course doesn't happen, hence the post on here.
public class LevelGUI implements Observer {
private Level lv;
private Display d;
public LevelGUI(Level level, String name) {
this.lv = level;
JFrame frame = new JFrame(name);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
d = new Display(lv, 500, 500);
frame.getContentPane().add(d);
frame.pack();
frame.setLocation(0, 0);
frame.setVisible(true);
}
private class Display extends JPanel {
public Display(Level fp, int x, int y) {
addKeyListener(new Listener());
setBackground(Color.GRAY);
setPreferredSize(new Dimension(x + 20, y + 20));
setFocusable(true);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
draw(g);
}
private void draw(Graphics g) {
Level lvl = new Level();
for(int i = 0; i < lvl.roomList.size(); i++) {
Room room = lvl.roomList.get(i);
g.setColor(room.floorColor);
g.drawRect(room.posX, room.posY, room.roomWidth, room.roomHeight);
}
}
}
}
To get some background info on the program. roomList is the ArrayList, and it is filled with various different sized and colored rooms. The rooms themselves are objects.
Here comes first Level class:
public class Level extends Observable {
private boolean Switch = true;
public ArrayList<Room> roomList = new ArrayList<Room>();
(...)
}
Here is the Class Room() that is used to create the rooms.
public class Room {
Color floorColor;
int roomWidth;
int roomHeight;
int posX;
int posY;
public Room(int dx, int dy, Color color) {
this.floorColor = color;
this.roomHeight = dy;
this.roomWidth = dx;
this.posY = 0;
this.posX = 0;
}
(...)
}
I've managed to locate where the problem is thought to occur, and it's the code in the for-loop. I tried switching the roomList.size() for an integer to test if it was the loop., But it wasn't. It is possible to draw a figure outside of the for-loop.
and again, the problem isn't an error message, the program simply doesn't draw the rooms that I've instructed it to draw in the method draw().
The display output looks like this:
Thanks beforehand!
Be aware that the paintComponent() method is invoked by Swing whenever the framework thinks the component needs to be rendered on screen. This usually is when the window is getting visible - initially or because some other window no longer hides the component. Such events are out of your control.
So your application should create a state and be ready to draw it anytime. Therefore you do not create state (like a level) inside the paint() or paintComponent() method. Put that elsewhere - if need be into the constructor.
Looking at you code:
As you are creating a new level inside paintComponent()/draw(), is it correct to assume that this level has no rooms associated? In that case the method is right to return without having painted anything.
If your application thinks the screen should be updated call repaint(), knowing that the paint() method will be called by the framework soon.
I'm sure I'm going about this all wrong but...
I have a 2D-Array of custom Tile objects that extend JComponent. They contain model information, as well as an override of the paintComponent() method. The tiles really only contain an Image, a String representing their type, and two boolean values indicating whether they contain a player and whether they are able to be walked upon. This Tile class is a superclass for all my Tile objects, which include NormalTile, ActionTile, and EmptyTile currently.
The 2D array itself is contained within a TilePanel class that extends JPanel, and this class has several methods for modifying this array of tiles.
At the top of this chain is the MapManager, which doesn't explicitly extend anything, and instead contains both an instance of TilePanel and JScrollPane, and the TilePanel is added to the JScrollPane.
When my TilePanel is first created, it initializes the 2D array with a series of NormalTiles, which easily display themselves with the test image they use. This all displays just perfectly.
However, immediately after the creation of the TilePanel, I call its addTileBlock(int width, int height, int x, int y, String type) method to change the tiles at a particular point in the 2D array. This is where the problem in updating the display arises.
The Tiles in the specified location do appear to change, according to the data I gather by printing them out, and their behavior when the player steps on them does work according to how their changed type would indicate, yet the change in display is never reflected. The new Tiles (in this case, I have used EmptyTiles) instead simply display the same image the previous NormalTiles used, despite their underlying objects having changed.
As it stands now, "updating" the tiles actually makes the old Tile references point to a new Tile instance, since my Tile objects themselves are immutable. Whenever the update is being done, the TilePanel class simply class an abstract class TileFactory to use its generateTile(String type) method to create the correct Tile instance. It simply uses a switch/case block to return the tiles.
It was suggested to me to attempt using the revalidate() method on the tile objects to fix the problem, which you can see where I've implemented it in the Tile class's constructor. I also previous attempted using it in its paintComponent method, but that did nothing as well.
Here is the addTileBlock method and Tile classes in full:
addTileBlock:
public void addTileBlock(int width, int height, int x, int y, String type)
{
for(int i = 0; i < width; i++)
for(int j = 0; j < height; j++)
{
tiles[i][j] = null;
tiles[i][j] = TileFactory.generateTile(type);
tiles[i][j].repaint();
}
}
Tile Class:
package games.tile.tiles;
import games.tile.Player;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.JComponent;
abstract public class Tile extends JComponent implements TileConstants
{
private String type;
private Image tileImage;
private boolean containsPlayer = false;
private boolean walkable;
public Tile(String type, int size, boolean walkable)
{
this.setSize(size, size);
this.type = type;
this.walkable = walkable;
this.setPreferredSize(new Dimension(this.getHeight(), this.getWidth()));
tileImage = null;
revalidate();
}
public Tile(String type, int size, boolean walkable, Image image)
{
this.setSize(size, size);
this.type = type;
this.walkable = walkable;
tileImage = image;
this.setPreferredSize(new Dimension(this.getHeight(), this.getWidth()));
this.revalidate();
}
public boolean getWalkable() { return walkable; }
public void setWalkable(boolean val) { walkable = val; }
public boolean containsPlayer() { return containsPlayer; }
public void setContainsPlayer(boolean val) { containsPlayer = val;}
public Image getImage() { return tileImage; }
public void setImage(Image image) { tileImage = image; }
public String getType() { return type; }
abstract public void applyEffect(Player player);
#Override
public void paintComponent(Graphics g)
{
if(type.equals(EMPTY) || tileImage == null)
{
g.setColor(Color.black);
g.fillRect(0, 0, 32, 32);
}
else
{
g.drawImage(tileImage, 0, 0, null);
}
if(containsPlayer)
{
g.drawImage(Player.PLAYER_IMAGE, 0, 0, null);
}
}
}
Can anyone inform me as to what I've probably done wrong?
It was suggested to me to attempt using the revalidate() method on the tile objects to fix the problem,
revalidate() is done when you add/remove a component from a panel. So the basic code is:
panel.add(...);
panel.revalidate();
panel.repaint();
This assumes you are using a proper layout manager. In your case I would guess a GridLayout.
As it stands now, "updating" the tiles actually makes the old Tile references point to a new Tile instance,
You can't just change the reference of a variable to point to a new Swing component. You need to actually add the component to the panel. So in your case because your are using a grid, I would guess you need to remove the component at the current point on the grid and then add another component at that point.
That is a lot of work. Since you say your components basically just contain an image, an easier approach is to probably create a changeImage(...) image and then invoke repaint() from within that method and the component will repaint itself and you don't need to worry about creating new components and adding them to the panel.
Im on to create a little "game", something like an 2d AirForce Shooter.
So, i have a problem with deleting unused enemys.
An Enemy is an simple JPanel, which is saved in the main logic as an array List.
public static ArrayList<Enemy> enemys = new ArrayList<Enemy>();
The Enemy run logic does the following:
while(!destroyed){
if(Game.running){
x--;
if(getBounds().intersects(Field.player.getBounding())){
Player.death = true;
}
if(x < 0){
Field.deleteEnemy(this);
}
setBounds((int) x, (int) y, 100, 50);
try{Thread.sleep(10);}catch(InterruptedException e){}
}
}
So you can seem there i already tried to call the method deleteEnemy, and just give it the unused Enemy.
But it isnt possible - when i just do this:
public static void deleteEnemy(Enemy e){
System.out.println("test");
enemys.remove(e);
}
It will be just removed from the list, but coninues existing on the Main JPanel.
And i cannot say
remove(e);
Because then i try to call a non static function in a static.
So, how could i delete an Enemy? Someone knows?
Thanks for help!
The hole code: (Game.java)
And, Enemy.java:
package Game;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
public class Field extends JPanel implements Runnable{
public static Player player = new Player();
public static ArrayList<Enemy> enemys = new ArrayList<Enemy>();
private Thread moveBackground = new Thread(this);
private boolean bgMoving = false;
public static boolean addMob = false;
private int x = 0;
private int bgSpeed = -1;
public Field(){
setBounds(0, 0, 800, 600);
setFocusable(true);
setLayout(null);
addKeyListener(new Handler());
add(player);
}
public void paintComponent(Graphics g){
Field.super.paintComponent(g);
g.drawImage(Images.images[0], x, 0, this);
}
public static void deleteEnemy(Enemy e){
System.out.println("test");
enemys.remove(e);
}
public void run(){
while(!Player.death){
if(bgMoving){
bgMoving = true;
x += bgSpeed;
if(x < -(Images.images[0].getWidth(this) - this.getWidth() - 20)){
bgMoving = false;
}
repaint();
try { Thread.sleep(20); } catch (InterruptedException e) {}
}
if(addMob){
enemys.add(new Enemy());
add(enemys.get(enemys.size() - 1));
addMob = false;
}
}
JOptionPane.showMessageDialog(null, "DIED!");
}
public class Handler extends KeyAdapter {
public void keyPressed(KeyEvent e) {
player.KeyPressed(e);
if(!bgMoving){
if(Game.running){
bgMoving = true;
if(moveBackground.getState().toString() == "NEW"){
moveBackground.start();
}
}
}
}
public void keyReleased(KeyEvent e) {
player.KeyReleased(e);
}
}
}
And, Enemy.java:
package Game;
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;
public class Enemy extends JPanel implements Runnable{
Thread t = new Thread(this);
private double x = Game.width();
private double y = Math.random() * Game.height();
private double xF = 0, yF = 0;
private boolean destroyed = false;
public Enemy(){
setBounds((int) x, (int) y, 100, 50);
setOpaque(false);
t.start();
}
public void paintComponent(Graphics g){
Enemy.super.paintComponent(g);
g.setColor(Color.GREEN);
g.drawImage(Images.images[2], 0, 0, this);
}
public void run() {
while(!destroyed){
if(Game.running){
x--;
if(getBounds().intersects(Field.player.getBounding())){
Player.death = true;
}
if(x < 0){
Field.deleteEnemy(this);
}
setBounds((int) x, (int) y, 100, 50);
try{Thread.sleep(10);}catch(InterruptedException e){}
}
}
}
}
After removing you will need to call revalidate() and repaint()
[Too long for a comment]
I think the problem is in your logic on removing an Enemy/JPanel:
You are removing it from the ArrayList only, what about the containing JPanel/JFrame you added it to?
You must remove the JPanel from its container (maybe another JPanel or the JFrame) not just the ArrayList via Component#remove(Component c).
If you drew the Enemy images directly in paintComponent(...) of your container via iterating the ArrayList; removing it from the ArrayList would be sufficient, as it will no longer be in the Array and thus no longer drawn on the next repaint().
+1 to #Optional, you may need to call revalidate() and repaint() on the container for the affects of the removed JPanel/Enemy to be shown.
Also as #darijan mentioned, the use of static variables along with instance is not really a great design (though for certain designs this may be fine).
In your case if you need access to an instance method of another class, within another class, simply pass the instance of the class whos method you would like to access to the object which will access it.
Here is some psuedo code expressing much of the above mentioned problems / solutions:
public class Field extends JPanel {
private ArrayList<Enemy> enemies;
public Field() {
...
enemies.add(new Enemy(this));//create a new enemy and pas it the JPanel instance so it may access instance methods of this class
}
//ONLY USED IF JPanel for Enemy is ommited and Enemy class created which represents Enemy object and not Enemy object and aJPanel
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
ArrayList<Enemy> enemiesClone = new ArrayList<>(enemies);//copy array into another so we don't get a ConcurrentModificaton exception if removeEnemy is called while iterating the list
if(!enemiesClone.isEmpty())
for(Enemy e:enemiesClone) {//iterate through array of images
draw(e.getImage(),e.getX(),e.getY(),this);
}
}
public void removeEnemy(Enemy e) {
enemies.remove(e);//remove from the array
//ONLY USED IF JPanels are used as Enemy
remove(e);//remove from the JPanel
//so the changes of removed panel can be visible seen
revalidate();
repaint();
}
}
class Enemy extends JPanel //extends JPanel should be ommited for paintComponent method of drawing an enemy onscreen
{
private int x,y;
private BufferedImage image;
private Field f;
public Enemy(Field f) {//constructor accepts Field instance to access instance method for the class
this.f=f;
}
public void update() {
if(offscreen||dead) {
f.removeEnemy(this);//call removeEnemy which is an instance method of Field
}
}
//BELOW METHODS ONLY USED WHEN Enemy represents object and not a JPanel which can draw its image itself (and update position by simply changing co-ordinates)
public BufferedImage getImage() {
return image;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
For a more detailed look check Game Development Loop, Logic and Collision detection Java Swing 2D I made which will give you the basics needed for most 2D games. However I do not use JPanels rather draw directly to a container.
Where do you add an Enemy to JPanel?
Basically, you should call remove on Field JPanel:
public void deleteEnemy(Enemy e){
System.out.println("test");
enemys.remove(e);
this.remove(e);
}
The method should not be static.
I am trying to paint one Oval over another oval.
Here i use a select statement to draw in the paint component method.
import java.awt.*;
public class test extends JPanel{
public static final int OVAL = 1;
public static final int SOLID = 2;
private int type = LINE;
public test(int type){
this.type = type;
}
public void piantComponent(Graphics g){
super.paintComponent(g);
switch(type)
{
case OVAL:
g.setColor(Color.YELLOW);
g.drawOval(0,0,150,150);
break;
case SOLID:
g.setColor(Color.BLUE);
g.fillOval(0,0,50,50);
break;
}
}
}
Now in my main method i want to display a solid blue oval (SOLID) inside of a yellow oval (OVAL).
import...;
public class Main{
public static void main (String [] args){
JFrame window = new JFrame();
window.add(new test(test.OVAL));
window.add(new test(test.SOLID));
window.setSize(300,300);
window.setVisible(true);
}
}
This does not do what I want it to do at all. This only displays a oval and not a oval and a solid. I think that i am overloading the window to only display the oval. I have tried to display with a layout manager(gridlayout) but that does not display the two paintings over each other it displays the two paintings next to each other.
How do i fix this without loosing the switch statement.
window.add(new test(test.OVAL));
window.add(new test(test.SOLID));
Your problem has to do with ZOrder. To simplify, Swing paints components in the reverse order they are found on the panel. So in your case the SOLID gets painted BEFORE the OVAL. Since the OVAL is larger in size it covers the SOLID.
You can force Swing to paint the components in the more intuitive order by doing:
window.add(0, new test(test.OVAL));
window.add(0, new test(test.SOLID));
Now the last component added will also be painted last.
However a better design is to create separate Oval and Solid classes so you can specify size, location and color independently. Then you can add as many components to your panel as you want.
Painting in Swing must be done for concrete JComponent
public class test{ (use proper naming convention and to change test to Text) doesn't contains any JComponents
use JPanel or plain JComponent
painting in AWT, Swing by default never returns PreferredSize, then container has zero Dimension, required to override getPreferredSize
modification in the test class. read comments for explaination of the modifications
import java.awt.*;
public class test extends JPanel {
public static final int OVAL = 1;
public static final int SOLID = 2;
**public static final int BOTH = 3;** //instance variable to represent the
//painting of both images
private static int type;
public test(int type){
this.type = type;
}
public void paintComponent(Graphics g){
switch(type)
{
case OVAL:
g.setColor(Color.BLACK);
g.drawOval(0,0,150,150);
break;
case SOLID:
g.setColor(Color.BLUE);
g.fillOval(0,0,50,50);
break;
case BOTH: //statements to draw both figures
g.setColor(Color.BLACK);
g.drawOval(0,0,150,150);
g.setColor(Color.BLUE);
g.fillOval(0,0,50,50);
break;
}
}//paint component
}
in the main method use
window.add( new test(test.BOTH);
Why don't you just paint one right after the other? Like that:
g.setColor(Color.YELLOW);
g.drawOval(0,0,150,150);
g.setColor(Color.BLUE);
g.fillOval(0,0,50,50);
Try something like this (Just using snippets):
private ArrayList<int> ovals = new ArrayList<int>();
public test(int type) {
ovals.add(type);
}
public void piantComponent(Graphics g)
{
super.paintComponent(g);
for(int oval : ovals)
{
switch(type)
{
case OVAL:
g.setColor(Color.YELLOW);
g.drawOval(0,0,150,150);
break;
case SOLID:
g.setColor(Color.BLUE);
g.fillOval(0,0,50,50);
break;
}
}
}
This will allow you to add as many ovals as you want (although with their static positioning it will just write over them n the canvas). You could try using a boolean array and iterating through that to see if you need to paint a specific type if you only want 1 of each.
Read edit 2 for what I'm actually missing to make it work
I'm currently trying to create some custom JButtons using images created in photoshop that have an alpha parameter.
So far, overriding the paint() method to draw the image has worked in the sense that the button is drawn showing the correct image. I'd like to improve it, though, by making its shape (clickable area) the same as the visible pixels on the image (right now if I draw the button's border, it's a square).
Is there an easy way to do that or do I have to parse the image and find the alpha pixels to make a custom border?
Which methods would I have to override to make it work the way I want?
Also, another question I'm going to have later: would it be better to use some kind of algorithm to change the images' colors to make it seem like it is being clicked when people click on it or am I better off creating a second image and drawing that one while the button is active?
Edit: I just read on some other question that I should redefine paintComponent() instead of paint(), I'd like to know why since redefining paint() works fine?
Edit 2: I changed everything to make sure my JButtons are created using the default constructor with an icon. What I'm trying to do is get the X and Y position of where the click was registered and grab the icon's pixel at that position and check its alpha channel to see if it is 0 (if it is, do nothing, else do the action it is supposed to do).
The thing is, the alpha channel always returns 255 (and blue, red and green are at 238 on transparent pixels). On other pixels, everything returns the value it should be returning.
Here's an example (try it with another image if you want) that recreates my problem:
public class TestAlphaPixels extends JFrame
{
private final File FILECLOSEBUTTON = new File("img\\boutonrondX.png"); //My round button with transparent corners
private JButton closeButton = new JButton(); //Creating it empty to be able to place it and resize the image after the button size is known
public TestAlphaPixels() throws IOException
{
setLayout(null);
setSize(150, 150);
closeButton.setSize(100, 100);
closeButton.setContentAreaFilled(false);
closeButton.setBorderPainted(false);
add(closeButton);
closeButton.addMouseListener(new MouseListener()
{
public void mouseClicked(MouseEvent e)
{
}
public void mousePressed(MouseEvent e)
{
}
public void mouseReleased(MouseEvent e)
{
System.out.println("Alpha value of pixel (" + e.getX() + ", " + e.getY() + ") is: " + clickAlphaValue(closeButton.getIcon(), e.getX(), e.getY()));
}
public void mouseEntered(MouseEvent e)
{
}
public void mouseExited(MouseEvent e)
{
}
});
Image imgCloseButton = ImageIO.read(FILECLOSEBUTTON);
//Resize the image to fit the button
Image newImg = imgCloseButton.getScaledInstance((int)closeButton.getSize().getWidth(), (int)closeButton.getSize().getHeight(), java.awt.Image.SCALE_SMOOTH);
closeButton.setIcon(new ImageIcon(newImg));
}
private int clickAlphaValue(Icon icon, int posX, int posY)
{
int width = icon.getIconWidth();
int height = icon.getIconHeight();
BufferedImage tempImage = (BufferedImage)createImage(width, height);
Graphics2D g = tempImage.createGraphics();
icon.paintIcon(null, g, 0, 0);
g.dispose();
int alpha = (tempImage.getRGB(posX, posY) >> 24) & 0x000000FF;
return alpha;
}
public static void main(String[] args)
{
try
{
TestAlphaPixels testAlphaPixels = new TestAlphaPixels();
testAlphaPixels.setVisible(true);
testAlphaPixels.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
catch(IOException ioe)
{
ioe.printStackTrace();
}
}
}
This is just a wild guess, but is it possible that when my image gets cast to an Icon, it loses its Alpha property and thus doesn't return the correct value? Anyway, I'd really appreciate it if someone could actually help me out and tell me what I should be changing to get the correct value.
I'm guessing that because when I try it with the original image, the alpha channel's value is fine, but I can't actually use that BufferedImage because I resize it, so I actually get the channel values of the image with the original size...
I think you are on the wrong way. You do not have to override neither paint() nor paintComponent() methods. JButton already "knows" to be shown with image only:
ImageIcon cup = new ImageIcon("images/cup.gif");
JButton button2 = new JButton(cup);
See the following tutorial for example: http://www.apl.jhu.edu/~hall/java/Swing-Tutorial/Swing-Tutorial-JButton.html
Moreover swing is fully customized. You can control opacity, border, color etc. You probably should override some mentioned methods to change functionality. But in most cases there is better and simpler solution.
Since there were good elements in multiple answers, but none of the answers were complete on their own, I'll answer my own question so other people that have the same problem can try something similar.
I created my buttons using a new class which extends JButton, with a new constructor that takes a BufferedImage as parameter instead of an icon. The reason for that is that when I did something like myButton.getIcon(), it would return an Icon, then I'd have to make various manipulations on it to make it a BufferedImage of the right size, and it ended up not working anyway because it seems like the first cast to Icon made it lose the alpha data in the pixels, so I couldn't check to see if the user was clicking on transparent pixels or not.
So I did something like this for the constructor:
public class MyButton extends JButton
{
private BufferedImage bufImg;
public MyButton(BufferedImage bufImg)
{
super(new ImageIcon(bufImg));
this.bufImg = bufImg;
}
}
Then I created an accessor for my bufImg that resized the image to fit the JButton using the getSize() method and then returned an image resized at the right size. I do the transformations in the getBufImg() accessor because the image size might change when the window gets resized. When you call the getBufImg(), it's usually because you clicked on the button and thus you're not currently resizing the window.
Something a little bit like this will return the image at the right size:
public BufferedImage getBufImg()
{
BufferedImage newImg = new BufferedImage(getSize().getWidth(), getSize().getHeight(), BufferedImage.TYPE_INT_ARGB); //Create a new buffered image the right size
Graphics2D g2d = newImg.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.drawImage(bufImg, 0, 0, getSize().getWidth(), getSize().getHeight(), null);
g2d.dispose();
return newImg;
}
With that buffered image, you can then code a method like this:
private int clickAlphaValue(BufferedImage bufImg, int posX, int posY)
{
int alpha;
alpha = (bufImg.getRGB(posX, posY) >>24) & 0x000000FF; //Gets the bit that contains alpha information
return alpha;
}
That you call on the button that implements a MouseListener, like this:
myButton.addMouseListener(new MouseListener()
{
public void mouseClicked(MouseEvent e)
{
}
public void mousePressed(MouseEvent e)
{
}
public void mouseReleased(MouseEvent e)
{
if(clickAlphaValue(((myButton)e.getSource()).getBufImg(), e.getX(), e.getY()) != 0) //If alpha is not set to 0
System.exit(0); //Or other things you want your button to do
}
public void mouseEntered(MouseEvent e)
{
}
public void mouseExited(MouseEvent e)
{
}
});
And voila! The button will only do the action if you clicked on non-transparent pixels.
Thanks for the help everyone, I couldn't have come up with this solutions on my own.
If you want to have shape-specific click points, you're better off using Shape and their contains method. If you want, you can create a shape when creating your custom button class as part of it, and implement a contains method by wrapping around the shape's contains method.
As for the custom JButton, create a class that extends JButton, like this:
import java.awt.*;
import javax.swing.*;
public class CustomButton extends JButton{
/** Filename of the image to be used as the button's icon. */
private String fileName;
/** The width of the button */
private int width;
/** The height of the button. */
private int height;
public CustomButton(String fileName, int width, int height){
this.fileName = fileName;
this.width = width;
this.height = height;
createButton();
}
/**
* Creates the button according to the fields set by the constructor.
*/
private void createButton(){
this.setIcon(getImageIcon(filename));
this.setPreferredSize(new Dimension(width, height));
this.setMaximumSize(new Dimension(width, height));
this.setFocusPainted(false);
this.setRolloverEnabled(false);
this.setOpaque(false);
this.setContentAreaFilled(false);
this.setBorderPainted(false);
this.setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
}
}
Here's how you can load the ImageIcon, if you want to do it like this.
public ImageIcon getImageIcon(String fileName){
String imageDirectory = "images/"; //relative to classpath
URL imgURL = getClass().getResource(imageDirectory + fileName);
return new ImageIcon(imgURL);
}
This will give you a button that will at least look like your image.
I asked a similar question regarding Image-based events on click, and Shapes helped wonders.
I guess it comes down to how complex your button images are.
Here's reference anyway:
How can you detect a mouse-click event on an Image object in Java?
PS: Maybe look into generating shapes from images, that go around all the pixels that aren't transparent. No idea if this is possible, but it would mean that a button would only be "pressed" if the user clicks on the image part of it. Just a thought.
If you want your button layout to be that of the non-transparent pixels in your image, then you should redefine the paintComponent() method. It is the most correct way of doing it (overriding paint() worked in old times but is now discouraged).
However I think it is not exactly what you want: you want a click on the button to be detected only if it is on a non-transparent pixel, right? In that case you have to parse your image and when clicked compare mouse coordinates to the pixel alpha channel of your image as JButton does not have such a feature.
If you have a round button, this is exactly what you need:
public class RoundButton extends JButton {
public RoundButton() {
this(null, null);
}
public RoundButton(Icon icon) {
this(null, icon);
}
public RoundButton(String text) {
this(text, null);
}
public RoundButton(Action a) {
this();
setAction(a);
}
public RoundButton(String text, Icon icon) {
setModel(new DefaultButtonModel());
init(text, icon);
if(icon==null) return;
setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
setContentAreaFilled(false);
setFocusPainted(false);
initShape();
}
protected Shape shape, base;
protected void initShape() {
if(!getBounds().equals(base)) {
Dimension s = getPreferredSize();
base = getBounds();
shape = new Ellipse2D.Float(0, 0, s.width, s.height);
}
}
#Override public Dimension getPreferredSize() {
Icon icon = getIcon();
Insets i = getInsets();
int iw = Math.max(icon.getIconWidth(), icon.getIconHeight());
return new Dimension(iw+i.right+i.left, iw+i.top+i.bottom);
}
#Override public boolean contains(int x, int y) {
initShape();
return shape.contains(x, y);
//or return super.contains(x, y) && ((image.getRGB(x, y) >> 24) & 0xff) > 0;
}
}
JButton has a contains() method. Override it and call it on mouseReleased();
paintComponent() instead of paint() depends if you paint() inside XxxButtonUI or just override paintComponent(), but there exists the option JButton#setIcon.