Creating custom JButton from images containing transparent pixels - java

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.

Related

Defining a background color and a foreground icon for a JButton?

I'm working on a Fire Emblem mock up using Swing (which I'm just getting the hang of, I used to work mostly on console programs). To those who are not familiar with the game, it's a tile based strategy game where you move units on a grid (akin to chess).
I'm thinking about using JButtons for the grid, so that the player is able to click on the unit he wants to move, and click on his destination. As you can see on the picture, the tile color behind the unit can vary (red means the unit can attack the unit on that tile, blue means a tile where the selected unit can move). I don't want to have 15 different tile designs for each unit (UnitX with blue background, UnitX with red background, UnitX with green background, etc.) so is there a way to use "layers" with JButtons? Draw a blue tile and draw the correct character over it?
Draw a blue tile and draw the correct character over it?
Use the setBackground(...) method to set the background
color.
Use the setIcon(...) method to set the character.
The answer from camickr is correct (assuming that I've understood your question).
The following code demonstrates how you can use setBackground and setIcon on a JButton. It shows one button with a background color and an icon. The button changes its background color when it's clicked:
import javax.swing.*;
import java.awt.*;
import java.net.*;
import java.util.*;
import java.util.List;
public class ButtonBackgroundAndIcon {
private static final List<Color> BACKGROUND_COLORS = Arrays.asList(
new Color(229, 119, 120),
Color.BLUE,
Color.CYAN,
Color.GREEN,
Color.YELLOW,
Color.RED
);
private int backgroundIndex;
public static void main(String[] arguments) {
SwingUtilities.invokeLater(new ButtonBackgroundAndIcon()::run);
}
private void run() {
JFrame frame = new JFrame("Stack Overflow");
frame.setBounds(100, 100, 800, 600);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
JButton tileButton = new JButton();
tileButton.setBackground(BACKGROUND_COLORS.get(0));
Icon icon = getRoyIcon();
if (icon != null) {
tileButton.setIcon(icon);
}
tileButton.addActionListener(actionEvent -> {
backgroundIndex = (backgroundIndex + 1) % BACKGROUND_COLORS.size();
tileButton.setBackground(BACKGROUND_COLORS.get(backgroundIndex));
});
frame.getContentPane().add(tileButton);
frame.setVisible(true);
}
private ImageIcon getRoyIcon() {
ImageIcon imageIcon;
try {
String iconLocation = "http://orig06.deviantart.net/fd0e/f/2008"
+ "/060/d/1/roy_sprite_by_chstuba007.gif";
imageIcon = new ImageIcon(new URL(iconLocation));
} catch (MalformedURLException e) {
imageIcon = null;
}
return imageIcon;
}
}
Assuming if you have a transparent image you can use this class to accomplish what you desire.
public class ColorBackgroundIcon implements Icon {
private ImageIcon image;
private Color background;
public ColorBackgroundIcon(ImageIcon image,
Color background) {
this.image = image;
this.background = background;
}
#Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2d = (Graphics2D) g;
g.setColor(background);
g.fillRect(x, y, getIconWidth(), getIconHeight());
g2d.drawImage(image.getImage(), x, y, null);
}
public void setColor(Color bgColor) {
this.background = bgColor;
}
#Override
public int getIconWidth() {
return image.getIconWidth();
}
#Override
public int getIconHeight() {
return image.getIconHeight();
}
}
Then just setIcon() using a instance created above. Works great for me since I have four states a certain icon can represent. Instead of creating four different icons I have one transparent icon and I change the background with the code provided.
Hope that helps for anyone,
DK

MigLayout and JLabel derivative show strange behaviour and don't follow layout

I'm using Java Swing and MigLayout (What a wonderful tool!) to create a project in java, and I got a problem.
To display every string as big as I could, I created a sub-class of JLabel, that changes the font according to the size of the component, I'll attach the code in the example I'll provide.
The project is really big and there are a lot of panels nested, I also change the content of the main window on the fly, validating everything after.
But, if I try to use the cell disposition of components within MigLayout, evrything is wrong.
If i use the same layout, with the same constraint, but instead of using my custom label, i use an ordinary JLabel, everything works like a charm.
Here the gist of the example:
https://gist.github.com/bracco23/c47975ede0d857ac3b134f197c4371a2
The code is in two files:
JAdaptiveLabel.java, the custom component that just recalculate the optimal font size whenever text is changed or on demand.
test.java, a mock example. Changing CUSTOM you can switch between my component and a plain JLabel and see the difference. The intended layout is the one with the plain JLabel.
Can anybody give me a clue of what's wrong and how could I fix it?
Ok, after trying hard I solved.
After the test, I came to the conclusion (obvious) that somethig was wrong with my JAdaptiveLabel. So I searched online for another version, to see if it was my implementation or the adaptivity itself the problem.
I came to this answer: #Warren K
I used his class as it was and it worked, so my implementation was bugged.
I started from his version and changed the resizing algorithm, since his was iterative (change size till you find the perfect one) and mine was mathematical (just get the involved measures and calculate the perfect size).
It worked. Now the layout get disposed properly and the label changes font size if I resize the window.
Here the code modified:
package it.bracco23.util;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
// Improved version of http://java-sl.com/tip_adapt_label_font_size.html
public class JAdaptiveLabel extends JLabel {
private Graphics g;
private boolean init = false;
public JAdaptiveLabel(String text, Icon icon, int horizontalAlignment) {
super(text, icon, horizontalAlignment);
init();
}
public JAdaptiveLabel(String text, int horizontalAlignment) {
super(text, horizontalAlignment);
init();
}
public JAdaptiveLabel(String text) {
super(text);
init();
}
public JAdaptiveLabel(Icon image, int horizontalAlignment) {
super(image, horizontalAlignment);
init();
}
public JAdaptiveLabel(Icon image) {
super(image);
init();
}
public JAdaptiveLabel() {
init();
}
protected void init() {
addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent e) {
adaptLabelFont(JAdaptiveLabel.this);
}
});
init = true;
}
protected void adaptLabelFont(JLabel l) {
if (g==null) {
return;
}
Rectangle r = l.getBounds();
Insets ins = l.getInsets();
r.x = 0;
r.y = 0;
Font f = l.getFont();
Dimension dim = getTextSize(l, f);
//0.9f is a scale factor to don't let the text take too much space
//without it will work, but the text may appear to close to the border
float xFactor = ((r.width - ins.left - ins.right) * 0.9f) / dim.width;
float yFactor = ((r.height - ins.top - ins.bottom) * 0.9f) / dim.height;
/*the next lines assure the scaling factors are not zero (can happen)
and are different enough from 1. Without this last check, it might happen
that the font starts to cycle between two sizes. */
xFactor = (xFactor != 0 && Math.abs(xFactor - 1)>0.1) ? xFactor : 1;
yFactor = (yFactor != 0 && Math.abs(xFactor - 1)>0.1) ? yFactor : 1;
float fontSize = f.getSize() * Math.min(xFactor, yFactor);
setFont(f.deriveFont(f.getStyle(), fontSize));
repaint();
}
private Dimension getTextSize(JLabel l, Font f) {
Dimension size = new Dimension();
FontMetrics fm = g.getFontMetrics(f);
size.width = fm.stringWidth(l.getText());
size.height = fm.getHeight();
return size;
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
this.g=g;
}
#Override
public void setText(String text) {
super.setText(text);
if(init){
adaptLabelFont(this);
}
}
}
If you use this version of the class inside the example I gave, everything works fine!
P.S. I also added a call to the resizing method in setText, since you must change the size when the label resize or change its content.

Resize an image to fit the entirety of a JLabel adapting to its size

before you ask, I've looked up this issue in the website and the solutions provided have, unfortunately, not worked for me, so I must resort to asking it once more to see what could I be doing wrong.
Closest achievement I've had with the code I've got is this (I should most definitely use a try and catch when I retrieve the image, I'll save that for later on):
private void asignarTile(Tile tile, JPanel panel){
if(tile.getTipo() == 0){
ImageIcon ii = new ImageIcon("pasto.png");
Image image = ii.getImage();
Image newimg = image.getScaledInstance(32, 32, java.awt.Image.SCALE_SMOOTH);
ii = new ImageIcon(newimg);
tile.setIcon(ii);
panel.add(tile);
}
}
Now, as oblivious as it is, I must mention that the code does work for a specific size, but it won't adapt the size of the image to the JLabel afterwards, that means that first I'll have this:
But after I resize it I'll have this:
I think it would be useful to note that Tile extends JLabel, and these are the changes (columna means column, fila means row, maybe I should start writing my code entirely in english)
package gui;
import javax.swing.JLabel;
public class Tile extends JLabel{
private int fila, columna, tipo;
public int getFila() {
return fila;
}
public void setFila(int x) {
this.fila = x;
}
public int getColumna() {
return columna;
}
public void setColumna(int y) {
this.columna = y;
}
public int getTipo(){
return tipo;
}
public void setTipo(int tipo){
if(tipo >= 0 || tipo <= 6)
this.tipo = tipo;
}
public Tile(int x, int y, int tipo) {
this.setFila(x);
this.setColumna(y);
this.setTipo(tipo);
}
}
As a conclusion I must say that I have considered adding a componentListener to the Tile since it extends a JLabel, but I have also tried to resize the image to the label's dimensions to no avail, as it gave me an exception saying that its dimensions were 0, and well, they can't be 0.
Thank you for reading!
You can try Darryl's Stretch Icon for dynamic resizing.
Otherwise you would extend JComponent (instead of JLabel) and do you own custom painting of the image. See Background Panel for an example of how to paint a scaled image.
I would recommend that you try to create or find a larger tile image. If you try to stretch the image, you will degrade it, but this is not so for shrinking.
This code looks good.
Here's a code review suggestion:
The serializable class Tile.java does not declare a static final serialVersionUID field of type long. Solution: Add a generated serial version ID to the selected type.

Setting multiple icons on JButton

I have a JButton which I have set a custom icon on. Now I want it to display another icon ontop of the one that is already displayed when I drag my mouse cursor over it but I can't figure out how to do it because if I use button.setIcon(icon); it will replace the icon that already is displayed. How would I do this in an as easy way as possible?
I have a JButton which I have set a custom icon on. Now I want it to
display another icon ontop of the one that is already displayed when I
drag my mouse cursor over it but I can't figure out how to do it
because if I use button.setIcon(icon); it will replace the icon that
already is displayed. How would I do this in an as easy way as
possible
I think thats about JButton.setRolloverIcon(myIcon);
JButton has implemented those methods in API
JButton.setIcon(myIcon);
JButton.setRolloverIcon(myIcon);
JButton.setPressedIcon(myIcon);
JButton.setDisabledIcon(myIcon);
for example
If your icons are already transparent you can easily implement your own Icon to combine the two -
public class CombineIcon implements Icon {
private Icon top;
private Icon bottom;
public CombineIcon(Icon top, Icon bottom) {
this.top = top;
this.bottom = bottom;
}
public int getIconHeight() {
return Math.max(top.getIconHeight(), bottom.getIconHeight());
}
public int getIconWidth() {
return Math.max(top.getIconWidth(), bottom.getIconWidth());
}
public void paintIcon(Component c, Graphics g, int x, int y) {
bottom.paintIcon(c, g, x, y);
top.paintIcon(c, g, x, y);
}
}
You use setRolloverIcon(icon) to specify the icon you want to show when the mouse is over the button.
Create a second version of that button icon which contains the overlay. On mouse over switch to the image with the overlay.
Another approach could be to combine the icon with its overlay to a new icon in memory and place it as an icon on the button. This might be a good approach if your icons are frequently changing. If that's not the case I would definitely use the first approach.
I find this pretty easy.
import java.awt.*;
import java.awt.image.BufferedImage;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.swing.*;
class CombinedIconButton {
public static BufferedImage getCombinedImage(BufferedImage i1, BufferedImage i2) {
if (i1.getHeight() != i2.getHeight()
|| i1.getWidth() != i2.getWidth()) {
throw new IllegalArgumentException("Images are not the same size!");
}
BufferedImage bi = new BufferedImage(
i1.getHeight(),
i1.getWidth(),
BufferedImage.TYPE_INT_ARGB);
Graphics g = bi.getGraphics();
g.drawImage(i1,0,0,null);
g.drawImage(i2,0,0,null);
g.dispose();
return bi;
}
public static void main(String[] args) throws Exception {
URL url1 = new URL("http://i.stack.imgur.com/gJmeJ.png"); // blue circle
URL url2 = new URL("http://i.stack.imgur.com/5v2TX.png"); // red triangle
final BufferedImage bi1 = ImageIO.read(url1);
final BufferedImage bi2 = ImageIO.read(url2);
final BufferedImage biC = getCombinedImage(bi1,bi2);
Runnable r = new Runnable() {
#Override
public void run() {
JPanel gui = new JPanel(new BorderLayout());
JToggleButton b = new JToggleButton();
b.setIcon(new ImageIcon(bi1));
b.setRolloverIcon(new ImageIcon(biC));
b.setSelectedIcon(new ImageIcon(bi2));
gui.add(b);
JOptionPane.showMessageDialog(null, gui);
}
};
// Swing GUIs should be created and updated on the EDT
// http://docs.oracle.com/javase/tutorial/uiswing/concurrency/initial.html
SwingUtilities.invokeLater(r);
}
}
Images borrowed from this answer.
One way to do that is:
Create an icon which you want to see when the pointer is hovered on top of the button by some image editing tool. And set that image once mousehover event occurs.
p.s. use any pic editing tool and you can easily create a overlay image.
I also saw now that there is a concept of roll over icon in AbsractButton class. You can use that as well.

Java MouseEvent position is inaccurate

I've got a problem in Java using a "canvas" class I created, which is an extended JPanel, to draw an animated ring chart. This chart is using a MouseListener to fetch click events.
The problem is that the mouse position does not seem to be accurate, meaning it does not seem to be relative to the "canvas" but instead relative to the window (in the left, upper corner I got about 30px for y coord).
This is my code:
I created a class, that extends JPanel and does have a BufferedImage as member.
public class Canvas extends JPanel {
public BufferedImage buf;
private RingChart _parent;
public Canvas(int width, int height, RingChart parent){
buf = new BufferedImage(width, height, 1);
...
In the paint component method I just draw the buffered image, so I am able to paint on the canvas from 'outside' by painting on the buffered image, which is public.
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.drawImage(buf, null, 0, 0);
}
Now there's a class RingChart which contains a "canvas":
public class RingChart extends JFrame{
public Canvas c;
...
And I create a Graphics2D from the bufferedImage in the canvas class. This g2d is used for painting:
public RingChart(){
c = new Canvas(1500,980,this);
add(c);
setSize(1500, 1000);
setTitle("Hans");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
g2d = (Graphics2D)c.buf.createGraphics();
...
What I now was trying to achieve, was a mouse listener that listened to mouse events happening on the canvas. So when the user clicks on the canvas I could retrieve the position he clicked on, upon the canvas, through the event variable.
So I created a mouse listener:
class MouseHandler implements MouseListener {
#Override
public void mouseClicked(MouseEvent e){
RingChart r = ((Canvas)e.getSource()).getParent();
r.mouseClick(e);
}
...
...and added this mouse listener to the canvas of the RingChart class (myChart is an instance of RingChart and c is the canvas it contains):
...
MouseHandler mouse = new MouseHandler();
myChart.c.addMouseListener(mouse);
...
But as I mentioned above, the mouse position, that's returned when the click event is called, does not seem to be accurate. I think the mistake must be somehow in the way I created that mouseListener or maybe assigned it to the wrong element or something like that. But I've tried quite a couple of things and it didn't change. Can maybe someone tell me, what I've done wrong?
UPDATE:
The code of the function "mouseClick" that is a member of RingChart and is called in the mouse listener:
public void mouseClick(MouseEvent evt){
//evt = SwingUtilities.convertMouseEvent(this, evt, c);
if(evt.getButton() == MouseEvent.BUTTON1 && animation == null){
for(Element e : elements){
if(e.getShape() != null && e.getShape().contains(evt.getPoint())){
//do some stuff
}
}
}
}
Again, the hierarchy of my classes:
RingChart --contains a--> Canvas --got a--> MouseListener.
The shapes in this function are shapes that have been painted on the canvas c. Now I want to check, if the user has clicked on one of them. So as I thought, the shapes should be in canvas-coordinates and the event position should be in canvas-coordinates and everything should fit together. But it doesn't.
Now user MadProgrammer told me, to use the ConvertMouseEvent function. But I currently don't see which exact way I should use this sensibly.
UPDATE:
I found a solution: All I had to do is adding the canvas not directly to the JFrame but to the ContentPane of the JFrame instead:
So instead:
public RingChart(){
c = new Canvas(1500,980,this);
add(c);
...
I do:
public RingChart(){
c = new Canvas(1500,980,this);
getContentPane().add(c);
...
Then I give the MouseListener to the ContentPane.
getContentPane().addMouseListener(new MouseHandler());
getContentPane().addMouseMotionListener(new MouseMoveHandler());
I don't know, if this is an elegant solution, but it works.
The mouse event is automatically converted to be relative to the component that it occurred in that is, point 0x0 is always the top left corner of the component.
By using RingChart r = ((Canvas)e.getSource()).getParent(), you've effectively changed the reference, which now means the location is no longer valid.
You need to convert the location so that its coordinates are in the context of the parent component. Take a look at SwingUtilities.convertMouseEvent(Component, MouseEvent, Component)
UPDATE with PICTURES
Lets take this example...
The blue box has a relative position of 50px x 50px to the red box. If you click in the blue box, lets say at 25x25, the mouse coordinates will be relative to the blue box (0x0 will be the top left of the blue box).
If you then pass this event to the red box and try and use the coordinates from it, you will find that the coordinates will now be half way between the top left of the red box and the blue box, because the coordinates are context sensitive.
In order to get it to work, you need to translate the mouse events location from the blue box to the red box, which would make it 75x75
Now, I don't know what you're doing when you pass the mouse event to the RingChart so I'm only guessing that this is the issue you're facing.
UPDATED with Click Code
Okay, lets say, you have a Canvas at 100x100. You click on that Canvas at 50x50. You then pass that value back up the chain.
public void mouseClick(MouseEvent evt){
//evt = SwingUtilities.convertMouseEvent(this, evt, c);
if(evt.getButton() == MouseEvent.BUTTON1 && animation == null){
for(Element e : elements){
// Here, we are asking the shape if it contains the point 50x50...
// Not 150x150 which would be the relative position of the click
// in the context to the RingChart, which is where all your objects
// are laid out.
// So even the original Canvas you clicked on will return
// false because it's position + size (100x100x width x height)
// does not contain the specified point of 50x50...
if(e.getShape() != null && e.getShape().contains(evt.getPoint())){
//do some stuff
}
}
}
}
UPDATED
I think you have your references around the wrong way...
public static MouseEvent convertMouseEvent(Component source,
MouseEvent sourceEvent,
Component destination)
I think it should read something like
evt = SwingUtilities.convertMouseEvent(evt.getComponent(), evt, this);
UPDATE with Code Example
Okay, so, I put this little example together...
public class TestMouseClickPoint extends JFrame {
private ContentPane content;
public TestMouseClickPoint() throws HeadlessException {
setSize(600, 600);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setLayout(new BorderLayout());
content = new ContentPane();
add(content);
}
protected void updateClickPoint(MouseEvent evt) {
content.updateClickPoint(evt);
}
protected class ContentPane extends JPanel {
private Point relativePoint;
private Point absolutePoint;
public ContentPane() {
setPreferredSize(new Dimension(600, 600));
setLayout(null); // For testing purpose only...
MousePane mousePane = new MousePane();
mousePane.setBounds(100, 100, 400, 400);
add(mousePane);
}
protected void updateClickPoint(MouseEvent evt) {
absolutePoint = new Point(evt.getPoint());
evt = SwingUtilities.convertMouseEvent(evt.getComponent(), evt, this);
relativePoint = new Point(evt.getPoint());
System.out.println(absolutePoint);
System.out.println(relativePoint);
repaint();
}
protected void paintCross(Graphics2D g2d, Point p) {
g2d.drawLine(p.x - 5, p.y - 5, p.x + 5, p.y + 5);
g2d.drawLine(p.x - 5, p.y + 5, p.x + 5, p.y - 5);
}
/*
* This is not recommended, but I want to paint ontop of everything...
*/
#Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
if (relativePoint != null) {
g2d.setColor(Color.BLACK);
paintCross(g2d, relativePoint);
}
if (absolutePoint != null) {
g2d.setColor(Color.RED);
paintCross(g2d, absolutePoint);
}
}
}
protected class MousePane extends JPanel {
private Point clickPoint;
public MousePane() {
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
clickPoint = e.getPoint();
TestMouseClickPoint.this.updateClickPoint(e);
repaint();
}
});
setBorder(new LineBorder(Color.RED));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.BLUE);
if (clickPoint != null) {
g2d.drawLine(clickPoint.x, clickPoint.y - 5, clickPoint.x, clickPoint.y + 5);
g2d.drawLine(clickPoint.x - 5, clickPoint.y, clickPoint.x + 5, clickPoint.y);
}
}
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException ex) {
} catch (InstantiationException ex) {
} catch (IllegalAccessException ex) {
} catch (UnsupportedLookAndFeelException ex) {
}
new TestMouseClickPoint().setVisible(true);
}
}
Basically, it will paint three points. The point that the mouse was clicked (relative to the source of the event), the unconverted point in the parent container and the converted point with the parent container.
The next thing you need to do is determine the mouse location is actually been converted, failing that. I'd probably need to see a working example of your code to determine what it is you're actually doing.

Categories