I'm a Windows user, and I have developed a small software to calculate few parameter based on some inputs. The GUI in Windows is displayed as expected, but in Mac OS it's different. Please let me know how to correct it. The code for the same is.
package home;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.SwingConstants;
import java.awt.Component;
import java.awt.ComponentOrientation;
/**
* Home Page for the application.
* #author Harshit Rathore
*
*/
public class Home_Main {
private JFrame f;
private JXTabbedPane tabbedpane;
private ImageIcon icon_view, icon_analysis, icon_algo, icon_info, icon_sche_gen;
Schedule_Generation schedule_gen;
View_JPanel2D view2d;
View_JPanel3D view3d;
MapJPanel mapPanel;
Home_Main() {
f = new JFrame();
f.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);
f.setTitle("Anti-Accidental Algorithm");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
tabbedpane = new JXTabbedPane(JTabbedPane.LEFT);
AbstractTabRenderer renderer = (AbstractTabRenderer)tabbedpane.getTabRenderer();
renderer.setPrototypeText("This text is a prototype->");
renderer.setHorizontalTextAlignment(SwingConstants.LEADING);
icon_view = new ImageIcon(this.getClass().getResource("/view_icon.png"));
icon_analysis = new ImageIcon(this.getClass().getResource("/analysis_icon.png"));
icon_algo = new ImageIcon(this.getClass().getResource("/algo_icon.png"));
icon_info = new ImageIcon(this.getClass().getResource("/info_icon.png"));
icon_sche_gen = new ImageIcon(this.getClass().getResource("/schedule_icon.png"));
mapPanel = new home.MapJPanel();
view2d = new home.View_JPanel2D();
view3d = new home.View_JPanel3D();
schedule_gen = new home.Schedule_Generation(view2d, view3d, mapPanel);
tabbedpane.addTab("Algorithm", icon_algo, new home.Algo_JPanel());
tabbedpane.addTab("<html>Schedule<br>Generation</html>", icon_sche_gen, schedule_gen);
tabbedpane.addTab("<html>View Data 2D<br>(Azimuth, Elevation)</html>", icon_view, view2d);
tabbedpane.addTab("<html>View Data 3D<br>(Azimuth, Elevation, Tilt)</html>", icon_view, view3d);
tabbedpane.addTab("MapPanel", icon_info, mapPanel);
tabbedpane.addTab("<html>Windload<br>Torque</html>", icon_analysis, new home.WindloadTorque());
f.getContentPane().add(tabbedpane);
f.setMinimumSize(new Dimension(1100, 800));
f.setSize(1600, 800);
f.setVisible(true);
f.setEnabled(true);
}
/**
* The main function of the software.
* #param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
new Home_Main();
}
// custom tabbedpane
class JXTabbedPane extends JTabbedPane {
/**
*
*/
private static final long serialVersionUID = 1L;
private ITabRenderer tabRenderer = new DefaultTabRenderer();
public JXTabbedPane() {
super();
}
public JXTabbedPane(int tabPlacement) {
super(tabPlacement);
}
public JXTabbedPane(int tabPlacement, int tabLayoutPolicy) {
super(tabPlacement, tabLayoutPolicy);
}
public ITabRenderer getTabRenderer() {
return tabRenderer;
}
public void setTabRenderer(ITabRenderer tabRenderer) {
this.tabRenderer = tabRenderer;
}
#Override
public void addTab(String title, Component component) {
this.addTab(title, null, component, null);
}
#Override
public void addTab(String title, Icon icon, Component component) {
this.addTab(title, icon, component, null);
}
#Override
public void addTab(String title, Icon icon, Component component, String tip) {
super.addTab(title, icon, component, tip);
int tabIndex = getTabCount() - 1;
Component tab = tabRenderer.getTabRendererComponent(this, title, icon, tabIndex);
super.setTabComponentAt(tabIndex, tab);
}
}
interface ITabRenderer {
public Component getTabRendererComponent(JTabbedPane tabbedPane, String text, Icon icon, int tabIndex);
}
abstract class AbstractTabRenderer implements ITabRenderer {
private String prototypeText = "";
private Icon prototypeIcon = new ImageIcon(this.getClass().getResource("/view_icon.png"));
private int horizontalTextAlignment = SwingConstants.CENTER;
private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
public AbstractTabRenderer() {
super();
}
public void setPrototypeText(String text) {
String oldText = this.prototypeText;
this.prototypeText = text;
firePropertyChange("prototypeText", oldText, text);
}
public String getPrototypeText() {
return prototypeText;
}
public Icon getPrototypeIcon() {
return prototypeIcon;
}
public void setPrototypeIcon(Icon icon) {
Icon oldIcon = this.prototypeIcon;
this.prototypeIcon = icon;
firePropertyChange("prototypeIcon", oldIcon, icon);
}
public int getHorizontalTextAlignment() {
return horizontalTextAlignment;
}
public void setHorizontalTextAlignment(int horizontalTextAlignment) {
this.horizontalTextAlignment = horizontalTextAlignment;
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return propertyChangeSupport.getPropertyChangeListeners();
}
public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
return propertyChangeSupport.getPropertyChangeListeners(propertyName);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
propertyChangeSupport.addPropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
}
protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
PropertyChangeListener[] listeners = getPropertyChangeListeners();
for (int i = listeners.length - 1; i >= 0; i--) {
listeners[i].propertyChange(new PropertyChangeEvent(this, propertyName, oldValue, newValue));
}
}
}
class DefaultTabRenderer extends AbstractTabRenderer implements PropertyChangeListener {
private Component prototypeComponent;
public DefaultTabRenderer() {
super();
prototypeComponent = generateRendererComponent(getPrototypeText(), getPrototypeIcon(),
getHorizontalTextAlignment());
addPropertyChangeListener(this);
}
private Component generateRendererComponent(String text, Icon icon, int horizontalTabTextAlignmen) {
JPanel rendererComponent = new JPanel(new GridBagLayout());
rendererComponent.setOpaque(false);
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(2, 4, 2, 4);
c.fill = GridBagConstraints.HORIZONTAL;
rendererComponent.add(new JLabel(icon), c);
c.gridx = 1;
c.weightx = 1;
rendererComponent.add(new JLabel(text, horizontalTabTextAlignmen), c);
return rendererComponent;
}
#Override
public Component getTabRendererComponent(JTabbedPane tabbedPane, String text, Icon icon, int tabIndex) {
Component rendererComponent = generateRendererComponent(text, icon, getHorizontalTextAlignment());
int prototypeWidth = prototypeComponent.getPreferredSize().width;
int prototypeHeight = prototypeComponent.getPreferredSize().height;
rendererComponent.setPreferredSize(new Dimension(prototypeWidth, prototypeHeight));
return rendererComponent;
}
#Override
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
if ("prototypeText".equals(propertyName) || "prototypeIcon".equals(propertyName)) {
this.prototypeComponent = generateRendererComponent(getPrototypeText(), getPrototypeIcon(),
getHorizontalTextAlignment());
}
}
}
}
The outputs are-
First one is from Windows (desired one) second one is from Mac OS(need modification).
Thank You
The reason for the buttons looking different is because of Java Swing's LAF (Look & Feel).
The Look and Feel is like the master “theme” for your Swing Application where it defines specific rules on how certain components should look, etc…
Java Swing on Windows looks different from on Unix & OSX is because there is a different provider for the LAF (known as LAF "metal")
You thus can pack a LAF library into your program, and you can find many here.
To call a theme to be used, you must place this before any GUI/Swing events are called (AKA repaints, component inits, etc.):
try {
UIManager.setLookAndFeel(myThemeLAF.class.getName());
} catch (UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
Alternatively, you could override the individual paintComponent(Graphics g) methods.
This can be achieved either through anonymous classes or just having a class extend that object and override it from there:
#1 Anonymous Inner Classes
JPanel myPanel = new JPanel() {
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
//... draw stuffs
}
};
#2 Extending existing objects
public class MyPanel extends JPanel {
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// paint stuffs
}
}
Related
I've got a JList whose elements consist of image files for which I'm creating thumbnails (in a background Thread). When these thumbnails become available, I'd like to force a repaint of just that item. However, I find that when I use the listModel's fireDataChanged method (see below), all the visible items in the list are repainted (using my custom ListCellRenderer).
public void updateElement(int index) {
frame.listModel.fireContentsChanged(frame.listModel, index, index);
}
Is there any way to cause ONLY the indexed item to be repainted?
Without some kind of runnable example which demonstrates your issue, it's impossible to make any concrete recommendations.
The following simple example makes use of a SwingWorker to change the value of the elements within the ListModel. To make it look more realistic, I've shuffled the List of indices and applied a short delay between each.
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private DefaultListModel<String> model = new DefaultListModel<>();
public TestPane() {
setLayout(new BorderLayout());
add(new JScrollPane(new JList(model)));
JButton load = new JButton("Load");
add(load, BorderLayout.SOUTH);
load.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent arg0) {
load.setEnabled(false);
model.removeAllElements();
for (int index = 0; index < 100; index++) {
model.addElement("[" + index + "] Loading...");
}
LoadWorker worker = new LoadWorker(model);
worker.addPropertyChangeListener(new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
System.out.println(evt.getPropertyName() + " == " + evt.getNewValue());
if ("state".equals(evt.getPropertyName())) {
Object value = evt.getNewValue();
if (value instanceof SwingWorker.StateValue) {
SwingWorker.StateValue stateValue = (SwingWorker.StateValue) value;
if (stateValue == SwingWorker.StateValue.DONE) {
load.setEnabled(true);
}
}
}
}
});
worker.execute();
}
});
}
}
public class LoadResult {
private int index;
private String value;
public LoadResult(int index, String value) {
this.index = index;
this.value = value;
}
public int getIndex() {
return index;
}
public String getValue() {
return value;
}
}
public class LoadWorker extends SwingWorker<Void, LoadResult> {
private DefaultListModel model;
public LoadWorker(DefaultListModel model) {
this.model = model;
}
public DefaultListModel getModel() {
return model;
}
#Override
protected void process(List<LoadResult> chunks) {
for (LoadResult loadResult : chunks) {
model.set(loadResult.index, loadResult.value);
}
}
#Override
protected Void doInBackground() throws Exception {
int count = model.getSize();
List<Integer> indicies = new ArrayList<>(count);
for (int index = 0; index < count; index++) {
indicies.add(index);
}
Collections.shuffle(indicies);
for (int index : indicies) {
Thread.sleep(15);
publish(new LoadResult(index, "[" + index + "] Has been loaded"));
}
return null;
}
}
}
The above is a linear progression, meaning it's processing each item in sequence, one at a time.
Because image loading can take time and is CPU intensive process, you could make use of a ExecutorService and use a pool of threads to help spread the load.
For example:
Java - Multithreading with ImageIO
Make images fetched from server display in real time
Extracting images from a text file of 100 image urls using java
I find that when I use the listModel's fireDataChanged method (see below), all the visible items in the list are repainted
You should NOT invoke that method manually. The fireXXX(...) methods should only be invoked by the model itself.
You should be updating the model by using the:
model.set(...);
The set(...) method will then invoke the appropriate method to notify the JList to repaint the cell.
Here is my attempt at a simple Thumbnail app. It attempts to add performance improvements by:
loading the model with a default Icon so the list doesn't continually need to resize itself
Use a ExecutorService to take advantage of multiple processors
Using an ImageReader to read the file. The sub sampling property allows you to use fewer pixels when scaling the image.
Just change the class to point to a directory containing some .jpg files and give it a go:
ThumbnailApp:
import java.io.*;
import java.util.concurrent.*;
import java.awt.*;
//import java.awt.datatransfer.*;
import java.awt.event.*;
import javax.swing.*;
class ThumbnailApp
{
private DefaultListModel<Thumbnail> model = new DefaultListModel<Thumbnail>();
private JList<Thumbnail> list = new JList<Thumbnail>(model);
public ThumbnailApp()
{
}
public JPanel createContentPane()
{
JPanel cp = new JPanel( new BorderLayout() );
list.setCellRenderer( new ThumbnailRenderer<Thumbnail>() );
list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
list.setVisibleRowCount(-1);
Icon empty = new EmptyIcon(160, 160);
Thumbnail prototype = new Thumbnail(new File("PortugalSpain-000.JPG"), empty);
list.setPrototypeCellValue( prototype );
cp.add(new JScrollPane( list ), BorderLayout.CENTER);
return cp;
}
public void loadImages(File directory)
{
new Thread( () -> createThumbnails(directory) ).start();
}
private void createThumbnails(File directory)
{
try
{
File[] files = directory.listFiles((d, f) -> {return f.endsWith(".JPG");});
int processors = Runtime.getRuntime().availableProcessors();
ExecutorService service = Executors.newFixedThreadPool( processors - 2 );
long start = System.currentTimeMillis();
for (File file: files)
{
Thumbnail thumbnail = new Thumbnail(file, null);
model.addElement( thumbnail );
// new ThumbnailWorker(file, model, model.size() - 1).execute();
service.submit( new ThumbnailWorker(file, model, model.size() - 1) );
}
long duration = System.currentTimeMillis() - start;
System.out.println(duration);
service.shutdown();
}
catch(Exception e) { e.printStackTrace(); }
}
private static void createAndShowGUI()
{
ThumbnailApp app = new ThumbnailApp();
JFrame frame = new JFrame("ListDrop");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane( app.createContentPane() );
frame.setSize(1600, 900);
frame.setVisible(true);
// File directory = new File("C:/Users/netro/Pictures/TravelSun/2019_01_Cuba");
File directory = new File("C:/Users/netro/Pictures/TravelAdventures/2018_PortugalSpain");
app.loadImages( directory );
}
public static void main(String[] args)
{
javax.swing.SwingUtilities.invokeLater(() -> createAndShowGUI());
}
}
ThumbnailWorker:
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.Iterator;
//import java.util.concurrent.*;
import javax.imageio.*;
import javax.imageio.stream.*;
import javax.swing.*;
class ThumbnailWorker extends SwingWorker<Image, Void>
{
private File file;
private DefaultListModel<Thumbnail> model;
private int index;
public ThumbnailWorker(File file, DefaultListModel<Thumbnail> model, int index)
{
this.file = file;
this.model = model;
this.index = index;
}
#Override
protected Image doInBackground() throws IOException
{
// Image image = ImageIO.read( file );
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("jpg");
ImageReader reader = readers.next();
ImageReadParam irp = reader.getDefaultReadParam();
// irp.setSourceSubsampling(10, 10, 0, 0);
irp.setSourceSubsampling(5, 5, 0, 0);
ImageInputStream stream = new FileImageInputStream( file );
reader.setInput(stream);
Image image = reader.read(0, irp);
int width = 160;
int height = 90;
if (image.getHeight(null) > image.getWidth(null))
{
width = 90;
height = 160;
}
BufferedImage scaled = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = scaled.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.drawImage(image, 0, 0, width, height, null);
g2d.dispose();
image = null;
return scaled;
}
#Override
protected void done()
{
try
{
ImageIcon icon = new ImageIcon( get() );
Thumbnail thumbnail = model.get( index );
thumbnail.setIcon( icon );
model.set(index, thumbnail);
System.out.println("finished: " + file);
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
ThumbnailRenderer
import java.awt.Component;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
public class ThumbnailRenderer<E> extends JLabel implements ListCellRenderer<E>
{
public ThumbnailRenderer()
{
setOpaque(true);
setHorizontalAlignment(CENTER);
setVerticalAlignment(CENTER);
setHorizontalTextPosition( JLabel.CENTER );
setVerticalTextPosition( JLabel.BOTTOM );
setBorder( new EmptyBorder(4, 4, 4, 4) );
}
/*
* Display the Thumbnail Icon and file name.
*/
public Component getListCellRendererComponent(JList<? extends E> list, E value, int index, boolean isSelected, boolean cellHasFocus)
{
if (isSelected)
{
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
}
else
{
setBackground(list.getBackground());
setForeground(list.getForeground());
}
//Set the icon and filename
Thumbnail thumbnail = (Thumbnail)value;
setIcon( thumbnail.getIcon() );
setText( thumbnail.getFileName() );
return this;
}
}
Thumbnail:
import java.io.File;
import javax.swing.Icon;
public class Thumbnail
{
private File file;
private Icon icon;
public Thumbnail(File file, Icon icon)
{
this.file = file;
this.icon = icon;
}
public Icon getIcon()
{
return icon;
}
public void setIcon(Icon icon)
{
this.icon = icon;
}
public String getFileName()
{
return file.getName();
}
}
I tested on a directory with 302 images. Using the ExecutorService got the load time down from 2:31 to 0:35.
In my java application, my objective is to display or output an image using MVC architecture. My java application is comprised of an imagecontroller(main), imageview, and image model. I am currently able to select an image, the compiler acknowledges where the image has been selected from in the c: drive however it does not output or display the image. Here is a copy of my code below:
package imagecontroller;
import javax.swing.SwingUtilities;
import java.io.File;
public class ImageController {
private final ImageModel model;
private final ImageView view;
public ImageController () {
this.view = new ImageView(this);
this.model = new ImageModel(this.view);
public static void launch () {
new ImageController();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(ImageController::launch);
}
}
package imagecontroller;
import java.awt.Dimension;
import java.io.File;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.filechooser.FileNameExtensionFilter;
public class ImageView extends JFrame{
private final ImageController controller;
public ImageView(ImageController controller) {
this.controller = controller;
JFileChooser Chooser = new JFileChooser();
FileNameExtensionFilter filter = new FileNameExtensionFilter("JPG & PNG Images", "jpg", "png");
Chooser.addChoosableFileFilter(filter);
Chooser.setCurrentDirectory(new File(System.getProperty("user.home")));
Chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
int result
Chooser.showOpenDialog(null);
if (result == JFileChooser.APPROVE_OPTION) {
Chooser.setAcceptAllFileFilterUsed(true);
File selectedFile = Chooser.getSelectedFile();
System.out.println("Selected file: " + selectedFile.getAbsolutePath());
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setTitle("ImageAnnotator");
frame.setVisible(true);
frame.setSize(new Dimension (500,500));
}
}
}
package imagecontroller;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
public class ImageModel extends JComponent {
private final ImageView view;
private BufferedImage image;
public ImageModel(ImageView view) {
this.view = view;
}
public void CustomComponent (File png) {
BufferedImage image = null;
setPreferredSize(new Dimension(400, 400));
try {
this.image = ImageIO.read(new File("640px-Pleiades_large.png"));
} catch (IOException x) {
JOptionPane.showMessageDialog(null, "Not an ImageFile, Please Select an Image");
}
}
#Override
public void paintComponent(Graphics g){
super.paintComponent(g);
g = g.create();
g.setColor(Color.WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
int margin = 20;
int w = (getWidth() - (2 * margin + 2)) / 2;
int h = this.image.getHeight() * w / this.image.getWidth();
g.drawImage(image, h, h, WIDTH, HEIGHT, view);
}
}
The reason that the MVC pattern is called the MVC pattern is that the name suggests the order. In other words, create the model, then the view, then the controllers.
Here's an image display GUI I put together.
So, let's start with the model. For an image viewer GUI, the model is pretty simple.
public class ImageDisplayModel {
private BufferedImage image;
private File file;
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
}
The ImageDisplayModel class is a plain Java class that holds a BufferedImage and a File path. By saving the File path, the next time the user selects an image, the directory will be set to be the same as the last image.
The ImageDisplayModel class is an ordinary getter / setter class.
The next step is to create the view.
public class ImageDisplay implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new ImageDisplay());
}
private ImageDisplayModel model;
private ImagePanel imagePanel;
private JFrame frame;
public ImageDisplay() {
this.model = new ImageDisplayModel();
}
#Override
public void run() {
frame = new JFrame("Image Display");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setJMenuBar(createMenuBar());
imagePanel = new ImagePanel(model);
frame.add(imagePanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private JMenuBar createMenuBar() {
JMenuBar menubar = new JMenuBar();
JMenu filemenu = new JMenu("File");
JMenuItem openitem = new JMenuItem("Open...");
openitem.addActionListener(new OpenFileListener(this, model));
filemenu.add(openitem);
menubar.add(filemenu);
return menubar;
}
public void updateImagePanel(int width, int height) {
imagePanel.setPreferredSize(width, height);
imagePanel.repaint();
frame.pack();
}
public JFrame getFrame() {
return frame;
}
}
We start the application with a call to the SwingUtilities invokeLater method. This method ensures that the Swing components are created and executed on the Event Dispatch Thread.
The view constructor instantiates the application model.
The JFrame code is in the run method.
The JMenuBar method allows us to select multiple images, one after the other.
The getFrame method allows the eventual controller class to access the JFrame instance. The updateImagePanel method allows the eventual controller class to update the image panel.
Next, we create the DrawingPanel class.
public class ImagePanel extends JPanel {
private static final long serialVersionUID = 1L;
private ImageDisplayModel model;
public ImagePanel(ImageDisplayModel model) {
this.model = model;
this.setPreferredSize(649, 480);
}
public void setPreferredSize(int width, int height) {
this.setPreferredSize(new Dimension(width, height));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
BufferedImage image = model.getImage();
if (image != null) {
g.drawImage(image, 0, 0, this);
}
}
}
Simple and straightforward. We draw the BufferedImage if one exists. We also adjust the size of the JPanel so that the image fits.
The only information that the DrawingPanel class needs is the model class. The drawing panel will draw the image. Period.
Finally, we create the controller class.
public class OpenFileListener implements ActionListener {
private ImageDisplay frame;
private ImageDisplayModel model;
public OpenFileListener(ImageDisplay frame, ImageDisplayModel model) {
this.frame = frame;
this.model = model;
}
#Override
public void actionPerformed(ActionEvent event) {
JFileChooser chooser = new JFileChooser();
FileNameExtensionFilter filter = new FileNameExtensionFilter(
"JPG & PNG Images", "jpg", "png");
chooser.addChoosableFileFilter(filter);
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
File file = model.getFile();
if (file != null) {
chooser.setCurrentDirectory(file);
}
int result = chooser.showOpenDialog(frame.getFrame());
if (result == JFileChooser.APPROVE_OPTION) {
File selectedFile = chooser.getSelectedFile();
model.setFile(selectedFile);
BufferedImage image;
try {
image = ImageIO.read(selectedFile);
model.setImage(image);
frame.updateImagePanel(image.getWidth(),
image.getHeight());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
The controller class updates the model information and instructs the view to repaint itself. The controller isn't concerned with the view internals. All the controller needs to know is that the view can update.
Finally, here's the complete runnable code. I made the classes inner classes so I could post this code as one block.
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.filechooser.FileNameExtensionFilter;
public class ImageDisplay implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new ImageDisplay());
}
private ImageDisplayModel model;
private ImagePanel imagePanel;
private JFrame frame;
public ImageDisplay() {
this.model = new ImageDisplayModel();
}
#Override
public void run() {
frame = new JFrame("Image Display");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setJMenuBar(createMenuBar());
imagePanel = new ImagePanel(model);
frame.add(imagePanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
private JMenuBar createMenuBar() {
JMenuBar menubar = new JMenuBar();
JMenu filemenu = new JMenu("File");
JMenuItem openitem = new JMenuItem("Open...");
openitem.addActionListener(new OpenFileListener(this, model));
filemenu.add(openitem);
menubar.add(filemenu);
return menubar;
}
public void updateImagePanel(int width, int height) {
imagePanel.setPreferredSize(width, height);
imagePanel.repaint();
frame.pack();
}
public JFrame getFrame() {
return frame;
}
public class ImagePanel extends JPanel {
private static final long serialVersionUID = 1L;
private ImageDisplayModel model;
public ImagePanel(ImageDisplayModel model) {
this.model = model;
this.setPreferredSize(649, 480);
}
public void setPreferredSize(int width, int height) {
this.setPreferredSize(new Dimension(width, height));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
BufferedImage image = model.getImage();
if (image != null) {
g.drawImage(image, 0, 0, this);
}
}
}
public class OpenFileListener implements ActionListener {
private ImageDisplay frame;
private ImageDisplayModel model;
public OpenFileListener(ImageDisplay frame, ImageDisplayModel model) {
this.frame = frame;
this.model = model;
}
#Override
public void actionPerformed(ActionEvent event) {
JFileChooser chooser = new JFileChooser();
FileNameExtensionFilter filter = new FileNameExtensionFilter(
"JPG & PNG Images", "jpg", "png");
chooser.addChoosableFileFilter(filter);
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
File file = model.getFile();
if (file != null) {
chooser.setCurrentDirectory(file);
}
int result = chooser.showOpenDialog(frame.getFrame());
if (result == JFileChooser.APPROVE_OPTION) {
File selectedFile = chooser.getSelectedFile();
model.setFile(selectedFile);
BufferedImage image;
try {
image = ImageIO.read(selectedFile);
model.setImage(image);
frame.updateImagePanel(image.getWidth(),
image.getHeight());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class ImageDisplayModel {
private BufferedImage image;
private File file;
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
}
}
I really found this interesting and thought to explore it a little more. Taking pointers from The MVC pattern and Swing (which I suggest you read especially the accepted answer) lets try have a go.
Explanation:
Brief Summary of the code to follow (based off of the above link)
The LoginView. The view is simply your UI components, with getter methods for any components needed by the LoginController as well as implementing a PropertyChangeListener which is used by the view to receive PropertyChangeEvents which could be fired by our LoginController or LoginModel.
The LoginController The controller has access to both the view and the model. The controller sets up the necessary events on the views components and reacts to them by validating input from the view and then asks the model to do its job and potentially that will change its state. The controller takes your views actions and interprets them. If you click on a button, it's the controller's job to figure out what that means and how the model should be manipulated based on that action.
The controller may also ask the view to change it does this by subscribing the view (which implements PropertyChangeListener) to its SwingPropertyChangeSupport. When the controller receives an action from the view, in this case the Login button pressed it validates inputs from the view and fires the necessary validation errors (if any). It then calls the model to do the actual validation.
The LoginModel The controller calls methods on the model which intern notifies the view when its state has changed. When something changes in the model, based either on some action you took (like clicking a button) the model notifies the view that its state has changed through the same mechanism as the controller.
The view can also ask the model for state. The view gets the state it displays directly from the model. For instance, if we pass in a model instance to our view which already has authenticated set to true, you will immediately receive the message to say you are logged in (this has not been demonstrated here, but would entail passing the model into the views constructor and after initializing the initView() doing something like onAuthenticatedPropertyChange(model.isAuthenticated())).
Some extra information is both controller and model implement the observer pattern (as in the view observes changes to the controller and models in order to react to them)
The controller also does all model execution on a background thread via a SwingWorker, and all property change listeners are fired back on the EDT
Here is the code which demonstrates the above:
TestApp.java:
import javax.swing.SwingUtilities;
public class TestApp {
public TestApp() {
createAndShowGUI();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(TestApp::new);
}
private void createAndShowGUI() {
LoginModel model = new LoginModel();
LoginView view = new LoginView();
LoginController controller = new LoginController(view, model);
}
}
LoginView.java:
import java.awt.Color;
import java.awt.Component;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.border.EmptyBorder;
public class LoginView implements PropertyChangeListener {
private JFrame frame;
private JPanel loginPanel;
private JTextField usernameTextField;
private JTextField passwordTextField;
private JLabel errorLabel;
private JButton loginButton;
public LoginView() {
initView();
}
private void initView() {
frame = new JFrame("TestApp");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
loginPanel = createLoginPanel();
frame.add(loginPanel);
frame.pack();
frame.setVisible(true);
}
private JPanel createLoginPanel() {
JPanel panel = new JPanel();
panel.setBorder(new EmptyBorder(20, 20, 20, 20));
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
JLabel usernameLabel = new JLabel("Username:");
usernameLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
usernameTextField = new JTextField();
usernameTextField.setAlignmentX(Component.CENTER_ALIGNMENT);
usernameTextField.setColumns(20);
JLabel passwordLabel = new JLabel("Password:");
passwordLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
passwordTextField = new JTextField();
passwordTextField.setAlignmentX(Component.CENTER_ALIGNMENT);
passwordTextField.setColumns(20);
loginButton = new JButton("Login");
loginButton.setAlignmentX(Component.CENTER_ALIGNMENT);
errorLabel = new JLabel();
errorLabel.setForeground(Color.RED);
errorLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
errorLabel.setVisible(false);
panel.add(usernameLabel);
panel.add(usernameTextField);
panel.add(passwordLabel);
panel.add(passwordTextField);
panel.add(errorLabel);
panel.add(loginButton);
return panel;
}
public JButton getLoginButton() {
return loginButton;
}
public JTextField getUsernameTextField() {
return usernameTextField;
}
public JTextField getPasswordTextField() {
return passwordTextField;
}
public JFrame getFrame() {
return frame;
}
#Override
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
Object newValue = evt.getNewValue();
switch (propertyName) {
case "authenticated":
// authentication has finished lets do something
onAuthenticatedPropertyChange((Boolean) newValue);
break;
case "authenticating":
// lets disable the UI as we authenticate
errorLabel.setVisible(false);
disableLoginPanelComponents(true);
break;
case "usernameInvalid":
showError((String) newValue);
break;
case "passwordInvalid":
showError((String) newValue);
break;
}
}
private void onAuthenticatedPropertyChange(Boolean authenticated) {
if (authenticated == true) {
errorLabel.setVisible(false);
JOptionPane.showMessageDialog(frame, "You are in!");
} else {
showError("Invalid username or password!");
}
// re-enable components after authentication regadless of fail or pass
disableLoginPanelComponents(false);
}
private void showError(String error) {
errorLabel.setText(error);
errorLabel.setVisible(true);
}
private void disableLoginPanelComponents(boolean disable) {
for (int i = 0; i < loginPanel.getComponentCount(); i++) {
Component component = (Component) loginPanel.getComponent(i);
if (component instanceof JTextField || component instanceof JButton) {
((JComponent) loginPanel.getComponent(i)).setEnabled(!disable);
}
}
}
}
LoginController:
import java.awt.event.ActionEvent;
import javax.swing.SwingWorker;
import javax.swing.event.SwingPropertyChangeSupport;
public class LoginController {
private final SwingPropertyChangeSupport propertyChangeSupport;
private final LoginView view;
private final LoginModel model;
public LoginController(LoginView view, LoginModel model) {
this.view = view;
this.model = model;
propertyChangeSupport = new SwingPropertyChangeSupport(this, true);
initController();
}
private void initController() {
// make the view a listener of both the model and controller as both can send events which the view must react too
model.addPropertyChangeListener(view);
propertyChangeSupport.addPropertyChangeListener(view);
view.getLoginButton().addActionListener((ActionEvent e) -> {
// lets do some basic validation of username and password fields
String username = view.getUsernameTextField().getText();
String password = view.getPasswordTextField().getText();
// validate user input before sending it to the model (this is the contorllers job - besdoes business rules i..e username doesnt exist etc)
if (username.isEmpty()) {
propertyChangeSupport.firePropertyChange("usernameInvalid", null, "Username cannot be empty!");
return;
}
if (password.isEmpty()) {
propertyChangeSupport.firePropertyChange("passwordInvalid", null, "Password cannot be empty!");
return;
}
// call the model to login on a background thread as the model may query the db or hit an API etc
new SwingWorker<Void, Void>() {
#Override
protected Void doInBackground() throws Exception {
model.login(username, password);
return null;
}
}.execute();
});
}
}
LoginModel.java:
import java.beans.PropertyChangeListener;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.SwingPropertyChangeSupport;
public class LoginModel {
private final SwingPropertyChangeSupport propertyChangeSupport;
private boolean authenticated;
public LoginModel() {
propertyChangeSupport = new SwingPropertyChangeSupport(this, true); // we pass in true to noifty observers on the Event Dispatch Thread
}
public void login(String username, String password) {
propertyChangeSupport.firePropertyChange("authenticating", null, true);
// lets simulate query a database or something for 3 seconds
try {
Thread.sleep(3000);
} catch (InterruptedException ex) {
Logger.getLogger(LoginModel.class.getName()).log(Level.SEVERE, null, ex);
}
authenticated = username.equals("admin") && password.equals("password");
propertyChangeSupport.firePropertyChange("authenticated", null, authenticated);
}
public void addPropertyChangeListener(PropertyChangeListener prop) {
propertyChangeSupport.addPropertyChangeListener(prop);
}
public boolean isAuthenticated() {
return authenticated;
}
}
The above is generic and doesn't directly answer YOUR question but gives you an example of an MVC pattern in Swing.
Update:
Here is an example specific to your issue
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.SwingPropertyChangeSupport;
import javax.swing.filechooser.FileNameExtensionFilter;
public class TestApp {
public TestApp() {
createAndShowGUI();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(TestApp::new);
}
private void createAndShowGUI() {
DisplayImageModel model = new DisplayImageModel();
DisplayImageView view = new DisplayImageView();
DisplayImageController controller = new DisplayImageController(view, model);
}
class DisplayImageView implements PropertyChangeListener {
private JFrame frame;
private ImagePanel imagePanel;
private JLabel imageLabel;
private JButton pickImageButton;
public DisplayImageView() {
initView();
}
private void initView() {
frame = new JFrame("TestApp");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = createImagePanel();
frame.add(panel);
frame.pack();
frame.setVisible(true);
}
private JPanel createImagePanel() {
JPanel panel = new JPanel();
panel.setBorder(new EmptyBorder(20, 20, 20, 20));
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
pickImageButton = new JButton("Pick an image");
pickImageButton.setAlignmentX(Component.CENTER_ALIGNMENT);
imagePanel = new ImagePanel();
imagePanel.getPanel().setAlignmentX(Component.CENTER_ALIGNMENT);
panel.add(imagePanel.getPanel());
panel.add(pickImageButton);
return panel;
}
public JButton getPickAnImageButton() {
return pickImageButton;
}
public JLabel getImageLabel() {
return imageLabel;
}
public JFrame getFrame() {
return frame;
}
#Override
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
Object newValue = evt.getNewValue();
switch (propertyName) {
case "image":
onImagePropertyChange((BufferedImage) newValue);
break;
}
}
private void onImagePropertyChange(BufferedImage image) {
imagePanel.setImage(image);
frame.pack();
}
}
class DisplayImageModel {
private final SwingPropertyChangeSupport propertyChangeSupport;
private BufferedImage image;
public DisplayImageModel() {
propertyChangeSupport = new SwingPropertyChangeSupport(this, true); // we pass in true to noifty observers on the Event Dispatch Thread
}
public void setImage(BufferedImage image) {
this.image = image;
propertyChangeSupport.firePropertyChange("image", null, image);
}
public void addPropertyChangeListener(PropertyChangeListener prop) {
propertyChangeSupport.addPropertyChangeListener(prop);
}
public BufferedImage getImage() {
return image;
}
}
class DisplayImageController {
private final SwingPropertyChangeSupport propertyChangeSupport;
private final DisplayImageView view;
private final DisplayImageModel model;
public DisplayImageController(DisplayImageView view, DisplayImageModel model) {
this.view = view;
this.model = model;
propertyChangeSupport = new SwingPropertyChangeSupport(this, true);
initController();
}
private void initController() {
// make the view a listener of both the model and controller as both can send events which the view must react too
model.addPropertyChangeListener(view);
propertyChangeSupport.addPropertyChangeListener(view);
view.getPickAnImageButton().addActionListener((ActionEvent e) -> {
JFileChooser chooser = new JFileChooser();
FileNameExtensionFilter filter = new FileNameExtensionFilter(
"JPG & PNG Images", "jpg", "png");
chooser.addChoosableFileFilter(filter);
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
int result = chooser.showOpenDialog(view.getFrame());
if (result == JFileChooser.APPROVE_OPTION) {
File selectedFile = chooser.getSelectedFile();
BufferedImage image;
try {
image = ImageIO.read(selectedFile);
model.setImage(image);
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
}
}
class ImagePanel {
private final JPanel panel;
private BufferedImage image;
public ImagePanel() {
this.panel = new JPanel() {
#Override
public Dimension getPreferredSize() {
if (image != null) {
return new Dimension(image.getWidth(), image.getHeight());
} else {
return new Dimension(200, 200);
}
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (image != null) {
g.drawImage(image, 0, 0, this);
}
}
};
panel.setBorder(new LineBorder(Color.GRAY, 1));
}
public void setImage(BufferedImage image) {
this.image = image;
panel.revalidate();
panel.repaint();
}
public JPanel getPanel() {
return panel;
}
}
}
I wanted to make a Glass Panel that contain a JPanel with white background, border and the msg "please wait".
Here is the code example:
JLabel glassLabel = new JLabel("Please wait");
FadingPanel msg = new FadingPanel();
glassLabel.setFont(new Font("Dialog", Font.BOLD, 26));
msg.setLayout(new BorderLayout());
msg.add(glassLabel,BorderLayout.NORTH);
msg.setBackground(Color.white);
msg.setFont(UIManager.getFont("Table.font").deriveFont(24f));
msg.setBorder(new CompoundBorder(new TitledBorder(""),
new EmptyBorder(20,20,20,20)));
It will fade in and out while waiting for the query.
the problem is that I am getting a bad result.
need help
the other is that none of them show it with glass panel
Animating the opacity state of a glassPane is no different from animating the state of any Swing component, after all, the glassPane is just another component.
one is that the Timer system doesn't know if the start function started and it keeps the panel hanging on because it closing it before fading the panel and then before it shows it and then it dont try to close it again
This is more about your own internal state management. The panel shouldn't care, it should just be responding to the request to change opacity level, forward or backwards
What you should have, is some kind of "engine" which can provide events when certain states are achieved, at which time, you make decisions about what should be done, removing the functionality from the "panel" itself.
Theory TL;DR
Okay, first, some theory.
Animation...
Animation is the illusion of change over time. In your case, you're moving from 0 to 1 and back again over a specified period of time. This is commonly known as "linear progression/animation". Most naive animation implementations will simple add a constant delta to a value and keep doing so until a desired state is reached. This is naive because not all systems are equal. Some will be able to achieve the desired state faster than others, making the animation uneven and providing a poor user experience.
Instead, you should be focused on perform a operation over a fixed period of time, calculating the required value as fast as the system will allow. This allows the animation to "drop" frames as required based on the system's capabilities. This is commonly known as "duration based animation".
This approach is much more powerful, as it allows you to play around with the speed of the animation in a very simply way. It also allows you do some very advanced operations, like easement, which wouldn't be easily achievable through a linear progression.
Swing and animation...
Swing is SINGLE threaded. This means you can't perform blocking or long running operations within the context of the Event Dispatching Thread.
Swing is also NOT thread safe. This means you shouldn't update the UI (or any state the UI depends on) from outside the context of the EDT.
For animation, what you need is some way to post, fast, repetitive, events onto the EDT, which will allow you to make changes to the UI safely. For this, the most common tool is a Swing Timer...
The Framework
So based on that, what we need is some kind of "engine", which given a "range" and a "duration" can notify us of "ticks" on a regular bases from which we can calculate the progression that the animation has played, and calculate the value we should use based on our inputs ... simple ...
I, personally, prefer to use an animation library, but the simple framework presented in the examples basically abstracts all these concepts into a re-usable framework.
Make it so...
nb: I ran out of room, so the underlying framework is included in the main example
Okay, that's all nice and fluffy, but how does this actually help us. Essentially, the idea of the above is to abstract common functionality out and make it re-usable (and yes, I actually do use it, a lot)
What we now need, is a component which can actually use it, something like...
public interface FaderListener {
public void fadeDidComplete(FadePane pane);
}
public class FadePane extends JPanel {
private double alpha = 1;
private boolean fadingIn = true;
private DoubleAnimatable animatable;
private Duration duration = Duration.ofSeconds(5);
private List<FaderListener> listeners = new ArrayList<>(5);
public FadePane() {
setOpaque(false);
}
public void addFadeListener(FaderListener listener) {
listeners.add(listener);
}
public void removeFadeListener(FaderListener listener) {
listeners.remove(listener);
}
public boolean isFadingIn() {
return fadingIn;
}
public double getAlpha() {
return alpha;
}
#Override
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setComposite(AlphaComposite.SrcOver.derive((float)getAlpha()));
g2d.setColor(getBackground());
g2d.fillRect(0, 0, getWidth(), getHeight());
super.paint(g2d);
g2d.dispose();
}
protected void fadeTo(double to) {
double currentAlpha = getAlpha();
if (animatable != null) {
animatable.stop();
animatable = null;
}
if (currentAlpha == to) {
fadeDidComplete();
return;
}
DoubleRange animationRange = new DoubleRange(currentAlpha, to);
double maxFrom = to == 1 ? 1 : 0;
double maxTo = to == 1 ? 0 : 1;
DoubleRange maxRange = new DoubleRange(maxFrom, maxTo);
animatable = new DoubleAnimatable(animationRange, maxRange, duration, new AnimatableListener<Double>() {
#Override
public void animationChanged(Animatable<Double> animatable) {
alpha = animatable.getValue();
repaint();
}
}, new AnimatableLifeCycleListenerAdapter<Double>() {
#Override
public void animationCompleted(Animatable<Double> animatable) {
fadeDidComplete();
}
});
Animator.INSTANCE.add(animatable);
}
public void fadeIn() {
fadingIn = true;
fadeTo(1);
}
public void fadeOut() {
fadingIn = false;
fadeTo(0);
}
protected void fadeDidComplete() {
for (FaderListener listener : listeners) {
listener.fadeDidComplete(this);
}
}
}
Okay, this is a pretty simple concept. It's a JPanel which has a alpha property which changes the opacity level of the component - basically, this is all faked, as Swing only support opaque and transparent components, not translucent components. So we set the component to be transparent and manually paint the background ourselves.
The component exposes two methods, fadeIn and fadeOut and supports a FaderListener which can be used to notify interested parties that the fade operation has been completed
Runnable example...
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
setBackground(Color.RED);
setLayout(new BorderLayout());
FadePane pane = new FadePane();
pane.setLayout(new GridBagLayout());
pane.add(new JLabel("Look ma, no hands"));
add(pane);
JButton btn = new JButton("Switch");
btn.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
btn.setEnabled(false);
if (pane.isFadingIn()) {
pane.fadeOut();
} else {
pane.fadeIn();
}
}
});
add(btn, BorderLayout.SOUTH);
pane.addFadeListener(new FaderListener() {
#Override
public void fadeDidComplete(FadePane pane) {
btn.setEnabled(true);
}
});
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
}
public interface FaderListener {
public void fadeDidComplete(FadePane pane);
}
public class FadePane extends JPanel {
private double alpha = 1;
private boolean fadingIn = true;
private DoubleAnimatable animatable;
private Duration duration = Duration.ofSeconds(5);
private List<FaderListener> listeners = new ArrayList<>(5);
public FadePane() {
setOpaque(false);
}
public void addFadeListener(FaderListener listener) {
listeners.add(listener);
}
public void removeFadeListener(FaderListener listener) {
listeners.remove(listener);
}
public boolean isFadingIn() {
return fadingIn;
}
public double getAlpha() {
return alpha;
}
public void setFaddedOut() {
alpha = 0;
fadingIn = false;
}
public void setFaddedIn() {
alpha = 1;
fadingIn = true;
}
#Override
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setComposite(AlphaComposite.SrcOver.derive((float)getAlpha()));
g2d.setColor(getBackground());
g2d.fillRect(0, 0, getWidth(), getHeight());
super.paint(g2d);
g2d.dispose();
}
protected void fadeTo(double to) {
double currentAlpha = getAlpha();
if (animatable != null) {
animatable.stop();
animatable = null;
}
if (currentAlpha == to) {
fadeDidComplete();
return;
}
DoubleRange animationRange = new DoubleRange(currentAlpha, to);
double maxFrom = to == 1 ? 1 : 0;
double maxTo = to == 1 ? 0 : 1;
DoubleRange maxRange = new DoubleRange(maxFrom, maxTo);
animatable = new DoubleAnimatable(animationRange, maxRange, duration, new AnimatableListener<Double>() {
#Override
public void animationChanged(Animatable<Double> animatable) {
alpha = animatable.getValue();
repaint();
}
}, new AnimatableLifeCycleListenerAdapter<Double>() {
#Override
public void animationCompleted(Animatable<Double> animatable) {
fadeDidComplete();
}
});
Animator.INSTANCE.add(animatable);
}
public void fadeIn() {
fadingIn = true;
fadeTo(1);
}
public void fadeOut() {
fadingIn = false;
fadeTo(0);
}
protected void fadeDidComplete() {
for (FaderListener listener : listeners) {
listener.fadeDidComplete(this);
}
}
}
public class DoubleAnimatable extends AbstractAnimatable<Double> {
public DoubleAnimatable(DoubleRange animationRange, DoubleRange maxRange, Duration duration, AnimatableListener<Double> listener, AnimatableLifeCycleListener<Double> lifeCycleListener) {
super(animationRange, duration, listener, lifeCycleListener);
double maxDistance = maxRange.getDistance();
double aniDistance = animationRange.getDistance();
double progress = Math.min(100, Math.max(0, Math.abs(aniDistance / maxDistance)));
Duration remainingDuration = Duration.ofMillis((long) (duration.toMillis() * progress));
setDuration(remainingDuration);
}
}
public interface AnimatableListener<T> {
public void animationChanged(Animatable<T> animatable);
}
public interface AnimatableLifeCycleListener<T> {
public void animationStopped(Animatable<T> animatable);
public void animationCompleted(Animatable<T> animatable);
public void animationStarted(Animatable<T> animatable);
public void animationPaused(Animatable<T> animatable);
}
public class AnimatableLifeCycleListenerAdapter<T> implements AnimatableLifeCycleListener<T> {
#Override
public void animationStopped(Animatable<T> animatable) {
}
#Override
public void animationCompleted(Animatable<T> animatable) {
}
#Override
public void animationStarted(Animatable<T> animatable) {
}
#Override
public void animationPaused(Animatable<T> animatable) {
}
}
public abstract class AbstractAnimatable<T> implements Animatable<T> {
private Range<T> range;
private LocalDateTime startTime;
private Duration duration = Duration.ofSeconds(5);
private T value;
private AnimatableListener<T> animatableListener;
private AnimatableLifeCycleListener<T> lifeCycleListener;
// private Easement easement;
private double rawOffset;
public AbstractAnimatable(Range<T> range, Duration duration, AnimatableListener<T> listener) {
this.range = range;
this.value = range.getFrom();
this.animatableListener = listener;
}
public AbstractAnimatable(Range<T> range, Duration duration, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
this(range, duration, listener);
this.lifeCycleListener = lifeCycleListener;
}
// public AbstractAnimatable(Range<T> range, Duration duration, Easement easement, AnimatableListener<T> listener) {
// this(range, duration, listener);
// this.easement = easement;
// }
//
// public AbstractAnimatable(Range<T> range, Duration duration, Easement easement, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
// this(range, duration, easement, listener);
// this.lifeCycleListener = lifeCycleListener;
// }
//
// public void setEasement(Easement easement) {
// this.easement = easement;
// }
//
// #Override
// public Easement getEasement() {
// return easement;
// }
public Duration getDuration() {
return duration;
}
public Range<T> getRange() {
return range;
}
public void setRange(Range<T> range) {
this.range = range;
}
#Override
public T getValue() {
return value;
}
protected void setDuration(Duration duration) {
this.duration = duration;
}
public double getCurrentProgress(double rawProgress) {
double progress = Math.min(1.0, Math.max(0.0, getRawProgress()));
// Easement easement = getEasement();
// if (easement != null) {
// progress = easement.interpolate(progress);
// }
return Math.min(1.0, Math.max(0.0, progress));
}
public double getRawProgress() {
if (startTime == null) {
return 0.0;
}
Duration duration = getDuration();
Duration runningTime = Duration.between(startTime, LocalDateTime.now());
double progress = rawOffset + (runningTime.toMillis() / (double) duration.toMillis());
return Math.min(1.0, Math.max(0.0, progress));
}
#Override
public void tick() {
if (startTime == null) {
startTime = LocalDateTime.now();
fireAnimationStarted();
}
double rawProgress = getRawProgress();
double progress = getCurrentProgress(rawProgress);
if (rawProgress >= 1.0) {
progress = 1.0;
}
value = getRange().valueAt(progress);
fireAnimationChanged();
if (rawProgress >= 1.0) {
fireAnimationCompleted();
}
}
#Override
public void start() {
if (startTime != null) {
// Restart?
return;
}
Animator.INSTANCE.add(this);
}
#Override
public void stop() {
stopWithNotification(true);
}
#Override
public void pause() {
rawOffset += getRawProgress();
stopWithNotification(false);
double remainingProgress = 1.0 - rawOffset;
Duration remainingTime = getDuration().minusMillis((long) remainingProgress);
setDuration(remainingTime);
lifeCycleListener.animationStopped(this);
}
protected void fireAnimationChanged() {
if (animatableListener == null) {
return;
}
animatableListener.animationChanged(this);
}
protected void fireAnimationCompleted() {
stopWithNotification(false);
if (lifeCycleListener == null) {
return;
}
lifeCycleListener.animationCompleted(this);
}
protected void fireAnimationStarted() {
if (lifeCycleListener == null) {
return;
}
lifeCycleListener.animationStarted(this);
}
protected void fireAnimationPaused() {
if (lifeCycleListener == null) {
return;
}
lifeCycleListener.animationPaused(this);
}
protected void stopWithNotification(boolean notify) {
Animator.INSTANCE.remove(this);
startTime = null;
if (notify) {
if (lifeCycleListener == null) {
return;
}
lifeCycleListener.animationStopped(this);
}
}
}
public interface Animatable<T> {
public Range<T> getRange();
public T getValue();
public void tick();
public Duration getDuration();
//public Easement getEasement();
// Wondering if these should be part of a secondary interface
// Provide a "self managed" unit of work
public void start();
public void stop();
public void pause();
}
public abstract class Range<T> {
private T from;
private T to;
public Range(T from, T to) {
this.from = from;
this.to = to;
}
public T getFrom() {
return from;
}
public T getTo() {
return to;
}
#Override
public String toString() {
return "From " + getFrom() + " to " + getTo();
}
public abstract T valueAt(double progress);
}
public class DoubleRange extends Range<Double> {
public DoubleRange(Double from, Double to) {
super(from, to);
}
public Double getDistance() {
return getTo() - getFrom();
}
#Override
public Double valueAt(double progress) {
double distance = getDistance();
double value = distance * progress;
value += getFrom();
return value;
}
}
public enum Animator {
INSTANCE;
private Timer timer;
private List<Animatable> properies;
private Animator() {
properies = new ArrayList<>(5);
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
List<Animatable> copy = new ArrayList<>(properies);
Iterator<Animatable> it = copy.iterator();
while (it.hasNext()) {
Animatable ap = it.next();
ap.tick();
}
if (properies.isEmpty()) {
timer.stop();
}
}
});
}
public void add(Animatable ap) {
properies.add(ap);
timer.start();
}
protected void removeAll(List<Animatable> completed) {
properies.removeAll(completed);
}
public void remove(Animatable ap) {
properies.remove(ap);
if (properies.isEmpty()) {
timer.stop();
}
}
}
}
But it's not a glassPane
... ok, as I said, a glassPane is just another component
This is a simple example which makes use of the frame's glassPane and will, when the panel is faded out, reset the glassPane to a default component
import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
setLayout(new GridBagLayout());
JButton btn = new JButton("Switch");
btn.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
Window window = SwingUtilities.getWindowAncestor(TestPane.this);
if (!(window instanceof JFrame)) {
System.out.println("Not out frame");
return;
}
JFrame frame = (JFrame) window;
FadePane pane = new FadePane();
pane.setLayout(new BorderLayout());
pane.add(new JLabel("All your base are belong to us"));
pane.setFaddedOut();
pane.addFadeListener(new FaderListener() {
#Override
public void fadeDidComplete(FadePane pane) {
System.out.println("Completed");
if (pane.getAlpha() == 1) {
System.out.println("Fade out");
pane.fadeOut();
} else {
System.out.println("Remove glasspane");
frame.setGlassPane(new JPanel());
}
}
});
frame.setGlassPane(pane);
System.out.println("Fade in");
pane.setVisible(true);
pane.fadeIn();
}
});
add(btn);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
}
}
nb: The required classes are in the previous example
Consider using JDialog container. When it is undecorated, you can change its opacity:
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Point;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.WindowConstants;
public class FadeDialog extends JDialog {
private float alfa = 1;
private JLabel label;
private boolean isFadeIn = true;
private JButton fadeIn, fadeOut;
FadeDialog() {
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setLocation(new Point(300, 300));
getContentPane().setLayout(new BorderLayout(5,0));
setUndecorated(true); //opacity supported for undecorated JDialogs
JButton close = new JButton("Close");
close.addActionListener(e -> dispose());
getContentPane().add(close, BorderLayout.PAGE_END);
getContentPane().add(new ContentPane(), BorderLayout.CENTER);
pack();
setVisible(true);
Timer timer = new Timer(2000, e -> fade());//endless fade-in-out loop
timer.setInitialDelay(100);
timer.start();
}
void fade() {
alfa = isFadeIn ? alfa + 0.1f : alfa -0.1f;
if(alfa <=0 ) {
alfa = 0; isFadeIn = true;
}else if(alfa >= 1) {
alfa = 1; isFadeIn = false;
}
fadeIn.setEnabled(! isFadeIn); fadeOut.setEnabled(isFadeIn);
label.setText("Alfa is " + alfa);
setOpacity(alfa); //set JDialog opacity
}
class ContentPane extends JPanel {
ContentPane() {
setPreferredSize(new Dimension(200, 100));
setLayout(new BorderLayout());
fadeIn = new JButton("Fade In");
fadeIn.addActionListener(e -> isFadeIn = true);
add(fadeIn, BorderLayout.PAGE_START);
label = new JLabel("Alfa is " + alfa);
add(label, BorderLayout.CENTER);
fadeOut = new JButton("Fade Out");
fadeOut.addActionListener(e -> isFadeIn = false);
add(fadeOut, BorderLayout.PAGE_END);
}
}
public static void main(String[] args) {
new FadeDialog();
}
}
So I have a GUI, a Tile class and a method class. I created four tiles in my Game class which consists of Tiles that has contains a letter and a color in each of them. I now want to create a method where when I click a key on my keyboard to that specific letter on the tile, it will remove the Tile . How would I go about that? Do I create that method in my model class and call it in my Game(GUI) class?
Your game is the "controller", it's responsible for managing the functionality and communication between the model and view.
Your view should be a representation of your model
Your model (and possibly your view) should be providing event notification support, to which you controller will need to monitor, in order to manage the requirements and logic.
To start with, you code is in mess. You are making to much use of static and it's not going to help you.
For example, I re-worked your Tile class to look more like this.
public class Tile extends JLabel {
public static Font font = new Font("Serif", Font.BOLD, 39);
private char _c;
public Tile(char c, Color background) {
setBackground(background);
setOpaque(true);
_c = c;
setText(convert());
}
public static char randomLetter() {
Random r = new Random();
char randomChar = (char) (97 + r.nextInt(25));
return randomChar;
}
public char getChar() {
return _c;
}
public String convert() {
return String.valueOf(getChar());
}
}
Rather then calling randomLetter each time you called getChar or convert, you should only be using it when you actually need a new character, otherwise you'll never know what the Tile's character actually is/was to begin with
Next, we need some kind of observer contract for the mode, so it can tell us when things have changed, for example.
public interface ModelListener {
public void tileWasRemoved(Tile tile);
}
It's nothing special, but this provides a means for the Model to provide notification when a Tile is removed and which Tile was actually removed.
Next, I updated the Model so that it actual "models" something. The Model now maintains a list of Tiles and provides functionality for adding and removing them. It also provides support for the ModelListener and event triggering
public class Model {
private ArrayList<Tile> list = new ArrayList<Tile>();
private List<ModelListener> listeners = new ArrayList<>(25);
public Model() {
}
public void addModelListener(ModelListener listener) {
listeners.add(listener);
}
public void removeModelListener(ModelListener listener) {
listeners.remove(listener);
}
protected void fireTileRemoved(Tile tile) {
for (ModelListener listener : listeners) {
listener.tileWasRemoved(tile);
}
}
public void removeByChar(char value) {
Iterator<Tile> iterator = list.iterator();
while (iterator.hasNext()) {
Tile tile = iterator.next();
if (value == tile.getChar()) {
fireTileRemoved(tile);
iterator.remove();
}
}
}
private void add(Tile tile) {
list.add(tile);
}
private Iterable<Tile> getTiles() {
return Collections.unmodifiableList(list);
}
}
Next, I went to the Game and updated it so it adds Tiles to the Model and uses the Model data to setup the UI. It then registers the KeyListener and ModelListener
public Game() {
model = new Model();
model.add(new Tile(Tile.randomLetter(), Color.WHITE));
model.add(new Tile(Tile.randomLetter(), Color.RED));
model.add(new Tile(Tile.randomLetter(), Color.GREEN));
model.add(new Tile(Tile.randomLetter(), Color.YELLOW));
JFrame frame = new JFrame();
frame.getContentPane().setLayout(new GridLayout(4, 1));
frame.setSize(500, 800);
frame.setVisible(true);
for (Tile tile : model.getTiles()) {
frame.add(tile);
}
model.addModelListener(new ModelListener() {
#Override
public void tileWasRemoved(Tile tile) {
frame.remove(tile);
frame.revalidate();
frame.repaint();
}
});
frame.getContentPane().addKeyListener(this);
frame.getContentPane().setFocusable(true);
frame.getContentPane().requestFocusInWindow();
}
And finally, the keyTyped event now asks the Model to remove a Tile of the given key...
#Override
public void keyTyped(KeyEvent e) {
model.removeByChar(e.getKeyChar());
}
As a proof of concept...
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Game implements KeyListener {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
new Game();
}
});
}
private Model model;
public Game() {
model = new Model();
model.add(new Tile(Tile.randomLetter(), Color.WHITE));
model.add(new Tile(Tile.randomLetter(), Color.RED));
model.add(new Tile(Tile.randomLetter(), Color.GREEN));
model.add(new Tile(Tile.randomLetter(), Color.YELLOW));
JFrame frame = new JFrame();
frame.getContentPane().setLayout(new GridLayout(4, 1));
frame.setSize(500, 800);
frame.setVisible(true);
for (Tile tile : model.getTiles()) {
frame.add(tile);
}
model.addModelListener(new ModelListener() {
#Override
public void tileWasRemoved(Tile tile) {
frame.remove(tile);
frame.revalidate();
frame.repaint();
}
});
frame.getContentPane().addKeyListener(this);
frame.getContentPane().setFocusable(true);
frame.getContentPane().requestFocusInWindow();
}
#Override
public void keyPressed(KeyEvent e) {
}
#Override
public void keyReleased(KeyEvent e) {
}
#Override
public void keyTyped(KeyEvent e) {
model.removeByChar(e.getKeyChar());
}
public interface ModelListener {
public void tileWasRemoved(Tile tile);
}
public class Model {
private ArrayList<Tile> list = new ArrayList<Tile>();
private List<ModelListener> listeners = new ArrayList<>(25);
public Model() {
}
public void addModelListener(ModelListener listener) {
listeners.add(listener);
}
public void removeModelListener(ModelListener listener) {
listeners.remove(listener);
}
protected void fireTileRemoved(Tile tile) {
for (ModelListener listener : listeners) {
listener.tileWasRemoved(tile);
}
}
public void removeByChar(char value) {
Iterator<Tile> iterator = list.iterator();
while (iterator.hasNext()) {
Tile tile = iterator.next();
if (value == tile.getChar()) {
fireTileRemoved(tile);
iterator.remove();
}
}
}
private void add(Tile tile) {
list.add(tile);
}
private Iterable<Tile> getTiles() {
return Collections.unmodifiableList(list);
}
}
public static class Tile extends JLabel {
public static Font font = new Font("Serif", Font.BOLD, 39);
private char _c;
public Tile(char c, Color background) {
setBackground(background);
setOpaque(true);
_c = c;
setText(convert());
}
public static char randomLetter() {
Random r = new Random();
char randomChar = (char) (97 + r.nextInt(25));
return randomChar;
}
public char getChar() {
return _c;
}
public String convert() {
return String.valueOf(getChar());
}
}
}
How ever...
As a general rule of thumb, KeyListener is a pain to work with and you should be making use of the key bindings API instead, for example
import java.awt.AWTKeyStroke;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Game {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
new Game();
}
});
}
private Model model;
public Game() {
model = new Model();
model.add(new Tile(Tile.randomLetter(), Color.WHITE));
model.add(new Tile(Tile.randomLetter(), Color.RED));
model.add(new Tile(Tile.randomLetter(), Color.GREEN));
model.add(new Tile(Tile.randomLetter(), Color.YELLOW));
JFrame frame = new JFrame();
frame.getContentPane().setLayout(new GridLayout(4, 1));
frame.setSize(500, 800);
frame.setVisible(true);
for (Tile tile : model.getTiles()) {
frame.add(tile);
KeyStroke ks = KeyStroke.getKeyStroke(tile.getChar());
String name = "typed." + tile.getChar();
Action action = new TileAction(model, tile.getChar());
registerKeyBinding((JComponent)frame.getContentPane(), name, ks, action);
}
model.addModelListener(new ModelListener() {
#Override
public void tileWasRemoved(Tile tile) {
frame.remove(tile);
frame.revalidate();
frame.repaint();
}
});
}
public void registerKeyBinding(JComponent parent, String name, KeyStroke ks, Action action) {
InputMap im = parent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
ActionMap am = parent.getActionMap();
im.put(ks, name);
am.put(name, action);
}
public class TileAction extends AbstractAction {
private Model model;
private char value;
public TileAction(Model model, char value) {
this.model = model;
this.value = value;
}
#Override
public void actionPerformed(ActionEvent e) {
model.removeByChar(value);
}
}
public interface ModelListener {
public void tileWasRemoved(Tile tile);
}
public class Model {
private ArrayList<Tile> list = new ArrayList<Tile>();
private List<ModelListener> listeners = new ArrayList<>(25);
public Model() {
}
public void addModelListener(ModelListener listener) {
listeners.add(listener);
}
public void removeModelListener(ModelListener listener) {
listeners.remove(listener);
}
protected void fireTileRemoved(Tile tile) {
for (ModelListener listener : listeners) {
listener.tileWasRemoved(tile);
}
}
public void removeByChar(char value) {
Iterator<Tile> iterator = list.iterator();
while (iterator.hasNext()) {
Tile tile = iterator.next();
if (value == tile.getChar()) {
fireTileRemoved(tile);
iterator.remove();
}
}
}
private void add(Tile tile) {
list.add(tile);
}
private Iterable<Tile> getTiles() {
return Collections.unmodifiableList(list);
}
}
public static class Tile extends JLabel {
public static Font font = new Font("Serif", Font.BOLD, 39);
private char _c;
public Tile(char c, Color background) {
setBackground(background);
setOpaque(true);
_c = c;
setText(convert());
}
public static char randomLetter() {
Random r = new Random();
char randomChar = (char) (97 + r.nextInt(25));
return randomChar;
}
public char getChar() {
return _c;
}
public String convert() {
return String.valueOf(getChar());
}
}
}
See How to Use Key Bindings for more details.
I've got an application that uses custom buttons all over the place for text and icons. Works great for windows and linux, but now OSX users are complaining. Text doesn't display on the mac, just '...'. The code seems simple enough, but I'm clueless when it comes to macs. How can I fix this?
Test case:
package example.swingx;
import example.utils.ResourceLoader;
import com.xduke.xlayouts.XTableLayout;
import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import java.awt.*;
import java.awt.event.ActionEvent;
public class SmallButton extends JButton {
private static ComponentUI ui = new SmallButtonUI();
public SmallButton() {
super();
/* final RepaintManager repaintManager = RepaintManager.currentManager(this);
repaintManager.setDoubleBufferingEnabled(false);
setDebugGraphicsOptions(DebugGraphics.FLASH_OPTION);*/
}
public SmallButton(AbstractAction action) {
super(action);
}
public SmallButton(String text) {
super(text);
}
public SmallButton(Icon icon) {
super(icon);
}
public void updateUI() {
setUI(ui);
}
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final JPanel buttonPanel = new JPanel(new XTableLayout());
SmallButton firstSmallButton = new SmallButton("One");
SmallButton secondSmallButton = new SmallButton("Two");
SmallButton thirdSmallButton = new SmallButton();
ImageIcon cameraIcon = (ImageIcon) ResourceLoader.getIcon("camera");
SmallButton fourth = new SmallButton();
fourth.setAction(new AbstractAction() {
public void actionPerformed(ActionEvent e) {
System.out.println("Fourth button pressed!");
}
});
fourth.setIcon(cameraIcon);
buttonPanel.add(firstSmallButton, "+");
buttonPanel.add(secondSmallButton, "+");
buttonPanel.add(thirdSmallButton, "+");
buttonPanel.add(fourth, "+");
final Container container = frame.getContentPane();
container.add(buttonPanel);
frame.pack();
frame.setVisible(true);
}
}
UI:
package example.swingx;
import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicButtonUI;
import java.awt.*;
public class SmallButtonUI extends BasicButtonUI {
private static final Color FOCUS_COLOR = new Color(0, 0, 0);
private static final Color BACKGROUND_COLOR = new Color(173, 193, 226);
private static final Color SELECT_COLOR = new Color(102, 132, 186);
private static final Color DISABLE_TEXT_COLOR = new Color(44, 44, 61);
private static final Insets DEFAULT_SMALLBUTTON_MARGIN = new Insets(2, 4, 2, 4);
private final static SmallButtonUI smallButtonUI = new SmallButtonUI();
public static ComponentUI createUI(JComponent component) {
return smallButtonUI;
}
protected Color getSelectColor() {
return SELECT_COLOR;
}
protected Color getDisabledTextColor() {
return DISABLE_TEXT_COLOR;
}
protected Color getFocusColor() {
return FOCUS_COLOR;
}
public void paint(Graphics g, JComponent c) {
((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
super.paint(g, c);
}
protected void paintButtonPressed(Graphics graphics, AbstractButton button) {
if (button.isContentAreaFilled()) {
Dimension size = button.getSize();
graphics.setColor(getSelectColor());
graphics.fillRect(0, 0, size.width, size.height);
}
}
public Dimension getMinimumSize(JComponent component) {
final AbstractButton button = ((AbstractButton) component);
// Handle icon buttons:
Icon buttonIcon = button.getIcon();
if (buttonIcon != null) {
return new Dimension(
buttonIcon.getIconWidth(),
buttonIcon.getIconHeight()
);
}
// Handle text buttons:
final Font fontButton = button.getFont();
final FontMetrics fontMetrics = button.getFontMetrics(fontButton);
final String buttonText = button.getText();
if (buttonText != null) {
final int buttonTextWidth = fontMetrics.stringWidth(buttonText);
return new Dimension(buttonTextWidth + 15,
fontMetrics.getHeight() + 5);
}
return null;
}
protected void installDefaults(AbstractButton button) {
super.installDefaults(button);
button.setMargin(DEFAULT_SMALLBUTTON_MARGIN);
button.setBackground(getBackgroundColor());
}
private Color getBackgroundColor() {
return BACKGROUND_COLOR;
}
public Dimension getPreferredSize(JComponent component) {
return getMinimumSize(component);
}
public Dimension getMaximumSize(JComponent component) {
return super.getMinimumSize(component);
}
public SmallButtonUI() {
super();
}
}
EDIT: After debugging, it seems getMinimumSize() is the same on both platforms. Also, when I stop on anywhere Graphics is used, it seems that the mac has a transY value of 47, while linux has 0. This 47 seems to feed into the clipping regions as well. Where could that be getting set?\
Your calculation of preferred size is incorrect.
BasicButtonUI uses SwingUtilities.layoutCompoundLabel, examined here. In a label, the ellipsis is added if the string is too long, but a button is typically sized to fit its entire text.
Absent a better understanding of your context, I would use a sizeVariant, shown below. I've also shown a simple BasicButtonUI example using a smaller, derived Font. The UI menu can be used in conjunction with Quaqua for testing.
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractButton;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.plaf.basic.BasicButtonUI;
/**
* #see https://stackoverflow.com/a/14599176/230513
* #see https://stackoverflow.com/a/11949899/230513
*/
public class Test {
private void display() {
JFrame f = new JFrame("Test");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setBackground(new Color(0xfff0f0f0));
f.setLayout(new GridLayout(0, 1));
f.add(createToolBar(f));
f.add(variantPanel("mini"));
f.add(variantPanel("small"));
f.add(variantPanel("regular"));
f.add(variantPanel("large"));
JPanel customPanel = new JPanel();
customPanel.add(createCustom("One"));
customPanel.add(createCustom("Two"));
customPanel.add(createCustom("Three"));
f.add(customPanel);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
private static JPanel variantPanel(String size) {
JPanel variantPanel = new JPanel();
variantPanel.add(createVariant("One", size));
variantPanel.add(createVariant("Two", size));
variantPanel.add(createVariant("Three", size));
return variantPanel;
}
private static JButton createVariant(String name, String size) {
JButton b = new JButton(name);
b.putClientProperty("JComponent.sizeVariant", size);
return b;
}
private static JButton createCustom(String name) {
JButton b = new JButton(name) {
#Override
public void updateUI() {
super.updateUI();
setUI(new CustomButtonUI());
}
};
return b;
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
new Test().display();
}
});
}
private static class CustomButtonUI extends BasicButtonUI {
private static final Color BACKGROUND_COLOR = new Color(173, 193, 226);
private static final Color SELECT_COLOR = new Color(102, 132, 186);
#Override
protected void paintText(Graphics g, AbstractButton b, Rectangle r, String t) {
super.paintText(g, b, r, t);
g.setColor(SELECT_COLOR);
g.drawRect(r.x, r.y, r.width, r.height);
}
#Override
protected void paintFocus(Graphics g, AbstractButton b,
Rectangle viewRect, Rectangle textRect, Rectangle iconRect) {
super.paintFocus(g, b, viewRect, textRect, iconRect);
g.setColor(Color.blue.darker());
g.drawRect(viewRect.x, viewRect.y, viewRect.width, viewRect.height);
}
#Override
protected void paintButtonPressed(Graphics g, AbstractButton b) {
if (b.isContentAreaFilled()) {
g.setColor(SELECT_COLOR);
g.fillRect(0, 0, b.getWidth(), b.getHeight());
}
}
#Override
protected void installDefaults(AbstractButton b) {
super.installDefaults(b);
b.setFont(b.getFont().deriveFont(11f));
b.setBackground(BACKGROUND_COLOR);
}
public CustomButtonUI() {
super();
}
}
private static JToolBar createToolBar(final Component parent) {
final UIManager.LookAndFeelInfo[] available =
UIManager.getInstalledLookAndFeels();
List<String> names = new ArrayList<String>();
for (UIManager.LookAndFeelInfo info : available) {
names.add(info.getName());
}
final JComboBox combo = new JComboBox(names.toArray());
String current = UIManager.getLookAndFeel().getName();
combo.setSelectedItem(current);
combo.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent ae) {
int index = combo.getSelectedIndex();
try {
UIManager.setLookAndFeel(
available[index].getClassName());
SwingUtilities.updateComponentTreeUI(parent);
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
});
JToolBar bar = new JToolBar("L&F");
bar.setLayout(new FlowLayout(FlowLayout.LEFT));
bar.add(combo);
return bar;
}
}