JLayeredPane, фоновое изображение + слой значков

Мне нужно 2 отдельных JPanel (или любых легких компонента) друг над другом и, в конечном итоге, встроенных в JPanel, либо напрямую, либо через что-то вроде JLayeredPane. Таким образом, никаких тяжелых компонентов или стекла. Нижняя панель JPanel (с именем BackgroundPanel) рисует фоновое изображение или воспроизводит видео, сохраняя соотношение сторон и используя альфа-канал. На верхней панели (называемой CompassPanel) есть значки, и пользователь может добавлять значки, удалять их и перемещать (подобно библиотеке диаграмм, эта функциональность не имеет прямого отношения к этому посту). Я не могу добавить много внешних зависимостей из-за ограничений пропускной способности моего приложения JNLP и среды развертывания. Однако, если кто-то знает об облегченной библиотеке диаграмм, которая может обрабатывать фоновые изображения и видео с альфа-каналом и соотношением сторон, я в игре. В противном случае я не могу понять, почему это пространство выделяется после изменения размера: JLayeredPane занимает место, которое BackgroundPanel должен  рисовать

Я прочитал руководство по Java по работе без менеджера компоновки. (не то, что я когда-либо хотел сделать, где ты, ГБЛ !?), но для этих требований масштабирования и того, чтобы значки оставались на одной и той же части изображения во время изменения размера и т. д. Я не могу придумать другого способа сделать это.

Вот код, я использую Java 1.7. Кроме того, это мой первый stackoverflow, не будьте нежны ;-)

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);
    }
}

person Jason    schedule 31.01.2013    source источник
comment
Я только что заметил, что графический компонент BackgroundPanel#paintComponent имеет другие границы клипа, чем я ожидал, он меньше, чем JLayerPane. Я пока не знаю почему, но похоже, что paintComponent рисует только подмножество того, что я думал.   -  person Jason    schedule 01.02.2013
comment
paintComponent попытается оптимизировать свои перерисовки, рисуя только те части экрана, которые, по его мнению, необходимо обновить. Вы можете использовать эту информацию, чтобы определить, следует ли вам красить определенные части вашего компонента, особенно если у вас сложная краска. Дополнительные сведения см. в разделе Рисование в AWT и Swing.   -  person MadProgrammer    schedule 01.02.2013
comment
Судя по тому, что я читал, вы делаете правильный выбор для достижения своих целей.   -  person MadProgrammer    schedule 01.02.2013


Ответы (1)


Дох. Я только что ответил на свой вопрос. Вызов #setBounds относится к вашему родительскому контейнеру, поэтому необходимо правильно учитывать смещения x и y, поэтому это исправляет следующее:

backgroundImagePanel.setBounds(0, 0, contentBounds.width, contentBounds.height);
compassPanel.setBounds(0, 0, contentBounds.width, contentBounds.height);
person Jason    schedule 31.01.2013