JList Individually Sized Cells - java

I have a horizontally arranged JList. All of the items in the list are of significantly different sizes, and by default the renderer scales each item to the size of the largest item in the list. I have attempted to implement a custom renderer as follows, but each item in the list remains the same size. Any advice?
The following is the ListCellRenderer:
package ui.wizards;
import java.awt.Component;
import java.awt.Dimension;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import javax.swing.SwingConstants;
public class WizHistoryCellRenderer extends JLabel
implements ListCellRenderer<String>
{
private static final long serialVersionUID = -3650608268225337416L;
JList<? extends String> list;
#Override
public Component getListCellRendererComponent(JList<? extends String> list,
String value, int index, boolean isSelected, boolean cellHasFocus)
{
this.list = list;
int width = this.getFontMetrics(this.getFont())
.stringWidth((String) value);
int height = 20;
setText(value);
if (list != null)
{
if (index == list.getSelectedIndex())
showSelected();
else
showUnselected();
}
setMaximumSize(new Dimension((int) (1.1 * width), height));
setPreferredSize(new Dimension((int) (1.1 * width), height));
setHorizontalAlignment(SwingConstants.CENTER);
setOpaque(true);
return this;
}
private void showSelected()
{
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
}
private void showUnselected()
{
setBackground(list.getBackground());
setForeground(list.getForeground());
}
}

Try calling the JList method setFixedCellHeight, with -1 as the argument. This causes JList to use the preferred height returned by any custom renderer when drawing the list items.

I think you could try to subclass javax.swing.plaf.basic.BasicListUI and override protected void paintCell(Graphics g, int row, Rectangle rowBounds, ListCellRenderer cellRenderer, ListModel dataModel, ListSelectionModel selModel, int leadIndex) and maybe some other methods performing cell size calculation.
Then use JList.setUI(ListUI) to apply your custom UI. The UI is responsible for drawing the JList using ListCellRenders. Please excuse that I do not provide a complete example as it is presumably a lot of work tweaking all aspects of the custom drawing process.

You will need 3 things:
A scale factor for each individual cell. You may for example add a scale factor variable of type double in each entry of the list.
A custom ListCellRenderer.
Make the method BasicListUI#updateLayoutState() public.
Follows self-contained code (read the comments for more info):
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.Icon;
import javax.swing.JButton;
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.plaf.basic.BasicListUI;
public class JListIndipendentCellSizes {
//Configuration constants:
private static final int ICON_SIZE = 20;
private static final double SCALE_STEP_SIZE = 0.125; //Smaller values of this makes zooming slower. Greater values makes zooming faster.
//Simple Icon implementation for demonstration purposes.
public static class TJIcon implements Icon {
private final Color rectColor, ovalColor;
public TJIcon(final Color rectColor, final Color ovalColor) {
this.rectColor = rectColor;
this.ovalColor = ovalColor;
}
#Override
public void paintIcon(final Component c, final Graphics g, final int x, final int y) {
g.setColor(rectColor);
g.fillRect(x, y, getIconWidth(), getIconHeight());
g.setColor(ovalColor);
g.fillOval(x, y, getIconWidth(), getIconHeight());
}
#Override public int getIconWidth() { return ICON_SIZE; }
#Override public int getIconHeight() { return ICON_SIZE; }
}
//A simple list entry. Contains a text, an icon and (on top of them all) the scale factor:
public static class TJListEntry {
private final String text;
private final Icon icon;
private double scaleFactor;
public TJListEntry(final String text,
final Icon icon) {
this.text = text;
this.icon = icon;
scaleFactor = 1;
}
public String getText() {
return text;
}
public Icon getIcon() {
return icon;
}
public double getScaleFactor() {
return scaleFactor;
}
public void zoomIn() {
scaleFactor = scaleFactor + SCALE_STEP_SIZE;
}
public void zoomOut() {
scaleFactor = Math.max(scaleFactor - SCALE_STEP_SIZE, SCALE_STEP_SIZE); //Do not allow underflow.
}
public void resetZoom() {
scaleFactor = 1;
}
}
public static class TJListCellRenderer extends JLabel implements ListCellRenderer<TJListEntry> {
private double currentScaleFactor;
public TJListCellRenderer() {
//Ensure every pixel is painted starting from the top-left corner of the label:
super.setVerticalAlignment(JLabel.TOP);
super.setHorizontalAlignment(JLabel.LEFT);
//We need to do this, because the scaling in paintComponent() is also relative to the top-left corner.
}
#Override
public void paintComponent(final Graphics g) {
//setRenderingHints here? Probably for ANTIALIAS...
((Graphics2D)g).scale(currentScaleFactor, currentScaleFactor); //Let's scale everything that is painted afterwards:
super.paintComponent(g); //Let's paint the (scaled) JLabel!
}
#Override
public Dimension getPreferredSize() {
final Dimension superPrefDim = super.getPreferredSize(); //Handles automatically insets, icon size, text font, etc.
final double w = superPrefDim.width * currentScaleFactor, //And we just scale the preferred size.
h = superPrefDim.height * currentScaleFactor; //And we just scale the preferred size.
return new Dimension((int)w + 1, (int)h + 1); //Add one extra pixel to spare.
}
#Override
public Component getListCellRendererComponent(final JList<? extends TJListEntry> list, final TJListEntry value, final int index, final boolean isSelected, final boolean cellHasFocus) {
currentScaleFactor = value.getScaleFactor(); //Probably the most important step.
setIcon(value.getIcon()); //Could be a loaded ImageIcon here (i.e. image)!
setText(value.getText());
if (isSelected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
} else {
setBackground(list.getBackground());
setForeground(list.getForeground());
}
setOpaque(true);
return this;
}
}
public static class TJListUI extends BasicListUI {
#Override
public void updateLayoutState() {
super.updateLayoutState(); //Just make the following method public.
/*Note: this is not really needed here:
The method could remain protected, but in the case you want this
code to be a bit more reusable, then you shall make it public.*/
}
}
public static void main(final String[] args) {
final TJListEntry[] entries = new TJListEntry[]{new TJListEntry("This is a sample text.", new TJIcon(Color.BLACK, Color.WHITE)),
new TJListEntry("This is a longer sample text.", new TJIcon(Color.GREEN, Color.RED)),
new TJListEntry("Small text", new TJIcon(Color.LIGHT_GRAY, Color.BLUE))};
final TJListUI ui = new TJListUI();
final JList<TJListEntry> list = new JList<>(entries);
list.setUI(ui); //Important step! Without setting our UI, it won't work (at least as I have looked for).
list.setCellRenderer(new TJListCellRenderer()); //Important step! Without setting our custom ListCellRenderer you will have to implement your own ListUI probably.
final JScrollPane scroll = new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
final JButton buttonZoomIn = new JButton("Zoom in selected cells"),
buttonZoomOut = new JButton("Zoom out selected cells"),
buttonResetZoom = new JButton("Reset zoom of selected cells");
buttonZoomIn.addActionListener(e -> {
for (final int i: list.getSelectedIndices())
list.getModel().getElementAt(i).zoomIn();
ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
list.revalidate(); //Update the JScrollPane.
list.repaint(); //Repaint the list.
});
buttonZoomOut.addActionListener(e -> {
for (final int i: list.getSelectedIndices())
list.getModel().getElementAt(i).zoomOut();
ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
list.revalidate(); //Update the JScrollPane.
list.repaint(); //Repaint the list.
});
buttonResetZoom.addActionListener(e -> {
for (final int i: list.getSelectedIndices())
list.getModel().getElementAt(i).resetZoom();
ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
list.revalidate(); //Update the JScrollPane.
list.repaint(); //Repaint the list.
});
final JPanel buttons = new JPanel(); //FlowLayout.
buttons.add(buttonZoomIn);
buttons.add(buttonZoomOut);
buttons.add(buttonResetZoom);
final JPanel panel = new JPanel(new BorderLayout());
panel.add(buttons, BorderLayout.PAGE_START);
panel.add(scroll, BorderLayout.CENTER);
final JFrame frame = new JFrame("Independent JList cell sizes demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(panel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
And a screenshot:
To run it on your own, just select at least one index, and play with the buttons.

Related

JScrollPane minimum width inside JSplitPane

I'm trying out the JSplitPane with a couple of scrollable side-by-side JTables.
However I'm experiment a behavior where the JScrollPane gets shrinked too much, as per gif.
Notice the left component, that besides having a minimum width of 250px, continues shrinking.
The relevant code is
final var objetsTable = new JTable();
final var objectsScrollPane = new JScrollPane(objetsTable);
objectsScrollPane.setMinimumSize(new Dimension(250, 0));
objectsScrollPane.setPreferredSize(new Dimension(400, 300));
final var stepsTable = new JTable();
final var stepsScrollPane = new JScrollPane(stepsTable);
stepsScrollPane.setMinimumSize(new Dimension(150, 0));
stepsScrollPane.setPreferredSize(new Dimension(200, 300));
final var splitPane = new JSplitPane();
splitPane.setLeftComponent(objectsScrollPane);
splitPane.setRightComponent(stepsScrollPane);
splitPane.setResizeWeight(1.0);
How can I avoid the JScrollPanes getting shrinked too much in this case?
The getMinimumSize called on a JSplitPane returns a size which is actually taking into account the minimum size of its left and right Components, plus the divider size. So one way to maybe solve your problem would be to make your JSplitPane implement Scrollable (in order to make it respect the minimum size of itself) and add it to a JScrollPane. This way you can ensure that the minimum size is respected and when the user continues shrinking the Scrollable JSplitPane past its minimum size, then the scroll bars will show up.
Here is some working code:
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Rectangle;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
public class Main {
private static class MyScrollableSplitPane extends JSplitPane implements Scrollable {
private int maxUnitIncrement = 10;
public void setMaxUnitIncrement(final int pixels) {
maxUnitIncrement = pixels;
}
public int getMaxUnitIncrement() {
return maxUnitIncrement;
}
/**
* This is being constantly checked by the scroll pane instead of the
* getPreferredScrollableViewportSize...
*/
#Override
public Dimension getPreferredSize() {
final Dimension minSz = getMinimumSize(),
curSz = getSize();
curSz.width = Math.max(curSz.width, minSz.width);
curSz.height = Math.max(curSz.height, minSz.height);
return curSz;
}
/**
* This is only checked once (at the beginning).
*/
#Override
public Dimension getPreferredScrollableViewportSize() {
return super.getPreferredSize();
}
/**
* Source: https://docs.oracle.com/javase/tutorial/uiswing/components/scrollpane.html .
*/
#Override
public int getScrollableUnitIncrement(Rectangle visibleRect,
int orientation,
int direction) {
//Get the current position.
int currentPosition;
if (orientation == SwingConstants.HORIZONTAL) {
currentPosition = visibleRect.x;
} else {
currentPosition = visibleRect.y;
}
//Return the number of pixels between currentPosition
//and the nearest tick mark in the indicated direction.
if (direction < 0) {
int newPosition = currentPosition -
(currentPosition / maxUnitIncrement)
* maxUnitIncrement;
return (newPosition == 0) ? maxUnitIncrement : newPosition;
} else {
return ((currentPosition / maxUnitIncrement) + 1)
* maxUnitIncrement
- currentPosition;
}
}
/**
* Source: https://docs.oracle.com/javase/tutorial/uiswing/components/scrollpane.html .
*/
#Override
public int getScrollableBlockIncrement(Rectangle visibleRect,
int orientation,
int direction) {
if (orientation == SwingConstants.HORIZONTAL) {
return visibleRect.width - maxUnitIncrement;
} else {
return visibleRect.height - maxUnitIncrement;
}
}
#Override
public boolean getScrollableTracksViewportWidth() {
final Container parent = getParent();
return (parent instanceof JViewport) && (getMinimumSize().width < ((JViewport) parent).getWidth());
}
#Override
public boolean getScrollableTracksViewportHeight() {
final Container parent = getParent();
return (parent instanceof JViewport) && (getMinimumSize().height < ((JViewport) parent).getHeight());
}
}
private static void createAndShowGUI() {
/*Since I don't add any Components to the 'left' and 'right' panels, I am going to set the
preferred size of them. This is only for demonstrating the concept. Setting the minimum size
though is somewhat required by the JSplitPane itself.*/
final JPanel left = new JPanel();
left.setMinimumSize(new Dimension(150, 100));
left.setPreferredSize(new Dimension(200, 200));
final JPanel right = new JPanel();
right.setMinimumSize(new Dimension(300, 100));
right.setPreferredSize(new Dimension(400, 200));
final JSplitPane split = new MyScrollableSplitPane();
split.setBorder(BorderFactory.createLineBorder(Color.CYAN.darker(), 3));
split.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
split.setLeftComponent(left);
split.setRightComponent(right);
final JFrame frame = new JFrame("MyScrollableSplitPane demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(new JScrollPane(split));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(Main::createAndShowGUI);
}
}

How do I bring JFrame images to the front of my window in Java?

So I'm currently having the problem that a large image will cover up smaller images in my program when i try to graphically display them in Java. I would like to know how to bring certain images to the front of the window so the large "background" image will stay in the background. Also, I do not believe it's a possibility in my program to simply implement the pictures in reverse order.
Here's the code I used:
my image manager class with the method I use to implement the images into the window,
import java.awt.*;
import javax.swing.*;
public class ImageManager extends JFrame {
private ImageIcon image1;
private JLabel label1;
private ImageIcon image2;
private JLabel label2;
private ImageIcon image3;
private JLabel label3;
public ImageManager() {
}
public void addBackground() {
image3 = new ImageIcon(getClass().getResource("background.png"));
label3 = new JLabel(image3);
add(label3);
}
public void addSeaweed() {
image1 = new ImageIcon(getClass().getResource("seaweed.png"));
label1 = new JLabel(image1);
add(label1);
}
public void addUnderwatervolcano() {
image2 = new ImageIcon(getClass().getResource("underwatervolcano.png"));
label2 = new JLabel(image2);
add(label2);
}
}
and here's where I use the methods from ImageManager:
a method to display a picture of seaweed using the grow() method,
public Seaweed() {
setLayout(new FlowLayout());
World.gui.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
World.gui.setVisible(true);
World.gui.pack();
World.gui.setTitle("seaweed and underwatervolcano");
carbon = 0;
}
public void grow() {
if(World.getOceanCarbon() >= 10) {
addCarbon(10);
World.addOceanCarbon(-10);
World.gui.addSeaweed();
World.gui.pack();
}
}
and heres the method in a different class that uses the grow() method from the Seaweed class and the gui.addBackground() from the ImageManager class,
public static void runWorld() {
gui.addBackground();
UnderwaterVolcano volcano = new UnderwaterVolcano();
Seaweed seaweed = new Seaweed();
volcano.erupt();
seaweed.grow();
gui.setMinimumSize(new Dimension(905, 560));
}
}
i would like to know how i make it so gui.addBackground() does not cover up the picture of seaweed from gui.addSeaweed() (which was invoked in the seaweed.grow() method) while still invoking gui.addBackground() before invoking gui.addSeaweed(). Is there anyway I can manipulate at the method call the order in which images display in a window? I don't have a very good understanding of JFrame so please be very explanatory with your answers, all help appreciated.
Well your current logic adds all the images to the frame. Swing actually paints the last component added first. That is components are painted based on highest ZOrder being painted first. The default ZOrder of the component is simply the component count at the time the component is added to the panel. So yes based on your current logic the background will paint over top of the other images.
A couple of simple solutions:
1) Manage the ZOrder of your components.
After you add the component to frame you can reset the ZOrder so the component is painted last. So the basic code is
add(aComponent);
setComponentZOrder(aComponent, 0);
2) Add the child images to the background image instead of add all images to the frame. So you have a structure like:
- frame
- background image
- seaweed
- volcano
So the basic logic would be something like:
frame.add( background );
background.add( seaweed );
background.add( volcano );
Since in looks like the seaweed/volcano images are at random places on the background you would still need to manage the size/location of each of these images.
Note when adding child components to the background the child components must be fully contained within the background image or the child image will be truncated.
This is the approach I would use since it better describes the structure of your application. That is your frame contains a background and the background contains other child components. Nesting of components is common to get a desired layout of a frame.
You have to use a LayeredPane. Here is a working example of my own. You only have to replace the used images by some of yours.
The Main class:
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.*;
public class Main{
static JFrameWin jFrameWindow;
public static void main(String[] args) {
SwingUtilities.invokeLater(runJFrameLater);
}
public static class JFrameWin extends JFrame{
public JFrameWin(){
// init
initJFrame(this);
JPanelSettings jPanelSettings = new JPanelSettings(this.getWidth(), this.getHeight(), this.getX(), this.getY());
JLayeredPane enclosingJLayeredPane = getEnclosingJLayeredPane(jPanelSettings);
jPanelSettings = new JPanelSettings(this.getWidth(), this.getHeight(), this.getX(), this.getY(), new File("billard_1.jpg"));
JPanel backgroundJPanel = getJPanel(jPanelSettings);
jPanelSettings = new JPanelSettings(this.getWidth()-100, this.getHeight()-100, this.getX()+5, this.getY()+20, new File("billard2.jpg"));
JPanel firstLayerJPanel = getJPanel(jPanelSettings);
jPanelSettings = new JPanelSettings(this.getWidth() - 200, this.getHeight() - 200, this.getX() + 60, this.getY() + 60, new File("painter.jpg"));
JPanel secondLayerJPanel = getJPanel(jPanelSettings);
// assemble
enclosingJLayeredPane.add(backgroundJPanel);
enclosingJLayeredPane.add(firstLayerJPanel);
enclosingJLayeredPane.add(secondLayerJPanel);
// adjust layers
enclosingJLayeredPane.setLayer(backgroundJPanel, 0);
enclosingJLayeredPane.setLayer(firstLayerJPanel, 1);
enclosingJLayeredPane.setLayer(secondLayerJPanel, 2);
// add object to JFrame
this.add(enclosingJLayeredPane, BorderLayout.CENTER);
}
}
static Runnable runJFrameLater = new Runnable() {
#Override
public void run() {
jFrameWindow = new JFrameWin();
jFrameWindow.setVisible(true);
}
};
private static void initJFrame(JFrame jFrame) {
jFrame.setTitle("Boxing Test");
jFrame.setSize(600, 600);
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
private static JLayeredPane getEnclosingJLayeredPane(JPanelSettings jPanelSettings){
JLayeredPane jLayeredPane = new JLayeredPane();
jLayeredPane.setBounds(jPanelSettings.getXPosition(), jPanelSettings.getYPosition(), jPanelSettings.getWidth(), jPanelSettings.getHeight());
return jLayeredPane;
}
private static JPanel getJPanel(JPanelSettings jPanelSettings){
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(jPanelSettings.getImagePath());
} catch (IOException ex) {
System.out.println("Error" + ex.toString());
}
// fit image to frame size
Image scaledBufferedImage = bufferedImage.getScaledInstance(jPanelSettings.getWidth(), jPanelSettings.getHeight(), Image.SCALE_DEFAULT);
JLabel jLabel = new JLabel(new ImageIcon(scaledBufferedImage));
JPanel jPanel = new JPanel();
jPanel.setBounds(jPanelSettings.getXPosition(), jPanelSettings.getYPosition(), jPanelSettings.getWidth(), jPanelSettings.getHeight());
jPanel.add(jLabel);
return jPanel;
}
}
A helper-class
import java.io.File;
public class JPanelSettings {
private int height;
private int width;
private int xPosition;
private int yPosition;
private File imagePath;
// Basic constructor
public JPanelSettings(){ }
// size and positioning constructor
public JPanelSettings(int width,int height, int xPosition, int yPosition ){
setWidth(width);
setHeight(height);
setXPosition(xPosition);
setYPosition(yPosition);
}
// Full constructor
public JPanelSettings(int width,int height, int xPosition, int yPosition, File imagePath ){
setWidth(width);
setHeight(height);
setXPosition(xPosition);
setYPosition(yPosition);
setImagePath(imagePath);
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getXPosition() {
return xPosition;
}
public void setXPosition(int xPosition) {
this.xPosition = xPosition;
}
public int getYPosition() {
return yPosition;
}
public void setYPosition(int yPosition) {
this.yPosition = yPosition;
}
public File getImagePath() {
return imagePath;
}
public void setImagePath(File imagePath) {
this.imagePath = imagePath;
}
}

How do I fire a custom event when a new Rectangle is drawn?

I created a graphical component that allows you to view an image and allows you to make a selection of a part of the image: the selection of a portion of the image is accomplished by drawing a rectangle on this image (using drag-and-drop).
To this purpose, I used this example, which created a subclass of JLabel in order to draw the image and in order to deal with the drawing of the rectangle. Then I put an instance of this subclass within a JPanel, in order to have the image always positioned at the center of the panel.
FigurePanel.java
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;
public class FigurePanel extends JPanel
{
private SelectionLabel imageLabel = null;
public FigurePanel()
{
this.setLayout(new GridBagLayout());
imageLabel = new SelectionLabel();
this.add(imageLabel, null);
}
public void setImage(Image image)
{
imageLabel.setImage(image);
}
private class SelectionLabel extends JLabel
{
private Rectangle currentRect = null;
private Rectangle rectToDraw = null;
private final Rectangle previousRectDrawn = new Rectangle();
public SelectionLabel()
{
super();
setOpaque(true);
SelectionListener listener = new SelectionListener();
addMouseListener(listener);
addMouseMotionListener(listener);
}
public void setImage(Image image)
{
currentRect = null;
rectToDraw = null;
previousRectDrawn.setBounds(0, 0, 0, 0);
setIcon(new ImageIcon(image));
}
private class SelectionListener extends MouseInputAdapter
{
#Override
public void mousePressed(MouseEvent e)
{
int x = e.getX();
int y = e.getY();
currentRect = new Rectangle(x, y, 0, 0);
updateDrawableRect(getWidth(), getHeight());
repaint();
}
#Override
public void mouseDragged(MouseEvent e)
{
updateSize(e);
}
#Override
public void mouseReleased(MouseEvent e)
{
updateSize(e);
}
/*
* Update the size of the current rectangle
* and call repaint. Because currentRect
* always has the same origin, translate it
* if the width or height is negative.
*
* For efficiency (though
* that isn't an issue for this program),
* specify the painting region using arguments
* to the repaint() call.
*
*/
void updateSize(MouseEvent e)
{
int x = e.getX();
int y = e.getY();
currentRect.setSize(x - currentRect.x,
y - currentRect.y);
updateDrawableRect(getWidth(), getHeight());
Rectangle totalRepaint = rectToDraw.union(previousRectDrawn);
repaint(totalRepaint.x, totalRepaint.y,
totalRepaint.width, totalRepaint.height);
}
}
#Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g); //paints the background and image
//If currentRect exists, paint a box on top.
if (currentRect != null) {
//Draw a rectangle on top of the image.
g.setXORMode(Color.white); //Color of line varies
//depending on image colors
g.drawRect(rectToDraw.x, rectToDraw.y,
rectToDraw.width - 1, rectToDraw.height - 1);
System.out.println("rectToDraw: " + rectToDraw);
}
}
private void updateDrawableRect(int compWidth, int compHeight)
{
int x = currentRect.x;
int y = currentRect.y;
int width = currentRect.width;
int height = currentRect.height;
//Make the width and height positive, if necessary.
if (width < 0) {
width = 0 - width;
x = x - width + 1;
if (x < 0) {
width += x;
x = 0;
}
}
if (height < 0) {
height = 0 - height;
y = y - height + 1;
if (y < 0) {
height += y;
y = 0;
}
}
//The rectangle shouldn't extend past the drawing area.
if ((x + width) > compWidth) {
width = compWidth - x;
}
if ((y + height) > compHeight) {
height = compHeight - y;
}
//Update rectToDraw after saving old value.
if (rectToDraw != null) {
previousRectDrawn.setBounds(
rectToDraw.x, rectToDraw.y,
rectToDraw.width, rectToDraw.height);
rectToDraw.setBounds(x, y, width, height);
} else {
rectToDraw = new Rectangle(x, y, width, height);
}
}
}
}
FigurePanelTest.java
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
public class FigurePanelTest extends JFrame
{
public FigurePanelTest()
{
FigurePanel imagePanel = new FigurePanel();
JScrollPane imageScrollPane = new JScrollPane();
imageScrollPane.setPreferredSize(new Dimension(420, 250));
imageScrollPane.setViewportView(imagePanel);
JButton imageButton = new JButton("Load Image");
imageButton.addActionListener(
new ActionListener()
{
#Override
public void actionPerformed(ActionEvent evt)
{
JFileChooser fc = new JFileChooser();
int returnValue = fc.showOpenDialog(null);
if (returnValue == JFileChooser.APPROVE_OPTION) {
File selectedFile = fc.getSelectedFile();
System.out.println(selectedFile.getName());
try
{
Image image = ImageIO.read(selectedFile.getAbsoluteFile());
imagePanel.setImage(image);
imageScrollPane.getViewport().setViewPosition(new Point(0, 0));
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
}
);
Container container = getContentPane();
container.setLayout(new BorderLayout());
container.add(imageScrollPane, BorderLayout.CENTER);
container.add(imageButton, BorderLayout.NORTH);
setSize(600, 400);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
/**
* #param args the command line arguments
*/
public static void main(String args[]) {
/* Create and display the form */
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new FigurePanelTest().setVisible(true);
}
});
}
}
The private class SelectionLabel is the class SelectionArea from this example.
When a new rectangle is drawn, a message is printed on the console. Now I would replace the printing of the message with the firing of a custom event, so that the position and size of the rectangle are accessible to the application business logic.
I read how to create a custom event in Java. Moreover, this article identifies two super types for creating events: EventObject and AWTEvent. This articles states:
Normally you extend AWTEvent for events generated by a graphical
component and EventObject any other time.
Since the event concerning the selection of a part of the image is generated by a graphical component (that is the FigurePanel panel), I could implement the ImageSelectionEvent class by extending AWTEvent, as the following code snippet.
public class ImageSelectionEvent extends AWTEvent
{
public ImageSelectionEvent(Object source, int id) {
super(source, id);
}
}
The documentation identifies the id as the event type. So, what value should be assigned to this parameter?
Moreover, why does the constructor of EventObject class be devoid of the id parameter?
When creating an event class, you must guarantee that the event is
immutable. The event generator will share the same event instance
among the listeners; so ensure any one listener cannot change the
event's state.
What about this?
I don't know what is needed to create a custom event.
However, since you are extending JLabel maybe you can just create a PropertyChangeEvent.
To generated the event you would just use something like:
firePropertyChange("selectionRectangle", oldRectangle, newRectangle);
Then you can use a PropertyChangeListener to listen for "selectionRectangle" changes.
The Javadoc for AWTEvent says:
Subclasses of this root AWTEvent class defined outside of the java.awt.event package should define event ID values greater than the value defined by RESERVED_ID_MAX.
This value is 1999. You can set it to whatever you want that's higher than that. This value is specified by all the different types of Swing events, and Swing uses values that are less than that. For example, the MouseEvent event types use values from 500-507.
The main thing is to use a consistent value for your events.
Finally, I would consider subclassing ComponentEvent over AWTEvent as the source of your event is a Component, not an Object.

Mirroring JLabel or JTextArea

I need to get a mirror of JLabel or JTextArea.
http://www.subirimagenes.net/i/150305101716451074.jpg
If I use a JTextArea, I'll need the letters are complety mirrored.
If I use a JLabel, I'll need format and the mirrored letters.
The example was created on Photoshop.
My idea is using graphics(), but I don't have idea how to do it.
Here's the bad news: It's not as straight-forward as we might wish, there's a limitation. In Swing, graphics transformations are applied only on the paint operation, not the general layout and event process. Therefore, in Swing the mirrored component is basically "unusable", it cannot be used for anything else than displaying the mirror image of the primary component. Coordinates of mouse clicks etc. will be wrong.
Therefore, this is all tricky stuff, a bit hackish.
There are multiple ways how you can do that.
One possibility is to use two views on one model and tell the Graphics of one of the views to flip horizontally.
Here's an example how to do so which demonstrates a flipped JEditorPane:
import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
public class MirrorText {
public static void main(final String... args) {
SwingUtilities.invokeLater(MirrorText::setupUI);
}
public static void setupUI() {
final JEditorPane editor = new JEditorPane("text/html", "<h1>Mirrored</h1><p>This is mirrored text.</p>");
final JEditorPane mirroredEditor = new JEditorPane("text/html", "") {
protected Graphics getComponentGraphics(final Graphics g) {
return horizontalFlip(super.getComponentGraphics(g), getWidth());
}
};
mirroredEditor.setDocument(editor.getDocument());
final JFrame frame = new JFrame("mirrored label");
final JPanel mirrorPanel = new JPanel(new GridLayout(1, 2));
mirrorPanel.add(new JScrollPane(editor));
mirrorPanel.add(new JScrollPane(mirroredEditor));
frame.add(mirrorPanel);
frame.pack();
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
public static Graphics horizontalFlip(final Graphics g, final int width) {
final Graphics2D g2d = (Graphics2D) g;
final AffineTransform tx = g2d.getTransform();
tx.scale(-1.0, 1.0);
tx.translate(-width, 0);
g2d.setTransform(tx);
return g2d;
}
}
The advantage of this solution is that the mirror entirely the original component's MVC and observers because it is the same type of component (View/Controller) on the very same model.
The disadvantage of this solution is that you have create the mirror component in a way that is very specific to the component that is mirrored.
Another possibility is to create a decorator JComponent Mirror which can mirror an arbitrary other JComponent. This is a bit tricky, as in Java, decorators cannot override methods of the decorated object, and the Mirror needs to be updated (repainted) as well whenever the original component is updated (repainted).
Here's an incomplete example using a Mirror which hooks into the corresponding events. Incomplete because it only hooks into DocumentEvent but should also hook onto other events as well, like CaretEvent. It would be nice if Swing would have something like a PaintEvent. As far as I am aware of, it hasn't. (Well, in fact it has, but there's no corresponding PaintListener and addPaintListener().)
Also incomplete because the Mirror doesn't observe the original component's attributes like size. It only works because the GridLayout on the MirrorPanel keeps the mirror size in sync with the original component.
import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.event.*;
public class MirrorText {
public static void main(final String... args) {
SwingUtilities.invokeLater(MirrorText::setupUI);
}
public static void setupUI() {
final JEditorPane editor = new JEditorPane("text/html", "<h1>Mirrored</h1><p>This is mirrored text.</p>");
final MirrorPanel mirrorPanel = new MirrorPanel(new JScrollPane(editor));
editor.getDocument().addDocumentListener(new DocumentListener() {
public void changedUpdate(final DocumentEvent e) { mirrorPanel.updateMirror(); }
public void insertUpdate(final DocumentEvent e) { mirrorPanel.updateMirror(); }
public void removeUpdate(final DocumentEvent e) { mirrorPanel.updateMirror(); }
});
final JFrame frame = new JFrame("mirrored label");
frame.add(mirrorPanel);
frame.pack();
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.setVisible(true);
}
}
class MirrorPanel extends JPanel {
public MirrorPanel(final JComponent c) {
super(new GridLayout(1, 2));
add(c);
add(new Mirror(c));
}
public void updateMirror() {
repaint();
}
}
class Mirror extends JComponent {
private final JComponent mirroredComponent;
public Mirror(final JComponent mirroredComponent) {
this.mirroredComponent = mirroredComponent;
}
public static Graphics horizontalFlip(final Graphics g, final int width) {
final Graphics2D g2d = (Graphics2D) g;
final AffineTransform tx = g2d.getTransform();
tx.scale(-1.0, 1.0);
tx.translate(-width, 0);
g2d.setTransform(tx);
return g2d;
}
public void paint(final Graphics g) {
mirroredComponent.paint(horizontalFlip(g, mirroredComponent.getWidth()));
}
}
There probably are more possibilities as well. For example, one could override the mirrored component's paint() method to update the mirror component as well. That would get rid of getting notified, but it would lead to unnecessary paint() calls in case painting isn't done due to content change but due to buffer destruction (i.e. other window moved away).
Here's one way to create a mirror image.
Basically, you print the contents of the JTextArea on a BufferedImage. Then you reverse the pixels of the BufferedImage on the X axis.
package com.ggl.testing;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
public class MirrorImage implements Runnable {
private JFrame frame;
public static void main(String[] args) {
SwingUtilities.invokeLater(new MirrorImage());
}
#Override
public void run() {
frame = new JFrame("Mirror Image Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new FlowLayout());
JPanel textPanel = new JPanel();
JTextArea textArea = new JTextArea(15, 30);
textPanel.add(textArea);
mainPanel.add(textPanel);
MirrorPanel mirrorPanel = new MirrorPanel();
mirrorPanel.setPreferredSize(textPanel.getPreferredSize());
mainPanel.add(mirrorPanel);
TextListener listener = new TextListener(textArea, mirrorPanel);
textArea.getDocument().addDocumentListener(listener);
frame.add(mainPanel);
frame.pack();
frame.setLocationByPlatform(true);
listener.createImage(textArea);
frame.setVisible(true);
}
private class MirrorPanel extends JPanel {
private static final long serialVersionUID = 2496058019297247364L;
private Image image;
public void setImage(Image image) {
this.image = image;
repaint();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(image, (getWidth() - image.getWidth(this)) / 2,
(getHeight() - image.getHeight(this)) / 2, this);
}
}
private class TextListener implements DocumentListener {
private JTextArea textArea;
private MirrorPanel mirrorPanel;
public TextListener(JTextArea textArea, MirrorPanel mirrorPanel) {
this.textArea = textArea;
this.mirrorPanel = mirrorPanel;
}
#Override
public void insertUpdate(DocumentEvent event) {
createImage(textArea);
}
#Override
public void removeUpdate(DocumentEvent event) {
createImage(textArea);
}
#Override
public void changedUpdate(DocumentEvent event) {
createImage(textArea);
}
public void createImage(JTextArea textArea) {
BufferedImage img = new BufferedImage(textArea.getWidth(),
textArea.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = img.createGraphics();
textArea.printAll(g2d);
g2d.dispose();
createMirrorImage(img);
mirrorPanel.setImage(img);
}
private void createMirrorImage(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
int[][] pixels = new int[width][height];
for (int i = width - 1; i >= 0; i--) {
int j = width - i - 1;
for (int k = 0; k < height; k++) {
pixels[j][k] = img.getRGB(i, k);
}
}
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
img.setRGB(i, j, pixels[i][j]);
}
}
}
}
}

How do I add a separator to a JComboBox in Java?

I have a JComboBox and would like to have a separator in the list of elements. How do I do this in Java?
A sample scenario where this would come in handy is when making a combobox for font-family-selection; similar to the font-family-selection-control in Word and Excel. In this case I would like to show the most-used-fonts at the top, then a separator and finally all font-families below the separator in alphabetical order.
Can anyone help me with how to do this or is this not possible in Java?
There is a pretty short tutorial with an example that shows how to use a custom ListCellRenderer on java2s
http://www.java2s.com/Code/Java/Swing-Components/BlockComboBoxExample.htm
Basically it involves inserting a known placeholder in your list model and when you detect the placeholder in the ListCellRenderer you return an instance of 'new JSeparator(JSeparator.HORIZONTAL)'
By the time I wrote and tested the code below, you probably got lot of better answers...
I don't mind as I enjoyed the experiment/learning (still a bit green on the Swing front).
[EDIT] Three years later, I am a bit less green, and I took in account the valid remarks of bobndrew. I have no problem with the key navigation that just works (perhaps it was a JVM version issue?). I improved the renderer to show highlight, though. And I use a better demo code. The accepted answer is probably better (more standard), mine is probably more flexible if you want a custom separator...
The base idea is to use a renderer for the items of the combo box. For most items, it is a simple JLabel with the text of the item. For the last recent/most used item, I decorate the JLabel with a custom border drawing a line on its bottom.
import java.awt.*;
import javax.swing.*;
#SuppressWarnings("serial")
public class TwoPartsComboBox extends JComboBox
{
private int m_lastFirstPartIndex;
public TwoPartsComboBox(String[] itemsFirstPart, String[] itemsSecondPart)
{
super(itemsFirstPart);
m_lastFirstPartIndex = itemsFirstPart.length - 1;
for (int i = 0; i < itemsSecondPart.length; i++)
{
insertItemAt(itemsSecondPart[i], i);
}
setRenderer(new JLRenderer());
}
protected class JLRenderer extends JLabel implements ListCellRenderer
{
private JLabel m_lastFirstPart;
public JLRenderer()
{
m_lastFirstPart = new JLabel();
m_lastFirstPart.setBorder(new BottomLineBorder());
// m_lastFirstPart.setBorder(new BottomLineBorder(10, Color.BLUE));
}
#Override
public Component getListCellRendererComponent(
JList list,
Object value,
int index,
boolean isSelected,
boolean cellHasFocus)
{
if (value == null)
{
value = "Select an option";
}
JLabel label = this;
if (index == m_lastFirstPartIndex)
{
label = m_lastFirstPart;
}
label.setText(value.toString());
label.setBackground(isSelected ? list.getSelectionBackground() : list.getBackground());
label.setForeground(isSelected ? list.getSelectionForeground() : list.getForeground());
label.setOpaque(true);
return label;
}
}
}
Separator class, can be thick, with custom color, etc.
import java.awt.*;
import javax.swing.border.AbstractBorder;
/**
* Draws a line at the bottom only.
* Useful for making a separator in combo box, for example.
*/
#SuppressWarnings("serial")
class BottomLineBorder extends AbstractBorder
{
private int m_thickness;
private Color m_color;
BottomLineBorder()
{
this(1, Color.BLACK);
}
BottomLineBorder(Color color)
{
this(1, color);
}
BottomLineBorder(int thickness, Color color)
{
m_thickness = thickness;
m_color = color;
}
#Override
public void paintBorder(Component c, Graphics g,
int x, int y, int width, int height)
{
Graphics copy = g.create();
if (copy != null)
{
try
{
copy.translate(x, y);
copy.setColor(m_color);
copy.fillRect(0, height - m_thickness, width - 1, height - 1);
}
finally
{
copy.dispose();
}
}
}
#Override
public boolean isBorderOpaque()
{
return true;
}
#Override
public Insets getBorderInsets(Component c)
{
return new Insets(0, 0, m_thickness, 0);
}
#Override
public Insets getBorderInsets(Component c, Insets i)
{
i.left = i.top = i.right = 0;
i.bottom = m_thickness;
return i;
}
}
Test class:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
#SuppressWarnings("serial")
public class TwoPartsComboBoxDemo extends JFrame
{
private TwoPartsComboBox m_combo;
public TwoPartsComboBoxDemo()
{
Container cont = getContentPane();
cont.setLayout(new FlowLayout());
cont.add(new JLabel("Data: ")) ;
String[] itemsRecent = new String[] { "ichi", "ni", "san" };
String[] itemsOther = new String[] { "one", "two", "three" };
m_combo = new TwoPartsComboBox(itemsRecent, itemsOther);
m_combo.setSelectedIndex(-1);
cont.add(m_combo);
m_combo.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
String si = (String) m_combo.getSelectedItem();
System.out.println(si == null ? "No item selected" : si.toString());
}
});
// Reference, to check we have similar behavior to standard combo
JComboBox combo = new JComboBox(itemsRecent);
cont.add(combo);
}
/**
* Start the demo.
*
* #param args the command line arguments
*/
public static void main(String[] args)
{
// turn bold fonts off in metal
UIManager.put("swing.boldMetal", Boolean.FALSE);
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
JFrame demoFrame = new TwoPartsComboBoxDemo();
demoFrame.setTitle("Test GUI");
demoFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
demoFrame.setSize(400, 100);
demoFrame.setVisible(true);
}
});
}
}
You can use a custom ListCellRenderer which would draw the separator items differently. See docs and a small tutorial.
Try adding this renderer. Just supply a list of index values that you want the separator to be above.
private class SeperatorComboRenderer extends DefaultListCellRenderer
{
private final float SEPARATOR_THICKNESS = 1.0f;
private final float SPACE_TOP = 2.0f;
private final float SPACE_BOTTOM = 2.0f;
private final Color SEPARATOR_COLOR = Color.DARK_GRAY;
private final List<Integer> marks;
private boolean mark;
private boolean top;
public SeperatorComboRenderer(List<Integer> marks)
{
this.marks = marks;
}
#Override
public Component getListCellRendererComponent(JList list, Object object, int index, boolean isSelected, boolean hasFocus)
{
super.getListCellRendererComponent(list, object, index, isSelected, hasFocus);
top = false;
mark = false;
marks.forEach((idx) ->
{
if(index - 1 == idx)
top = true;
if(index == idx)
mark = true;
});
return this;
}
#Override
protected void paintComponent(Graphics g)
{
if(mark)
g.translate(0, (int)(SEPARATOR_THICKNESS + SPACE_BOTTOM));
Graphics2D g2 = (Graphics2D)g;
super.paintComponent(g);
if(mark)
{
g2.setColor(SEPARATOR_COLOR);
g2.setStroke(new BasicStroke(SEPARATOR_THICKNESS));
g2.drawLine(0, 0, getWidth(), 0);
}
}
#Override
public Dimension getPreferredSize()
{
Dimension pf = super.getPreferredSize();
double height = pf.getHeight();
if(top) height += SPACE_TOP;
else if(mark) height += SEPARATOR_THICKNESS + SPACE_BOTTOM;
return new Dimension((int)pf.getWidth(), (int)height);
}
}

Categories

Resources