JLayeredPane, background image + “icon” layer

2019-08-13 02:16发布

问题:

I need 2 separate JPanels (or any lightweight components) on top of each-other and ultimately embedded within a JPanel, either directly or through something like a JLayeredPane. Thus, no heavy-weight components or glass pane. The lower JPanel (named BackgroundPanel) paints a background image or plays a video while maintaining aspect ratio and using an alpha. The upper panel (called CompassPanel) has icons on it and allows the user to add icons, delete them, and move them around (like a diagramming library, this functionality is not directly relevant to this post though). I cannot add many external dependencies due to bandwidth constraints with my JNLP app and deployment environment. However, if someone knows of a lightweight diagramming library that can handle alpha & aspect-ratio maintained background images and videos, I'm game. Otherwise, I cannot for the life of me figure out why this space is being allocated after a resize:

I have read the JAVA tutorial on going without a layout manager (not something I ever wanted to do, where are you GBL!?), but for these scaling requirements and having icons stay over the same portion of the image during resize, etc. I can't think of another way to do this.

Here is the code, I am using Java 1.7. Also, this is my first stackoverflow, don't be gentle ;-)

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.border.BevelBorder;

public class Panel extends JPanel {

    private static final Logger logger = Logger.getLogger(Panel.class.getName());

    public Panel() throws IOException {

        final BufferedImage backgroundImage = ImageIO.read(new URL(
                "http://www.windpoweringamerica.gov/images/windmaps/us_windmap_80meters_820w.jpg"));
        final Dimension backgroundImageSize = new Dimension(backgroundImage.getWidth(), backgroundImage.getHeight());
        logger.log(Level.INFO, "Image dimensions: {0}", backgroundImageSize);
        setToolTipText("This is the panel");

        final JLayeredPane layeredPane = new JLayeredPane();
        layeredPane.setBorder(BorderFactory.createLineBorder(Color.RED, 10));
        layeredPane.setToolTipText("This is the layered pane!");
        layeredPane.getInsets().set(0, 0, 0, 0);

        final BackgroundPanel backgroundImagePanel = new BackgroundPanel(backgroundImage);
        final CompassPanel compassPanel = new CompassPanel();
        backgroundImagePanel.setToolTipText("You'll probably never see me, I'm in the background, forever beneath the compass panel");
        compassPanel.setToolTipText("I'm the compass panel");

        // Per http://docs.oracle.com/javase/tutorial/uiswing/layout/none.html, for every container w/o a layout manager, I must:
        // 1) Set the container's layout manager to null by calling setLayout(null). -- I do this here
        // 2) Call the Component class's setbounds method for each of the container's children. --- I do this when resizing
        // 3) Call the Component class's repaint method. --- I do this when resizing

        setLayout(null);
        add(layeredPane);
        layeredPane.add(backgroundImagePanel, JLayeredPane.DEFAULT_LAYER);
        layeredPane.add(compassPanel, JLayeredPane.PALETTE_LAYER);

        // Whenever this panel gets resized, make sure the layered pane gets resized to preserve the aspect ratio of the background image
        addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent evt) {
                Dimension availableSize = calculateAvailableSize(Panel.this);
                Rectangle contentBounds = calculateBoundsToFitImage(availableSize, backgroundImageSize);
                // Ok, this is a big deal. Now I know how big everything has to be, lets force it all to be the right size & repaint.
                layeredPane.setBounds(contentBounds);
                backgroundImagePanel.setBounds(contentBounds);
                compassPanel.setBounds(contentBounds);

                Panel.this.repaint();
                logger.info(String.format("Panel size: %s. Available size: %s. Content Bounds: %s", getSize(), availableSize, contentBounds));
            }
        });
    }

    /**
     * Paints the constant fitted aspect-ratio background image with an alpha of 0.5
     */
    private static class BackgroundPanel extends JPanel {

        private static final Logger logger = Logger.getLogger(BackgroundPanel.class.getName());
        private final AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f);
        private final BufferedImage backgroundImage;

        BackgroundPanel(BufferedImage backgroundImage) {
            setLayout(null);
            this.backgroundImage = backgroundImage;
        }
        private Dimension lastPaintedDimensions = null;

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            final Dimension size = getSize();
            if (lastPaintedDimensions == null || !size.equals(lastPaintedDimensions)) {
                logger.log(Level.INFO, String.format("Painting background on %d x %d", size.width, size.height));
            }
            final Image paintMe = backgroundImage.getScaledInstance(size.width, size.height, Image.SCALE_SMOOTH);
            final Graphics2D g2 = (Graphics2D) g.create();
            g2.drawImage(paintMe, 0, 0, this);
            g2.setColor(Color.BLUE);
            g2.dispose();
            lastPaintedDimensions = size;
        }
    };

    private static class CompassPanel extends JPanel {

        final List<Compass> compassLabels = new ArrayList<>();

        CompassPanel() {
            setLayout(null);
            setOpaque(false);
            setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
        }
    }

    private static class Compass extends JLabel {

        private static final BufferedImage compassImage;

        static {
            try {
                compassImage = ImageIO.read(new URL("http://cdn1.iconfinder.com/data/icons/gur-project-1/32/1_7.png"));
            } catch (IOException ex) {
                throw new RuntimeException("Failed to read compass image", ex);
            }
        }
        final float xPercent, yPercent;

        public Compass(float xPercent, float yPercent) {
            this.xPercent = xPercent;
            this.yPercent = yPercent;
            setIcon(new ImageIcon(compassImage));
            setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED));
            setOpaque(true);
            setCursor(Cursor.getDefaultCursor());
        }
    }

    public static void main(String[] args) throws IOException {
        final JFrame frame = new JFrame("Hello Stackoverflowwwwwww! Here is a Dynamic Layered Pane Question.");
        frame.setLayout(null);
        frame.setContentPane(new Panel());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 600);
        frame.setVisible(true);
    }

    private static Dimension calculateAvailableSize(final JComponent component) {

        int availableHeight = component.getSize().height;
        int availableWidth = component.getSize().width;

        final Insets insets = component.getInsets();

        availableHeight -= insets.top;
        availableHeight -= insets.bottom;

        availableWidth -= insets.left;
        availableWidth -= insets.right;

        if (component.getBorder() != null) {
            Insets borderInsets = component.getBorder().getBorderInsets(component);
            if (borderInsets != null) {
                availableHeight -= borderInsets.top;
                availableHeight -= borderInsets.bottom;

                availableWidth -= borderInsets.left;
                availableWidth -= borderInsets.right;
            }
        }

        return new Dimension(availableWidth, availableHeight);
    }

    private static Rectangle calculateBoundsToFitImage(Dimension parentSize, Dimension imageSize) {
        final double scaleFactor;
        final int xOffset, yOffset, scaledHeight, scaledWidth;
        {
            final double xScaleFactor = (double) parentSize.width / imageSize.width;
            final double yScaleFactor = (double) parentSize.height / imageSize.height;
            scaleFactor = xScaleFactor > yScaleFactor ? yScaleFactor : xScaleFactor;
            scaledHeight = (int) Math.round(scaleFactor * imageSize.height);
            scaledWidth = (int) Math.round(scaleFactor * imageSize.width);
        }

        xOffset = (int) ((parentSize.width - scaledWidth) / 2.0);
        yOffset = (int) ((parentSize.height - scaledHeight) / 2.0);
        return new Rectangle(xOffset, yOffset, scaledWidth, scaledHeight);
    }
}

回答1:

Doh. I just answered my own question. Calling #setBounds is relative to your parent container, so the x & y offsets need to be properly accounted for, so this fixes that:

backgroundImagePanel.setBounds(0, 0, contentBounds.width, contentBounds.height);
compassPanel.setBounds(0, 0, contentBounds.width, contentBounds.height);