Its been a while since i built a desktop JAVA application.. after lots of documentation and doing implementation tests, i still have not found an image grid solution.
Either Java lacks such a ready-to-use component (?!) or you tell me to brush up my google-fu. :)
I have a very simple technical premises: a JDialog that allows the user to pick an image. Input is a Map<Integer, String> list that holds filenames. Output is the Integer key the user chose. GUI also is simple: user chooses 1 image using mouse or keyboard, and dialog closes. All images are 80x80px and loaded from filename, not a resource.
I tried several approaches so far this morning:
Search for components/widgets that show scrollable imagegrid that can flow to the left. (no dice)
Search for components/widgets that show scrollable imagegrid (no dice)
Search for any components/widgets/gui-libs (no dice .. do these even exist?!)
Try and implement myJList.setModel(), but i cant get it to just take my Map<> and show thumbnails. (overcomplicates!)
Try and implement myJPanel.setlayout(new FlowLayout(..)) with several myJPanel.add(new JButton(..)) which just creates a bunch of JButton on a JPanel, which each need a event handler. I wonder how scrolling and keyboard input is going to work out, and how i'm supposed to keep/reference my Map<> key values. (overcomplicates?)
In lieu of your answer, i am now working on the latter, which should work but i cant believe everyone needs to reinvent the same GUI wheel here. How to have the user select an image from my Map<Integer, String>? Are there JAVA libraries/widgets/components that i should look to avoid this?
I hope this isn't being modded down, i have no working implementation with error to show you guys.. this question is about how/where to find the components or what approaches would be better. Its 2014 and i cant believe that JAVA still requires me to build my own "GUI component" just to see some images.. not even Delphi or Mono does that.
If all you want is a grid of images, and having them selectable, consider using a JList, filling it with appropriate ImageIcons, and giving it a ListSelectionListener. In the Listener you can close the enclosing dialog when a selection has been made.
You state:
Try and implement myJList.setModel(), but i cant get it to just take my Map<> and show thumbnails. (overcomplicates!)
You need to use your Map to populate your ListModel, and set that Model to the JList's model.
For example:
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dialog.ModalityType;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
#SuppressWarnings("serial")
public class ImageGridPanel extends JPanel {
public static final String PATH = "http://images-2.drive.com.au/2011/";
public static final String[] CARS = {
"04/15/2308961/giulietta_1024-80x80.jpg",
"11/18/2781958/audi-a1-sportback_600-80x80.jpg",
"12/23/2856762/fiat-500-80x80.jpg",
"01/12/2129944/Honda-Civic-Sedan-concept-1_600-80x80.jpg",
"12/23/2856581/mini-roadster-80x80.jpg",
"12/23/2856571/hyundai-veloster-80x80.jpg",
"12/23/2856771/hyundai-i30-80x80.jpg",
"12/23/2856580/mini-coupe-80x80.jpg" };
private DefaultListModel<Car> carModel = new DefaultListModel<>();
final JTextField textField = new JTextField(20);
public ImageGridPanel() {
for (String carPath : CARS) {
String path = PATH + carPath;
try {
URL imgUrl = new URL(path);
BufferedImage img = ImageIO.read(imgUrl);
ImageIcon icon = new ImageIcon(img);
String name = carPath.substring(carPath.lastIndexOf("/"));
name = name.substring(1, name.lastIndexOf("-"));
carModel.addElement(new Car(name, icon));
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
}
ShowGridAction showAction = new ShowGridAction("Car Grid", carModel);
JButton showGridBtn = new JButton(showAction);
add(showGridBtn);
add(textField);
}
private class ShowGridAction extends AbstractAction {
private CarGridPanel carGridPanel;
public ShowGridAction(String name, DefaultListModel<Car> carModel) {
super(name);
carGridPanel = new CarGridPanel(carModel);
}
public CarGridPanel getCarGridPanel() {
return carGridPanel;
}
#Override
public void actionPerformed(ActionEvent e) {
Window win = SwingUtilities.getWindowAncestor((Component) e.getSource());
JDialog dialog = new JDialog(win, "Cars", ModalityType.APPLICATION_MODAL);
dialog.add(carGridPanel);
dialog.pack();
dialog.setLocationRelativeTo(null);
int x = dialog.getLocation().x;
int y = dialog.getLocation().y - 150;
dialog.setLocation(x, y);
dialog.setVisible(true);
Car selectedCar = carGridPanel.getSelectedCar();
if (selectedCar != null) {
textField.setText(selectedCar.getName());
}
}
}
private static void createAndShowGui() {
JFrame frame = new JFrame("ImageGrid");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(new ImageGridPanel());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
class Car {
String name;
Icon icon;
public Car(String name, Icon icon) {
this.name = name;
this.icon = icon;
}
public String getName() {
return name;
}
public Icon getIcon() {
return icon;
}
}
#SuppressWarnings("serial")
class CarGridPanel extends JPanel {
private JList<Car> carList = new JList<>();
private Car selectedCar;
public CarGridPanel(ListModel<Car> model) {
carList.setModel(model);
carList.setLayoutOrientation(JList.HORIZONTAL_WRAP);
carList.setVisibleRowCount(2);
carList.setCellRenderer(new DefaultListCellRenderer() {
#Override
public Component getListCellRendererComponent(JList<?> list,
Object value, int index, boolean isSelected, boolean cellHasFocus) {
if (value != null) {
Car carValue = (Car) value;
value = carValue.getIcon();
} else {
value = "";
}
return super.getListCellRendererComponent(list, value, index,
isSelected, cellHasFocus);
}
});
setLayout(new BorderLayout());
add(new JScrollPane(carList));
carList.addListSelectionListener(new ListListener());
}
public Car getSelectedCar() {
return selectedCar;
}
private class ListListener implements ListSelectionListener {
#Override
public void valueChanged(ListSelectionEvent e) {
selectedCar = carList.getSelectedValue();
Window win = SwingUtilities.getWindowAncestor(CarGridPanel.this);
win.dispose();
}
}
}
No, Java doesn't have what you want.
Java is a general-purpose programming language, not a toolset, particularly not a specialized desktop GUI toolset. This is not a denigration of the language, just a statement of a purpose that it was not developed to fulfill.
If Delphi or Mono or anything has your particular widget, then I suggest you program in that, instead. This is not a denigration of you, just an observation that, if you do not want to put together the widget you want from lower-level components and code, then Java is not the right language/tool for you to use to do it.
As for not believing that Java "still requires" you to build your own component, I can only say that you don't get to choose which languages provide which features. I'm just as glad Java isn't littered with your component and the hundreds of others that people like you would come up with that they think Java should provide. It's big enough as it is.
Related
I would like to modify my JScrollPane so that when the horizontal position is changed, the position hvalue is sent to another class. I don't want the other class to have to retrieve this value every time as this will happen repeatedly during a task which must be finished as quickly as possible.
I haven't been able to find any documentation on the exact syntax used by default but I'm guessing what I want will look something like this:
import java.lang.Math.*;
import javax.swing.*;
public class scroll {
public static void main(String[] args) {
JFrame f = new JFrame();
f.setDefaultCloseOperation(3);
panel p = new panel();
JScrollPane sp = new JScrollPane(p,21,31) {
#Override
public final void setHvalue(double value) {
hvalue = value;
p.sendH((int) Math.round(value));
}
};
f.add(sp);
f.setSize(333,333);
f.setVisible(true);
}
}
which will include a panel object governed by the class:
import javax.swing.*;
public class panel extends JPanel {
private static int H;
public panel() {}
public static void sendH(int x) {
H=x;
System.out.println(x);
}
}
Attempting to compile this code yields the error messages:
scroll.java.11: error: method does not override or implement a method from supertype
#Override
scroll.java.13: error: cannot find symbol
hvalue = value;
symbol: variable hvalue
which, as I understand it, is basically their way of saying I can't modify setHvalue(). Is there some way around this or an alternative way of sending hvalue when it's changed instead of retrieving it every time it's needed?
when the horizontal position is changed, the position hvalue is sent to another class.
This is done by adding an AdjustmentListener to the horizontal scrollbar.
However, this is extra overhead that is NOT required if you do not use the information every time it changes.
I do not recommend using this approach.
I don't want the other class to have to retrieve this value every time as this will happen repeatedly during a task which must be finished as quickly as possible.
Invoking the getValue() method of the scrollbar will NOT add any measurable overhead to your task.
Your task just needs a reference to the scroll pane so you can access the scrollbar and get the current value.
Actually I figured out the answer, sorry about that. It turns out that the method getHvalue is only for awt ScrollPane and is not inherited by swing JScrollPane. I will post a working version here, in case someone else is interested some day (although I'm guessing that camickr's approach is better).
The JScrollPane class is:
import java.awt.*;
import javax.swing.*;
import javax.accessibility.*;
public class scroll {
public static void main(String[] args) {
JFrame f = new JFrame();
f.setDefaultCloseOperation(3);
panel p = new panel() {
#Override
public Dimension getMinimumSize(){
return new Dimension(getWidth(),getHeight());
}
#Override
public Dimension getPreferredSize(){
return new Dimension(getWidth(),getHeight());
}
#Override
public Dimension getMaximumSize(){
return new Dimension(getWidth(),getHeight());
}
};
JScrollPane sp = new JScrollPane(p,22,32);
sp.setHorizontalScrollBar( new JScrollBar(JScrollBar.HORIZONTAL){
#Override
public void setValue(int value) {
BoundedRangeModel m = getModel();
int oldValue = m.getValue();
m.setValue(value);
if (accessibleContext != null) {
accessibleContext.firePropertyChange(
AccessibleContext.ACCESSIBLE_VALUE_PROPERTY,
Integer.valueOf(oldValue),
Integer.valueOf(m.getValue()));
}
p.sendH(value);
}
});
f.add(sp);
f.setSize(333,333);
p.setSize(400,400);
f.setVisible(true);
}
}
and the panel class is:
import javax.swing.*;
public class panel extends JPanel {
private static int H;
public panel() {}
public static void sendH(int x) {
H=x;
System.out.println(x);
}
}
I have a problem in which I hope SwingWorker can help me, but I am not quite sure how to integrate it in my program.
The problem:
In a CardLayout I have a button on Card1 that opens Card2.
Card2 has a JList with a custom renderer(extending JLabel) which will display on average 1 to 6 images which are:
PNGs
around 500kb in size
loaded via imageIO with the change of cards
the renderer applies heavy operations such as image scaling or blurring and than sets the image as JLabel icon.
This can almost take up to a second if around 6 images have to be rendered, which is does not happen frequently but even that occasional split second of unresponsiveness feels bad.
Now I thought a SwingWorker might help here, but I'm thoroughly confused as to how I would have to integrate it.
Assuming we had this Code snippet
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class Example {
private JPanel mainPanel = new JPanel();
private JList<Product> list = new JList();
private JScrollPane scroll = new JScrollPane();
private Map<String, Color> colorMap = new HashMap<>();
public Example() {
colorMap.put("red", Color.red);
colorMap.put("blue", Color.blue);
colorMap.put("cyan", Color.cyan);
colorMap.put("green", Color.green);
colorMap.put("yellow", Color.yellow);
mainPanel.setBackground(new Color(129, 133, 142));
scroll.setViewportView(list);
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setPreferredSize(new Dimension(80,200));
list.setCellRenderer(new CustomRenderer());
DefaultListModel model = new DefaultListModel();
model.addElement(new Product("red"));
model.addElement(new Product("yellow"));
model.addElement(new Product("blue"));
model.addElement(new Product("red"));
model.addElement(new Product("cyan"));
list.setModel(model);
mainPanel.add(scroll);
}
public static void main(String[] args) throws IOException {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame("WorkerTest");
frame.setContentPane(new Example().mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(300, 300);
frame.setMinimumSize(new Dimension(160, 255));
frame.setVisible(true);
}
});
}
class CustomRenderer extends JLabel implements ListCellRenderer<Product> {
private Product product;
public CustomRenderer() {
setOpaque(false);
}
#Override
public Component getListCellRendererComponent(JList<? extends Product> list, Product product, int index, boolean isSelected, boolean cellHasFocus) {
this.product = product;
/**
* in the actual code image is png with alpha channel respectively named to the productID of the JList object
*
* String id = product.getId();
* image = ImageIO.read(getClass().getResource("../../resources/images/" + id + ".png"));
*/
BufferedImage image1 = new BufferedImage(80, 50, BufferedImage.TYPE_INT_RGB);
BufferedImage image2 = new BufferedImage( 80, 75, BufferedImage.TYPE_INT_RGB);
Graphics g = image2.getGraphics();
/**
* this is only an example, in the actual code I might also apply gaussian blurs or rescale several time
*/
g.drawImage(image1,0,0,null);
setIcon(new ImageIcon(image2));
return this;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(colorMap.get(product.getColor()));
g.fillRect(0,0,80,75);
}
}
class Product {
String productID;
String color;
public Product(String color) {
this.color = color;
}
public String getColor() {
return color;
}
public String getProductID() {
return productID;
}
}
}
would I have to call a SwingWorker from every getListCellRendererComponent call
to take over the image operations ?
Is SwingWorker even the right tool for this problem?
any help as to how I can make this part of my GUI faster would be greatly appreciated.
EDIT:
Hovercraft Full Of Eels mentioned that preloading the images could help and that loading the images from the renderer is fundamentally wrong.
This leads me to another Question:
I have a list(let's call it list1) with around 3000 objects each object has a 8kb jpg thumbnail which is load via object ID (also during the rendering)
The list displays around 6 to 12 of these thumbnail at the same time (due to the List's Dimension)
when the user selects an object he can press a button to display Card2 from the Cardlayout mentioned in the original question and it's list(list2) with the Object
and all it's related Object in non thumbnail view (500kb png + heavy image operation). Now I think it would be feasible to preload the non thumbnail image of the Object and it's relations selected in the first list which would be around 1-6 images. If I understood correctly what Hovercraft Full Of Eels said, then I could use a SwingWorker to load these Images after the selection of an Object from list1.
But what about the around 3000 images from list1, the program seemingly is not slowed down or becomes unresponsive because they are rather small in size and there are no heavy operations on the thumbnails, but they are still load form the list1's renderer. Would it make sense to preload the several thousand thumbnails ?
btw. feel free to tell me if this kind of question edit is not wished for and if it should be made into a question of itself.
One approach might be the following:
Whenever a cell renderer component for a certain element (Product) is requested, you check whether the matching image is already loaded. If not, you start a Swing worker that does the work of loading and processing the image in the background. When the worker is done, the image is placed into a cache for later lookup. In the meantime, you let the renderer just say "Loading..." or something.
A very quick implementation is here:
And as an MCVE:
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.SwingWorker;
public class LazyImageLoadingCellRendererTest
{
private JPanel mainPanel = new JPanel();
private JList<Product> list = new JList<Product>();
private JScrollPane scroll = new JScrollPane();
public LazyImageLoadingCellRendererTest()
{
mainPanel.setBackground(new Color(129, 133, 142));
scroll.setViewportView(list);
scroll.setHorizontalScrollBarPolicy(
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setPreferredSize(new Dimension(80, 200));
list.setCellRenderer(new LazyImageLoadingCellRenderer<Product>(list,
LazyImageLoadingCellRendererTest::loadAndProcessImage));
DefaultListModel<Product> model = new DefaultListModel<Product>();
for (int i=0; i<1000; i++)
{
model.addElement(new Product("id" + i));
}
list.setModel(model);
mainPanel.add(scroll);
}
public static void main(String[] args) throws IOException
{
EventQueue.invokeLater(new Runnable()
{
#Override
public void run()
{
JFrame frame = new JFrame("WorkerTest");
frame.setContentPane(
new LazyImageLoadingCellRendererTest().mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(300, 300);
frame.setMinimumSize(new Dimension(160, 255));
frame.setVisible(true);
}
});
}
private static final Random random = new Random(0);
private static BufferedImage loadAndProcessImage(Product product)
{
String id = product.getProductID();
int w = 100;
int h = 20;
BufferedImage image =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.GREEN);
g.fillRect(0, 0, w, h);
g.setColor(Color.BLACK);
g.drawString(id, 10, 16);
g.dispose();
long delay = 500 + random.nextInt(3000);
try
{
System.out.println("Load time of " + delay + " ms for " + id);
Thread.sleep(delay);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return image;
}
class Product
{
String productID;
public Product(String productID)
{
this.productID = productID;
}
public String getProductID()
{
return productID;
}
}
}
class LazyImageLoadingCellRenderer<T> extends JLabel
implements ListCellRenderer<T>
{
private final JList<?> owner;
private final Function<? super T, ? extends BufferedImage> imageLookup;
private final Set<T> pendingImages;
private final Map<T, BufferedImage> loadedImages;
public LazyImageLoadingCellRenderer(JList<?> owner,
Function<? super T, ? extends BufferedImage> imageLookup)
{
this.owner = Objects.requireNonNull(
owner, "The owner may not be null");
this.imageLookup = Objects.requireNonNull(imageLookup,
"The imageLookup may not be null");
this.loadedImages = new ConcurrentHashMap<T, BufferedImage>();
this.pendingImages =
Collections.newSetFromMap(new ConcurrentHashMap<T, Boolean>());
setOpaque(false);
}
class ImageLoadingWorker extends SwingWorker<BufferedImage, Void>
{
private final T element;
ImageLoadingWorker(T element)
{
this.element = element;
pendingImages.add(element);
}
#Override
protected BufferedImage doInBackground() throws Exception
{
try
{
BufferedImage image = imageLookup.apply(element);
loadedImages.put(element, image);
pendingImages.remove(element);
return image;
}
catch (Exception e)
{
e.printStackTrace();
return null;
}
}
#Override
protected void done()
{
owner.repaint();
}
}
#Override
public Component getListCellRendererComponent(JList<? extends T> list,
T value, int index, boolean isSelected, boolean cellHasFocus)
{
BufferedImage image = loadedImages.get(value);
if (image == null)
{
if (!pendingImages.contains(value))
{
//System.out.println("Execute for " + value);
ImageLoadingWorker worker = new ImageLoadingWorker(value);
worker.execute();
}
setText("Loading...");
setIcon(null);
}
else
{
setText(null);
setIcon(new ImageIcon(image));
}
return this;
}
}
Note:
This is really just a quick example showing the general approach. Of course, this could be improved in many ways. Although the actual loading process is already pulled out into a Function (thus making it generically applicable for any sort of image, regardless of where it comes from), one major caveat is that: It will try to load all images. A nice extension would be to add some smartness here, and make sure that it only loads the images for which the cells are currently visible. For example, when you have a list of 1000 elements, and want to see the last 10 elements, then you should not have to wait for 990 elements to be loaded. The last elements should be priorized higher and loaded first. However, for this, a slightly larger infrastructure (mainly: an own task queue and some stronger connection to the list and its scroll pane) may be necessary. (I'll possibly tackle this one day, because it might be a nice and interesting thing to have, but until then, the example above might do it...)
would I have to call a SwingWorker from every getListCellRendererComponent call to take over the image operations ?
No, you would in fact never call a background thread from within a key rendering method. In fact this appears to be the main problem with the code above -- you're reading in images from within a rendering method, significantly reducing the perceived responsiveness of your program.
Is SwingWorker even the right tool for this problem?
Perhaps, but not where you're thinking about using it. A SwingWorker will not speed anything up, but by performing long-running tasks in the background, it would avoid blocking the Swing event thread, freezing the GUI. Best would be to read the images, once, perhaps in a SwingWorker if not done during program initiation, and save them to a variable. Do not re-read the image every time you want to render it, if this can be avoided. And again, do not read in the image from within your rendering code as this will significantly reduce the perceived responsiveness of the program.
I'm trying to move a JButton to the location of another one but the button I want to move moves to a wrong point. My idea is that this happens because I use multiple JPanels. I tried: getLocationOnScreen, getBounds and getLocation, but none of them worked, how to solve it? When an user selects a card on the table or from a player by clicking this card the the target is set, the sender is set by clicking a card from the top panel. playerCardSpotTargetand playerCardSpotSender are both of type Card. When I try to move the for example eight of diamonds this card moves to a point behind the eight and nine of clubs.
Code:
This events belong to the blue cards on the table and the cards for the players(I have to change the name of the event, I know).
private void PlayerOneMouseClicked(java.awt.event.MouseEvent evt){
playerCardSpotTarget=(Card)evt.getSource();
if(playerCardSpotTarget.isBorderPainted()){
playerCardSpotTarget.setBorderPainted(false);
}
else{
playerCardSpotTarget.setBorderPainted(true);
}
}
This event belongs to the cards in the top panel.
private void MouseClicked(java.awt.event.MouseEvent evt) {
playerCardSpotSender=(Card)evt.getSource();
System.out.println(playerCardSpotSender.suit+" "+playerCardSpotSender.kind);
if (playerCardSpotTarget != null && playerCardSpotTarget.isBorderPainted()) {
playerCardSpotSender.setLocation(playerCardSpotTarget.getLocation());
System.out.println(playerCardSpotTarget.getLocationOnScreen());
System.out.println(playerCardSpotSender.getLocationOnScreen());
}
}
Layout for the center panel in the JFrame (BorderLayout.CENTER)
JPanel centerPanelNorth;
JPanel centerPanelCenter;
JPanel centerPanelEast;
JPanel centerPanelSouth;
JPanel centerPanelWest;
JLabel tablePicture;
JPanel centerPanel;
centerPanel=new JPanel(new BorderLayout());
tablePicture = new JLabel(new ImageIcon(this.getClass().getResource(Constants.POKERTABLE_ICON)));
centerPanelNorth=new JPanel();
centerPanelEast=new JPanel();
centerPanelSouth=new JPanel();
centerPanelWest=new JPanel();
centerPanelCenter=new JPanel();
centerPanel.add(centerPanelCenter,BorderLayout.CENTER);
centerPanelCenter.add(tablePicture);
//add
tablePicture.add(boardCard1);
tablePicture.add(boardCard2);
tablePicture.add(boardCard3);
tablePicture.setLayout(new GridBagLayout());
//PLAYER NORTH
centerPanel.add(centerPanelNorth,BorderLayout.NORTH);
centerPanelNorth.add(playerOneCardOne);
centerPanelNorth.add(playerOneCardTwo);
//PLAYER EAST
centerPanel.add(centerPanelEast,BorderLayout.EAST);
centerPanelEast.setLayout(new BoxLayout(centerPanelEast,BoxLayout.X_AXIS));
centerPanelEast.add(playerTwoCardOne);
centerPanelEast.add(playerTwoCardTwo);
//PLAYER SOUTH
centerPanel.add(centerPanelSouth,BorderLayout.SOUTH);
centerPanelSouth.add(playerThreeCardOne);
centerPanelSouth.add(playerThreeCardTwo);
//PLAYER WEST
centerPanel.add(centerPanelWest,BorderLayout.WEST);
centerPanelWest.setLayout(new BoxLayout(centerPanelWest,BoxLayout.X_AXIS));
centerPanelWest.add(playerFourCardOne);
centerPanelWest.add(playerFourCardTwo);
Card.java
public class Card extends JButton{
int suit;
int kind;
boolean known;
String iconPath;
Integer boardPosition;
}
Animating the button movement isn't actually the hardest problem, the hardest problem is trying to move the data about in away in which you can manage it and how to connect the source component with the target...
To start with, you need a means by which you can move a component across container boundaries. While there are probably a few ways to do this, the simplest is to probably use the glass pane of the frame
public class AnimationPane extends JPanel {
public AnimationPane() {
setOpaque(false);
setLayout(null);
}
}
This is nothing special, it's just a JPanel which is transparent and has no layout manager, normally, not recommended, but in the case, we're going to take control..
Now, we need some way to animate the movement...
public enum Animator {
INSTANCE;
private List<IAnimatable> animatables;
private Timer timer;
private Animator() {
animatables = new ArrayList<>(25);
timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
IAnimatable[] anins = animatables.toArray(new IAnimatable[animatables.size()]);
for (IAnimatable animatable : anins) {
animatable.update();
}
}
});
timer.start();
}
public void addAnimatable(IAnimatable animatable) {
animatables.add(animatable);
}
public void removeAnimatable(IAnimatable animatable) {
animatables.remove(animatable);
}
}
public interface IAnimatable {
public void update();
}
public interface IMoveAnimatable extends IAnimatable{
public JComponent getSourceComponent();
public IImportable getImportable();
}
So the Animator is the core "engine", it's basically a Swing Timer which simply calls update on any IAnimatables it might be managing. The intention with this approach is you can have a number of animations running, but it won't degrade the system (greatly) as you only have a single update/timer point.
Now, normally I'd just use something like the Timing Framework or the Trident Framework or even the Universal Tween Engine
The IAnimatable interfaces just define the basic contracts that provide functionality for the animation.
We need to define some kind of contract the defines objects which can take part in the animation process and receive information, or the "target"
public interface IImportable {
public JComponent getView();
public void importValue(String value);
}
public abstract class AbstractImportable extends JPanel implements IImportable {
#Override
public JComponent getView() {
return this;
}
}
Now it occurs to me that we could tap into the pre-existing Transferable API, which would allow you to also implement drag-n-drop (and even copy/cut and paste), this would be used to define a lookup mechanism where you match a given data type with potential targets based on the DataFlavor ... but I'll leave you to investigate how that might work...
The core mechanism basically removes the source component from it's current container, adds it to the AnimationPane, moves the source component across the AnimationPane and then imports the data into the target...
The problem is, you need to translate the location of component from it's current context to the AnimationPane.
A components location is relative to it's parents context. It's relatively easy to do with SwingUtilities.convertPoint(Component, Point, Component)
We calculate the origin point of the source component and the target point, relative to the AnimationPane. We then, on each call to update, calculate the progress of the animation. Instead of using a "delta" movement, we calculate the different between the time we started and a predefined duration (1 second in this case), this generally produces a more flexible animation
public class DefaultAnimatable implements IMoveAnimatable {
public static final double PLAY_TIME = 1000d;
private Long startTime;
private JComponent sourceComponent;
private IImportable importable;
private JComponent animationSurface;
private Point originPoint, destinationPoint;
private String value;
public DefaultAnimatable(JComponent animationSurface, JComponent sourceComponent, IImportable importable, String value) {
this.sourceComponent = sourceComponent;
this.importable = importable;
this.animationSurface = animationSurface;
this.value = value;
}
public String getValue() {
return value;
}
public JComponent getAnimationSurface() {
return animationSurface;
}
#Override
public JComponent getSourceComponent() {
return sourceComponent;
}
#Override
public IImportable getImportable() {
return importable;
}
#Override
public void update() {
if (startTime == null) {
System.out.println("Start");
IImportable importable = getImportable();
JComponent target = importable.getView();
originPoint = SwingUtilities.convertPoint(getSourceComponent().getParent(), getSourceComponent().getLocation(), getAnimationSurface());
destinationPoint = SwingUtilities.convertPoint(target.getParent(), target.getLocation(), getAnimationSurface());
destinationPoint.x = destinationPoint.x + ((target.getWidth() - getSourceComponent().getWidth()) / 2);
destinationPoint.y = destinationPoint.y + ((target.getHeight() - getSourceComponent().getHeight()) / 2);
Container parent = getSourceComponent().getParent();
getAnimationSurface().add(getSourceComponent());
getSourceComponent().setLocation(originPoint);
parent.invalidate();
parent.validate();
parent.repaint();
startTime = System.currentTimeMillis();
}
long duration = System.currentTimeMillis() - startTime;
double progress = Math.min(duration / PLAY_TIME, 1d);
Point location = new Point();
location.x = progress(originPoint.x, destinationPoint.x, progress);
location.y = progress(originPoint.y, destinationPoint.y, progress);
getSourceComponent().setLocation(location);
getAnimationSurface().repaint();
if (progress == 1d) {
getAnimationSurface().remove(getSourceComponent());
Animator.INSTANCE.removeAnimatable(this);
animationCompleted();
}
}
public int progress(int startValue, int endValue, double fraction) {
int value = 0;
int distance = endValue - startValue;
value = (int) Math.round((double) distance * fraction);
value += startValue;
return value;
}
protected void animationCompleted() {
getImportable().importValue(getValue());
}
}
Okay, now this produces a linear animation, which is pretty boring, now if you have plenty of time, you could create an easement like this or just use one of the animation frameworks...
Now, we need to put it together...
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.LineBorder;
public class AnimationTest {
public static void main(String[] args) {
new AnimationTest();
}
public AnimationTest() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
AnimationPane animationPane = new AnimationPane();
LeftPane leftPane = new LeftPane(animationPane);
RightPane rightPane = new RightPane();
leftPane.setImportabale(rightPane);
rightPane.setImportabale(leftPane);
JFrame frame = new JFrame("Testing");
frame.setLayout(new GridLayout(1, 2));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(leftPane, BorderLayout.WEST);
frame.add(rightPane, BorderLayout.WEST);
frame.setGlassPane(animationPane);
animationPane.setVisible(true);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class RightPane extends AbstractImportable {
private IImportable source;
private JButton imported;
private String importedValue;
public RightPane() {
setLayout(new GridBagLayout());
setBorder(new LineBorder(Color.DARK_GRAY));
}
public void setImportabale(IImportable source) {
this.source = source;
}
#Override
public void importValue(String value) {
if (imported != null) {
// May re-animate the movement back...
remove(imported);
}
importedValue = value;
imported = new JButton(">> " + value + "<<");
add(imported);
revalidate();
repaint();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
}
public class LeftPane extends AbstractImportable {
private IImportable importable;
public LeftPane(AnimationPane animationPane) {
setLayout(new GridBagLayout());
JButton btn = new JButton("Lefty");
btn.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
DefaultAnimatable animatable = new DefaultAnimatable(animationPane, btn, importable, "Lefty");
Animator.INSTANCE.addAnimatable(animatable);
}
});
add(btn);
setBorder(new LineBorder(Color.DARK_GRAY));
}
public void setImportabale(IImportable target) {
this.importable = target;
}
#Override
public void importValue(String value) {
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
}
}
Maybe use mousePressed(),when you move the card,you press it until the target.And in the process, you get the information about JButton.getLocation() by the event,and than you need to solve the collision problem between two cards.So it's good!Of course, this is my advice ,you should have better idea!
I would like to create a new Swing JComponent based on an existing one, but with a different API. In other words, I don't want to extend the existing component, because I don't want it's API to be accessible.
Here an example to clarify my needs:
A replacement of the JCheckBox which show two buttons ON/OFF. This could be based on a pre-configured JCommandButtonStrip (some info here) but exposing exactly the same API of JCheckBox. The configuration of the JCommandButtonStrip must not be altered.
What is the best approach for such a problem?
Clarifications:
As someone pointed out, what I wrote about API is not clear.
Of course JComponent have a number of public fields and methods which will be available for each sub-class. Then each sub-class of JComponent may add its own public fields and methods. For example, AbstractButton adds the isSelected() method, while JCommandButtonStrip adds the getButtonCount() method.
So, what I meant is: I want to create a new JComponent sub-class MyJComponent, which is based on an existing one ExistingJComponent. I don't want the public methods of ExistingJComponent, except those of JComponent, to be exposed by my class MyJComponent. Then I want to add some public methods to MyJComponent.
Please note that I'm not looking for an alternative to the JCommandButtonStrip/JCheckBox example. I'm interested in a general approach to such a problem.
You can create a new class which extends JComponent then inside the constructor insert a checkbox into itself.
public class MyCoolCheckbox extends JComponent{
private JCheckBox checkbox;
public MyCoolCheckbox(String label) {
checkbox= new JCheckBox(label);
this.setLayout(new BorderLayout());
this.add(checkbox, BorderLayout.CENTER);
}
}
This is obviously incomplete and you may need to delegate certain methods to the child. It might get messy. IDEs like IntelliJ IDEA will generate all this for you if you hit alt-ins (by default) then delegate, then select the checkbox member and pick the entries you want to delegate. For example:
public void setForeground(Color fg) {
checkbox.setForeground(fg);
}
public void setBackground(Color bg) {
checkbox.setBackground(bg);
}
public Color getForeground() {
return checkbox.getForeground();
}
public Color getBackground() {
return checkbox.getBackground();
}
Keep in mind that because the child is within the Swing component tree, other code will have access to the children even though they are marked private.
((JCheckBox)myCoolCheckbox.getComponents()[0]).setSelected(true);
As shown here, you can use two instances of JToggleButton in a ButtonGroup to "show two buttons ON / OFF." The ButtonGroup causes only one button in the group to be selected at a time. The following change is illustrated:
private final JLabel label = new JLabel(" \u2713 ");
Based on this picture of JCommandButtonStrip:
I think you are looking for JToggleButton as #trashgod suggested, but I'm not sure about buttons group given the current description of your "problem". If you need buttons group then use it.
Anyway my answer points to this line:
This could be based on a pre-configured JCommandButtonStrip (some info
here) but exposing exactly the same API of JCheckBox.
Once again it's not clear if you're trying to do a buttons bar such as JCommandButtonStrip or you want to do something else. However you can make your own component extending from JComponent and delegate only those methods that are needed from the outside. For example let's say you want to do a buttons bar such as JCommandButtonStrip. Then you can have:
One class extending from JComponent: your buttons bar.
Another one providing an API to add "commands" to the buttons bar.
Note: There's already a JToolBar component which can perfectly be used without reinvent the wheel. The example below is just to show you that you can control the API offered to the developers.
MyCommandBar.java
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.event.ActionListener;
import java.util.HashMap;
import java.util.Map;
import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import javax.swing.event.ChangeListener;
public class MyCommandBar extends JComponent {
private final JPanel content;
private final Map<String, CommandItem> map = new HashMap<>();
public MyCommandBar() {
super();
content = new JPanel(new GridLayout(1, 0));
content.setOpaque(false);
setLayout(new FlowLayout());
add(content);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics graphics = g.create();
graphics.setColor(getBackground());
graphics.fillRect(0, 0, getWidth(), getHeight());
graphics.dispose();
}
public void addCommandItem(String actionCommand, CommandItem commandItem) {
if(map.get(actionCommand) != null) {
removeCommandItem(actionCommand);
}
content.add(commandItem.getComponent());
map.put(actionCommand, commandItem);
}
public void removeCommandItem(String actionCommand) {
CommandItem commandItem = map.get(actionCommand);
if(commandItem != null) {
content.remove(commandItem.getComponent());
content.revalidate();
content.repaint();
map.remove(actionCommand);
}
}
public CommandItem getCommandItem(String actionCommand) {
return map.get(actionCommand);
}
public static class CommandItem {
public static final int TOGGLE_BUTTON_STYLE = 0;
public static final int CHECK_BOX_STYLE = 1;
public static final int DEFAULT_BUTTON_STYLE = 2;
private final AbstractButton component;
public CommandItem(String text, boolean state, Icon icon, int style) {
switch(style) {
case TOGGLE_BUTTON_STYLE : component = new JToggleButton(text, icon, state); break;
case CHECK_BOX_STYLE : component = new JCheckBox(text, icon, state); break;
default: component = new JButton(text, icon);
}
}
protected AbstractButton getComponent() {
return component;
}
public void addActionListener(ActionListener listener) {
component.addActionListener(listener);
}
public void addChangeListener(ChangeListener listener) {
component.addChangeListener(listener);
}
public void setAction(Action action) {
component.setAction(action);
}
}
}
Example of use
This code snippet shows how MyCommandBar class should be used:
MyCommandBar commandBar = new MyCommandBar();
commandBar.setBorder(BorderFactory.createLineBorder(Color.black, 1));
commandBar.addCommandItem("BOLD", new MyCommandBar.CommandItem("<html><b>Bold</b></html>", true, null, MyCommandBar.CommandItem.TOGGLE_BUTTON_STYLE));
commandBar.addCommandItem("ITALICS", new MyCommandBar.CommandItem("<html><i>Italics</i></html>", false, null, MyCommandBar.CommandItem.CHECK_BOX_STYLE));
commandBar.addCommandItem("UNDERLINE", new MyCommandBar.CommandItem("<html><u>Underline</u></html>", false, null, MyCommandBar.CommandItem.DEFAULT_BUTTON_STYLE));
And you'll see something like this:
You can create MyJComponent subclass of JComponent with a private field that references a forwarding class for ExistingComponent.
The interactions with ExistingComponent are done with the forwarding class through methods of MyJComponent, and you are free to add more methods to MyJComponent.
Please see Effective Java item 16, for the delegation pattern used with the forwarding class.
I am wondering how would I change the name of the item list in my JComboBox?here's my code
I want to change it to Dog, Panda, bee. rather than choosing their path.
import java.awt.FlowLayout;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.JFrame;
import javax.swing.ImageIcon;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JComboBox;
public class ComboTest {
private JLabel imageLabel;
private JComboBox comboImage;
private String[] names = {"images/dog.gif","images/bee.gif","images/Panda.gif"};
private Icon[] icons = {
new ImageIcon(getClass().getResource(names[0])),
new ImageIcon(getClass().getResource(names[1])),
new ImageIcon(getClass().getResource(names[2])),
};
public ComboTest(){
initComponents();
}
public void initComponents(){
JFrame frame = new JFrame("Test Combo");
frame.setVisible(true);
frame.setSize(320, 160);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new FlowLayout());
comboImage = new JComboBox(names);
comboImage.addItemListener(new ItemListener(){
public void itemStateChanged(ItemEvent event){
if(event.getStateChange() == ItemEvent.SELECTED){
imageLabel.setIcon(icons[comboImage.getSelectedIndex()]);
}
}
});
frame.add(comboImage);
imageLabel = new JLabel(icons[0]);
frame.add(imageLabel);
}
}
You probably want to make an object with two properties, the path and the text you want to display.
Then you'll set the toString method to return the text property. Disclaimer: I haven't tested any of this code.
public class ValueText {
private String text;
private String value;
public ValueText(final String text, final String value) {
this.text = text;
this.value = value;
}
#Override
public String toString() {
return text;
}
public String getValue() {
return value;
}
}
Then you can change your initial array to something like:
private Object[] names = {
new ValueText("Dog", "images/dog.gif"),
new ValueText("Bee", "images/bee.gif"),
new ValueText("Panda", "images/Panda.gif")
};
And it should work similarly, only now, when you are inspecting the selected item, you can use the getValue() method to get the path.
You also might be interested in a custom renderer, but it's probably not necessary for your use:
http://docs.oracle.com/javase/tutorial/uiswing/components/combobox.html#renderer
Update I'll go ahead and make a correction after kleopatra made some convincing arguments in the comments, which you should read below.
A more general purpose and cleaner way of doing this is with a custom renderer, even if it's very simple (see link above).